test(core): add a rdpeusb (URBDRC) decode fuzz target - #1690
Conversation
3c3ca41 to
de8b296
Compare
de8b296 to
25e03e8
Compare
There was a problem hiding this comment.
Pull request overview
Adds fuzz coverage for attacker-controlled RDPEUSB client PDUs.
Changes:
- Adds an RDPEUSB decode oracle and fuzz target.
- Adds regression-test corpus integration.
- Adds required crate dependencies and lockfile updates.
Reviewed changes
Copilot reviewed 5 out of 8 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
fuzz/fuzz_targets/rdpeusb_decode.rs |
Invokes the new oracle. |
fuzz/Cargo.toml |
Registers the fuzz binary. |
fuzz/Cargo.lock |
Updates fuzz dependencies. |
crates/ironrdp-testsuite-core/tests/fuzz_regression.rs |
Adds regression replay test. |
crates/ironrdp-testsuite-core/test_data/fuzz_regression/rdpeusb_decode/seed-empty.bin |
Initializes the regression corpus. |
crates/ironrdp-fuzzing/src/oracles/mod.rs |
Adds RDPEUSB decoding oracle. |
crates/ironrdp-fuzzing/Cargo.toml |
Adds the RDPEUSB dependency. |
Cargo.lock |
Records the dependency edge. |
| use ironrdp_core::decode; | ||
| use ironrdp_rdpeusb::pdu::completion::ts_urb_result::Raw; | ||
| use ironrdp_rdpeusb::pdu::{UrbdrcClientControlPdu, UrbdrcClientDevicePdu}; | ||
|
|
||
| let _ = decode::<UrbdrcClientControlPdu>(data); | ||
| let _ = decode::<UrbdrcClientDevicePdu<Raw>>(data); |
There was a problem hiding this comment.
Good catch — you're right, and this is exactly the class macrdp hit historically. UrbdrcClientDevicePdu<Raw> only slurps each URB result body into Raw; the length-prefixed reinterpretation happens later in the stateful handlers via the pub(crate) into_expected, so <Raw> alone never reaches those decoders.
Fixed in the latest push: the oracle now also drives the public result decoders directly on the raw input — TsUrbSelectConfigResult, TsUrbSelectInterfaceResult, TsUrbGetCurrFrameNumResult, TsUrbIsochTransferResult (inherent decode), plus TsUsbdInterfaceInfoResult / TsUsbdPipeInfoResult (via Decode). Those are the length-prefixed / count-prefixed reads (TsUsbdInterfaceInfoResult's Length-slice, TsUrbSelectConfigResult's NumInterfaces loop, TsUrbIsochTransferResult's NumberOfPackets loop) that are the motivating attack surface. Driving the stateful completion path would also work but needs the request-ID correlation state, which doesn't fit a stateless byte-slice target — so I decode the payload types directly instead.
There was a problem hiding this comment.
Do we really need an empty seed here?
There was a problem hiding this comment.
It's needed for a mechanical reason: the check! macro does std::fs::read_dir(folder).unwrap(), so the corpus folder has to exist in-tree — and git won't track an empty directory, so it needs at least one file. seed-empty.bin is the existing convention for a target with no crash reproducer yet: egfx_round_trip, egfx_multi_frame, egfx_avc420_decode, bulk_decompress_ncrush, bulk_decompress_xcrush, bulk_round_trip, and pdu_round_trip all carry the same placeholder. Empty input is also a legitimate trivial case (decode-of-nothing must not panic). Happy to drop it and .gitkeep instead, or leave the whole regression check out until there's a real crash to replay — whichever you prefer.
There was a problem hiding this comment.
I would lean towards .gitkeep, thank you!
There was a problem hiding this comment.
Done — swapped seed-empty.bin → .gitkeep (the check! macro just needs the corpus dir to exist in-tree; .gitkeep reads as the empty-input case exactly the same, and check_rdpeusb_decode still passes). Also rebased onto master to clear the conflict with #1707 (its rdpeudp/rdpemt fuzz targets touched the same fuzz/Cargo.toml / ironrdp-fuzzing dep list — additive, kept both). PR is green + mergeable again; oracle still compiles against the rebased tree (incl. #1683's usb module).
25e03e8 to
f28e433
Compare
ironrdp-rdpeusb had no fuzz coverage despite decoding attacker-controlled client->server PDUs off two DVC channels — the device channel's URB / IO-control / interface-info result payloads are length-prefixed and must be bounds-checked before they're read. Add an rdpeusb_decode oracle, the fuzz target (auto-discovered by `cargo xtask fuzz list`), and the regression check + corpus folder. The oracle decodes the two top-level client PDU families (UrbdrcClientControlPdu, UrbdrcClientDevicePdu<Raw>) and also drives the length-prefixed URB / interface-info *result* decoders directly (TsUrbSelectConfigResult, TsUrbSelectInterfaceResult, TsUrbGetCurrFrameNumResult, TsUrbIsochTransferResult, TsUsbdInterfaceInfoResult, TsUsbdPipeInfoResult). Decoding UrbdrcClientDevicePdu<Raw> only slurps each URB result body into Raw — the operation-specific reinterpretation happens later in the stateful server handlers via into_expected, so <Raw> alone never reaches those decoders, which are the historical home of unchecked-read panics.
f28e433 to
bf0266c
Compare
04320a0
into
Devolutions:master
Summary
ironrdp-rdpeusbhas no fuzz coverage, even though it decodesattacker-controlled client→server PDUs off two DVC channels. The device
channel in particular carries URB completions, IO-control completions and
interface-info results whose length-prefixed payloads must be bounds-checked
before they're read — historically a good place for unchecked-read panics.
(A downstream server built on
ironrdp-server, macrdp,hit exactly that class of panic in these decoders in the past, which is what
motivated adding coverage here.)
This adds a fuzz target for the two top-level client PDU families a server
decodes off the wire.
What's added
oracles::rdpeusb_decode— decodes the two client→server entrypoints via
ironrdp_core::decode:UrbdrcClientControlPdu(main-channel family)UrbdrcClientDevicePdu<Raw>(per-device family — recursively exercises theURB / IO-control / interface-info result payloads)
fuzz/fuzz_targets/rdpeusb_decode.rs(auto-discovered bycargo xtask fuzz list, so it joins the CI matrix with no further wiring).check_rdpeusb_decodeinfuzz_regression.rs, plus therdpeusb_decode/corpus folder, so any crash found becomes a permanentreplay test.
Verification
cargo build -p ironrdp-fuzzing— the oracle compiles against the real API.cargo test -p ironrdp-testsuite-core check_rdpeusb_decode— passes.cargo fmt --all -- --check(stable + nightly) clean;cargo clippy -p ironrdp-fuzzingclean.cargo xtask fuzz listlistsrdpeusb_decode.Purely additive (a new oracle, target, regression check + the
ironrdp-rdpeusbdev-dependency of
ironrdp-fuzzing); no existing code changes. Complements thein-flight rdpeusb server work by giving the crate's decode surface ongoing
fuzzing.