Fuzz Corpus Management: Seeds, Minimization, and Long-Term Corpus Strategy
The corpus is the fuzzer's accumulated learning — a collection of inputs that together exercise the target's reachable code space. Corpus quality determines fuzzing quality.
A poorly seeded corpus means the fuzzer spends days rediscovering what valid inputs look like. A bloated corpus means millions of redundant entries slowing each cycle. A well-maintained corpus amplifies fuzzer effectiveness by orders of magnitude.
Seed Selection: Starting Corpus Quality
The seed corpus is where the fuzzer begins. Good seeds reduce time to coverage; bad seeds waste it.
What makes a good seed
Valid structure: Seeds should be valid (or near-valid) examples of the target input format. A fuzzer given random bytes for a PDF parser spends enormous effort generating syntactically valid PDFs. Start with real PDFs and the fuzzer immediately reaches the interesting logic.
Diverse coverage: Seeds should collectively cover as many code paths as possible. Don't use 1,000 variations of the same PDF — use one simple PDF, one complex PDF, one encrypted PDF, one PDF with embedded images.
Minimal size: Smaller seeds are better starting points. A 100-byte input that exercises the same code as a 10MB input is strictly better — mutations explore more variation per byte.
Real-world inputs: The best seeds are actual production inputs. They represent the input space the software encounters in practice, including format quirks and edge cases that synthetic seeds miss.
Seed sources
Unit test fixtures: Your test suite likely contains example inputs. Extract them.
Real-world corpora: Public collections exist for common formats:
- afl-testcases: audio, images, archives, docs
- fuzzing-corpus: organized by format
- Network captures for protocol fuzzing
Generated seeds: For formats without public corpora, write a seed generator. Generate the minimal valid example that exercises each major parsing path.
Coverage-driven seed selection: Run the target with AddressSanitizer and a few manual inputs. Use llvm-cov to identify uncovered code paths. Write seeds that cover those paths.
Seed validation
Before using seeds, validate them against the target:
# AFL++: check all seeds execute without crash
afl-cmin -i seeds/ -o seeds_validated/ -- ./target @@
# libFuzzer: run all seeds once
./target_fuzz seeds/Seeds that crash the target immediately are bugs — file them. Seeds that time out should be excluded. Only clean-executing seeds belong in the initial corpus.
The Three Corpus Operations
1. Corpus merging
When you have multiple corpus directories (from parallel fuzzing runs, or combining an existing corpus with new inputs), merge them:
# libFuzzer merge: produces minimized union of two corpora
./target_fuzz -merge=1 merged_corpus/ corpus_a/ corpus_b/
# AFL++ merge: combines and deduplicates
afl-cmin -i all_inputs/ -o merged_corpus/ -- ./target @@Merge regularly during long campaigns to share coverage between parallel fuzzer instances.
2. Corpus minimization (cmin)
afl-cmin / libFuzzer -merge removes redundant corpus entries — those whose coverage is a subset of another entry's coverage.
# AFL++ corpus minimization
afl-cmin -i corpus/ -o corpus_min/ -- ./target @@
# libFuzzer corpus minimization (merge with empty destination)
./target_fuzz -merge=1 corpus_min/ corpus/A corpus of 100,000 inputs covering 50,000 edges likely reduces to 5,000–10,000 inputs covering the same edges after minimization. Same coverage, 10–20x fewer entries, dramatically faster cycling.
Run minimization:
- Weekly during active campaigns
- Before committing corpus to source control
- Before sharing corpus with others
3. Input distillation (tmin)
Minimization reduces the number of corpus entries. Distillation reduces the size of individual entries — finding the smallest input that triggers the same coverage.
# AFL++: minimize a single input
afl-tmin -i large_input.bin -o minimal_input.bin -- ./target @@
# libFuzzer: minimize a crash (or any input)
./target_fuzz -minimize_crash=1 -runs=100000 crash_inputDistilled inputs:
- Are faster to mutate (fewer bytes = more variation per mutation)
- Are easier to understand (analyzing why 8 bytes crash is simpler than analyzing why 8KB crashes)
- Produce clearer crash reports
Distill crash inputs before filing bug reports. A minimized crash that reproduces in 20 bytes is much more useful than an 8KB crash input.
Managing Long-Running Campaigns
Fuzzing at scale — multiple machines, weeks of runtime — requires systematic corpus management.
Corpus checkpointing
Save corpus state at regular intervals:
# Snapshot current corpus
cp -r output/queue/ snapshots/$(date +%Y%m%d)/Checkpoints let you:
- Track coverage growth over time
- Roll back if a bad corpus entry causes systematic timeouts
- Analyze which inputs were added in which time window
Parallel fuzzing corpus sharing
AFL++ parallel instances share a corpus directory automatically via rsync or shared filesystem. libFuzzer parallel instances need periodic merging:
# Run 8 parallel libFuzzer instances
for i in {0..7}; do
./target_fuzz -jobs=1 -workers=1 corpus_$i/ &
done
# Periodically merge (every 4 hours)
./target_fuzz -merge=1 merged/ corpus_*/Sharing coverage between instances accelerates progress. Each instance explores different mutations; coverage found by one feeds into mutations by others.
Corpus growth tracking
Track corpus size over time. A healthy campaign shows:
- Rapid initial growth (new coverage found easily)
- Slower growth (coverage requires deeper exploration)
- Near-plateau (most reachable coverage found)
A corpus that grows linearly forever usually means: the target has effectively infinite state space (network protocols, stateful APIs), or the coverage model is too coarse (catching too many trivial variations).
A corpus that stops growing immediately usually means: the seeds are too restrictive (not reaching interesting code), or the fuzz target is rejecting too many inputs before reaching the target function.
Corpus pruning schedule
| Frequency | Operation | Purpose |
|---|---|---|
| Daily | Merge parallel instances | Share coverage |
| Weekly | afl-cmin / libFuzzer merge |
Remove redundant entries |
| Monthly | afl-tmin on large entries |
Reduce individual input sizes |
| Per campaign | Full minimization before archiving | Clean storage |
Corpus Storage and Versioning
Git for corpus storage
Small corpora (< 100MB) store well in Git:
git add corpus/
git commit -m "corpus: add seeds for JSON parser fuzzing"Benefits: versioned, reviewable, reproducible fuzzing campaigns.
Limitations: large binary files bloat Git history. Use Git LFS for corpora > 100MB.
Corpus naming and organization
Organize corpora by target, not by date:
corpora/
json_parser/
seeds/ # initial seeds
corpus/ # accumulated fuzzing corpus (minimized)
crashes/ # crash-producing inputs
http_handler/
seeds/
corpus/
crashes/Each target gets its own corpus. Sharing corpora between functionally different targets wastes cycles on irrelevant mutations.
Crash corpus management
Crashes need separate management:
crashes/
2026-05-01_heap-overflow_6a3b2f.bin # date_type_hash
2026-05-15_null-deref_1c7d8e.binFor each crash:
- Run
afl-tminor libFuzzer-minimize_crash=1to reduce to minimal reproducer - Confirm reproducibility with sanitizers
- File bug report
- Keep in corpus (the bug might be fixed but the input remains a useful coverage entry)
Deduplicate crashes using stack traces. AFL++ afl-collect and libFuzzer's -dedup_coverage_opts help automate this.
Corpus Transfer and Sharing
Sharing across teams
When sharing a corpus between teams or open-sourcing it:
- Minimize first — reduce to coverage-representative subset
- Sanitize — remove inputs containing sensitive data (PII, internal IPs, credentials)
- Validate — confirm all seeds still execute cleanly
- Document — note which target version the corpus was generated against
Stale corpora from different target versions may contain inputs that crash the current target as false bugs. Version-pin corpus releases.
OSS-Fuzz corpus access
Google's OSS-Fuzz provides accumulated corpora for all projects it fuzzes. For open source targets:
# Download corpus for a project
gsutil cp gs://oss-fuzz-clusterfuzz-bucket/latest/corpus.zip .Starting with an existing OSS-Fuzz corpus can save months of fuzzing — it represents years of accumulated coverage.
Seed Quality: The Multiplier
Seed quality compounds over time. A fuzzer with excellent seeds finds deep bugs faster, because the corpus starts at a higher coverage baseline and mutations explore variation rather than rediscovering basics.
Benchmark: run the fuzzer from your new seeds vs. from random seeds for 24 hours. Compare:
- Edge coverage achieved
- Unique crashes found
- Mean corpus size
If the random-seed run achieves similar coverage, your seeds weren't adding value. If your seeds achieve 2–3x more coverage in the same time, they're doing their job.
Invest time in seeds proportional to how long you'll fuzz. For a one-time 1-hour campaign, good seeds matter less. For a continuous fuzzing campaign running for months, seed quality is one of your highest-leverage investments.
Summary
Corpus management is the operational side of fuzzing. Good seeds reduce warm-up time. Regular minimization keeps the corpus efficient. Crash minimization makes bugs reproducible and reportable.
The core routine: start with real-world seeds, fuzz continuously, merge and minimize regularly, commit minimized crashes as regression tests.
A maintained corpus is an asset that improves over time. An unmanaged corpus becomes bloated noise.