Repository navigation
feat(pdu,graphics,egfx): add ClearCodec bitmap compression codec - #1174
Benoît Cortier (CBenoit) merged 1 commit into
Conversation
Wire ClearCodec into the EGFX client's WireToSurface1 codec dispatch, following the same pattern as AVC420 and Uncompressed decode. - Add ClearCodecDecoder field (always enabled, no external codec needed) - Decode ClearCodec bitmap data and convert BGRA output to RGBA - Reset decoder caches on ResetGraphics (V-bar + glyph state) - Add 3 integration tests: basic decode, RGBA output, reset survival - Add unit test for BGRA-to-RGBA channel reordering Depends on the ClearCodec codec PR (Devolutions#1174).
There was a problem hiding this comment.
Pull request overview
Adds ClearCodec (MS-RDPEGFX 2.2.4.1) support across the wire-format layer (ironrdp-pdu), codec logic (ironrdp-graphics), and EGFX server API (ironrdp-egfx), with extensive new tests in ironrdp-testsuite-core.
Changes:
- Introduces
ironrdp-pdu::codecs::clearcodecwith residual/bands/subcodec parsing and helpers. - Adds
ironrdp-graphics::clearcodecencoder/decoder plus persistent V-bar and glyph caching. - Extends
GraphicsPipelineServerwithsend_clearcodec_frame()and adds ClearCodec integration tests.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| crates/ironrdp-testsuite-core/tests/graphics/mod.rs | Registers the new ClearCodec integration test module. |
| crates/ironrdp-testsuite-core/tests/graphics/clearcodec.rs | Adds ClearCodec end-to-end and adversarial integration tests. |
| crates/ironrdp-pdu/src/codecs/mod.rs | Exposes the new ClearCodec codec module. |
| crates/ironrdp-pdu/src/codecs/clearcodec/mod.rs | Defines ClearCodec bitmap stream + composite payload parsing and exports layer decoders. |
| crates/ironrdp-pdu/src/codecs/clearcodec/residual.rs | Implements residual (BGR RLE) encode/decode. |
| crates/ironrdp-pdu/src/codecs/clearcodec/bands.rs | Implements bands layer decoding (V-bar references + inline data). |
| crates/ironrdp-pdu/src/codecs/clearcodec/subcodec.rs | Implements subcodec layer parsing (raw/NSCodec/RLEX region headers). |
| crates/ironrdp-pdu/src/codecs/clearcodec/rlex.rs | Implements RLEX subcodec parsing. |
| crates/ironrdp-graphics/src/lib.rs | Exposes the new clearcodec module. |
| crates/ironrdp-graphics/src/clearcodec/mod.rs | Implements ClearCodec decoder/encoder and compositing across layers. |
| crates/ironrdp-graphics/src/clearcodec/glyph_cache.rs | Adds glyph cache backing storage used by encoder/decoder. |
| crates/ironrdp-graphics/src/clearcodec/vbar_cache.rs | Adds V-bar/short V-bar cache backing storage used by decoder. |
| crates/ironrdp-egfx/src/server.rs | Adds send_clearcodec_frame() EGFX server API entry point. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
…items Address review feedback on Devolutions#1174: - Validate subcodec region bounds against surface dimensions - Validate RLEX palette indices and enforce region pixel budget - Remove silent pixel skipping that caused coordinate desync - Add glyph cache dimension mismatch check on hit - Use checked arithmetic for raw subcodec data length - Remove unused expected_seq field from ClearCodecDecoder - Use saturating_mul in encoder for pixel count - Add missing QoE backpressure recording for ClearCodec frames - Document vbar_cache wrap constant derivation
…items Address review feedback on Devolutions#1174: - Validate subcodec region bounds against surface dimensions - Validate RLEX palette indices and enforce region pixel budget - Remove silent pixel skipping that caused coordinate desync - Add glyph cache dimension mismatch check on hit - Use checked arithmetic for raw subcodec data length - Remove unused expected_seq field from ClearCodecDecoder - Use saturating_mul in encoder for pixel count - Add missing QoE backpressure recording for ClearCodec frames - Document vbar_cache wrap constant derivation
012af8b to
8605ed9
Compare
Wire ClearCodec into the EGFX client's WireToSurface1 codec dispatch, following the same pattern as AVC420 and Uncompressed decode. - Add ClearCodecDecoder field (always enabled, no external codec needed) - Decode ClearCodec bitmap data and convert BGRA output to RGBA - Reset decoder caches on ResetGraphics (V-bar + glyph state) - Add 3 integration tests: basic decode, RGBA output, reset survival - Add unit test for BGRA-to-RGBA channel reordering Depends on the ClearCodec codec PR (Devolutions#1174).
8605ed9 to
7113058
Compare
…items Address review feedback on Devolutions#1174: - Validate subcodec region bounds against surface dimensions - Validate RLEX palette indices and enforce region pixel budget - Remove silent pixel skipping that caused coordinate desync - Add glyph cache dimension mismatch check on hit - Use checked arithmetic for raw subcodec data length - Remove unused expected_seq field from ClearCodecDecoder - Use saturating_mul in encoder for pixel count - Add missing QoE backpressure recording for ClearCodec frames - Document vbar_cache wrap constant derivation
Wire ClearCodec into the EGFX client's WireToSurface1 codec dispatch, following the same pattern as AVC420 and Uncompressed decode. - Add ClearCodecDecoder field (always enabled, no external codec needed) - Decode ClearCodec bitmap data and convert BGRA output to RGBA - Reset decoder caches on ResetGraphics (V-bar + glyph state) - Add 3 integration tests: basic decode, RGBA output, reset survival - Add unit test for BGRA-to-RGBA channel reordering Depends on the ClearCodec codec PR (Devolutions#1174).
973f325 to
662548b
Compare
|
Reviewing against Haven's downstream ClearCodec decoder — your factorisation (separate Note that the Haven would drop our |
|
Thanks for the careful review against Haven's downstream implementation, that is exactly the kind of independent confirmation that helps these PRs land. The factoring into separate Cross-linking your RDPGFX_RECT16 finding back to #1175 where the actual fix lives, and confirming this PR's decoder is unaffected since it takes raw |
662548b to
43d1f14
Compare
43d1f14 to
6e142c3
Compare
|
Force-pushed Bugs fixed1. Integer overflow panic in 2. Adversarial-dimensions OOM in 3. Alpha-channel contract was undocumented on Spec audit (MS-RDPEGFX 2.2.4.1)While preparing the fixes I also re-read MS-RDPEGFX 2.2.4.1 against the implementation, using FreeRDP 3.15.0's IronRDP applies these length checks incrementally per-read; another implementation pattern is to apply them upfront in a single shot per layer entry-point. Both styles satisfy the spec-required bounded-read outcome; the spec does not require either over the other. How these were surfacedFor transparency: the three bugs were surfaced by local fuzz oracles (decode + encode/decode round-trip on the codec's public API) before maintainer review. The fuzz infrastructure is on a separate branch and will follow as a small standalone PR after this one merges; the codec's public API is already fuzz-friendly so no prep work is needed in this PR. Verification post-amend: |
6e142c3 to
38f2f31
Compare
|
Greg Lamberson (@glamberson) Thank you very much. Just confirming with you: can I go ahead and merge this PR, or any prerequisite I’m missing? |
Add ClearCodec (MS-RDPEGFX 2.2.4.1) codec support across two crates: ironrdp-pdu: Wire-format decode/encode for all ClearCodec layers (residual BGR RLE, bands with V-bar caching, subcodec dispatch including RLEX palette-indexed RLE). Full round-trip test coverage. ironrdp-graphics: ClearCodecDecoder with persistent V-bar and glyph caches. ClearCodecEncoder with residual-only encoding and glyph deduplication for server-side bitmap compression. Hardening pass before maintainer review (local fuzz oracles surfaced three bug classes that escaped the initial review): - Per-dimension cap at 8192 on decode, replacing the previous per- pixel-count cap at 8192*8192. The pixel-count form accepted degenerate aspect ratios (e.g. 63961x771 = 49M pixels, under the 67M cap) that allocated ~197MB from a few attacker-controlled bytes. The per-dimension form rejects implausible tile shapes regardless of total area while preserving headroom for 8K displays (7680x4320). MS-RDPEGFX caps surfaces at 32767x32767 but does not mandate a separate tile cap; 8192 is a defensive choice that fits realistic tile sizes. - Fix integer overflow at bands::decode_band on `usize::from(x_end - x_start + 1)`. When x_end = u16::MAX and x_start = 0, the `+ 1` overflowed the u16 arithmetic before the usize cast and triggered a release-mode panic under overflow-checks. The existing `if x_end < x_start` guard prevents underflow but not the +1 overflow. Fix is to cast to usize first: `usize::from(x_end - x_start) + 1`. - Document the alpha contract on ClearCodecEncoder::encode and ClearCodecDecoder::decode. ClearCodec is lossless on the three color channels (B, G, R) per spec; the wire format does not transmit alpha. The encoder reads B/G/R from each input pixel and discards alpha; the decoder fills alpha with 0xFF unconditionally. Callers needing alpha must transport it separately. Previously the doc-comment claim of "pixel-perfect fidelity" was ambiguous; alpha-channel mismatches in any round-trip with non-0xFF alpha inputs are by design, not bugs. Local fuzz infrastructure (clearcodec_decode + clearcodec_round_trip oracles) follows in a separate PR once this lands; the codec's public API is already fuzz-friendly so no prep is needed here. A separate spec-driven audit of input-validation completeness (per-layer stream-length pre-checks in subcodec / residual / bands) is queued as follow-up work; deferring from this PR pending review of MS-RDPEGFX 2.2.4.1. Closes Devolutions#1158 ClearCodec subtask.
38f2f31 to
59b3de3
Compare
|
Benoît Cortier (@CBenoit) Yes, no prerequisite blocks merge. Rebasing onto current master ( |
Benoît Cortier (CBenoit)
left a comment
There was a problem hiding this comment.
Thank you! Let’s merge
059ca90
into
Devolutions:master
Document evidence-based dispositions from a targeted re-scan of master..upstream/master (139 commits, upstream HEAD 069786c): - IMPORTED Devolutions#1236 (connector: name actual PDU on unexpected Share Control PDU); gates green (workspace 1348/0, release client build, clippy clean). - AVC444 decode and ARC reconnect-cookie: no upstream source exists (client.rs:720 stubs AVC444; rdp.rs:774 TODOs the cookie) — both stay DEFERRED/from-scratch. Corrected the reconnect-cookie note: the SecurityVerifier is HMAC-MD5(ArcRandomBits, ClientRandom), not a copy; gate any future impl behind an opt-in --auto-reconnect flag. - dirty-rect stays from-scratch/verify-gated (fork's own app.rs/rdp.rs). - gap-4 autodetect: importable part already in the fork; Devolutions#1178 handles a different framing and not the handover re-advertise blocker (no upstream fix) — SKIPPED. - Devolutions#1395 already present in the fork (progressive.rs:835). Devolutions#1132/Devolutions#1174/Devolutions#1305 skipped with reasons. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire ClearCodec decoder into the EGFX client WireToSurface1 codec dispatch. A persistent decoder with V-bar and glyph caches is stored on GraphicsPipelineClient. BGRA output is converted to RGBA for the uniform BitmapUpdate format. ResetGraphics deliberately does not reset the decoder. Per MS-RDPEGFX 3.3.5.14 it only resizes the Graphics Output Buffer; cache lifetime is driven by the stream through CLEARCODEC_FLAG_CACHE_RESET (2.2.4.1), which the decoder already honors by resetting the V-bar cursors. Dropping the decoder there would also drop the glyph cache and fail a legitimate post-reset GLYPH_HIT. Two decoder corrections from review land in code introduced by Devolutions#1174, since the defects are there rather than in the dispatch: - ClearCodecDecoder::decode required a glyph hit's destination dimensions to match the cached entry exactly. Per MS-RDPEGFX 4.1.1.5 a glyph is a dimensionless linear pixel stream, so any destination rectangle of the same area is valid. The check now compares pixel count. - The RLEX segment decoder clamped an out-of-range suite with saturating_sub, rendering a shorter suite instead of rejecting the stream. Per MS-RDPEGFX 2.2.4.1.1.3.1.1.2 startIndex is stopIndex - suiteDepth and must itself be a palette entry, so it now returns a decode error. Includes RLEX stop_index bounds validation against palette count and client-side decode tests in ironrdp-testsuite-core.
Follow-up to #1174. Supersedes #1195 (the standalone server-helper PR; its 46-line `send_clearcodec_frame()` is included here). Wires ClearCodec into the EGFX client's WireToSurface1 codec dispatch, matching the existing AVC420 and Uncompressed decode patterns. Changes: - ClearCodecDecoder field on GraphicsPipelineClient (always enabled, pure Rust, no external codec dependency). Constructed eagerly in GraphicsPipelineClient::new(), so V-bar (32K full + 16K short entries) and glyph (4000 entries) ring buffers are allocated up front regardless of whether ClearCodec frames are subsequently received. - Codec1Type::ClearCodec arm decodes bitmap data and converts BGRA output to RGBA for the uniform BitmapUpdate pixel format - ResetGraphics does NOT reset the ClearCodec decoder. Per MS-RDPEGFX 3.3.5.14 it only resizes the Graphics Output Buffer; cache lifetime is driven by the stream through CLEARCODEC_FLAG_CACHE_RESET (2.2.4.1), which the decoder already honors by resetting the V-bar cursors. Dropping the decoder there would also drop the glyph cache and break legitimate post-reset GLYPH_HITs. - GraphicsPipelineServer::send_clearcodec_frame() helper for symmetric server-side use (mirrors send_avc420_frame / send_avc444_frame pattern, including QoE backpressure recording) - 4 integration tests (basic decode, RGBA output verification, glyph-cache survival across ResetGraphics, glyph-hit reshape) - 1 unit test for BGRA-to-RGBA channel reordering Two decoder corrections from review. Both land in code introduced by #1174, since the defects are there rather than in the dispatch: - ClearCodecDecoder::decode required a glyph hit's destination dimensions to match the cached entry exactly. Per MS-RDPEGFX 4.1.1.5 a glyph is a dimensionless linear pixel stream, so any destination rectangle of the same area is valid (cached 2x8, hit 4x4). The check now compares pixel count. - The RLEX segment decoder clamped an out-of-range suite with saturating_sub, rendering a shorter suite instead of rejecting the stream. Per MS-RDPEGFX 2.2.4.1.1.3.1.1.2 startIndex is stopIndex - suiteDepth and must itself be a palette entry, so it now returns a decode error. Unlike H.264 which requires an external decoder via the H264Decoder trait, ClearCodec is pure Rust (ironrdp-graphics). Part of #1464.
Part of the multi-codec EGFX implementation described in #1158 (Section 3).
Summary
Add ClearCodec (MS-RDPEGFX 2.2.4.1), the mandatory lossless bitmap codec
for all EGFX versions (V8-V10.7). ClearCodec uses three-layer compositing
(residual BGR RLE, bands with V-bar caching, subcodec regions) to encode
text, UI elements, and icons with pixel-perfect fidelity.
This is the first ClearCodec encoder in any open-source RDP implementation.
FreeRDP has client-side decode only.
What it adds
ironrdp-pdu (wire format):
RLE decode/encode, bands with V-bar cache references (full hit, short
hit, short miss), RLEX subcodec, raw/NSCodec dispatch
ironrdp-graphics (codec logic):
ClearCodecEncoder with residual-only encoding and glyph deduplication
ironrdp-egfx (server integration):
ironrdp-testsuite-core:
adversarial input handling (max run_length, OOM dimensions, malformed
streams, uncached glyph hits), bands layer compositing through the full
decode pipeline, cache state management across multi-frame sessions,
compression quality assertions
Security hardening
Three rounds of code review against FreeRDP ClearCodec CVEs:
(MS-RDPEGFX 2.2.4.1.1.2.1.1.3)
NSCodec
The NSCodec subcodec (Layer 3 option) is not implemented in this PR.
The encoder avoids generating NSCodec tiles, and the decoder stub leaves
NSCodec regions at their residual-layer values. This can be added later
without API changes.
Test plan