Repository navigation
test(net): fuzz group ingest and request framing with invariants - #7320
Conversation
Quest-Claim: 59a150c4dac89f40
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>
|
Warning Review limit reachedYou'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. View limit details
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. Comment |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
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 (Written by Claude Opus 5.5) |
kixelated
left a comment
There was a problem hiding this comment.
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.
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
ingesttarget (rs/moq-net/src/fuzz/ingest.rs) runs one group stream through lite'sFrameIngestor moq-transport'sGroupIngest. The input's first bytes pick the version and track settings: timed or not, compressed, theietfextension, and the publisher draft. Each input runs three ways: whole, one byte at a time, and in chunks whose size the input picks.requesttarget (rs/moq-net/src/fuzz/request.rs) reads a request stream throughNext<Framed>, at most one GOAWAY included.Body::messagemust 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.UpdateandNamespaceUpdatereaders 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:
MAX_CACHE_BYTES)MAX_PROPERTIES)MAX_OBJECT_EXTENSIONS)Before this, every seed body was six bytes or less.
stream_seeds_decodechecks 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 replaysfuzz/regressions/, and now coversrequest/andingest/too.To make the ingest machines reachable from the hidden
fuzzmodule,FrameIngest,GroupIngest,Framed,Next,Update, andNamespaceUpdateare nowpub(crate), re-exported only undercfg(any(test, feature = "fuzz")).GroupIngest::newnow takes the version instead of the wholeSubscriber.Findings
Next::decodelostneed()on a short type. It peeked the message type on a clonedDecoder, so a type cut short returnedShortwithneed() == 0.need()'s documented contract is that it is non-zero after a short read. Production is unaffected:Readerfalls back toneed().max(len + 1)and retries a byte at a time. Fix: a short peek now falls through toT, which reads the type on the real decoder. The input is committed asfuzz/regressions/request/draft22-next-short-type.rustuporcargo-fuzz, so I couldn't run libFuzzer. Instead, a throwaway stable-toolchain random mutator ran over the seeds (not committed): 400k inputs each foringestandrequest, and 200k each forlite,ietf, andannounce. Nightly's 5-minute libFuzzer runs are the first coverage-guided runs of the new targets.Choices made without asking
SubgroupRecv,FollowUp, andwhole_message, which the quest named, no longer exist; they were removed along with drafts 14–17 in moq-dev/moq#6753. Therequesttarget covers what replaced them:Body,Framed,Next,Update, andNamespaceUpdate.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
just rs miri, as both quests already say.fuzzjob innightly.yml. Expect a trivial merge conflict on thetarget: [...]line. If it adds a crate, it also needs a row inrs/justfile'sno_fuzz. Target names must stay unique across crates, becausesh/rs/fuzz.shresolves 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
Follow-ups
Review
quest review(quest pinned byorigin/main'sflake.nix).has_extensions, with regressionfuzz/regressions/ingest/draft22-untimed-extension-block.(Written by Claude Opus 5.5)
🤖 Generated with Claude Code