Skip to content

Plain-libFuzzer baseline ablation (no-LLM) for tree-sitter sweep - #161

Merged
quinn-dougherty merged 2 commits into
mainfrom
issue-154-baseline-ablation
Aug 23, 2026
Merged

Plain-libFuzzer baseline ablation (no-LLM) for tree-sitter sweep#161
quinn-dougherty merged 2 commits into
mainfrom
issue-154-baseline-ablation

Conversation

@quinn-dougherty

Copy link
Copy Markdown
Member

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 only runtime/triage/models):

  • targets_from_results reconstructs 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).
  • Emits the same TSLoopIteration JSONL schema with fix=None, so existing tooling reads both sweeps identically.
  • libFuzzer fork mode (-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_TRIAGED caps flappy targets and records the cap in notes (no 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 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.
  • Reference box (24 threads / 12 physical cores): ~24h serial → ~2–3h at -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 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.

Blog (comms/blog/main.md) — negative-result draft with the real numbers and a clearly-marked ⚠️ PLACEHOLDER — baseline not yet run block 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.

Note: the baseline has not been executed yet — this PR is code + tooling only. It reads results/treesitter/*.jsonl (gitignored; synced from the bucket) as input.

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.
  • Existing treesitter tests still pass.
  • treesitter baseline --help / baseline-compare render; self-compare against real hybrid data reports 10/10.
  • Run the ablation (needs nix toolchain): 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

quinn-dougherty and others added 2 commits August 19, 2026 10:26
…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
@quinn-dougherty
quinn-dougherty merged commit 524bfcb into main Aug 23, 2026
1 check passed
@quinn-dougherty
quinn-dougherty deleted the issue-154-baseline-ablation branch August 24, 2026 12:35
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.

1 participant