Plain-libFuzzer baseline ablation (no-LLM) for tree-sitter sweep - #161
Merged
Conversation
…ve-result blog draft Adds the discovery-side ablation that the neurosymbolic hypothesis needs, and the funder-facing writeup it feeds. Context: the hybrid fuzz->fix loop swept 118 tree-sitter grammars and found genuine memory-safety bugs in 9 (10 distinct crashes), all in hand-written scanner.c -- unlike the OSS-Fuzz/ARVO sweep, which found ~zero because those targets are already fuzzed to fixpoint. But for tree-sitter the harness is FIXED (bytes -> parser), so the LLM writes no harness; its only contribution is the fix phase. That makes the load-bearing question: does vanilla libFuzzer, no LLM anywhere, find the same bugs in the same walltime? New module `treesitter/baseline.py`: - `run_baseline_grammar` / `run_baseline_sweep`: reuse the hybrid's exact build, seed-corpus, and stack-hash triage, but with NO fixer and NO telemetry/keys in the path (imports only runtime/triage/models). Emits the same TSLoopIteration JSONL schema with fix=None so existing tooling reads both sweeps identically. - `targets_from_results`: reconstructs the identical grammar set AND each grammar's fuzzing walltime straight from the hybrid's JSONL outputs, so the comparison is apples-to-apples (LLM/rebuild time excluded -- generous to the baseline). - libFuzzer fork mode (`-fork=1 -ignore_crashes=1`) so a crash doesn't halt the campaign; the process keeps finding DISTINCT crashes, the fair analogue of the hybrid patching bug #1 to reach bug #2. Crash artifacts are deduped by stack hash; hang/OOM artifacts are classified from the filename without replay. MAX_ARTIFACTS_TRIAGED caps triage on flappy targets and records the cap in `notes` (never a silent truncation). Capped parallelism (`--jobs/-j`, ThreadPoolExecutor): - Each fuzz process is ~1 core (fork=1). The hybrid ran serially, one core per grammar, so keeping jobs <= physical cores preserves per-grammar throughput parity; oversubscription would starve each proc of CPU (fewer execs/sec, spurious wall-clock timeouts) and RAM (jobs x rss_limit), skewing the result. The CLI estimates physical cores (logical/2 for SMT), suggests `-j <phys>`, and warns past it. Shared tree-sitter runtime is cloned once up front to avoid a concurrent-clone race; per-grammar clones/work dirs/outputs are unique. - On this 24-thread / 12-physical-core box: ~24h serial -> ~2-3h at `-j 12`. New module `treesitter/baseline_compare.py`: - Diffs hybrid vs baseline sweeps on MEMORY-SAFETY stack hashes (buffer overflows, SEGV, UAF, scanner leaks), discounting timeout/OOM resource exhaustion (12 grammars, two generic stack signatures). Prints the headline "baseline reproduced N/M" overlap plus per-grammar shared / hybrid-only / baseline-only. Self-compare of the real hybrid data checks at 10/10. CLI: `treesitter baseline --hybrid DIR [-j N]` and `treesitter baseline-compare --hybrid DIR --baseline DIR`. Tests: `test_treesitter_baseline.py` (15 hermetic unit tests -- walltime summing, build-failure skipping, hang/OOM classification without replay, stack-hash dedup, triage cap, happy/no-crash/build-fail JSONL emission). Authored via the antigravity (agy/Gemini 3) executor, then verified here: pytest/ruff/ty all green. Blog: `comms/blog/main.md` drafted with the real numbers and a clearly-marked "PLACEHOLDER -- baseline not yet run" block where the overlap figure lands once the ablation runs. Frames the honest scope: on hardened targets the LLM adds nothing (fuzzer already sufficed); on soft targets the fuzzer alone plausibly finds the same bugs (pending this baseline); the LLM's defensible value is patching, not discovery. Baseline not yet executed -- code + tooling only. Reads results/treesitter/*.jsonl (gitignored; synced from the bucket) as input. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01No4gHVHxMx6qCDxCquCWxj
…ile) The baseline captures each libFuzzer proc's output (no live stream), never touched Logfire, and only printed a bare per-grammar "done" line to stderr — so a 2-3h detached run gave almost no signal. Adds a human heartbeat without changing the machine-readable per-grammar JSONL. - `run_baseline_sweep` progress callback now fires on COMPLETION with the grammar's iterations `(done, total, name, iters)`, in both serial and parallel paths, so callers can report the live outcome (crash class, build-fail, clean). - CLI `treesitter baseline` tees every line to stdout AND a run log (`<out>/baseline-run.log`, override with `--log-file`), each line UTC-stamped and the file line-buffered so `tail -f` works on a detached run. Emits: a start banner (grammar count, fuzzing-hours, jobs, wall ETA), a per-grammar line with outcome + running tally (crashing / total crashes / build-fails), and a final summary. Log handle closed in a `finally`. Verified via CliRunner with a stubbed sweep: stdout and the log file match, outcomes and tally render correctly. ruff/ty/pytest green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01No4gHVHxMx6qCDxCquCWxj
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.
Summary
Adds the discovery-side ablation the neurosymbolic hypothesis needs, plus the funder-facing writeup it feeds.
Context. The hybrid fuzz→fix loop swept 118 tree-sitter grammars and found genuine memory-safety bugs in 9 of them (10 distinct crashes), all in hand-written
scanner.c— unlike the OSS-Fuzz/ARVO sweep, which found ~zero because those targets are already fuzzed to fixpoint. But for tree-sitter the harness is fixed (bytes → parser), so the LLM writes no harness; its only contribution is the fix phase. That makes the load-bearing question: does vanilla libFuzzer — no LLM anywhere — find the same bugs in the same walltime?treesitter/baseline.py— plain-libFuzzer sweep, no fixer, no telemetry/keys in the path (imports onlyruntime/triage/models):targets_from_resultsreconstructs the identical grammar set and each grammar's fuzzing walltime straight from the hybrid's JSONL outputs → apples-to-apples (LLM/rebuild time excluded, generous to the baseline).TSLoopIterationJSONL schema withfix=None, so existing tooling reads both sweeps identically.-fork=1 -ignore_crashes=1) so a crash doesn't halt the campaign — keeps finding distinct crashes (the fair analogue of the hybrid patching bug Implement ARVO dataset ingestion #1 to reach bug Implement oss-fuzz data ingestion and target import #2). Crashes deduped by stack hash; hang/OOM classified from filename without replay;MAX_ARTIFACTS_TRIAGEDcaps flappy targets and records the cap innotes(no silent truncation).Capped parallelism (
--jobs/-j,ThreadPoolExecutor):fork=1). The hybrid ran serially, one core per grammar, so keepingjobs ≤ physical corespreserves per-grammar throughput parity; oversubscription starves each proc of CPU (fewer execs/sec, spurious wall-clock timeouts) and RAM (jobs × rss_limit), skewing the result. CLI estimates physical cores (logical/2 for SMT), suggests-j <phys>, warns past it. Shared runtime cloned once up front to avoid a concurrent-clone race.-j 12.treesitter/baseline_compare.py— diffs hybrid vs baseline on memory-safety stack hashes (overflows, SEGV, UAF, scanner leaks), discounting timeout/OOM resource exhaustion. Prints the headlinebaseline reproduced N/Moverlap plus per-grammar shared / hybrid-only / baseline-only. Self-compare of the real hybrid data checks at 10/10.CLI:
treesitter baseline --hybrid DIR [-j N]andtreesitter baseline-compare --hybrid DIR --baseline DIR.Blog (
comms/blog/main.md) — negative-result draft with the real numbers and a clearly-marked⚠️ PLACEHOLDER — baseline not yet runblock where the overlap figure lands. Honest scope: on hardened targets the LLM adds nothing; on soft targets the fuzzer alone plausibly finds the same bugs (pending this baseline); the LLM's defensible value is patching, not discovery.Test plan
uv run pytest src/tests/test_treesitter_baseline.py— 15 hermetic unit tests (walltime summing, build-failure skipping, hang/OOM classification without replay, stack-hash dedup, triage cap, JSONL emission). Authored via the antigravity (agy/Gemini 3) executor, verified here.uv run ruff check/ruff format/uv run ty check— clean on all new/changed files.treesitter baseline --help/baseline-comparerender; self-compare against real hybrid data reports 10/10.nix-shell scaffold/treesitter-shell.nix --run 'cd scaffold && uv run parser-security-eval treesitter baseline --hybrid results/treesitter -j 12 && uv run parser-security-eval treesitter baseline-compare --hybrid results/treesitter --baseline results/treesitter-baseline'→ fills the blog placeholder.🤖 Generated with Claude Code