Skip to content

fix(graph): don't panic hashing a spec with a Subset build-dep - #693

Merged
twitchyliquid64 merged 1 commit into
mainfrom
fix/spec-hasher-subset-panic
Jul 9, 2026
Merged

fix(graph): don't panic hashing a spec with a Subset build-dep#693
twitchyliquid64 merged 1 commit into
mainfrom
fix/spec-hasher-subset-panic

Conversation

@bryan-minimal

Copy link
Copy Markdown
Member

A BuildDep::Subset in build_deps crashes spec-hash computation.

build_attrs_hash (spec_hasher.rs:244) filtered build_deps with .filter(|i| i.as_build().is_none()), which only excludes the Build variant. A Subset therefore reached build_input_hash, whose first arm is Build(_) | Subset(_) => unreachable!() (:167) -> panic.

Subset build-deps are a real state: loader.rs:116 constructs them and SpecHasher::process (:106) already handles them as edges. So hashing any spec with a subset build-dep aborts the process (spec-hash is computed during planning/build, so this is not niche).

Fix: exclude Subset from the inline-input pass (it is already hashed as an edge by index). No hash values change — specs without subset build-deps are byte-identical, and specs with them previously panicked — so there is zero cache invalidation. Regression test included (it panics before the fix, passes after).

Found during an Aeneas/Lean formal-verification scoping pass on the spec hasher; a broader tracking issue (collision/prefix-free rewrite + verification roadmap) is coming separately.

🤖 Generated with Claude Code

build_attrs_hash filtered build_deps with `.filter(|i| i.as_build().is_none())`,
which only excludes the Build variant — so a BuildDep::Subset fell through to
build_input_hash, whose `Build(_) | Subset(_) => unreachable!()` arm panicked.
Subset build-deps are a real, supported state (constructed in loader.rs:116,
and SpecHasher::process already handles them as edges), so hashing any spec
with one aborted the process.

Exclude Subset from the inline-input pass too; it's already hashed as an edge.
No existing hash changes (specs without subset build-deps are unaffected; specs
with them previously panicked), so there is no cache invalidation. Adds a
regression test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 9, 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: 39 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: 6374ba1d-d9e6-42c2-8e00-144a3fd61fc7

📥 Commits

Reviewing files that changed from the base of the PR and between 71b364b and db3e788.

📒 Files selected for processing (1)
  • crates/graph/src/spec_hasher.rs

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

@bryan-minimal
bryan-minimal requested a review from msample July 9, 2026 21:23
@twitchyliquid64
twitchyliquid64 merged commit cd1c95e into main Jul 9, 2026
9 checks passed
@twitchyliquid64
twitchyliquid64 deleted the fix/spec-hasher-subset-panic branch July 9, 2026 21:45
bryan-minimal added a commit that referenced this pull request Jul 30, 2026
…uzzing guide (#1106)

* test: add cargo-fuzz harnesses for the untrusted-decode boundary

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>

* build(justfile): add fuzz-check bitrot guard and a fuzz runner

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>

* docs: add the fuzzing guide

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>

---------

Co-authored-by: Claude Opus 5 (1M context) <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.

2 participants