Skip to content

feat(server): let the display decline the Display Control channel - #1985

Open
Greg Lamberson (glamberson) wants to merge 5 commits into
Devolutions:masterfrom
lamco-admin:feat/server-display-control-opt-out
Open

Greg Lamberson (glamberson) wants to merge 5 commits into
Devolutions:masterfrom
lamco-admin:feat/server-display-control-opt-out

Conversation

@glamberson

@glamberson Greg Lamberson (glamberson) commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • A server that cannot resize, for example one mirroring a fixed-size physical screen, had no way to stop ironrdp-server from offering the Display Control channel. MS-RDPEDISP section 1.3 defines no reject message: When the requested configuration is not possible, the server simply does not update the session. A client that sends a layout the server cannot apply is left waiting.
  • Added a default RdpServerDisplay::offers_display_control() method (returns true, matching today's behavior), queried once per connection before the dynamic channels are attached.
  • When it returns false, attach_channels skips DisplayControlServer, so the channel is not offered on that connection.

Validation

cargo xtask check fmt/lints/tests/typos/locks all pass. Two end-to-end tests in ironrdp-testsuite-extra connect a real client that registers a Display Control channel and an echo channel: A display that offers Display Control gets the channel created on the client, and a display that declines it does not. The declining test fails if the gate is removed.

Notes

  • This is the rule gnome-remote-desktop applies: It creates its Display Control channel only in extend (virtual monitor) mode.
  • Additive only: One new trait method with a default body. No existing implementor is affected unless it opts in.

@github-actions github-actions Bot added needs-review A human reviewer is the current next actor risk/unknown Risk could not be determined automatically; needs maintainer-level scrutiny size/S Size: up to 199 counted lines and 5 files; exceeds XS in either measure labels Sep 22, 2026
@glamberson
Greg Lamberson (glamberson) force-pushed the feat/server-display-control-opt-out branch from e68daa9 to ef12e2a Compare September 28, 2026 04:45
@github-actions github-actions Bot added kind/protocol Affects RDP or related protocol behavior risk/medium Behavioral change that does not substantially alter a core public API needs-review A human reviewer is the current next actor and removed risk/unknown Risk could not be determined automatically; needs maintainer-level scrutiny needs-review A human reviewer is the current next actor labels Sep 28, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additive, low-risk PR: a default-true RdpServerDisplay::offers_display_control() hook gates registration of DisplayControlServer in attach_channels, letting fixed-size displays (e.g., mirrored physical screens) suppress the MS-RDPEDISP offer and avoid the no-reject layout-request hang. Both call sites (serve_negotiated and run_connection_inner) consistently query the hook before channel setup, the conditional dvc construction matches the existing shadowing-builder idiom used for echo/rdpei/rdpeai in the same function, and no capability sets or PDU formats change, so no protocol defect exists. Independently verified against pr-head. Two valid low-severity candidates from specialists are accepted unchanged: the new test only asserts the hook was consulted rather than that the channel is actually omitted from the connection, and serve_negotiated acquires the display mutex twice back-to-back where one held guard suffices.

  1. [code-compressor] Back-to-back display mutex acquisitions in serve_negotiated can share one guard — low 🟡 — crates/ironrdp-server/src/server.rs
    serve_negotiated acquires the display tokio Mutex twice in a row (monitor_count, then the new offers_display_control query). Holding one guard across both awaited calls is behaviorally equivalent, removes one lock round-trip, and closes a window where another task could interleave between the two queries. The same pattern in run_connection_inner is separated by pre-existing code and is deliberately out of scope; only the adjacent pair in serve_negotiated is a clean, low-risk cleanup.

Comment thread crates/ironrdp-testsuite-core/tests/server/display_control.rs Outdated
@github-actions github-actions Bot added ai-reviewed/1 One automated review completed and removed needs-review A human reviewer is the current next actor labels Sep 29, 2026
@github-actions github-actions Bot added risk/high Substantial core public API impact, or fail-closed triage; needs maintainer-level scrutiny size/M Size: up to 449 counted lines and 10 files; exceeds S in either measure and removed risk/medium Behavioral change that does not substantially alter a core public API size/S Size: up to 199 counted lines and 5 files; exceeds XS in either measure labels Sep 29, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR #1985 adds a defaulted RdpServerDisplay::offers_display_control() hook queried once per connection, letting a display omit the MS-RDPEDISP Display Control dynamic channel when it cannot honor layout requests. Assessment confirms the change is additive and safe: the true default preserves behavior for all existing implementors, omitting the DYNVC_CREATE_REQ is specification-consistent (MS-RDPEDYC makes dynamic channel creation a server-initiated per-channel decision, and MS-RDPEDISP imposes no obligation to open the channel), and the gate is applied at both connection paths before channels attach. New e2e tests cover both outcomes with a real client. No correctness, protocol, or API defects were found. The remaining candidates are low-severity maintainability items, all independently verified: monitor_count() is awaited and discarded when the display declines (contradicting its documented contract tied to the channel opening), the test re-hardcodes ironrdp-displaycontrol::CHANNEL_NA…

  1. [skeptical] monitor_count() is awaited and discarded when the display declines Display Control — low 🟡 — crates/ironrdp-server/src/server.rs
    Both call sites unconditionally await monitor_count() immediately before offers_display_control() (serve_negotiated at server.rs:2049-2050; the TLS path likewise), but when the gate returns false the DisplayControlBackend is never built and the value is unused. The monitor_count doc states it is 'called once, before the Display Control Virtual Channel opens' (display.rs:345-346), so an opting-out display now observes a call whose contract no longer holds, paying any implementation cost for nothing. Conditioning the monitor_count fetch (or folding both queries into attach_channels) restores the documented behavior.
  2. [code-compressor] offer_display_control parameter threading and duplicated display-lock query could be folded into attach_channels — low 🟡 — crates/ironrdp-server/src/server.rs
    attach_channels has exactly two callers, both async, and each repeats the same pattern: query the display under its lock, then pass the result as a parameter, with monitor_count already consumed only inside attach_channels. Making attach_channels async and querying monitor_count() and offers_display_control() inside it deletes the parameter threading, the monitor_count parameter, and the duplicated lock-and-query line at both call sites, with identical query timing relative to channel attachment. This would also resolve the discarded monitor_count call when the display declines.

Comment thread crates/ironrdp-testsuite-extra/tests/e2e.rs Outdated
Comment thread crates/ironrdp-testsuite-extra/tests/e2e.rs Outdated
Comment thread crates/ironrdp-testsuite-extra/tests/e2e.rs Outdated
@github-actions github-actions Bot added ai-reviewed/2 Two automated reviews completed and removed ai-reviewed/1 One automated review completed labels Sep 29, 2026
@glamberson

Copy link
Copy Markdown
Contributor Author

Both points are right. monitor_count() was awaited at both call sites and discarded when the display declined, which contradicted its documented contract. attach_channels is now async and asks the display itself: It queries offers_display_control() first and only asks monitor_count() when the channel is offered, so the two parameters and the duplicated query at each call site are gone.

@github-actions github-actions Bot added the needs-review A human reviewer is the current next actor label Sep 29, 2026
@glamberson

Copy link
Copy Markdown
Contributor Author

The two display queries now share one guard. attach_channels holds the display lock across offers_display_control() and, only when the channel is offered, monitor_count(), so no other task can interleave between them, and it saves the second lock acquisition.

@github-actions github-actions Bot added risk/medium Behavioral change that does not substantially alter a core public API and removed risk/high Substantial core public API impact, or fail-closed triage; needs maintainer-level scrutiny labels Sep 29, 2026
MS-RDPEDISP section 1.3 gives a server no reject message for a monitor
layout it cannot apply, so a client offered Display Control on a
fixed-size display waits for a reconfiguration that never comes.
ironrdp-client, for instance, waits out its 3 second resize deadline and
then reconnects.

Add RdpServerDisplay::offers_display_control(), queried once per
connection before the dynamic channels are built. Returning false skips
DisplayControlServer. Defaults to true, so existing implementations are
unchanged.
The old test only showed offers_display_control was consulted. Two
end-to-end tests in ironrdp-testsuite-extra now connect a real client that
registers a Display Control channel and an echo channel. The server creates
its dynamic channels in registration order, so once the echo channel has
opened the answer for Display Control is final: a display that offers it gets
the channel created on the client, a display that declines it does not.

The e2e harness gains client_server_with_display so a test can supply its own
RdpServerDisplay.
…the tests

attach_channels is now async and asks the display itself. It queries
offers_display_control first and only calls monitor_count when the channel is
offered, so a display that declines is no longer asked for a value its
documented contract ties to the channel opening, and the two parameters and
the duplicated query at both call sites are gone.

In the end-to-end tests the Display Control channel name comes from
ironrdp::displaycontrol::CHANNEL_NAME (the displaycontrol feature is enabled on
the ironrdp dev-dependency), TestDisplay carries an offers_display_control
field in place of the ConfigurableDisplay wrapper, and the probe settles on a
quiet connection after echo opens instead of relying on registration order.
offers_display_control and monitor_count were asked under two separate lock
acquisitions in a row. attach_channels now holds one guard across both, asking
monitor_count only when the channel is offered, so no other task can interleave
between the two queries and the second acquisition is gone.
The rebase onto master brought in the stale_events and server_udp_addr
arguments of client_server_impl, two new TestDisplay literals and the
ironrdp_dvc import that covers the names StartRecorder uses.
client_server_with_display now takes the two arguments and passes them
on, the two other TestDisplay literals offer Display Control as they did
before, and the import line is master's again.
@glamberson
Greg Lamberson (glamberson) force-pushed the feat/server-display-control-opt-out branch from d542fbf to 89f17cd Compare October 8, 2026 04:10
@github-actions github-actions Bot added risk/high Substantial core public API impact, or fail-closed triage; needs maintainer-level scrutiny needs-review A human reviewer is the current next actor and removed needs-review A human reviewer is the current next actor risk/medium Behavioral change that does not substantially alter a core public API labels Oct 8, 2026

This branch was successfully deployed

1 active deployment
llm-providers — 89f17cd9 Deployed Oct 8, 2026 by glamberson via Classify pull request #1875
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-reviewed/2 Two automated reviews completed kind/protocol Affects RDP or related protocol behavior needs-review A human reviewer is the current next actor risk/high Substantial core public API impact, or fail-closed triage; needs maintainer-level scrutiny size/M Size: up to 449 counted lines and 10 files; exceeds S in either measure

Development

Successfully merging this pull request may close these issues.

1 participant