diff --git a/Cargo.lock b/Cargo.lock index 209e53361..1d16f0193 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2669,6 +2669,7 @@ dependencies = [ "ironrdp-cliprdr-format", "ironrdp-core", "ironrdp-displaycontrol", + "ironrdp-dvc", "ironrdp-egfx", "ironrdp-graphics", "ironrdp-pdu", diff --git a/crates/ironrdp-fuzzing/Cargo.toml b/crates/ironrdp-fuzzing/Cargo.toml index 437233517..9985c7571 100644 --- a/crates/ironrdp-fuzzing/Cargo.toml +++ b/crates/ironrdp-fuzzing/Cargo.toml @@ -20,7 +20,8 @@ ironrdp-rdpdr.path = "../ironrdp-rdpdr" ironrdp-rdpsnd.path = "../ironrdp-rdpsnd" ironrdp-cliprdr-format.path = "../ironrdp-cliprdr-format" ironrdp-displaycontrol.path = "../ironrdp-displaycontrol" -ironrdp-egfx.path = "../ironrdp-egfx" +ironrdp-dvc.path = "../ironrdp-dvc" +ironrdp-egfx = { path = "../ironrdp-egfx", features = ["arbitrary"] } ironrdp-svc.path = "../ironrdp-svc" [lints] diff --git a/crates/ironrdp-fuzzing/src/oracles/mod.rs b/crates/ironrdp-fuzzing/src/oracles/mod.rs index 35891c895..dc8deeb6e 100644 --- a/crates/ironrdp-fuzzing/src/oracles/mod.rs +++ b/crates/ironrdp-fuzzing/src/oracles/mod.rs @@ -357,6 +357,88 @@ pub fn egfx_round_trip(data: &[u8]) { pdu_round_trip_one!(data, Avc444BitmapStream<'_>); } +/// Multi-frame oracle for the EGFX graphics pipeline client. +/// +/// H.264 decoding maintains reference-picture state, SPS/PPS context, and +/// decoder configuration across frames; surface caching and codec dispatch +/// state in egfx all carry forward across PDUs. Single-shot fuzzers cannot +/// reach frame-to-frame state corruption because they construct a fresh +/// decoder per iteration. This oracle constructs ONE `GraphicsPipelineClient` +/// at iteration start and drives a sequence of `GfxPdu`s through it, exposing +/// cross-PDU state to the fuzzer. +/// +/// Harness shape: `Arbitrary`-derived `Vec` (each variant `Arbitrary` +/// via the cascade in PR #1334). Each PDU is encoded back to wire bytes, +/// wrapped in a single uncompressed ZGFX segment, and fed to the client's +/// public `DvcProcessor::process` entry point. This exercises the same path +/// production traffic takes: ZGFX decompress -> `GfxPdu` decode -> dispatch +/// to per-variant handler -> state machine + surface cache update. +/// +/// What this catches: panics or sanitizer reports along the dispatch + state +/// machine path when fed adversarially-ordered or malformed-payload PDUs; +/// inconsistent surface-cache state under attacker-controlled +/// CreateSurface / DeleteSurface / Map* orderings; corrupted frame-id state +/// from interleaved StartFrame / EndFrame / FrameAcknowledge sequences; +/// ZGFX-wrapper integration bugs separate from the standalone ZGFX coverage +/// in `egfx_zgfx_decompress`. +/// +/// What this does NOT catch: cross-frame H.264 decoder state corruption. +/// The client is constructed with `h264_decoder: None`, so H264-bearing +/// PDUs (WireToSurface1 with AVC codecs) don't reach the H.264 decoder. +/// The standalone `egfx_avc420_decode` and `egfx_avc444_decode` targets +/// cover the H.264 wrapper. Wiring a real (or mock) H.264 decoder into +/// this harness can be a follow-up if frame-to-frame H.264 state coverage +/// surfaces as a gap. +pub fn egfx_multi_frame(data: &[u8]) { + use arbitrary::{Arbitrary as _, Unstructured}; + use ironrdp_core::encode_vec; + use ironrdp_dvc::DvcProcessor as _; + use ironrdp_egfx::client::{GraphicsPipelineClient, GraphicsPipelineHandler}; + use ironrdp_egfx::pdu::GfxPdu; + use ironrdp_graphics::zgfx::wrap_uncompressed; + + /// No-op handler. Every callback default-impls in the trait, so the empty + /// struct gets all defaults for free. The handler exists to satisfy + /// `GraphicsPipelineClient::new`'s API; the fuzz oracle does not inspect + /// any of the dispatched events. + struct NoOpHandler; + impl GraphicsPipelineHandler for NoOpHandler {} + + let mut unstructured = Unstructured::new(data); + let Ok(pdus) = Vec::::arbitrary(&mut unstructured) else { + return; + }; + + let mut client = GraphicsPipelineClient::new(Box::new(NoOpHandler), None); + + // Initialise the channel state by invoking the DvcProcessor::start entry. + // The returned advertise message is discarded; the call's side effect is + // putting the client's internal state machine into its post-start state. + const FUZZ_CHANNEL_ID: u32 = 0; + let _ = client.start(FUZZ_CHANNEL_ID); + + for pdu in pdus { + // Encode each PDU back to wire bytes so the client processes through + // the same decode + dispatch path real traffic takes. Skip PDUs whose + // encoder rejects the Arbitrary-generated values rather than aborting + // the iteration; the next PDU may still exercise interesting state. + let Ok(pdu_bytes) = encode_vec(&pdu) else { + continue; + }; + + // Wrap the encoded PDU in an uncompressed ZGFX segment so the client's + // ZGFX decompressor produces the PDU bytes unmodified. This bypasses + // the ZGFX decoder layer (covered separately by egfx_zgfx_decompress) + // and concentrates fuzz pressure on the dispatch + state machine. + let payload = wrap_uncompressed(&pdu_bytes); + + // Errors and panics propagate to libFuzzer naturally; we discard the + // Result since the oracle's job is to surface bugs, not to enforce + // dispatcher semantics. + let _ = client.process(FUZZ_CHANNEL_ID, &payload); + } +} + pub fn rle_decompress_bitmap(input: BitmapInput<'_>) { let mut out = Vec::new(); diff --git a/crates/ironrdp-testsuite-core/test_data/fuzz_regression/egfx_multi_frame/seed-empty.bin b/crates/ironrdp-testsuite-core/test_data/fuzz_regression/egfx_multi_frame/seed-empty.bin new file mode 100644 index 000000000..e69de29bb diff --git a/crates/ironrdp-testsuite-core/tests/fuzz_regression.rs b/crates/ironrdp-testsuite-core/tests/fuzz_regression.rs index becbe6ab9..c1c4a50d5 100644 --- a/crates/ironrdp-testsuite-core/tests/fuzz_regression.rs +++ b/crates/ironrdp-testsuite-core/tests/fuzz_regression.rs @@ -56,3 +56,8 @@ fn check_pdu_round_trip() { fn check_egfx_round_trip() { check!(egfx_round_trip); } + +#[test] +fn check_egfx_multi_frame() { + check!(egfx_multi_frame); +} diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 1780d536f..62d91ed3f 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -78,6 +78,9 @@ name = "bitflags" version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a" +dependencies = [ + "arbitrary", +] [[package]] name = "bitvec" @@ -340,6 +343,7 @@ dependencies = [ name = "ironrdp-egfx" version = "0.2.0" dependencies = [ + "arbitrary", "bit_field", "bitflags", "ironrdp-core", @@ -371,6 +375,7 @@ dependencies = [ "ironrdp-cliprdr-format", "ironrdp-core", "ironrdp-displaycontrol", + "ironrdp-dvc", "ironrdp-egfx", "ironrdp-graphics", "ironrdp-pdu", @@ -398,6 +403,7 @@ dependencies = [ name = "ironrdp-pdu" version = "0.8.0" dependencies = [ + "arbitrary", "bit_field", "bitflags", "byteorder", diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 74ba4c0f6..e3f0a48f6 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -97,3 +97,10 @@ test = false doc = false bench = false +[[bin]] +name = "egfx_multi_frame" +path = "fuzz_targets/egfx_multi_frame.rs" +test = false +doc = false +bench = false + diff --git a/fuzz/fuzz_targets/egfx_multi_frame.rs b/fuzz/fuzz_targets/egfx_multi_frame.rs new file mode 100644 index 000000000..dbb94e4d0 --- /dev/null +++ b/fuzz/fuzz_targets/egfx_multi_frame.rs @@ -0,0 +1,7 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + ironrdp_fuzzing::oracles::egfx_multi_frame(data); +});