Coverage-guided fuzzing across every mainstream engine - libFuzzer (C/C++ in-process), AFL++ (out-of-process, QEMU mode for closed-source binaries), cargo-fuzz (Rust), Go native fuzzing (go test -fuzz), Atheris (Python), and Jazzer (JVM, @FuzzTest). Body covers choosing the right fuzzer for the language and build type (the routing tree) plus the engine-generic workflow: writing a small deterministic fuzz target, seed-corpus + dictionary construction, sanitizer selection (ASan + UBSan default, compatibility matrix), corpus minimisation, crash-artifact handling, and CI smoke-fuzz wiring with a cached corpus. Per-engine depth (flags, harness syntax, CI jobs) lives in references, as do the corpus-management and sanitizer-integration catalogs. Use when a project needs fuzz coverage and no fuzzer is chosen yet, or when authoring / running / maintaining a fuzz campaign with any of these engines. For triaging the resulting crashes see crash-triage-reference.
70
88%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Shared reference for coverage-guided-fuzzing; the per-engine references
and the umbrella workflow live alongside this file.
Pure-reference catalog of corpus-management practices across libFuzzer, AFL++, native Go fuzz, cargo-fuzz, Atheris, Jazzer, and OSS-Fuzz. Consumed by the per-engine references and the fuzz-target-author agent.
A fuzzing corpus has three roles:
| Role | What it is | Where it lives |
|---|---|---|
| Seed corpus | Hand-curated initial inputs covering known interesting paths | Versioned in repo (fuzz/seeds/) |
| Evolved corpus | Inputs the fuzzer added because they hit new coverage | Output directory (fuzz/corpus/) - typically not committed |
| Crash artefacts | Inputs that triggered a sanitiser / crash / timeout | Output directory + bug-report attachments |
Per llvm.org/docs/LibFuzzer.html:
fuzz/
fuzz_target.cc # the harness
seeds/ # initial inputs (read on startup)
corpus/ # evolved corpus (read + written)
fuzz_target # compiled binaryInvocation:
./fuzz_target -max_total_time=3600 corpus/ seeds/The first directory listed is the output corpus (writable); subsequent directories are read-only seeds.
Crash artefacts saved to current directory as crash-<sha1>,
leak-<sha1>, timeout-<sha1> per LLVM docs.
fuzz/
inputs/ # seed corpus
output/ # AFL++ output (queue/, crashes/, hangs/)
fuzz_target # AFL-instrumented binary (afl-clang-fast)Invocation:
afl-fuzz -i inputs/ -o output/ -- ./fuzz_target @@Crash format: output/default/crashes/id:<num>,sig:<signal>,src:<id>,op:<mutator>,....
AFL++ corpora are not directly compatible with libFuzzer - the
queue files have a different on-disk format. Convert via
afl-fuzz -i + afl-cmin round-trip.
go test -fuzz)package/
fuzz_test.go # contains FuzzXxx functions
testdata/
fuzz/
FuzzXxx/
seedfile1 # seed inputs (hand-curated)
seedfile2Failures auto-write to testdata/fuzz/FuzzXxx/ with a generated
filename - they're meant to be committed as regression cases.
fuzz/
Cargo.toml
fuzz_targets/
fuzz_target_1.rs # the harness (one per fuzz target)
corpus/
fuzz_target_1/ # per-target corpus
artifacts/
fuzz_target_1/
crash-<sha1>Invocation:
cargo fuzz run fuzz_target_1fuzz/
fuzz_target.py # uses atheris.Setup + atheris.Fuzz
corpus/Invocation (Atheris uses libFuzzer's CLI under the hood):
python fuzz_target.py corpus/fuzz/
FuzzTarget.java # with @FuzzTest annotation
corpus/Invocation:
jazzer --cp=target/test-classes \
--target_class=com.example.FuzzTarget \
corpus/OSS-Fuzz aggregates corpora across Google's infrastructure:
oss-fuzz/
projects/<project>/
Dockerfile # build the fuzzer
build.sh # produces $OUT/fuzz_target_1, $OUT/fuzz_target_1_seed_corpus.zipPer google.github.io/oss-fuzz.
The corpus syncs to gs://<project>-corpus.clusterfuzz-external.appspot.com/.
| Strategy | When | Example |
|---|---|---|
| From spec keywords | New target, no inputs exist | Extract JSON keywords from a JSON parser spec, write each as a tiny file |
| From test fixtures | Existing unit-test inputs cover paths | Copy fixtures from tests/fixtures/*.json to seeds/ |
| From production data | Mature target, prod logs available | Sample 1000 prod requests, strip PII per qa-test-data-privacy's pii-categories catalog (pii-masking-pipeline-builder references/), seed |
| From corpus minimisation | Reduce a large corpus to its coverage-equivalent core | Run afl-cmin or libFuzzer -merge=1 |
| From OSS-Fuzz cousin | Same format, different target | Reuse <format>_seed_corpus.zip from a related OSS-Fuzz project |
A seed corpus of 5-50 hand-curated diverse inputs is typically enough to bootstrap. Bigger isn't always better - the fuzzer finds new paths via mutation.
A dictionary lists tokens the fuzzer prefers when mutating. For a JSON parser:
# fuzz.dict
"{"
"}"
"["
"]"
"true"
"false"
"null"
"\":\""Invocation: libFuzzer -dict=fuzz.dict or afl-fuzz -x fuzz.dict.
Dictionaries dramatically improve fuzzer effectiveness on structured formats (JSON, XML, protobuf, SQL).
A corpus that grows over time becomes redundant - many inputs hit the same coverage. Minimise periodically:
# libFuzzer merge mode
mkdir minimised/
./fuzz_target -merge=1 minimised/ corpus/
# AFL++ minimisation
afl-cmin -i corpus/ -o minimised/ -- ./fuzz_target @@
# Per-input minimisation (find smallest input triggering same coverage)
afl-tmin -i crash-input -o min-crash-input -- ./fuzz_target @@Minimisation reduces fuzz cycle time + reproducibility surface.
When the fuzzer finds a crash:
./fuzz_target crash-<sha1>
# Triggers the same sanitiser reportbug-report-template (qa-bug-repro plugin) with the minimised crash
as an attachment.Long-running fuzz campaigns run continuously; CI runs short "smoke fuzz" campaigns (~5 min):
- name: Smoke fuzz
run: ./fuzz_target -max_total_time=300 corpus/ seeds/Persist evolved corpus to a CI cache so coverage accumulates across runs:
- uses: actions/cache@v4
with:
path: corpus/
key: fuzz-corpus-${{ github.sha }}
restore-keys: fuzz-corpus-| Anti-pattern | Why it fails | Fix |
|---|---|---|
| No seed corpus | Fuzzer wanders randomly; takes hours to find shallow bugs | Always provide 5-50 hand-curated seeds |
| Committing evolved corpus to repo | Repo bloats; PR diffs hide signal | Persist via CI cache or object storage |
| Mixing seed and evolved corpus in one directory | Loses provenance | Separate directories; seeds read-only |
| No dictionary for structured formats | Fuzzer spends cycles re-discovering keywords | Always provide a dict for JSON / XML / protobuf / SQL |
| Never minimising | Cycle time grows; coverage redundant | Minimise weekly or per major change |
| Crash artefact deleted after fix | Lose regression coverage | Add minimised crash to seed corpus |
| Single fuzzer assumed | Different fuzzers find different bugs | Run libFuzzer + AFL++ on same target periodically |
| Sharing prod-sourced corpus without PII review | GDPR / HIPAA leak | Pass corpus through PII detection before sharing |
coverage-guided-fuzzing umbrella (../SKILL.md).