Skip to content

fix(displaycontrol): decode the full headered DISPLAYCONTROL_CAPS_PDU#1442

Open
truebest wants to merge 4 commits into
Devolutions:masterfrom
truebest:fix/displaycontrol-headered-caps-pdu
Open

fix(displaycontrol): decode the full headered DISPLAYCONTROL_CAPS_PDU#1442
truebest wants to merge 4 commits into
Devolutions:masterfrom
truebest:fix/displaycontrol-headered-caps-pdu

Conversation

@truebest

Copy link
Copy Markdown

Summary

DisplayControlClient::process() decoded the server's capability payload as a raw DisplayControlCapabilities body. In reality, the server sends the capability set prefixed with a DISPLAYCONTROL_HEADER (Type + Length) as defined by MS-RDPEDISP 2.2.2.1 — the full DISPLAYCONTROL_CAPS_PDU, not a bare DISPLAYCONTROL_MONITOR_LAYOUT_PDU-style caps body.

Because the header bytes were being read as capability fields, Type (5) was misread as MaxNumMonitors and Length (20) was misread as MaxMonitorAreaFactorA. This made max_monitor_area() come out far too small, which silently dropped monitor layouts produced by the on_capabilities_received callback (they were rejected as exceeding the bogus max area).

This is an internal inconsistency, not an intentional design choice: DisplayControlServer::start in this same crate, and the ironrdp-testsuite-core golden vectors for displaycontrol, already assume/produce the headered shape. Only the client's decode path was wrong.

Fix

process() now decodes the full DisplayControlPdu via decode() and matches on DisplayControlPdu::Caps(..). A DisplayControlPdu::MonitorLayout(..) received from the server (which would be unexpected — that variant is client→server) is now rejected with an explicit error instead of being silently misparsed.

How this was found

Found while running IronRDP as a client against gnome-remote-desktop as the RDP server, which sends the full headered DISPLAYCONTROL_CAPS_PDU. The client's max_monitor_area() came out tiny, and dynamically-resized monitor layouts sent from the callback were silently dropped as "too large."

Test plan

  • cargo check -p ironrdp-displaycontrol
  • cargo test -p ironrdp-displaycontrol (crate has no unit tests of its own; golden vectors live in ironrdp-testsuite-core)
  • cargo test -p ironrdp-testsuite-core -- displaycontrol — all 10 tests pass, including capabilities_decode and invalid_caps
  • cargo clippy -p ironrdp-displaycontrol --all-targets -- -D warnings
  • cargo fmt -p ironrdp-displaycontrol -- --check

The server sends the capability set prefixed with the DISPLAYCONTROL_HEADER
(Type + Length) per MS-RDPEDISP 2.2.2.1 -- DisplayControlServer::start and
the testsuite golden vectors both use that shape -- but process() decoded
the payload as a raw DisplayControlCapabilities body. The header bytes were
read as caps fields (Type=5 as MaxNumMonitors, Length=20 as FactorA), so
max_monitor_area() came out tiny and callback-produced monitor layouts were
never sent. Decode the full DisplayControlPdu and reject an unexpected
MonitorLayout from the server.
Copilot AI review requested due to automatic review settings July 12, 2026 16:21

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@glamberson

Copy link
Copy Markdown
Contributor

Hi truebest, nice catch. Confirmed against master: DisplayControlServer::start already assumes the headered shape, so process() was the actual outlier here, not a design choice. The fix is correct and matches the existing pattern.

One suggestion: None of the existing testsuite-core coverage exercises DisplayControlClient::process() itself, only DisplayControlPdu decode/encode symmetry. A small test hitting process() directly with the headered caps bytes (plus one for the MonitorLayout rejection) would pin down the actual regression path, CBenoit tends to ask for this on behavior-changing fixes.

Otherwise looks good. Nice find running against gnome-remote-desktop.

Existing coverage only exercised DisplayControlPdu decode/encode
symmetry, not process() itself -- the actual site of the headered-CAPS
regression this PR fixes. Add a test decoding a real headered caps PDU
through process() and asserting the callback receives the right
capabilities, plus a test confirming a MONITOR_LAYOUT_PDU from the server
is rejected rather than silently accepted.
@truebest

truebest commented Jul 13, 2026

Copy link
Copy Markdown
Author

Good point, added in 447104a: a test decoding a real headered-caps PDU through process() and checking the callback receives the right capabilities, plus one confirming a MONITOR_LAYOUT_PDU from the server is rejected. Both hit process() directly, not just PDU decode/encode symmetry.

An agent will work on test coverage, let me know if it generates any issues.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 447104acd3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "Codex (@codex) review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "Codex (@codex) address that feedback".

let caps = DisplayControlCapabilities::decode(&mut ReadCursor::new(payload)).map_err(|e| decode_err!(e))?;
// The server sends the full DISPLAYCONTROL_CAPS_PDU, i.e. the capability set prefixed
// with the DISPLAYCONTROL_HEADER (Type + Length), per MS-RDPEDISP 2.2.2.1.
let caps = match decode(payload).map_err(|e| decode_err!(e))? {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Validate DISPLAYCONTROL_HEADER length before accepting caps

When the server sends a CAPS PDU with Type = 5 but an advertised Length that does not match the 20-byte caps PDU (for example 0xffff_ffff followed by a valid caps body), this new path accepts it because DisplayControlPdu::decode only subtracts the header size and never checks the advertised length against payload.len() or the caps size, even though MS-RDPEDISP defines Length as the PDU length. process then marks the channel ready and invokes the callback with capabilities from malformed server framing, so please reject a mismatched Length before accepting Caps.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 0b00767 — DisplayControlPdu::decode now validates Length against the actual remaining payload size before decoding the body, rejecting a mismatch (e.g. 0xffff_ffff) instead of silently accepting whatever bytes follow. Added a test covering exactly this case.

DisplayControlPdu::decode computed the claimed body size from the header
but never checked it against the actual payload -- a Caps PDU with a
bogus Length (e.g. 0xffff_ffff) followed by a valid body decoded
successfully anyway. Validate Length against the remaining bytes before
decoding the body.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0b007672a5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "Codex (@codex) review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "Codex (@codex) address that feedback".

.checked_sub(Self::FIXED_PART_SIZE.try_into().expect("always in range"))
.ok_or_else(|| invalid_field_err!("Length", "Display control PDU length is too small"))?;

if usize::try_from(payload_length).unwrap_or(usize::MAX) != src.len() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject overlong CAPS payloads

Fresh evidence in this revision is that the new Length check only compares the advertised payload length with the bytes handed to the decoder; it does not ensure the selected inner PDU consumes that payload. A server packet with Type = 5, Length = 24, and a 16-byte body will pass this check, DisplayControlCapabilities::decode will read the fixed 12-byte caps body, and the extra 4 bytes are ignored while DisplayControlClient::process marks the channel ready. Please bound the inner decode to payload_length and reject any trailing bytes, or require the CAPS payload to be exactly 12 bytes.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in d06b30e — decode() now checks the cursor is empty after decoding the inner PDU body, rejecting trailing bytes for both Caps and MonitorLayout. Added a test with Length=24 over a 16-byte payload where the caps body only consumes 12.

The Length check only verified the header's claimed size against the
total payload, not that the specific decoded variant (Caps is a fixed
12 bytes) actually consumed all of it. A Length that matched the payload
size but left bytes unconsumed after decoding Caps or MonitorLayout was
silently accepted. Check the cursor is empty after decoding the body.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants