Skip to content

test(net): fuzz group ingest and request framing with invariants - #7320

Merged
moq-bot[bot] merged 5 commits into
mainfrom
quest/m2/wire-fuzz-coverage
Oct 31, 2026
Merged

moq-bot[bot] merged 5 commits into
mainfrom
quest/m2/wire-fuzz-coverage

Conversation

@kixelated

@kixelated kixelated commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

The moq-net half of quest/m2/wire-fuzz-coverage.md. It adds two fuzz targets and near-cap seeds. Both targets assert more than "it did not panic". The quest is rewritten to what's left: the moq-sock and moq-uring cmsg targets, and Miri for moq-c, moq-uring, and moq-sock.

Changes

  • ingest target (rs/moq-net/src/fuzz/ingest.rs) runs one group stream through lite's FrameIngest or moq-transport's GroupIngest. The input's first bytes pick the version and track settings: timed or not, compressed, the ietf extension, and the publisher draft. Each input runs three ways: whole, one byte at a time, and in chunks whose size the input picks.

    • All three runs must leave the same frames (timestamps, payloads, Object IDs and property blocks, inflated sizes) and end the same way.
    • A complete stream must settle within a bounded number of polls.
    • Every frame must match its track's timeline, and Object IDs must strictly increase.
    • The frames must never hold more bytes than the stream carried.
    • On a clean end, every byte must be a frame, a header field, or a property block. This only holds where no block length is hidden, so it is checked on moq-lite and on moq-transport subgroups without extensions.
  • request target (rs/moq-net/src/fuzz/request.rs) reads a request stream through Next<Framed>, at most one GOAWAY included.

    • Each message must be exactly its type, a two-byte length, and that body.
    • Body::message must accept a body exactly when the control stream's length-prefixed decode does, and produce the same message. This is checked for every message type that has the ID, since REQUEST_UPDATE and REQUEST_OK each share theirs.
    • What decodes must re-encode stably.
    • The hand-written Update and NamespaceUpdate readers must stop where the framing did, and never run short inside a complete frame.
  • Seeds at every cap. These are length prefixes at the cap and one past it, followed by a far shorter body:

    • lite message sizes (0xffff, and 64 MiB for ANNOUNCE_INIT) for every kind
    • the moq-transport two-byte length for every kind
    • frame size (MAX_CACHE_BYTES)
    • the lite record property block (MAX_PROPERTIES)
    • the moq-transport extension block (MAX_OBJECT_EXTENSIONS)

    Before this, every seed body was six bytes or less.

  • stream_seeds_decode checks two things: every well-formed stream seed decodes, so each version's path is reached, and every limit seed is refused.

  • Both targets are added to nightly.yml's fuzz matrix. The merge gate already replays fuzz/regressions/, and now covers request/ and ingest/ too.

  • To make the ingest machines reachable from the hidden fuzz module, FrameIngest, GroupIngest, Framed, Next, Update, and NamespaceUpdate are now pub(crate), re-exported only under cfg(any(test, feature = "fuzz")). GroupIngest::new now takes the version instead of the whole Subscriber.

Findings

  • Next::decode lost need() on a short type. It peeked the message type on a cloned Decoder, so a type cut short returned Short with need() == 0. need()'s documented contract is that it is non-zero after a short read. Production is unaffected: Reader falls back to need().max(len + 1) and retries a byte at a time. Fix: a short peek now falls through to T, which reads the type on the real decoder. The input is committed as fuzz/regressions/request/draft22-next-short-type.
  • No other target crashed. This host has no rustup or cargo-fuzz, so I couldn't run libFuzzer. Instead, a throwaway stable-toolchain random mutator ran over the seeds (not committed): 400k inputs each for ingest and request, and 200k each for lite, ietf, and announce. Nightly's 5-minute libFuzzer runs are the first coverage-guided runs of the new targets.

Choices made without asking

  • SubgroupRecv, FollowUp, and whole_message, which the quest named, no longer exist; they were removed along with drafts 14–17 in moq-dev/moq#6753. The request target covers what replaced them: Body, Framed, Next, Update, and NamespaceUpdate.
  • I check the stream position with byte-accounting bounds rather than an exact re-encode. The decoders accept non-minimal varints, and the publisher's frame writer is entangled with session state, so an exact comparison isn't practical. Chunk-split invariance carries most of the weight.
  • I split the quest. The moq-sock cmsg target would fail nightly on CMsg::decode's known overread until Bound cmsg payload reads lands. The Miri recipe and job are shared with quest/m2/miri-memory.md, which is in progress in parallel. The rewritten quest records both orderings.

Coordination

  • moq-dev/moq#7315 (miri-memory): no overlap yet, since this PR adds no Miri recipe. Whichever quest lands first creates just rs miri, as both quests already say.
  • moq-dev/moq#7313 (media-fuzz): that quest will add matrix rows to the same fuzz job in nightly.yml. Expect a trivial merge conflict on the target: [...] line. If it adds a crate, it also needs a row in rs/justfile's no_fuzz. Target names must stay unique across crates, because sh/rs/fuzz.sh resolves them globally.

Checks

  • quest gate just check origin/main: pass. The Rust suites ran 9094 tests: 9094 passed, 97 skipped. The flake check was skipped because the local Nix builder can't create user namespaces.
  • quest check --base origin/main: pass.

Decisions

  1. Land the moq-net half now, or keep this branch open for the cmsg targets and Miri?
    • ✅ Land the moq-net half now. Maintainer decision, 2026-10-30. The rewritten quest is self-contained, and its moq-sock half waits on cmsg-decode-bound anyway.
    • Keep this branch open for the cmsg targets and Miri.

Follow-ups

  • None beyond the rewritten quest.

Review

  • Reviewer: Codex, gpt-6.1-sol, medium effort, via quest review (quest pinned by origin/main's flake.nix).
  • Round 1, head 7da6d34: one finding, fixed. The ingest upper bound ignored extension blocks on untimed IETF tracks. It is now gated on the subgroup's has_extensions, with regression fuzz/regressions/ingest/draft22-untimed-extension-block.
  • Round 2, head 758e2b7: no findings.

(Written by Claude Opus 5.5)

🤖 Generated with Claude Code

kixelated and others added 4 commits October 30, 2026 02:04
Two moq-net fuzz targets, plus seeds at every length cap:

- ingest drives lite's FrameIngest and moq-transport's GroupIngest over a
  stream delivered whole, byte by byte, and in fuzzer-chosen chunks, and
  requires the same frames and ending from each, no stall, and payload and
  header accounting that matches the stream's length.
- request reads a request stream through Next<Framed>, checks the framing
  is exact, and requires Body::message to agree with the control stream's
  length-prefixed decode for every message type sharing the ID.

The request target found Next::decode peeking the type on a clone, so a
type cut short reported need() == 0. Let it fall through to T, which
records what it needed on the real decoder.

Rewrite quest/m2/wire-fuzz-coverage.md to the cmsg targets and Miri.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An untimed, uncompressed moq-transport track still carries extension
blocks when its subgroup header says so, and their length is not kept,
so the upper bound only holds without them. Commit the input that
exceeded it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title The audited moq-net decoders and peer-reachable unsafe have invariant fuzz targets test(net): fuzz group ingest and request framing with invariants Oct 30, 2026
@kixelated
kixelated marked this pull request as ready for review October 30, 2026 20:55
@coderabbitai

coderabbitai Bot commented Oct 30, 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 34 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: eb9f901c-59fd-4052-97bf-7484ff629aec
Commits

Reviewing files that changed from the base of the PR and between bdc4785 and 758e2b7.

Files selected for processing: 18
  • .github/workflows/nightly.yml
  • quest/m2/README.md
  • quest/m2/wire-fuzz-coverage.md
  • rs/moq-net/fuzz/Cargo.toml
  • rs/moq-net/fuzz/README.md
  • rs/moq-net/fuzz/fuzz_targets/ingest.rs
  • rs/moq-net/fuzz/fuzz_targets/request.rs
  • rs/moq-net/fuzz/regressions/README.md
  • rs/moq-net/fuzz/regressions/ingest/draft22-untimed-extension-block
  • rs/moq-net/fuzz/regressions/request/draft22-next-short-type
  • rs/moq-net/src/fuzz.rs
  • rs/moq-net/src/fuzz/ingest.rs
  • rs/moq-net/src/fuzz/request.rs
  • rs/moq-net/src/ietf/mod.rs
  • rs/moq-net/src/ietf/request_stream.rs
  • rs/moq-net/src/ietf/subscriber.rs
  • rs/moq-net/src/lite/mod.rs
  • rs/moq-net/src/lite/subscriber.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

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-30T20:56:27.168772Z 758e2b7 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@kixelated

Copy link
Copy Markdown
Collaborator Author

Landing. Maintainer decision (2026-10-30): land the moq-net half of the wire fuzz targets. The quest keeps the cmsg targets and Miri. Codex reviewed 758e2b745e with no findings, and just check origin/main passed on it.

(Written by Claude Opus 5.5)

@kixelated
kixelated enabled auto-merge (squash) October 30, 2026 20:55

@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: 758e2b7.

[P2] Account for the consumed prefix, not an ignored suffix (rs/moq-net/src/fuzz/ingest.rs:182–188). For an untimed IETF subgroup without extensions, append an END_OF_GROUP status object followed by 100 arbitrary bytes. GroupIngest::poll deliberately returns Ok(Ended::Group) immediately at that status (ietf/subscriber.rs:6057–6066,6094–6097); it does not consume the suffix. With zero frames, this assertion nevertheless compares the entire input against header + 27, so the new nightly fuzz target reports a crash unrelated to a parser invariant violation. Record the consumed stream position and bound that prefix, or distinguish explicit termination from FIN; add a terminal-object-plus-suffix regression.

Overall direction: exercising the real ingest state machines with chunk-split invariance and keeping regression replay on stable is a useful, appropriately scoped approach. The byte-accounting oracle needs the termination distinction above. No prior published review findings from this automation were present.

Verification: static inspection of the diff and surrounding ingest/request code only; I did not run tests or libFuzzer.

@moq-bot
moq-bot Bot merged commit e01b0c0 into main Oct 31, 2026
7 checks passed
@moq-bot
moq-bot Bot deleted the quest/m2/wire-fuzz-coverage branch October 31, 2026 05:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant