Repository navigation
Conversation
|
Related: #1977 also changes the SRL decoder, in a different way. Both stop requiring the terminator, read past the end as zeros, and remove |
|
Another data point for this one: we hit the same failure independently, with Windows 11 over the graphics pipeline through the web client ( I haven't run this branch itself, but the cases it handles cover what we saw. We'll drop our local patch once this or #1977 lands, so we're not opening a competing PR. |
…#2007) Windows lists every dynamic channel it intends to move in its Soft-Sync request, including the ones the client declined with NO_LISTENER. Against a Windows 11 host the request lists channels 2, 6, 7, 8, 9, 10, 11 and 12 (CoreInput, MouseCursor, Graphics, Video, Geometry, ...), and only channel 7, the graphics pipeline, is open. `process_soft_sync_request` dropped a whole channel list as soon as one ID in it was not open. The tunnel was then never switched, and the channels the client had opened stayed on TCP while the server was already sending them on the tunnel (MS-RDPEDYC 3.2.5.3.1). Unopened channels are now skipped one by one, and the tunnel is switched for the rest. ## Testing - New `dvc::client::soft_sync_skips_channels_the_client_did_not_open` in `ironrdp-testsuite-core`. - Live, against a Windows 11 host over RDP-UDP version 2, with the viewer built from a branch that also carries the tunnel and client PRs of this series: the Soft-Sync request above now switches the tunnel, and the graphics pipeline moves onto it. ## Checks - `cargo fmt --all -- --check` - `cargo clippy --workspace --all-targets --features helper,__bench --locked -- -D warnings` - `cargo test --locked -p ironrdp-testsuite-core -p ironrdp-testsuite-extra`, plus the lib tests of the crates touched here - `cargo test --workspace --locked` on a branch that merges this PR with the other Windows interop PRs from this series - `typos` on the changed files ## Series These PRs port the Windows interop fixes and Linux backends from a downstream IronRDP fork, so the fork can be retired. Each one is based on `master` and can be reviewed and merged on its own. I also checked that all of them merge cleanly together in this order. - #2007 fix(dvc): Soft-Sync tunnel with declined channels - #2008 fix(session)!: channels and graphics on the tunnel - #2009 fix(rdpeudp): auto-detect on the tunnel - #2010 fix(graphics)!: SRL streams from Windows - #2011 fix(egfx): bitmap cache across ResetGraphics - #2012 feat(session): bandwidth measurements during the session - #2013 feat(client): graphics pipeline and RDP-UDP version options - #2014 fix(client): resize reconnects on the graphics pipeline - #2015 feat(client): transport event - #2016 feat(cliprdr): Linux clipboard backend - #2017 feat(rdpdr): printer on Linux and macOS Co-authored-by: AKolenda <testedemail2222@gmail.com>
There was a problem hiding this comment.
PR #2010 relaxes the RFX Progressive SRL decoder to accept three Windows constructions: an optional trailing zero byte, omitted trailing entries (bits past end read as zeros), and zero runs overshooting one component (capped at 4096 instead of rejected). The progressive.rs changes are test-only. The leniency is protocol-permissible and matches FreeRDP behavior, the interop motivation is credible with failing-session reports, and the tile-untouched-on-error property is preserved. All six candidates are valid: one skeptical finding about the trailing-byte strip is refined because its concrete example is arithmetically wrong, though a corrected construction confirms the underlying ambiguity; the remaining skeptical findings (silent truncation, stale doc) and all five code-compressor dead-plumbing findings are accepted as low-severity. No correctness defect in the main decode paths was found.
- [skeptical] Stripping a trailing 0x00 byte can drop a final positive max-magnitude value — medium 🟠 ❓ — crates/ironrdp-graphics/src/srl.rs
new() removes any trailing 0x00, so a terminator-less stream whose final data byte is all zeros is indistinguishable from one carrying the terminator. Bits in a stripped byte read identically to past-end zeros, so the divergence is confined to nonzero_pending = !is_exhausted() (line 107): when a zero-run codeword ends exactly at the stripped-payload boundary, a following positive max-magnitude value whose sign and unary zeros formed the stripped byte is decoded as 0 instead. Example: data [0x86, 0x00] decoded for 3 entries gives [3, 0, 0] stripped versus [3, 15, 0] with the byte kept; the doc comment's claim that a stripped zero data byte is harmless is therefore overstated. Whether Windows can emit this shape is unverifiable from the repository. - [skeptical] Arbitrary mid-stream truncation now decodes silently with no signal — low 🟡 — crates/ironrdp-graphics/src/srl.rs
The tolerance is justified for Windows omitting trailing entries, but is_exhausted() cannot distinguish an intentional early stop from corruption: truncation anywhere yields zeros, or a spurious positive maximum if a value was pending. This drops the malformed-stream detection half of#1696's guarantee while keeping only tile atomicity, so a transport bit error that ended the session now produces silent visual corruption with no log or counter. A trace/debug signal when the exhausted branch or the MAX_ZERO_RUN cap engages would partially restore observability at negligible cost. - [skeptical] decode_upgrade_pass doc still promises rejection of truncated streams — low 🟡 — crates/ironrdp-graphics/src/progressive.rs
The Errors section at line 168 says the function returns SrlError for a malformed or truncated SRL stream, but after this PR truncation decodes as zeros or positive maxima and succeeds, mutating the tile. A reader would wrongly assume truncation still fails the pass. One-line doc fix on a changed path whose central behavioral claim it contradicts. - [code-compressor] SrlDecoder::new can no longer fail; drop the Result — low 🟡 — crates/ironrdp-graphics/src/srl.rs
With MissingTerminator removed, the payload-stripping match in new() has no failing path, so it can return Self. This deletes the Ok wrapper, the propagation in decode_srl, the transpose at progressive.rs line 190, and test unwraps. The PR is already a breaking change (two error variants removed), so the signature change costs nothing extra. - [code-compressor] read_bit, read_bits, and decode_zero_run are now infallible; unwrap the Results — low 🟡 — crates/ironrdp-graphics/src/srl.rs
read_bit returns Ok(false) past the end with no error path, read_bits only forwards it, and decode_zero_run's only propagated errors came from those two, so all three can never fail. Their Result wrappers and propagation at call sites in decode and decode_nonzero are dead plumbing. The tail computation also simplifies to a plain cast: k = kp/8 <= 10, so the value fits usize without try_from/unwrap_or. All private, so no API impact. - [code-compressor] decode_nonzero's i16 conversion error branch is unreachable — low 🟡 — crates/ironrdp-graphics/src/srl.rs
max_magnitude bounds num_bits to 1..=15 so maximum is at most 32767, and the unary loop yields magnitude no greater than maximum, so magnitude always fits i16 and the try_from with MagnitudeOutOfRange cannot fail. Pre-existing code not added by this diff, so lowest priority, but it is unreachable error handling in a file whose change here removes dead error handling.
|
Verified on Windows Server 2022 (Standard, build 20348.5622) with the graphics pipeline on and no H.264 decoder. On Tested with AI assistance; results are from automated runs against the server. |
|
The account paying for this security review has reached its Codex usage limits. The payer can check the Codex usage dashboard. For personal accounts, using credits requires enabling “Use credits for security reviews” in Code review settings. If you do not manage the paying account, contact this repository's admins. |
|
Pushed f47878a to address the latest review. The decoder no longer strips a final zero byte before reading coefficients, and it preserves a pending nonzero value when its sign or magnitude reaches EOF. Added the reviewed [0x86, 0x00] case, a pending value crossing a band boundary, and round trips with and without terminators. Removed the redundant exhaustion/cap logic and corrected the truncation documentation and PR description. All 245 graphics library tests, targeted Clippy with warnings denied, and formatting passed. These tests run in normal workspace CI. I did not add a corruption log: omitted trailing entries and truncation have the same zero-filled representation, so the decoder cannot reliably distinguish them. The documentation now states that limit. |
ignore this, my external review agent, idk why it activated on external PR |
|
Rechecked the failed notification against the latest commit: the normal build/test CI suite passes. The separate public API check fails before comparing changes because its fresh dependency resolution selects incompatible picky-krb 0.12.5 with sspi 0.21.3. The focused workflow repair is #2071, which builds both revisions with their committed lockfiles. It has passed a real IronRDP API build and unchanged/breaking/stale-lockfile fixtures locally. The workflow runs from the base branch, so this check needs that repair merged before a rerun can use it. |
|
Update: no rebase needed anymore. picky-krb 0.12.5 was yanked from crates.io (re-released as 0.13.0), so the API check builds again without changes to this branch. PR automation has been re-run here and passes. |
## Problem `OpenH264Decoder::decode` always reads its input as AVC format (4-byte big-endian length-prefixed NAL units) and converts it to Annex B before handing it to OpenH264. MS-RDPEGFX defines the [`RFX_AVC420_BITMAP_STREAM`](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpegfx/5f12c20e-2ea1-4ad1-a2a0-019ee3893731) bitstream as "conforming to the byte stream format specified in [ITU-H.264-201201] Annex B". Servers that follow the spec send start codes, not lengths. When the decoder gets Annex B, the first start code `00 00 00 01` is read as a NAL length of 1. The bytes after it are then read as the next length, which runs past the buffer, and the frame is dropped: ``` AVC NAL extends beyond buffer, discarding remaining data nal_len=805306368 offset=9 data_len=159657 ``` GNOME Remote Desktop 50.2 and Windows Server 2022 both start every frame with an access unit delimiter (`00 00 00 01 09 30 00 00 00 01 ...`), so no AVC420 frame from either one decodes. `805306368` is `0x30000000`, the bytes after the delimiter. ## Fix A new public function, `pdu::is_avc_format`, decides the format. `OpenH264Decoder::decode` uses it: 1. **Starts with a start code** (`00 00 01` or `00 00 00 01`): Annex B. Passed to OpenH264 unchanged. 2. **Otherwise, the length prefixes chain exactly to the end of the buffer**, and every length is nonzero: AVC format. Converted with `avc_to_annex_b_into` as before. 3. **Anything else:** passed to OpenH264 unchanged as Annex B. The start code is checked first because the chain check alone misreads valid Annex B. `00 00 01 67` read as a length is 359, so a 363-byte frame that starts with a 3-byte start code and an SPS also parses as a one-unit AVC buffer. The same happens to a 325-byte P slice (`00 00 01 41`), and small single-slice P frames are what an idle screen produces. Checking the start code first leaves one ambiguity: AVC senders whose first NAL unit is 1 byte or 256–511 bytes long are read as Annex B. Those senders don't follow the spec. `is_avc_format` sits next to `avc_to_annex_b` in `pdu/avc.rs` so other `H264Decoder` implementations can use it, and the `egfx_avc420_decode` fuzz oracle runs it. ## Behavior changes to review - **Malformed AVC input.** Previously, an AVC buffer with a truncated last NAL unit or a zero-length NAL unit still decoded the complete units before the bad one. Now it fails the chain check and goes to OpenH264 as Annex B, which usually finds no picture. Conforming senders aren't affected. - **AVC input whose first NAL unit is 1 or 256–511 bytes** is now read as Annex B (the ambiguity above). - **`H264Decoder` trait docs** now say implementations may receive either format. Third-party decoders written against the old docs may only handle AVC input; they already fail against spec-conforming servers. - **New public API:** `ironrdp_egfx::pdu::is_avc_format`. - **Cost.** Input that starts with a start code is not scanned. Other input is scanned once by the chain check, and a second time by the conversion if it is AVC. ## Docs The docs that said the wire format is length-prefixed are corrected: `decode.rs` module and trait docs, `avc_to_annex_b`, `annex_b_to_avc`, `encode_avc420_bitmap_stream`, the encoder round-trip test comment, and the two EGFX fuzz oracle comments. The spec link in `decode.rs` returned 404 and now points to the `RFX_AVC420_BITMAP_STREAM` page. `encode.rs` already said the encoder produces Annex B, "the format `RFX_AVC420_BITMAP_STREAM` carries on the wire", and the glutin renderer (`crates/ironrdp-glutin-renderer/src/surface.rs`) already passes wire bytes straight to OpenH264. This change makes the decoder agree with both. ## Tests Added to `crates/ironrdp-testsuite-core/tests/egfx/decode.rs`: | Test | Case | Fails on | | --- | --- | --- | | `test_openh264_decode_annex_b` | Encoder's Annex B output decodes as-is | `master` | | `test_openh264_decode_annex_b_with_access_unit_delimiter` | Frame starting with an AUD (GNOME Remote Desktop, Windows) | `master` | | `test_openh264_decode_annex_b_three_byte_start_codes` | 3-byte start codes | `master` | | `test_openh264_decode_annex_b_that_also_parses_as_avc` | 363-byte `00 00 01 67` frame that also parses as AVC | `master`, and a chain-first check | | `test_is_avc_format` | Annex B, AVC, empty, and overrunning length | (new function) | The existing AVC tests pass unchanged. Three error-path test comments were updated to describe the new path. Run locally on 8fac24c: - `cargo xtask check fmt`, `cargo xtask check lints` - `typos` on the changed crates - `cargo test -p ironrdp-testsuite-core --features openh264-bundled egfx`: 83 passed - `cargo test -p ironrdp-egfx --features openh264-bundled`: 55 passed ## Tested against servers - **GNOME Remote Desktop 50.2** (VA-API encoding, Intel N100): a macOS client ([rdp123](https://github.com/asd123ch/rdp123)) carrying the same check on `ironrdp-egfx` 0.3.0 decodes AVC420 frames. Without it, none decode. - **Windows Server 2022** ([report](#1986 (comment))): with this PR and #2010, 960 of 960 AVC420 frames decode. Without this PR, the first AVC420 frame fails the same way and the session ends. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… them The SRL decoder rejected streams that Windows servers send in ordinary RFX Progressive upgrade passes, and the resulting error ended the session on TCP and on the UDP tunnel alike. Three constructions tripped it: - The trailing zero byte is not always present. It is now stripped when present and not required, matching FreeRDP, whose `progressive_rfx_upgrade_state_finish` only skips it when one byte is left. - The encoder stops writing once every remaining entry of a component is zero. Bits past the end of the stream now read as zeros, as they do from the reference decoder's zero-filled bit accumulator, so the omitted entries decode as zeros, also across band boundaries. - A zero run may overshoot the entries that are left: at KP = 80 a single `0` bit adds 1024 zeros and is cheaper than an exact run. The decoder now caps the run at one component instead of rejecting it; anything past the cap could only be trailing zeros. The encoder is unchanged and still emits the terminator and exact runs. A failed upgrade pass still leaves the tile untouched; the test for that now uses a magnitude width SRL cannot represent. BREAKING CHANGE: `SrlError::MissingTerminator` and `SrlError::Truncated` are removed because the decoder no longer produces them.
f47878a to
add736d
Compare
add736d to
95d343b
Compare
|
Rebased on master. On the overlap notice: this PR and #2085 are complementary. Neither duplicates the other's fix.
The DWT variant applies after coefficients are reconstructed, so neither change affects the other's behaviour. They merge without conflicts in either order. On the merged result, the |
There was a problem hiding this comment.
PR #2010 relaxes SRL decoding for RFX Progressive upgrade passes to match Windows-encoded streams: the trailing zero byte is no longer stripped or validated, bits past EOF read as zeros, and zero-run events are consumed incrementally without a decoder-side cap. I independently verified the event-per-iteration state machine is semantically equivalent to the previous accumulate loop (same KP updates), the decode loop terminates, the expect() in decode_nonzero is unreachable (num_bits <= 15 bounds magnitude at 32767), and the breaking API change (SrlDecoder::new now infallible; MissingTerminator/Truncated removed) has no remaining in-repo consumers. The three published findings are all low-severity: the by-design loss of truncation detection (documented, matches the reference decoder's zero-filled accumulator), a vestigial Option<SrlDecoder>/has_srl_values gate left from the old fallible constructor, and a leftover u16 return width in BitReader::read_bits forcing a conversion at its only…
- [code-compressor] Option<SrlDecoder> and has_srl_values gate are vestigial now that SrlDecoder::new is infallible — low 🟡 — crates/ironrdp-graphics/src/progressive.rs
The PR made SrlDecoder::new infallible but kept the Option wrapper in decode_upgrade_pass: has_srl_values (an any-closure over bands) gates has_srl_values.then(|| SrlDecoder::new(srl_data)), and the loop matches on as_mut() with a None => Vec::new() fallback. The gate is redundant because when it is false every non-LL3 band with num_bits != 0 has zero_count == 0, and decode(0, num_bits) returns an empty Vec without consuming bits or validating num_bits. An unconditional SrlDecoder::new plus decode is behavior-preserving and deletes the closure, .then(...), and match (~11 lines, one branch); a short comment can carry the decode-only-when-values-exist intent. Null lines: only line 191 of this region is added by the PR. - [code-compressor] read_bits returning u16 forces a usize::from conversion at its only call site — low 🟡 — crates/ironrdp-graphics/src/srl.rs
read_bits was narrowed to u16 in this PR after the removed overflow-cap checking, but its sole decode call site (decode, line 97) immediately converts again with usize::from(self.reader.read_bits(k)). Since k = kp/8 is at most 10 (MAX_KP = 80), the value always fits; having read_bits return usize directly removes the conversion and matches the natural width for a run length, with no behavior change. Null lines: line 97 is added by the diff while the read_bits signature and body are mostly unchanged context.
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.
| /// Reads one bit; bits past the end of the stream read as zero, like the reference | ||
| /// decoder's zero-filled bit accumulator, so a stream that omits its trailing zero | ||
| /// entries still decodes. | ||
| fn read_bit(&mut self) -> bool { |
There was a problem hiding this comment.
[skeptical] Removed truncation detection makes corrupt streams decode as maximum-magnitude coefficients — low 🟡 — read_bit now returns zeros past EOF with no error path, so a truncated or corrupt stream is silently accepted: a pending value whose sign/magnitude bits fall past the end decodes as the positive maximum (the PR's own tests assert coefficients[0] == 15 and SIGN_POSITIVE), and cut-off run tail bits read as zeros. Impact is limited to rendered pixel data: coefficients are clamped i16, num_values is bounded by band counts, and there is no memory-safety or non-termination exposure. The limitation is documented in SrlDecoder::new and decode_upgrade_pass and is inherent to the zero-filled-reader design that fixes the Windows interop failure; recorded so the loss of the malformed-stream signal is a conscious acceptance.
Windows RFX Progressive upgrade streams may omit the final zero byte and trailing zero entries, or encode zero runs longer than the remaining coefficients. The decoder now accepts those streams without ending the session.
All bytes remain available until the requested coefficients are decoded. A final zero byte can contain sign or magnitude bits, so it is never stripped in advance. Zero-run events are consumed incrementally, and a pending nonzero coefficient is preserved even when its remaining bits come from the zero-filled reader at EOF. This matches FreeRDP's SRL reader; its optional trailing-byte skip happens after decoding.
The encoder is unchanged. Invalid magnitude widths still fail the upgrade pass without partially updating the tile. The decoder cannot distinguish omitted trailing entries from truncation, and the documentation now states that limitation.
Breaking changes
SrlError::MissingTerminatorandSrlError::Truncated.SrlDecoder::newreturnsSelfbecause constructing a decoder is infallible.Validation
ironrdp-graphicslibrary tests pass, including new zero-byte/EOF boundary and round-trip regressions with and without terminators.cargo clippy --locked -p ironrdp-graphics --all-targets -- -D warningsgit diff --checkpass.Part of the Windows interoperability series #2007–#2017.