test(fuzz): add pdu_round_trip oracle and target#1291
Conversation
Adds a fuzz oracle that exercises decode -> encode_vec -> re-decode for each PDU type currently covered by pdu_decode, plus a dedicated pdu_round_trip target binary (auto-discovers into CI per Devolutions#1290). The property tested is *no internal panic from inside the encoder or decoder when fed a decoder-accepted input through both directions of the round-trip*. Asymmetric Err returns (decoder accepts something the encoder reports as "Encoding not implemented", or vice-versa) are not in scope: those are tolerated incomplete-impl cases tracked separately. The oracle catches: - unreachable!() reached during encoding of a valid decoded state - Integer overflow / index-out-of-bounds inside the encoder on decoder-accepted inputs - Panics in the decoder when fed encoder-produced bytes (re-decode path) Initial type coverage mirrors pdu_decode minus two known-asymmetric types that hit `unreachable!()` in the encoder: - `capability_sets::CapabilitySet`: encoder match at crates/ironrdp-pdu/src/rdp/capability_sets/mod.rs:447 reaches `unreachable!()` on a variant the decoder accepts but the encoder's match arms don't cover. Smoke-fuzz reproducer: `[6, 0, 4, 0]`. - `headers::ShareControlHeader`: transits through CapabilitySet's encoder for DemandActive/ConfirmActive PDU contents and hits the same internal panic. Both are tracked as follow-up filings; the oracle re-includes them once the encoder's match arms are completed. Verification: - cargo xtask check fmt/lints/typos/locks/tests all green - check_pdu_round_trip regression-replay test passes - 60-second smoke fuzz: 2,220,481 runs, no crashes on the current type set Refs Devolutions#1120 (umbrella) and Devolutions#1124 (oracle infrastructure).
|
Filed #1292 for the underlying |
There was a problem hiding this comment.
Pull request overview
Adds a new fuzzing oracle and target to exercise PDU decode/encode round-trips, with a companion regression-replay test to ensure minimized inputs remain covered in CI. This extends the existing fuzz oracle suite in ironrdp-fuzzing and integrates cleanly with the fuzz target discovery CI approach referenced in #1290.
Changes:
- Add
pdu_round_triporacle that performsdecode -> encode_vec -> re-decode, droppingErrresults but surfacing internal panics. - Add a dedicated
pdu_round_triplibFuzzer target and register it infuzz/Cargo.toml. - Add regression-replay coverage via
check_pdu_round_tripplus an initial (empty) seed corpus file.
Reviewed changes
Copilot reviewed 4 out of 5 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
crates/ironrdp-fuzzing/src/oracles/mod.rs |
Adds the pdu_round_trip oracle and helper macro, mirroring the pdu_decode type list with documented exclusions. |
fuzz/fuzz_targets/pdu_round_trip.rs |
New fuzz target wrapper calling the oracle on raw input bytes. |
fuzz/Cargo.toml |
Registers the new fuzz target as a [[bin]] so it’s discoverable/buildable. |
crates/ironrdp-testsuite-core/tests/fuzz_regression.rs |
Adds a regression-replay test entry for the new oracle. |
crates/ironrdp-testsuite-core/test_data/fuzz_regression/pdu_round_trip/seed-empty.bin |
Seeds the regression folder so the replay test has at least one input to run. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
There's real overlap.
The type lists also differ slightly: This matches the existing workspace pattern too: |
610cfd0
into
Devolutions:master
Closes Devolutions#1124 task 2 ("message_decoding_invariants"). New assertion-based oracle that complements `pdu_round_trip` (Devolutions#1291, task 1) by asserting the `Encode::size()` soundness contract on every decoder-accepted input: `pdu.size() == encode_vec(&pdu).len()`. The Encode trait's `size()` is the contract caller code relies on for buffer sizing (`ensure_size!`, `cast_length!`, downstream allocations). A size that lies about its own length produces under-allocation (truncated encode) or over-allocation (wasted memory + buffer-overflow risk depending on surrounding context). The workspace already asserts this property against known-good byte fixtures in `ironrdp-testsuite-core/tests/pdu/rdp.rs::buffer_length_is_correct_for_*`; this oracle extends the assertion across the fuzz input surface. Distinct from `pdu_round_trip`: - `pdu_round_trip` silently drops `Err` and catches panics only. - `message_decoding_invariants` ASSERTS on the size contract, so a violation aborts the fuzz iteration as a libFuzzer crash (the bug reporting mechanism per the oracle module doc). What this catches (that `pdu_round_trip` does not): - `Encode::size()` lies about its own size (returns N but `encode` writes M ≠ N), causing buffer over/under-allocation in callers. - Decode-acceptable bytes that the encoder cannot reconstruct to the same length (lossy decode that loses framing structure). What this does NOT catch (covered by other oracles): - Decode-time panics or OOM (covered by `pdu_decode`). - Re-decode equality after round-trip (covered by `pdu_round_trip`'s silent-drop pattern; assertion-based re-decode is intentionally out of scope here to keep the oracle's failure mode unambiguous). Type coverage mirrors `pdu_round_trip` exactly. This includes `capability_sets::CapabilitySet` and `headers::ShareControlHeader` (the latter transits through `CapabilitySet`'s encoder via the Active variants). Both currently hit the `BitmapCacheV3` encoder `unreachable!()` panic at `capability_sets/mod.rs:447` per issue Devolutions#1292. **Merge dependency: this PR depends on Devolutions#1313** (the fix for Devolutions#1292) landing first. Until Devolutions#1313 merges, smoke fuzz on this oracle reproduces Devolutions#1292 in under 1 second on input `[6, 0, 6, 0, 0, 0]` (a longer variant of Devolutions#1292's original 4-byte reproducer). After Devolutions#1313 lands, the panic is gone and this oracle covers the full type cohort cleanly. New target auto-discovers into CI via `cargo xtask fuzz list` per PR Devolutions#1290's dynamic CI fan-out. Test plan: `cargo xtask check fmt/lints/tests/typos/locks` all pass; new `check_message_decoding_invariants` regression-replay test in `crates/ironrdp-testsuite-core/tests/fuzz_regression.rs` runs and passes against the seed corpus. Smoke fuzz with the two Devolutions#1313-blocked types excluded was clean over 6,667,709 iterations in 121 seconds at ~55K exec/s sustained, indicating no additional size-contract violations beyond the known Devolutions#1313-blocked panic in the covered type cohort. Refs Devolutions#1120 (umbrella), closes Devolutions#1124 task 2.
The inner match in `CapabilitySet::Encode::encode`'s raw-buffer fallback (`mod.rs:448`) used a `_ => unreachable!()` catch-all. Replace it with an explicit enumeration of the 17 structured `CapabilitySet` variants already handled by the outer match's specific arms, so adding a new variant to `CapabilitySet` is a compile error here until the variant is routed in this `Encode` impl. The arm body stays `unreachable!()` because it is genuinely unreachable under the current outer-match structure. PR Devolutions#1313 (BitmapCacheV3 encoder `unreachable!()` reached on decoder-accepted input) demonstrated why a runtime catch-all is the wrong shape for this match. The nested `match guid { ... _ => unreachable!() }` in `CodecProperty` decode for `GUID_REMOTEFX | GUID_IMAGE_REMOTEFX` (`bitmap_codecs/mod.rs`) keeps its structure. Per review, `unreachable!()` is the right tool there: `guid` is already validated by the outer arm, so the `_` branch is a redundant correctness check that fires under tests and fuzzing if a future change breaks the invariant. The only change is a message documenting that invariant inline. This is part of the audit pass on issue Devolutions#1314. Both sites are class (a) (provably unreachable in current code), confirmed by `pdu_round_trip` (Devolutions#1291) and `message_decoding_invariants` (Devolutions#1320) exercising `CapabilitySet` and `ShareControlHeader` for millions of iterations post-Devolutions#1313 with no panics. Refs Devolutions#1314.
Summary
Adds a fuzz oracle and dedicated target for round-trip exercise of PDU
encode/decode pathways. For each PDU type currently covered by
pdu_decode, the oracle attemptsdecode->encode_vec-> re-decodeon the encoded bytes. Auto-discovers into CI per #1290.
Property tested
No internal panic from inside the encoder or decoder when fed a
decoder-accepted input through both directions of the round-trip.
Asymmetric
Errreturns (decoder accepts something the encoder reportsas
"Encoding not implemented", or vice-versa) are NOT in scope here:those are tolerated incomplete-impl cases tracked separately. The
oracle silently drops
Errresults from any of the three stages.What this catches:
unreachable!()reached during encoding of a valid decoded state(i.e. the encoder's match arms are missing a variant the decoder
produces).
decoder-accepted inputs.
path).
Initial type coverage
Mirrors
pdu_decode's type list minus two exclusions documented inlinewith their reproducers. As new PDU types gain
Encodeimpls, theyextend coverage when added to the macro list.
Excluded pending follow-up
Two types are temporarily excluded because they hit
unreachable!()inthe encoder during smoke fuzzing, which is an internal-panic bug that
the oracle cannot silently drop:
capability_sets::CapabilitySet: the encoder's match atcrates/ironrdp-pdu/src/rdp/capability_sets/mod.rs:447reachesunreachable!()on a variant the decoder accepts but the encoder'smatch arms don't cover. Smoke-fuzz reproducer:
[6, 0, 4, 0].headers::ShareControlHeader: transits throughCapabilitySet'sencoder for
DemandActive/ConfirmActivePDU contents and hitsthe same internal panic.
Both inclusions return once the encoder's match arms are completed. I
will file the underlying bug as a separate issue and link it back here.
Files
crates/ironrdp-fuzzing/src/oracles/mod.rs: newpdu_round_tripfunction and
pdu_round_trip_one!helper macro.fuzz/fuzz_targets/pdu_round_trip.rs: new fuzz target binary.fuzz/Cargo.toml:[[bin]]entry for the new target.crates/ironrdp-testsuite-core/tests/fuzz_regression.rs: newcheck_pdu_round_tripregression-replay test.crates/ironrdp-testsuite-core/test_data/fuzz_regression/pdu_round_trip/seed-empty.bin:empty seed file for the regression-replay test.
Verification
cargo xtask check fmt -vgreencargo xtask check lints -vgreencargo xtask check typos -vgreencargo xtask check locks -vgreencargo xtask check tests -vgreen (includingcheck_pdu_round_trip)type set
cargo xtask fuzz list(per ci(fuzz): discover fuzz targets dynamically #1290)Refs #1120 (umbrella) and #1124 (oracle infrastructure).