Skip to content

Actually fuzz on a schedule, instead of only replaying the corpus - #96

Merged
martinus merged 1 commit into
mainfrom
scheduled-fuzzing
Aug 5, 2026
Merged

Actually fuzz on a schedule, instead of only replaying the corpus#96
martinus merged 1 commit into
mainfrom
scheduled-fuzzing

Conversation

@martinus

@martinus martinus commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Closes #86.

Why

The 1651 inputs in data/fuzz/api are replayed by the test suite on every run. That is a regression guard and by construction can only find what has already been found — nothing was doing the finding. fuzz_api is even built in CI already, by the sanitizers job, because it is gated on the compiler being clang. It was just never run as a fuzzer.

It is worth running. A 30 second local session produced 26 inputs, 20 of which survived -merge=1 as coverage-increasing. The corpus is nowhere near saturated, and the defects this container has had — #63, #65 through #70, #74 — were exception-safety and aliasing bugs of exactly the kind a fuzzer reaches.

How

A nightly job (also startable by hand from the Actions tab, with a duration) builds fuzz_api under clang and runs it for ten minutes.

It cannot touch the committed corpus. libFuzzer writes new inputs to the first corpus directory it is given, so the job passes a scratch directory first and data/fuzz/api second — seeded from it, never writing to it. I verified this locally: after a 30 second run, git status on data/fuzz/api was clean and the file count was unchanged at 1651.

What it uploads is minimized. Afterwards it runs the same -merge=1 that scripts/fuzz_merge.sh does locally, then diffs against the committed corpus, so the corpus-new artifact is the handful of inputs that actually add coverage rather than everything the fuzzer happened to keep. Without that the artifact would grow without bound.

A crash fails the job loudly and uploads the reproducer as fuzz-crash.

The corpus policy, which the issue asked to decide

New entries are committed by hand, never by CI. The job uploads and stops. A job that pushes to the repository — or opens a PR — needs write access it has no other reason to have, and this corpus is small on purpose. Documented in CONTRIBUTING.md, which had no fuzzing section at all despite scripts/fuzz_run.sh and scripts/fuzz_merge.sh existing, along with how to run both locally and what to do with a crash.

Also

Drops -isystem /usr/lib64/clang/14.0.0/include/ from the fuzz compile arguments. That is a Fedora path for a clang eight major versions old; it exists on no CI runner and was only harmless because clang silently ignores an include directory that is not there. The sanitizers job builds fuzz_api, so this PR's own CI covers that change.

What this PR's CI does not cover

fuzz.yml only triggers on schedule and workflow_dispatch, so none of it runs on this PR — same as release.yml. That is why I gave it a workflow_dispatch trigger with a duration input: once this is merged I intend to dispatch a short run to prove the whole thing end to end before trusting the nightly. The parts I could verify locally are the fuzz, merge and diff pipeline, and that the target still builds without the stale include path.

🤖 Generated with Claude Code

The 1651 inputs in data/fuzz/api are replayed by the test suite on every run,
which is a regression guard and by construction can only find what has already
been found. Nothing was doing the finding. fuzz_api is even built in CI already,
by the sanitizers job, because it is gated on the compiler being clang -- it was
just never run as a fuzzer.

It is worth running: a 30 second local session produced 26 inputs, 20 of which
survived -merge=1 as coverage-increasing. The corpus is nowhere near saturated,
and the defects this container has had -- #63, #65 through #70, #74 -- were
exception safety and aliasing bugs of exactly the kind this reaches.

The job seeds from data/fuzz/api but writes new inputs to a scratch directory,
since libFuzzer writes to the first corpus directory it is given, so a run can
never modify what is committed. Afterwards it does the same -merge=1 that
scripts/fuzz_merge.sh does locally, so the uploaded artifact is the handful of
inputs that add coverage rather than everything the fuzzer happened to keep. A
crash fails the job and uploads the reproducer.

Committing what it finds stays a human decision, documented in CONTRIBUTING.md
along with the local workflow: a job that pushes to the repository needs write
access it has no other reason to have, and the corpus is small on purpose.

Also drops '-isystem /usr/lib64/clang/14.0.0/include/' from the fuzz arguments.
That is a Fedora path for a clang that is eight major versions old, it exists on
no CI runner, and it was only harmless because clang ignores an include
directory that is not there.

See #86.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@martinus
martinus force-pushed the scheduled-fuzzing branch from e0538c9 to c57bfef Compare August 5, 2026 03:40
@martinus
martinus merged commit c3ffda3 into main Aug 5, 2026
12 checks passed
@martinus
martinus deleted the scheduled-fuzzing branch August 5, 2026 03:41
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.

CI: run the fuzzer on a schedule instead of only replaying the corpus

1 participant