Fuzz Corpus Management: Seeds, Minimization, and Long-Term Corpus Strategy

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:

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_input

Distilled 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.bin

For each crash:

  1. Run afl-tmin or libFuzzer -minimize_crash=1 to reduce to minimal reproducer
  2. Confirm reproducibility with sanitizers
  3. File bug report
  4. 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:

  1. Minimize first — reduce to coverage-representative subset
  2. Sanitize — remove inputs containing sensitive data (PII, internal IPs, credentials)
  3. Validate — confirm all seeds still execute cleanly
  4. 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.

Start now free