Skip to content

feat(fuzz): add egfx_avc444_decode oracle and target#1327

Open
Greg Lamberson (glamberson) wants to merge 2 commits into
Devolutions:masterfrom
lamco-admin:feat/egfx-avc444-decode-fuzz
Open

feat(fuzz): add egfx_avc444_decode oracle and target#1327
Greg Lamberson (glamberson) wants to merge 2 commits into
Devolutions:masterfrom
lamco-admin:feat/egfx-avc444-decode-fuzz

Conversation

@glamberson
Copy link
Copy Markdown
Contributor

Sibling of egfx_avc420_decode (PR #1326), applied to the AVC444 wire
shape. AVC444 wraps two Avc420BitmapStreams plus a 2-bit encoding tag
per MS-RDPEGFX 2.2.4.4 (LUMA_AND_CHROMA, LUMA, or CHROMA). The wrapper
layer for each sub-stream is the same avc_to_annex_b function that
#1326 lifted to a public free function; this PR adds an oracle that
exercises the wrapper on both sub-streams.

This PR is stacked on PR #1326 (which introduced
avc_to_annex_b as a public function in ironrdp_egfx::pdu). It
depends on #1326 landing first. CI on this branch will not be green
against Devolutions/IronRDP:master until #1326 merges; that is the
correct mechanical signal of the dependency. After #1326 merges, this
PR rebases cleanly because the only commit unique to this branch is
the AVC444 oracle scaffolding.

The new oracle exercises three paths on every fuzz call:

  • Direct path: the oracle calls avc_to_annex_b(data) on the raw fuzz
    input. This exercises the wrapper on arbitrary byte distributions,
    including inputs that do not parse as Avc444BitmapStream.
  • Decode-chain path 1: the oracle tries Avc444BitmapStream::decode(data);
    on success it calls avc_to_annex_b(stream.stream1.data). Stream 1
    is the luma stream (or the combined luma-and-chroma stream when
    encoding == LUMA_AND_CHROMA).
  • Decode-chain path 2: when stream.stream2.is_some(), the oracle also
    calls avc_to_annex_b(stream2.data). Stream 2 carries the auxiliary
    chroma data per the tagged-encoding split.

The oracle catches panics in the wrapper, OOM allocation from
attacker-controlled NAL length encoding inside either sub-stream,
contract violations on the produced Annex-B byte stream, and any
framing slip in Avc444BitmapStream::decode that misallocates
stream1 vs stream2 byte ranges.

The oracle does NOT catch OpenH264 internal bugs (OSS-Fuzz coverage),
the post-OpenH264 YUV-to-RGBA conversion path downstream of OpenH264,
the AVC444 luma plus chroma packing on the encoder side, or cross-stream
state correlation across frames (multi-frame oracle is a future
sibling target under the same umbrella).

Smoke fuzz ran 10,246,473 iterations in 31 seconds at ~330K exec/s
sustained with zero crashes.

The new target auto-discovers into CI via the cargo xtask fuzz list
dynamic fan-out mechanism. The new check_egfx_avc444_decode
regression-replay test in
crates/ironrdp-testsuite-core/tests/fuzz_regression.rs runs against
the seed corpus and passes.

Refs #1316. Stacked on #1326.

This change adds an assertion-or-panic fuzz oracle for the AVC
length-prefix to Annex-B conversion that runs inside
`OpenH264Decoder::decode` before any OpenH264 entry point. The same
change refactors the conversion from a private method on
`OpenH264Decoder` into two public free functions in
`ironrdp_egfx::pdu::avc`. The first function, `avc_to_annex_b`,
returns a fresh `Vec<u8>` and is symmetric with the existing
`annex_b_to_avc`. The second function, `avc_to_annex_b_into`, writes
into a caller-provided buffer and preserves the per-frame buffer-reuse
optimization that `OpenH264Decoder` relied on.

The conversion is now available unconditionally to any consumer of
`ironrdp-egfx`. The previous `#[cfg(feature = "openh264")]` gating
went with the location, not the bytes-to-bytes logic, so lifting the
function out of `decode.rs` removed the gate too.

The new oracle exercises two input distributions on every fuzz call:

- Direct path: the oracle calls `avc_to_annex_b(data)` on the raw fuzz
  input. This exercises the wrapper on arbitrary byte distributions,
  including inputs that do not parse as `Avc420BitmapStream`.
- Decode-chain path: the oracle tries `Avc420BitmapStream::decode(data)`;
  on success it calls `avc_to_annex_b(stream.data)`. This exercises the
  wrapper on the realistic post-decode payload distribution.

The oracle catches panics in the wrapper, OOM allocation from
attacker-controlled NAL length encoding, and contract violations on the
produced Annex-B byte stream. The oracle does NOT catch OpenH264
internal bugs (OSS-Fuzz coverage), the YUV-to-RGBA conversion path
downstream of OpenH264 (separate workstream), or AVC444 luma plus
chroma split (sibling target).

Smoke fuzz ran 10,922,695 iterations in 31 seconds at ~352K exec/s
sustained with zero crashes. Coverage settled at 158 lines, 456
features, 69 corpus entries.

The new target auto-discovers into CI via the `cargo xtask fuzz list`
dynamic fan-out mechanism. The new `check_egfx_avc420_decode`
regression-replay test in
`crates/ironrdp-testsuite-core/tests/fuzz_regression.rs` runs against
the seed corpus and passes.

Refs Devolutions#1316.
Sibling of `egfx_avc420_decode` (PR Devolutions#1326), applied to the AVC444 wire
shape. AVC444 wraps two `Avc420BitmapStream`s plus a 2-bit encoding tag
per MS-RDPEGFX 2.2.4.4 (LUMA_AND_CHROMA, LUMA, or CHROMA). The wrapper
layer for each sub-stream is the same `avc_to_annex_b` function that
Devolutions#1326 lifted to a public free function; this PR adds an oracle that
exercises the wrapper on both sub-streams.

This PR is stacked on PR Devolutions#1326 (which introduced
`avc_to_annex_b` as a public function in `ironrdp_egfx::pdu`). It
depends on Devolutions#1326 landing first. CI on this branch will not be green
against `Devolutions/IronRDP:master` until Devolutions#1326 merges; that is the
correct mechanical signal of the dependency. After Devolutions#1326 merges, this
PR rebases cleanly because the only commit unique to this branch is
the AVC444 oracle scaffolding.

The new oracle exercises three paths on every fuzz call:

- Direct path: the oracle calls `avc_to_annex_b(data)` on the raw fuzz
  input. This exercises the wrapper on arbitrary byte distributions,
  including inputs that do not parse as `Avc444BitmapStream`.
- Decode-chain path 1: the oracle tries `Avc444BitmapStream::decode(data)`;
  on success it calls `avc_to_annex_b(stream.stream1.data)`. Stream 1
  is the luma stream (or the combined luma-and-chroma stream when
  `encoding == LUMA_AND_CHROMA`).
- Decode-chain path 2: when `stream.stream2.is_some()`, the oracle also
  calls `avc_to_annex_b(stream2.data)`. Stream 2 carries the auxiliary
  chroma data per the tagged-encoding split.

The oracle catches panics in the wrapper, OOM allocation from
attacker-controlled NAL length encoding inside either sub-stream,
contract violations on the produced Annex-B byte stream, and any
framing slip in `Avc444BitmapStream::decode` that misallocates
`stream1` vs `stream2` byte ranges.

The oracle does NOT catch OpenH264 internal bugs (OSS-Fuzz coverage),
the post-OpenH264 YUV-to-RGBA conversion path downstream of OpenH264,
the AVC444 luma plus chroma packing on the encoder side, or cross-stream
state correlation across frames (multi-frame oracle is a future
sibling target under the same umbrella).

Smoke fuzz ran 10,246,473 iterations in 31 seconds at ~330K exec/s
sustained with zero crashes.

The new target auto-discovers into CI via the `cargo xtask fuzz list`
dynamic fan-out mechanism. The new `check_egfx_avc444_decode`
regression-replay test in
`crates/ironrdp-testsuite-core/tests/fuzz_regression.rs` runs against
the seed corpus and passes.

Refs Devolutions#1316. Stacked on Devolutions#1326.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant