fix(rust): seed libFuzzer fuzz_target! harnesses as reachability entry points - #228
Merged
Conversation
…Go-wire --library-mode Recovers the untrusted decode surface a QUIC/parser library exposes via its libFuzzer harnesses (neqo 782->903), by lifting each fuzz_target! closure body into a synthetic entry-point unit whose nested fns/impls become their own same-file units. Keeps the blackout safety net for pure-fuzz-only libraries. Wires the pre-existing --library-mode flag into the Go parse/scan CLI. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The lifted fuzz_target! harness seeds reachability so its decode callees are analyzed, but the harness body is synthetic instrumentation (references the dropped closure param) — emitting it as a Stage-1 unit shipped non-compiling code to the LLM and re-analyzed the decoder. Skip it in dataset unit generation (kept in analyzer_output for call-graph symmetry). neqo 903 reachable, 891 units. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nesses test_every_extracted_function_has_a_unit asserted unit_ids == all extracted functions; seed-only makes that conditionally false (a fuzz_target! harness is in functions but not a dataset unit). The fixture has no harness so it passed, but the invariant was latent-broken. Exclude synthetic_harness from the RHS. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The macro-body call scanner (_MACRO_CALL_RE / _scan_macro_body) recovered only bare and dotted call names from a macro's opaque token tree, dropping the `::` qualifier on scoped calls. So `assert!(Type::method(d))` degraded to a bare `method` resolved same-file-only, yielding no edge for a cross-file associated/module fn. On synthetic fuzz_target! harnesses — whose most common idiom is `assert!(Type::method(fuzz_input))` — this seeded zero edges, silently pruning the decode target in hybrid repos (a real entry point present, so the keep-all net does not fire). Extend the regex to capture a scoped prefix and emit a `scoped` site routed through the existing _resolve_scoped path (resolver unchanged). Add-only: neqo (production config) reachable set 966->966 unchanged, +211 edges, the 5 removed edges all phantom retargets to the correct scoped target. Tests: 3 added (harness reachability + macro-scanner scoped/bare units), via `grep -c "def test_"` -> fuzz_target.py 13->14, macro_string_literal.py 2->4. Full suite: pytest tests/ -q -> 2685 passed, 30 skipped (was 2682). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adding parseLibraryMode widened the var() block; re-align the existing fields so gofmt -l is clean (master baseline was clean). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
/work-audit caught B-docs drift: the docstring still said the scanner 'can only find bare/dotted names' after the fix added scoped (Type::method) recovery. Comment-only; no behavior change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…param)
The expr-form closure lift in _handle_fuzz_target had two silent
false-negatives found by an adversarial bug-hunt:
- A trailing `//` line comment inside the macro parens
(`fuzz_target!(|d| Frame::decode(d) // note\n)`) was swallowed by the
appended `;}`, making the synthesized `fn` unparseable -> the harness was
dropped with no warning, removing the fuzzed path from reachability in
hybrid repos.
- A struct-destructure closure param (`|Wrap { inner }: Wrap| body`) was
mis-lifted: the param's `{ inner }` brace was taken as the body, dropping
the real expression.
Both stem from not respecting the closure-param boundary. Compute the
second top-level `|` first; only braces AFTER it are body candidates
(excludes the param destructure), and terminate the expr body on a fresh
line so a trailing line comment can't swallow the terminator.
Add-only: neqo (production config) reachable set unchanged (harnesses there
are block-form); this only recovers previously-dropped expr-form harnesses.
Tests: 2 added (grep -c def test_ -> test_rust_fuzz_target.py 14->16).
Full suite: pytest tests/ -q -> 2687 passed, 30 skipped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The F1 macro scoped-call recovery could DROP an edge master produced: when a scoped call's qualifier binds nothing in _resolve_scoped Steps 1-3 and the leaf name is shared by a free fn AND a type method, Step 4's raw unique-name fallback saw len(candidates)==2 and returned [] -- whereas the pre-scoped bare capture ran _resolve_bare, which free-filters and returns the unique free function. So `assert!(alpha::beta::c(1))` with a free `c` and a method `S::c` lost the `-> c` edge (master had it). Delegate Step 4's ambiguous branch to _resolve_bare(leaf) -- exactly what the bare path returns (free-filter + import/external handling). Purely additive: only reached where Step 4 previously returned []. Measured neqo (production config) master -> HEAD: reachable 854->967 (+113, -0), edges +305 with the 5 removed all bare->scoped RETARGETS to the correct method (0 pure loss, 0 reachability lost) -- add-only now holds at the reachable-set level and the edge collision-loss is closed. Tests: 1 added (grep -c def test_ -> test_rust_macro_string_literal.py 4->5). Full suite: pytest tests/ -q -> 2688 passed, 30 skipped. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
gadievron
requested review from
dgeyshis,
shahar-davidson and
sounil
as code owners
August 13, 2026 01:27
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Treat libFuzzer
fuzz_target!harnesses as reachability entry points in the Rust parser, so a library's fuzzed untrusted-decode surface is analyzed instead of pruned as dead code.Why
OpenAnt's reachability filter seeds a forward BFS from entry points (main / route / CLI handler). A pure library ships none of those, so its public decode API — the exact surface a fuzz harness drives — was pruned before analysis. A crate's
fuzz_target!harnesses are the authors' in-code declaration of that untrusted-input surface, but the Rust parser saw them as empty macro bodies: tree-sitter leaves a macro's token-tree opaque, so the closure body emitted nofunction_itemand its decode calls never entered the call graph (libs/openant-core/parsers/rust/function_extractor.py).How
fuzz_target!closure body, lifted as ordinary Rust and walked so nestedfn/impl/structbecome their own same-file units (function_extractor.py_handle_fuzz_target). Seed-only: the harness is kept inanalyzer_outputfor call-graph symmetry but excluded from Stage-1 analysis units (unit_generator.py:51).parsers/rust/test_pipeline.py).Type::method(calls inside scannable macros (assert!/assert_eq!/ …), so the most common harness idiomassert!(Type::roundtrip(d))seeds its target rather than zero edges (call_graph_builder.py_MACRO_CALL_RE+_scan_macro_body, routed through the existing_resolve_scoped).//comment no longer swallows the body terminator and drops the harness, and a struct-destructure closure param (|Wrap { inner }: Wrap| …) is no longer mis-lifted as the body (function_extractor.py).call_graph_builder.py_resolve_scopedStep 4).--library-modethrough the Go CLI on both the parse and scan paths (apps/openant-cli/cmd/parse.go,scan.go).Reachability impact
Measured whole-branch vs
master, production config (--skip-tests), via the real pipeline:tests::is_known_type → WebTransportFrame.is_known_type), 0 pure loss.The change adds real reachability (recovers the fuzz/decode/scoped surface) and removes phantom production→test reachability. On hyper the branch is reachable −2 (
tests::length,Encoder.is_eof) — a correct de-phantoming, not a regression:mastermis-resolved a productionEncoder::length(...)call to a same-named#[cfg(test)]helperlength, and scoped recovery now binds it to the real method. Both de-reached nodes were reachable only through that production→test phantom (verified:tests::length's solemasterinbound was the productionServer.encode_headers). No genuinely-reachable production code is de-reached.Tests
19 new test functions (16 in
test_rust_fuzz_target.py, 3 intest_rust_macro_string_literal.py), each a RED→GREEN regression for one behavior above. Static analysis on the changed files:ruff0,go vet0,semgrep9 files / 373 rules / 0 findings,codeqlpython-security-and-quality 174 queries / 0 findings.Compatibility
No breaking change.
fuzz_target!seeding is default-on and structural (no LLM call).--library-modeis opt-in. The keep-all net only widens output (never prunes more) for pure-library-plus-fuzz repos.Notes
--library-mode); 607 LOC. Cohesive around one feature (library-reachability via fuzz harnesses).tests/parsers/rust/test_callgraph_symmetry.py(only Python ships that canonical file today). Suggest a follow-up infra PR rather than bundling it.Author notes
function_extractor.py_handle_fuzz_target(harness synthesis + expr-form lift);call_graph_builder.py_MACRO_CALL_RE/_scan_macro_body(scoped-call recovery) and_resolve_scopedStep 4 (add-only fallback);unit_generator.py:51(seed-only skip);test_pipeline.py(keep-all net).fuzz_target!(|d| { assert!(Codec::roundtrip(d)); })— old code seeded zero edges and silently prunedCodec::roundtripand its transitive decode sink; new code seeds them.masterworktree across 8 repos.