Skip to content

fix(moq-mux): walk an H.265 SPS before scuffle_h265 parses it - #7685

Merged
kixelated merged 5 commits into
mainfrom
quest/m1/fuzz-findings/sps-overflow
Nov 3, 2026
Merged

kixelated merged 5 commits into
mainfrom
quest/m1/fuzz-findings/sps-overflow

Conversation

@kixelated

@kixelated kixelated commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Closes quest quest/m1/fuzz-findings/sps-overflow.md: a hostile H.265 SPS reaching moq-mux (fMP4 hvcC, Annex-B, or MPEG-TS) is now refused before scuffle-h265 can overflow on it. The fuzzers' SPS inputs are committed as regressions for the annexb, fmp4, and ts targets.

Is it still needed on today's main?

Yes, for H.265. Every reproducer below panicked on main (3d927ca) before this change, except the original exp-golomb input (see the Annex-B entries). On today's main that input stops early at h264-reader's sps_id read. A version with valid emulation prevention still reached scuffle-expgolomb's wrap.

For H.264, moq-dev/moq#7398 moved SPS parsing to h264-reader 0.9. Its read_ue stops at 2^32 - 2, and pixel_dimensions uses checked_add/checked_mul/checked_sub for the width, height, and crop math. The old h264-parser reproducer passes, and is committed as fmp4/h264-sps-width. That check was done by reading the code. The longer fuzz rerun stays with quest/m2/sps-fuzz-h264-reader.md, whose text this PR updates.

Changes

  • codec/h265/sps.rs: new parse_full(nal) runs check first, then calls SpsNALUnit::parse. check walks the whole SPS with h264-reader's bounded BitReader, following scuffle-h265 0.2.2's layout, and refuses:

    • MinTbLog2SizeY > CtbLog2SizeY. scuffle subtracts these unchecked for the transform-depth range_check! (sps/mod.rs:303,305).
    • A scaling_list_pred_matrix_id_delta that reaches before matrix 0. scuffle's matrix_id - delta wraps at scaling_list.rs:90, then indexes out of bounds at :92, which panics in release builds too.
    • Any ue(v) of 32 or more leading zeros, which ITU-T H.265 9.2 forbids. This covers scuffle-expgolomb's result - 1 wrap at 64+ zeros, and the + 3 / + / * sums near u64::MAX (CB size, PCM size, VUI display-window sums).
    • palette_max_size > 64 (7.4.3.3.8). scuffle allocates palette_max_size-sized predictor initializers, and one ~2^32 entries long aborts the process.
  • Callers: Sps::parse, sps_reorder, sps_hrd, and the TS HEVC timing path (container/ts/video_timing.rs) all go through parse_full. Sps::parse treats a walk refusal (InvalidData) like a scuffle refusal: it keeps the SPS head and drops the VUI. A truncated SPS (UnexpectedEof) is still an error.

  • Tests:

    • check_reads_what_scuffle_reads runs every moq-mux H.265 SPS fixture, two of scuffle's own test SPSes (scaling lists; 26 inter-predicted RPSs), and a synthetic SPS. The synthetic one enables PCM, long-term refs, a VUI with sub-picture NAL+VCL HRD, and all four extensions. For each SPS the test asserts scuffle accepts it and the walk ends exactly at its stop bit.
    • overflowing_fields_are_refused covers each overflow with a synthetic SPS. With check disabled, the first three panic at the reported scuffle lines, and the palette case aborts on allocation.
  • Regressions (rs/moq-mux/fuzz/regressions/):

    Each H.265 input panics with check disabled and passes with it.

Choices made without asking

  • The walk follows scuffle's layout, not the spec's, where the two differ. scuffle reads sub_layer_level_idc twice when a sub-layer signals both profile and level, and reads iv_di_mc_enabled_flag/iv_mv_scal_enabled_flag once in the 3D extension. A walk on the spec layout would fall out of step with scuffle, and an input built on that gap could get past the checks. SpsHead's own reader keeps the spec layout. skip_sub_layers gained a level_twice flag for the two cases.
  • Spec bounds, not scuffle's stricter ones. The walk refuses only what overflows, or what the spec forbids where a loop or allocation needs a bound (≤ 64 RPSs, ≤ 16 delta POCs per list, ≤ 32 CPBs and long-term pictures, palette_max_size ≤ 64). Everything else is left to scuffle's own checks, so a valid SPS is never refused by the walk.
  • An SPS with a byte sequence emulation prevention forbids (00 00 00/01/02) is now refused by the walk, so it keeps its head without the VUI. Before, scuffle passed those bytes through. Annex-B splitting never yields one, and inside hvcC one is malformed.

Verification

  • cargo fmt --check, cargo clippy -p moq-mux --all-targets --all-features -D warnings, cargo doc -p moq-mux -D warnings, cargo test -p moq-mux --all-features: all pass. On the final head 5ca8238eee, clippy and cargo test -p moq-mux --all-features pass again (2005 lib tests); fuzz::tests::regressions replays the committed inputs.
  • A local mutation sweep (not committed) applied 400k random byte mutations to the SPS fixtures. 17,591 of them panic raw SpsNALUnit::parse, and check refused every one. Running parse_full and Sps::parse over 404k mutations panicked nowhere.
  • cargo-fuzz isn't installed on this host, so no libFuzzer session ran. The nightly workflow runs these targets.
  • quest check --base origin/main passes.

Follow-ups

Review

Codex (quest review), gpt-6.1-sol, medium effort, round 1 of a new session on head 1846351d6831e13f585e370647690bf151079fcf against origin/main (151ed3820d81): no findings.

Iteration on 2026-11-03:

  • Merged origin/main twice (the second time up to 151ed3820d81, giving head 5ca8238eee6). The only conflict both times was the fuzz-findings README's Required list, which quest's merge driver resolved. git diff origin/main...HEAD -- rs is identical to the reviewed head's diff, so the review above still covers the code.
  • Applied pullfrog's suggestion to scope parse_full's doc comment to moq-mux.
  • The failing Check job on 18ed9dbc9f was a network flake: the ffmpeg tarball download for js/libav got connection resets during check biome. This PR changes nothing there.

🤖 Generated with Claude Code

(Written by Claude Opus 5.5)

kixelated and others added 2 commits November 2, 2026 23:02
scuffle-h265 0.2.2 subtracts and indexes with SPS fields unchecked, and
scuffle-expgolomb 0.1.5 wraps an Exp-Golomb code of 64 or more leading
zeros, so a hostile SPS panicked debug builds and fed wrapped values (or a
multi-gigabyte palette allocation) to release builds.

Every scuffle_h265 SPS parse in moq-mux now goes through
`sps::parse_full`, which first walks the whole SPS in scuffle_h265's layout
with h264_reader's bounded reader and refuses the values that reach those
overflows. An SPS the walk refuses falls back to its head, as a scuffle
refusal already did.

Commits the fuzzers' SPS inputs as regressions for the annexb, fmp4 and ts
targets, and closes quest/m1/fuzz-findings/sps-overflow.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title SPS fields cannot overflow fix(moq-mux): walk an H.265 SPS before scuffle_h265 parses it Nov 3, 2026
@kixelated
kixelated marked this pull request as ready for review November 3, 2026 07:32
@coderabbitai

coderabbitai Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 15 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 4 included reviews currently available.

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 49869910-c452-4acd-9287-10a7ba7710d1
Commits

Reviewing files that changed from the base of the PR and between c4f8abc and 1846351.

Files selected for processing: 11
  • quest/m1/fuzz-findings/README.md
  • quest/m1/fuzz-findings/sps-overflow.md
  • quest/m2/sps-fuzz-h264-reader.md
  • rs/moq-mux/fuzz/regressions/annexb/hevc-sps-exp-golomb-64-zeros
  • rs/moq-mux/fuzz/regressions/annexb/scuffle-expgolomb
  • rs/moq-mux/fuzz/regressions/annexb/scuffle-h265-transform-depth
  • rs/moq-mux/fuzz/regressions/fmp4/h264-sps-width
  • rs/moq-mux/fuzz/regressions/ts/hevc-sps-transform-depth
  • rs/moq-mux/src/codec/h265/mod.rs
  • rs/moq-mux/src/codec/h265/sps.rs
  • rs/moq-mux/src/container/ts/video_timing.rs
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

CodeRabbit Verify

  • Run CodeRabbit Verify to test the changes in this pull request.

@kixelated kixelated left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review by review (OpenAI)

Reviewed commit: 18ed9db

The bounded pre-walk is a reasonable mitigation for the dependency’s known overflow paths, though maintaining a second parser tied to its exact layout carries ongoing risk. Removing the duplicate once the dependency is fixed would be simpler. No actionable bugs found in this static review. I did not run tests or fuzz the parser.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ Minor suggestion inline. The guard itself checks out.

Reviewed changes

  • codec/h265/sps.rs — new check/walk: a bit-exact walk of the SPS in scuffle-h265 0.2.2's layout (including its two layout quirks) that refuses the values scuffle would overflow on — MinTbLog2SizeY > CtbLog2SizeY, a scaling-list reference before matrix 0, any ue(v) of 32+ leading zeros, and palette_max_size > 64 / oversized predictor initializers — before SpsNALUnit::parse runs.
  • Sps::parse fallback: a walk refusal (InvalidData) falls back to the head-only parse, same as a scuffle refusal; a truncated SPS (UnexpectedEof) stays an error.
  • Callers: config_from_sps/Sps::parse, sps_reorder, sps_hrd, and the MPEG-TS HEVC timing path all route through parse_full.
  • Tests: check_reads_what_scuffle_reads asserts the walk reaches the same stop bit scuffle does across the fixtures, scuffle's own test SPSes, and a synthetic all-branches SPS; overflowing_fields_are_refused covers each overflow.
  • Regressions / quest: 5 fuzz inputs committed; sps-overflow closed and sps-fuzz-h264-reader updated.

I compared every walk branch against scuffle-h265 0.2.2's SpsRbsp::parse (and the sub-modules) field by field. The bit consumption matches on every branch — including the general-profile 96-bit skip (additional flags are always 43 bits), the twice-read sub_layer_level_idc, the HRD fixed_pic_rate_within_cvs_flag default, and the short-term-RPS derivation order — so no SPS that scuffle would overflow on can pass check. h264_reader's read_ue cap (31 leading zeros, max 2^32-2) keeps every downstream u64 sum in range. No false positive on a spec-valid stream found.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

Comment thread rs/moq-mux/src/codec/h265/sps.rs Resolved

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

Reviewed the delta since the prior pullfrog review at 18ed9db; nothing behavioral changed.

  • Scoped parse_full's doc guarantee: codec/h265/sps.rs now states that every scuffle_h265 SPS parse in moq-mux goes through parse_full, applying the prior review's suggestion and leaving the moq-video follow-up tracked separately.
  • Quest bookkeeping: the fuzz-findings README gained a ## Required heading and dropped the resolved TS step-scan bullet.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash-1017 (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

Reviewed the delta from 1846351 to 5ca8238; the Rust code is unchanged.

  • Quest cleanup: quest/m1/fuzz-findings/sps-overflow.md is deleted on completion, its bullet is removed from the README's ## Required list, and the mp4-atom entries now cite upstream kixelated/mp4-atom#267 for the sgpd/iref/iloc regressions.
  • Second origin/main merge: brings the branch to 151ed3820d81 with no rs/ diff change, so the reviewed guard is exactly what was approved previously.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash-1017 (free via Pullfrog for OSS) | 𝕏

@kixelated
kixelated added this pull request to the merge queue Nov 3, 2026
Merged via the queue into main with commit 51b1ff2 Nov 3, 2026
7 of 8 checks passed
@kixelated
kixelated deleted the quest/m1/fuzz-findings/sps-overflow branch November 3, 2026 21:37
kixelated added a commit that referenced this pull request Nov 3, 2026
Resolve lite.md to main's SUBSCRIBE_STARTED decision (#7675) and keep the
prefix-set Related link in the routes epic README (#7676). Per the PR's Merge
order table, drop the edges to sps-overflow.md (#7685 merged) and repoint
routes.md links to routes/README.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant