Skip to content

test: cargo-fuzz harnesses for the decode boundary + bitrot guard + fuzzing guide - #1106

Merged
bryan-minimal merged 3 commits into
mainfrom
test/fuzzing-infrastructure
Jul 30, 2026
Merged

test: cargo-fuzz harnesses for the decode boundary + bitrot guard + fuzzing guide#1106
bryan-minimal merged 3 commits into
mainfrom
test/fuzzing-infrastructure

Conversation

@bryan-minimal

@bryan-minimal bryan-minimal commented Jul 30, 2026

Copy link
Copy Markdown
Member

Adds the cargo-fuzz suite for minimal's untrusted-decode boundary — the code that turns attacker-influenceable bytes into in-memory structures — plus a bitrot guard and docs/fuzzing.md so the campaign is pick-up-able.

This is the tooling behind six decoder bugs already fixed and merged: #651 (tar path traversal → arbitrary write), #653 (two allocation OOMs in the graph wire decoder), #656 (out-of-bounds slice panic + filename traversal), #661 (jq panic on malformed JSON), #693 (unreachable!() on a Subset build-dep).

The eight targets

Target Crate Decodes Trust Platform
graph_from_bytes graph Graph::from_bytes — remote-execution wire format NET any
graph_roundtrip graph structure-aware from_bytes(to_bytes(g)) == g any
remote_index_from_reader rcache IndexFile::from_readerindex.shisha NET Linux only
spec_hash_from_hex common SpecHash::from_hex NET any
target_from_str common Target::from_str OWN any
mfile_from_toml mfile minimal.toml via the custom serde visitors OWN any
arg_schema_parse args ArgSchema::try_from OWN any
jq_parse_json common jq::parse_file, JSON branch SUPPLY any

remote_index_from_reader is Linux-only because rcachelcache → the Linux-only common::renameat2.

Production-code surface is deliberately tiny

Everything else is new files under crates/*/fuzz/. Default builds are byte-for-byte unchanged — the fuzzing feature is off and the fuzz workspaces are excluded from the root members.

File Change
crates/graph/src/graph.rs Graph::fuzz_roundtrip behind a new off-by-default fuzzing feature; insert_build gated on any(test, feature = "fuzzing") instead of test alone
crates/graph/Cargo.toml, root Cargo.toml optional arbitrary, enabled only by that feature
crates/mfile/src/lib.rs File::from_toml_bytes — the pure, filesystem-free core of from_dir

Graphs aren't hand-constructible from outside the crate, hence the in-crate fuzz_roundtrip entry point rather than widening real public API.

just fuzz-check — the bitrot guard

Each fuzz/ dir is its own workspace so the nightly + sanitizer build can't perturb the main one. The cost: no workspace-wide build ever compiles these targets, so they rot silently. Not hypothetical — this suite sat 253 commits before being rebuilt, by which point one target referenced a renamed type (RemoteIndexIndexFile, #689).

just fuzz-check runs a plain cargo check over every fuzz workspace: stable, no nightly, no sanitizer — just "does this still compile against today's API." Cheap enough to treat like a red build.

just fuzz <crate> <target> [libfuzzer args] runs one target and applies the -rss_limit_mb cap that turns an unbounded-allocation bug into a catchable crash rather than an ambient OOM.

.github/workflows/ is frozen and CODEOWNER-gated, so per the test-extension contract in CONTRIBUTING.md this integrates through the justfile — following the miri / msrv precedent (opt-in recipes, not wired into just ci). If maintainers want it gated, fuzz-check is the recipe to add to a lane.

Validation

  • just fuzz-check — all four macOS-buildable fuzz workspaces check clean against current main.
  • cargo build -p graph -p mfile (feature off) and cargo build -p graph --features fuzzing both clean.
  • cargo clippy -p graph -p mfile --all-targets -- -D warnings, and again with --features fuzzing — clean.
  • cargo fmt --all -- --check — clean.
  • Pre-existing/unrelated: three spec_hasher hash-assertion tests fail on macOS (arch-pinned expected values). Verified identical failures with this branch's graph.rs change stashed — not introduced here.

Follow-ups (not in this PR)

  • Run remote_index_from_reader on Linux — it has never been executed, only compiled.
  • New targets for lcache::ReadTracker, the minimald-rpc request enums, and the paths/sessions containment gates.
  • A libFuzzer dictionary (JSON keys + wire tag bytes) and a persisted corpus for longer campaigns.

🤖 Generated with Claude Code

Note

Add cargo-fuzz harnesses for decode boundary testing across multiple crates

  • Adds cargo-fuzz workspaces for args, common, graph, mfile, and rcache crates, each with one or more fuzz targets covering parsing and decode entry points.
  • Introduces Graph::fuzz_roundtrip (behind a fuzzing feature flag) in crates/graph/src/graph.rs that encodes and decodes a generated Graph and asserts invariants, panicking on mismatch.
  • Adds File::from_toml_bytes to crates/mfile/src/lib.rs as a public API for parsing TOML content from raw bytes.
  • Adds fuzz-check and fuzz recipes to the justfile to catch bitrot and run targets with nightly under an RSS limit.
  • Adds docs/fuzzing.md covering setup, running, corpus seeding, and maintenance.

Macroscope summarized 0ac7bf8.

bryan-minimal and others added 3 commits July 30, 2026 13:19
Adds eight fuzz targets over the code that turns attacker-influenceable bytes
into in-memory structures — the sharpest trust boundary in the tree:

  graph_from_bytes          Graph::from_bytes (remote-execution wire format)
  graph_roundtrip           structure-aware from_bytes(to_bytes(g)) == g
  remote_index_from_reader  IndexFile::from_reader (index.shisha)  [Linux only]
  spec_hash_from_hex        SpecHash::from_hex
  target_from_str           Target::from_str
  mfile_from_toml           minimal.toml through the custom serde visitors
  arg_schema_parse          ArgSchema::try_from
  jq_parse_json             jq::parse_file, JSON branch

Each fuzz/ dir is its own workspace so the nightly + sanitizer build cannot
perturb the main one. Three small production hooks are needed:

  * graph: `Graph::fuzz_roundtrip` behind a new off-by-default `fuzzing`
    feature (graphs are not hand-constructible from outside the crate), and
    `insert_build` gated on `any(test, feature = "fuzzing")` rather than test
    alone. Default builds are unchanged.
  * mfile: `File::from_toml_bytes`, the pure filesystem-free core of
    `from_dir`, so the harness need not re-implement it.
  * workspace: `arbitrary`, optional and only enabled by graph's `fuzzing`.

Also carries the graph corpus seeds; a valid graph-with-local-file seed is
what let the fuzzer reach the local-file frame decoder.

This is the tooling behind the six decoder fixes already merged in #651,
#653, #656, #661 and #693.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Each fuzz/ dir declares its own [workspace], so no workspace-wide build ever
compiles the fuzz targets and they rot silently as the crates they fuzz
evolve. The first version of this suite went 253 commits before anyone
rebuilt it, by which point a target referenced a renamed type.

`just fuzz-check` is the guard: a plain `cargo check` over every fuzz
workspace. No nightly, no sanitizer, no libFuzzer runtime — just "does this
still compile against today's API", so it runs anywhere and is cheap enough
to treat like a red build.

`just fuzz <crate> <target> [args]` runs one target, applying the RSS cap
that turns an unbounded-allocation bug into a catchable crash rather than an
ambient OOM.

The rcache target is excluded on macOS (rcache -> lcache -> the Linux-only
common::renameat2), following the existing `scope` idiom.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Covers prerequisites, the eight targets and their trust levels, running via
the just recipes, corpus seeding (and why seeding is what unlocks the deep
decode paths), reproducing and minimizing a crash, keeping targets from
bitrotting, the bugs the campaign found, and where to take it next.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@bryan-minimal
bryan-minimal requested a review from a team as a code owner July 30, 2026 20:20
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 2 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0eb8e728-4eb3-4d7e-87f0-1a5024c01ce4

📥 Commits

Reviewing files that changed from the base of the PR and between 6c94948 and 0ac7bf8.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (26)
  • Cargo.toml
  • crates/args/fuzz/.gitignore
  • crates/args/fuzz/Cargo.toml
  • crates/args/fuzz/fuzz_targets/arg_schema_parse.rs
  • crates/common/fuzz/.gitignore
  • crates/common/fuzz/Cargo.toml
  • crates/common/fuzz/fuzz_targets/jq_parse_json.rs
  • crates/common/fuzz/fuzz_targets/spec_hash_from_hex.rs
  • crates/common/fuzz/fuzz_targets/target_from_str.rs
  • crates/graph/Cargo.toml
  • crates/graph/fuzz/.gitignore
  • crates/graph/fuzz/Cargo.toml
  • crates/graph/fuzz/fuzz_targets/graph_from_bytes.rs
  • crates/graph/fuzz/fuzz_targets/graph_roundtrip.rs
  • crates/graph/fuzz/seeds/seed_local_file
  • crates/graph/fuzz/seeds/seed_multi_local
  • crates/graph/src/graph.rs
  • crates/mfile/fuzz/.gitignore
  • crates/mfile/fuzz/Cargo.toml
  • crates/mfile/fuzz/fuzz_targets/mfile_from_toml.rs
  • crates/mfile/src/lib.rs
  • crates/rcache/fuzz/.gitignore
  • crates/rcache/fuzz/Cargo.toml
  • crates/rcache/fuzz/fuzz_targets/remote_index_from_reader.rs
  • docs/fuzzing.md
  • justfile

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

@bryan-minimal
bryan-minimal merged commit 7fc1197 into main Jul 30, 2026
30 checks passed
@bryan-minimal
bryan-minimal deleted the test/fuzzing-infrastructure branch July 30, 2026 20:51
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.

2 participants