Skip to content

fix(mux,quic): clear the fmp4 and connection fuzz findings - #7961

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

kixelated merged 3 commits into
mainfrom
quest/m1/fuzz-findings/README

Conversation

@kixelated

@kixelated kixelated commented Nov 5, 2026 •

Copy link
Copy Markdown
Collaborator

Advances quest/m1/fuzz-findings/README.md ("Media fuzz findings", an [S] watcher). Triages every Fuzz job that failed in the scheduled Nightly runs of 2026-11-03 to 2026-11-05, fixes the two that still fail on main, and updates the epic. The epic stays: it is deleted once the scheduled runs show every target green (decided 2026-11-05, maintainer).

Triage per target

Each job's crash artifact was downloaded and replayed through the target's fuzz::tests::regressions on current main.

Target Runs Finding On main Disposition
fmp4 11-04, 11-05 2,036-byte input allocated 7.46 MB (bound 6.1 MB) reproduces real bug, fixed here
connection 11-04 "datagram 5373952 was never sent" reproduces harness bug, fixed here
cmaf 11-04 OOM, malloc(2.68 GB) passes fixed by moq-dev/moq#7843; input committed here
ts 11-03 subtract overflow, ts/step.rs:431 passes fixed by moq-dev/moq#7683 (input ts/step-scan)
annexb 11-03 scuffle-h265 SPS subtract overflow passes fixed by moq-dev/moq#7685 (input annexb/scuffle-h265-transform-depth)
ingest 11-02 to 11-05 "the ingest stalled on a complete N-byte stream" n/a owned by quest/m0/nightly/ingest-fuzz.md, PR moq-dev/moq#7808

None was intermittent: every failing input reproduced deterministically or was already fixed at a known commit, so nothing goes to m0's test flakes.

For ingest, the 11-04 (16-byte) and 11-05 (15-byte) stall inputs are newer than the one #7808 was written for. Both pass fuzz::tests::regressions on its head 63fdf85642, so they need no new quest.

fmp4: field-less trun counts

The input's last moof is 104 bytes with one trun whose flags carry only a data offset and whose count is 65,792. Such a run stores just a count, and begin_mdat plans every sample (about 113 bytes each) when the mdat header arrives, before any payload. MAX_FRAGMENT_SAMPLES (131,072) bounds that at about 26 MB but not in proportion to the input, which is what the fuzz target checks.

Fix: metadata::Scan::finish now refuses a fragment declaring more than min(MAX_FRAGMENT_SAMPLES, 4096 + metadata bytes) samples with the existing Error::TooManySamples. A trun entry is at least 4 bytes, so only field-less runs can reach the per-byte limit; a fragment that stores entries is unaffected. One crate-private constant (FRAGMENT_SAMPLE_BASE), no new error variant.

connection: harness taint stopped too early

Two Corrupt ops hit one 106-byte datagram holding two coalesced packets (0..54 and 54..106). The harness tracks only the first changed byte (52), which lies in packet one's tag, and Peer::taint returned there as if nothing later changed. The second change fell in packet two's DATAGRAM frame, which the connection delivered, and the harness counted it as a never-sent datagram. taint now skips only that packet and goes on decoding later ones. Not a moq-quic bug: the connection delivered exactly the frame it received.

Changes

  • rs/moq-mux/src/container/fmp4/{mod,metadata,import}.rs: the per-byte sample limit, docs, and tests. huge_trun_sample_counts_are_refused covers the limit and the value just past it. fragment_plans_within_the_held_bound now expects a field-less run at the cap to be refused, and adds a cap-sized fragment whose field-less samples are paid for by 4-byte entries. counts_are_checked_across_runs_and_tracks_before_parsing pads the moof to reach the cap.
  • rs/moq-quic/src/fuzz/connection.rs: return → continue in taint, with the doc corrected.
  • Regression inputs: rs/moq-mux/fuzz/regressions/fmp4/fieldless-trun-count (the nightly crash), rs/moq-mux/fuzz/regressions/cmaf/metadata-preflight (the 11-04 OOM), and rs/moq-quic/fuzz/regressions/connection/crash-a0ef11af….
  • rs/moq-mux/CHANGELOG.md: Fixed entry.
  • quest/m1/fuzz-findings/README.md: the triage, and what remains.

Public API: no signature change. fmp4::Import and the CMAF metadata checks now refuse a moof declaring more than 4,096 samples beyond one per metadata byte, with Error::TooManySamples, whose message no longer names MAX_FRAGMENT_SAMPLES. Wire: none.

Choices made without asking

  • Took over from the earlier session, which left an uncommitted fmp4 fix in worktree t3-cced8ddf about three hours before. I replaced its approach (a separate field-less count, a new TooManyInheritedSamples variant, and three constants) with one limit on the existing sample count. The earlier worktree is untouched.
  • Base of 4,096 samples (about 0.5 to 0.8 MB of plan). Field-less runs need every sample the same size, so they come from constant-size audio. 4,096 AC-3 frames is over two minutes at 48 kHz.
  • Fixed the connection harness here although the epic covers only media targets. The brief listed connection, the fix is one line, and an extra quest would cost more than the fix.
  • Committed the 11-04 cmaf input although fix(mux): preflight received CMAF metadata before allocation #7843 had fixed it, since that PR committed none. Before fix(mux): preflight received CMAF metadata before allocation #7843 (at 1b68a5983c) replaying it ran out the test's time while allocating. On main it passes.
  • Did not commit the 11-03 annexb and ts inputs: the PRs that fixed them committed inputs for the same crash sites.

Validation

  • Reproduced on main before the fix: fuzz::tests::regressions failed with "2036 input bytes allocated 7461897 bytes in 670 allocations" (fmp4) and "datagram 5373952 was never sent" (connection). Both pass on this branch.
  • just rs fuzz fmp4 -- -max_total_time=300 (569,306 runs) and just rs fuzz connection -- -max_total_time=300 (120,106 runs) on the final code: no findings.
  • quest gate just check: passes.

Review

  • Round 1: quest review --base origin/main, Codex gpt-6.1-sol at medium effort, head 6da6455b093f9237eeb39a6828417298fea7befe. 0 findings.

Follow-ups

None.

(Written by Claude Opus 5.5)

kixelated and others added 3 commits November 5, 2026 06:46
fmp4: a `trun` without per-sample fields stores only a count, so a
104-byte moof planned 65,792 samples (7 MB) before any payload arrived,
breaking the fuzz target's allocation bound. A fragment now declares at
most one sample per byte of its metadata past 4,096, besides
MAX_FRAGMENT_SAMPLES.

connection: the harness stopped tainting a tampered datagram at the
first packet whose tag held the first changed byte, so a later
coalesced packet's corrupted DATAGRAM frame tripped the check. Later
packets are now decoded.

Commits the nightly inputs for both, and the 2026-11-04 cmaf OOM input
that #7843 fixed. Updates the fuzz findings epic with the
triage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title Media fuzz findings fix(mux,quic): clear the fmp4 and connection fuzz findings Nov 5, 2026
@kixelated
kixelated marked this pull request as ready for review November 5, 2026 20:25
@coderabbitai

coderabbitai Bot commented Nov 5, 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 1 minute.

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: 9a385754-ab7b-4f5d-975c-3028404dbde0
Commits

Reviewing files that changed from the base of the PR and between d2a7ead and 6da6455.

Files selected for processing: 9

  • quest/m1/fuzz-findings/README.md
  • rs/moq-mux/CHANGELOG.md
  • rs/moq-mux/fuzz/regressions/cmaf/metadata-preflight
  • rs/moq-mux/fuzz/regressions/fmp4/fieldless-trun-count
  • rs/moq-mux/src/container/fmp4/import.rs
  • rs/moq-mux/src/container/fmp4/metadata.rs
  • rs/moq-mux/src/container/fmp4/mod.rs
  • rs/moq-quic/fuzz/regressions/connection/crash-a0ef11af573b106e4f66ab4007f9f5ebc61cffee
  • rs/moq-quic/src/fuzz/connection.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

@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 head 6da6455b093f9237eeb39a6828417298fea7befe against base d2a7eadc2b218089fa3b59177d2003d9afbfcc97.

No actionable findings. The direction looks sound: the size-relative sample cap runs before metadata expansion and sample planning, retains the absolute cap, and has boundary and mixed-run coverage. Continuing past a packet whose tag was modified correctly lets the taint scan inspect subsequent coalesced packets. The committed regression paths are picked up by the existing replay tests.

Verification limits: GitHub-only static review of the diff and surrounding callers/tests. I did not run tests or fuzzers, or inspect/replay the binary crash inputs. The Check workflow for this head was queued at review time; the validation results in the PR description were not independently reproduced.

@kixelated
kixelated added this pull request to the merge queue Nov 5, 2026

@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.

Important

The per-byte bound also refuses valid, previously accepted audio imports, and the user-facing refusal list still names only the 131,072 cap.

Reviewed changes

  • fMP4 sample budget — metadata::Scan::finish now refuses a moof declaring more than min(MAX_FRAGMENT_SAMPLES, 4096 + metadata bytes) samples, closing the field-less trun amplification before any sample is planned. Tests cover both sides of the new boundary, and a cap-sized fragment whose field-less samples are paid for by 4-byte entries.
  • QUIC fuzz harness — Peer::taint now continues past a coalesced packet whose tag holds the first changed byte instead of returning, so later packets' frames are marked forged. The change is monotone (it only adds markings), the inequality selects exactly the tag region, and the skipped packet's frame bytes are all before from hence unchanged, so it cannot cause a new false positive.
  • Regression inputs and records — fmp4/fieldless-trun-count, cmaf/metadata-preflight, and connection/crash-a0ef11af…, all replayed by fuzz::tests::regressions; the CHANGELOG entry and the epic triage.

I found no hole in either fix: every sample-plan site is reachable only after a checked moof, and the absolute cap keeps a fragment at this bound within MAX_HELD_BYTES. The inline comment is about the class of input the new ratio bound refuses.

ℹ️ The documented refusal list is now stale

doc/lib/rs/moq-mux.md's fMP4 row itemizes when an import is refused and still says only a moof of more than 131,072 samples is refused, so a reader can believe a much larger fragment is accepted. Add the per-byte rule there, and, if it stays, the practical minimum fragment length.

Technical details
# Stale fMP4 limit list in the user docs

## Affected sites
- `doc/lib/rs/moq-mux.md:20` — "a `moof` of more than 131,072 samples ... is refused" omits the ratio bound added at `rs/moq-mux/src/container/fmp4/metadata.rs:48`.
- `rs/moq-mux/CHANGELOG.md:25` — carries the new rule, but the library page is where the refusal conditions are listed together.

## Required outcome
A reader of the library page knows a short `moof` declaring thousands of samples is refused before parsing, not only one above 131,072.

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

if self.samples > MAX_FRAGMENT_SAMPLES {
/// Check the counts gathered from `len` bytes of metadata.
fn finish(self, len: usize) -> Result<()> {
if self.samples > MAX_FRAGMENT_SAMPLES.min(FRAGMENT_SAMPLE_BASE + len as u64) {

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.

FRAGMENT_SAMPLE_BASE assumes a fragment is live and short, but this check also governs moq import fmp4 from a file or stdin, where a producer can put the whole stream in one moof. ffmpeg always writes tfhd default size and duration and adds per-sample fields only when a sample differs, while FF_MOV_FLAG_FRAG_KEYFRAME flushes only on video, so an audio-only AC-3 or CBR Opus file arrives as one field-less trun; at ~4,180 samples (≈2 min of AC-3) that file is now refused, where the 131,072 cap accepted ≈70 min.

Technical details
# A long field-less audio fragment is now refused

## Affected sites
- `rs/moq-mux/src/container/fmp4/metadata.rs:48` — `len` is the `moof` body on the streaming path (`import.rs:508`), so the budget is 4096 + body bytes.
- `rs/moq-mux/src/container/fmp4/mod.rs:53-59` — `FRAGMENT_SAMPLE_BASE` and its "A live fragment is a handful of samples" doc.
- `rs/moq-mux/src/container/fmp4/sample.rs:147` — a sample costs at least one `mdat` byte and the claimed ranges are disjoint, so the plan grows with delivered payload; only the reservation happens eagerly at `begin_mdat`.

## Concrete input
`ffmpeg -f lavfi -i sine=frequency=440:duration=180 -c:a ac3 -f mp4 -movflags empty_moov - | moq import fmp4` produces one ~90-byte `moof` whose `trun` carries only a count: `mov_write_tfhd_tag` sets `MOV_TFHD_DEFAULT_SIZE|MOV_TFHD_DEFAULT_DURATION`, `mov_write_trun_tag` adds `MOV_TRUN_SAMPLE_SIZE`/`_DURATION` only if a sample differs, and the `FF_MOV_FLAG_FRAG_KEYFRAME` flush requires `par->codec_type == AVMEDIA_TYPE_VIDEO` (movenc.c ~5734, ~5806, ~7630). AC-3 frames are fixed size and duration, so 5,760 samples > ~4,180.

## Required outcome
Either this file imports, or the limitation is recorded where users read the other refusal conditions (`doc/lib/rs/moq-mux.md`). The fuzz invariant itself is not at risk: one sample needs a byte of payload, so a plan grown with delivered bytes stays proportional.

## Open questions for the human
- Is a single-fragment audio file an intended import, or is the importer only for live/short fragments?
- If it is intended, could the plan be built as `mdat` bytes arrive (`begin_mdat` already has the declared length at `import.rs:1095`, and `write_mdat` walks chunks) instead of reserving it from the declared count?

Merged via the queue into main with commit a14995f Nov 5, 2026
11 checks passed
@kixelated
kixelated deleted the quest/m1/fuzz-findings/README branch November 5, 2026 21:50
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