Skip to content

fix(pdu): check remaining length when skipping padding and reading client core data - #2130

Merged
Marc-André Moreau (mamoreau-devolutions) merged 3 commits into
Devolutions:masterfrom
se-wo:fix/decode-bounds-checks
Oct 24, 2026
Merged

Marc-André Moreau (mamoreau-devolutions) merged 3 commits into
Devolutions:masterfrom
se-wo:fix/decode-bounds-checks

Conversation

@se-wo

@se-wo se-wo commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • read_padding! now checks the remaining length and returns a "not enough bytes" decode error from the enclosing function, like ensure_size!, instead of advancing the read cursor past the end of the buffer (which made a later read panic). All existing call sites are covered without modification; the error carries the caller's name and the cursor offset.
  • The two remaining direct ironrdp_core::read_padding calls (ironrdp-rdpeusb) use read_padding! too. The function itself keeps its signature and behavior, now documented as unchecked.
  • Removes the duplicate PER read_padding; ConferenceCreateRequest::decode uses read_padding!.
  • ReadCursor::len() saturates at zero instead of overflowing when the cursor was advanced past the end, and ReadCursor::advance gains a debug assertion so debug, test and fuzz builds report such an advance where it happens.
  • ClientCoreOptionalData::decode checks the remaining length before reading desktopPhysicalHeight and deviceScaleFactor. Both are required by [MS-RDPBCGR] 2.2.1.3.2 once their predecessor is present, so a block ending in between is rejected with a "not enough bytes" error.
  • The optional Padding byte of DR_DRIVE_QUERY_DIRECTORY_RSP ([MS-RDPEFS] 2.2.3.4.10) is only skipped when present, so a length-0 response without it is accepted.

Compatibility

  • No public item is added, removed or changed: cargo-semver-checks on the ironrdp facade, with the feature set used by PR automation, reports no semver update required.
  • read_padding! changes its contract: it now returns early with a DecodeError, so it must be used in a function returning Result<_, DecodeError>, the same requirement as ensure_size!. cargo-semver-checks cannot see macro bodies. All uses in the workspace already satisfy this, but a downstream crate using read_padding! elsewhere would stop compiling, so this may warrant treating the change as breaking for the macro.

Tests

  • pdu::mcs: decoding every prefix of a valid Connect Initial returns a result instead of panicking; snapshot of the error for the prefix ending before the PER padding byte.
  • pdu::gcc: same for Client Core Data prefixes, plus blocks ending right after desktopPhysicalWidth / desktopScaleFactor.
  • padding: read_padding! on truncated input; the advance debug assertion.
  • rdpdr: empty query-directory response with and without the trailing padding byte.

cargo xtask check fmt -v, cargo xtask check lints -v and the workspace test suite pass locally (the IPv6 loopback test is skipped: no IPv6 on the test machine).

se-wo and others added 2 commits October 15, 2026 16:04
`read_padding!` advanced the read cursor without checking the remaining
length, so decoding truncated input could move the cursor past the end of
the buffer and panic on a later read.

- `read_padding!` now returns a "not enough bytes" error from the
  enclosing function, like `ensure_size!`, with the caller's name and
  cursor offset as context. Existing call sites are covered without
  modification.
- Add `ironrdp_core::try_read_padding`, the fallible counterpart of
  `read_padding`, whose signature and behavior are unchanged.
- The duplicate PER `read_padding` is removed; the conference create
  request decoder uses `read_padding!`.
- `ReadCursor::len()` saturates at zero instead of overflowing when the
  cursor was advanced past the end, and `advance` gains a debug assertion
  that reports such an advance where it happens.

The optional Padding of DR_DRIVE_QUERY_DIRECTORY_RSP ([MS-RDPEFS]
2.2.3.4.10) is now only skipped when present.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
desktopPhysicalHeight and deviceScaleFactor were read with an unchecked
`read_u32` once their preceding field was present, so a Client Core Data
block ending right after desktopPhysicalWidth or desktopScaleFactor was
read past its end. Both fields are required by [MS-RDPBCGR] 2.2.1.3.2
when their predecessor is present, so such a block is now rejected with a
"not enough bytes" error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added breaking-change Includes a breaking change, and requires special scrutiny at the boundaries kind/protocol Affects RDP or related protocol behavior risk/high Substantial core public API impact, or fail-closed triage; needs maintainer-level scrutiny scope/core Touches the core architectural tier scope/cross-cutting Spans multiple architectural boundaries size/L Size: up to 899 counted lines and 20 files; exceeds M in either measure automation-failed Exact-head automated classification or review failed or was unavailable labels Oct 15, 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 #2130 fixes a real cursor-overrun panic class: read_padding! now checks remaining length via ensure_size! before advancing, ReadCursor::len saturates and advance gains a debug assertion, ClientCoreOptionalData bounds-checks paired mandatory optional fields, and the optional DR_DRIVE_QUERY_DIRECTORY_RSP padding byte is skipped only when present. The changes match MS-RDPBCGR 2.2.1.3.2 and MS-RDPEFS 2.2.3.4.10, and the bundled tests demonstrate the previously panicking truncation paths. No blocking defects found. Four low-severity maintainability candidates from specialists were verified against pr-head: the exported macro's new requirement on the enclosing function makes the 'no public API change' claim incomplete; the newly added try_read_padding is unused in-repo and overlaps read_padding! (two duplicate candidates, merged); and the free read_padding function is now an unchecked duplicate of ReadCursor::advance with no remaining callers, worth deprecating. All four are accepted as …

  1. [code-compressor] Free read_padding function is now an unchecked duplicate of ReadCursor::advance with no remaining in-repo callers — low 🟡 — crates/ironrdp-core/src/padding.rs
    The macro no longer routes through the free read_padding (its body is just src.advance(n)), and repo searches find no remaining callers (cliprdr and egfx use read_padding!); the PR documents it as unchecked and steers users to the macro. Deprecating it ahead of removal would eliminate a redundant public entry point alongside advance (unchecked), read_padding! (checked, function-name context), and try_read_padding (checked, fixed context). Removal would be a breaking API change for a pre-existing export, so deprecation is the appropriate first step. Optional maintainability suggestion, not a defect.

Push a commit after addressing these findings. If no code change is needed, you may resolve inline threads and comment @github-actions review-ready to request human review.

Comment on lines +542 to +547
($src:expr, $n:expr) => {{
let n: usize = $n;
let src: &mut $crate::ReadCursor<'_> = $src;
$crate::ensure_size!(in: src, size: n);
src.advance(n);
}};

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.

[skeptical] read_padding! now requires the enclosing function to return a decode Result, making the PR's 'no public API change' claim incomplete — low 🟡 — read_padding! is #[macro_export] and its new expansion performs ensure_size! followed by an early Err return, so enclosing functions not returning a Result compatible with ironrdp_core::Error no longer compile; previously the macro advanced the cursor in any context. cargo-semver-checks cannot detect macro semantic changes, so the PR body's compatibility claim overstates safety for downstream users. The change is intentional, documented, and mirrors ensure_size!'s existing requirement, so it is a documentation/communication gap rather than a code defect.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed, the compatibility note was overstated. 32e840e documents on read_padding! that, like ensure_size!, it must be used in a function returning Result<_, DecodeError>, and the PR description now states this contract change explicitly. All workspace uses already satisfy it.

Comment thread crates/ironrdp-core/src/padding.rs Outdated
Comment on lines +43 to +53
/// Moves read cursor, ignoring padding bytes, after checking that `n` bytes remain.
///
/// # Errors
///
/// Returns a "not enough bytes" error, leaving the cursor untouched, when fewer than `n` bytes remain.
#[inline]
pub fn try_read_padding(src: &mut ReadCursor<'_>, n: usize) -> DecodeResult<()> {
ensure_size!(ctx: "padding", in: src, size: n);
src.advance(n);
Ok(())
}

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.

[skeptical + code-compressor] Newly exported try_read_padding has no callers and its logic is duplicated inline in read_padding! — low 🟡 — try_read_padding is added and re-exported from ironrdp-core, but nothing in the workspace calls it; only the new testsuite tests exercise it. read_padding! expands to the same ensure_size!-plus-advance sequence inline instead of delegating to try_read_padding(...)?, the only difference being error context (enclosing function name versus the fixed "padding" context). Since the function is newly added by this PR, no external callers can exist yet, so this is speculative public API surface: keeping one of the two entry points (or implementing the macro via try_read_padding with an accepted context change) would be a narrower change. Non-blocking design/maintainability note, not a defect.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed in 32e840e; read_padding! is now the only checked entry point. The two remaining read_padding calls in ironrdp-rdpeusb use the macro too. I kept the read_padding function itself unchanged, documented as unchecked: deprecating it is reported by cargo-semver-checks as an API change, so I’d rather do that in a separate PR.

@github-actions github-actions Bot added the ai-reviewed/1 One automated review completed label Oct 21, 2026
…y point

- Remove `try_read_padding`: it had no callers and duplicated
  `read_padding!`.
- Use `read_padding!` for the two remaining `read_padding` calls in
  ironrdp-rdpeusb, so no decoder in the workspace skips padding unchecked.
- Document that `read_padding!`, like `ensure_size!`, must be used in a
  function returning a `Result` whose error type is `DecodeError`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot removed scope/cross-cutting Spans multiple architectural boundaries automation-failed Exact-head automated classification or review failed or was unavailable labels Oct 21, 2026
@se-wo

se-wo commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

Marc-André Moreau (@mamoreau-devolutions) I addressed the low findings. Could you please approve the outstanding CI runs again

@mamoreau-devolutions
Marc-André Moreau (mamoreau-devolutions) merged commit b69cda2 into Devolutions:master Oct 24, 2026
57 of 59 checks passed
@se-wo
se-wo deleted the fix/decode-bounds-checks branch October 24, 2026 13:15
Anton Mostovoy (antonmos) added a commit to antonmos/IronRDP that referenced this pull request Oct 26, 2026
…ktop-scale-factor

* upstream/master: (92 commits)
  fix(server): advertise horizontal mouse wheel input (Devolutions#2236)
  fix(pdu): reject RSA moduli below 512 bits (Devolutions#2225)
  ci(ffi): verify generated bindings (Devolutions#2166)
  build(ffi)!: move to Diplomat 0.16.1-devo.2 (Devolutions#2165)
  build: make the client's video, camera and decoder crates publishable (Devolutions#2239)
  ci(release): release the newly published crates (Devolutions#2218)
  perf(graphics): replace the RLGR decoder's bit reader (Devolutions#2150)
  chore(graphics): add RemoteFX decode benchmarks (Devolutions#2149)
  fix(server)!: pass a GfxContext to the GfxServerFactory build methods (Devolutions#2104)
  fix(pdu): check remaining length when skipping padding and reading client core data (Devolutions#2130)
  fix(session): avoid hang on empty QOIZ updates (Devolutions#2147)
  feat(session): expose EGFX compositing (Devolutions#2120)
  fix(server): send RDP as the Demand Active source descriptor (Devolutions#2119)
  feat(activex)!: broaden mstscax compatibility (Devolutions#2211)
  feat(viewer): run RemoteApp programs in native windows (Devolutions#2201)
  fix(graphics): reject RLGR3 values larger than their remainder (Devolutions#2132)
  feat(egfx)!: composite surfaces mapped to RemoteApp windows (Devolutions#2197)
  feat: redirect USB devices over URBDRC with nusb and RemoteFX USB backends (Devolutions#2181)
  build: mark exposed dependencies as public (Devolutions#2213)
  chore(release): prepare for publishing (Devolutions#2212)
  ...

# Conflicts:
#	crates/ironrdp-testsuite-extra/tests/e2e.rs

This branch was previously deployed

1 inactive deployment
llm-providers — 32e840e1 Deployed Oct 21, 2026 by se-wo via Classify pull request #5599
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-reviewed/1 One automated review completed breaking-change Includes a breaking change, and requires special scrutiny at the boundaries kind/protocol Affects RDP or related protocol behavior risk/high Substantial core public API impact, or fail-closed triage; needs maintainer-level scrutiny scope/core Touches the core architectural tier size/L Size: up to 899 counted lines and 20 files; exceeds M in either measure

Development

Successfully merging this pull request may close these issues.

2 participants