| name | libfuzzer-seed-generator |
| description | Generate high-quality initial seed corpora for LibFuzzer / AFL fuzz harnesses written for a C or C++ library. Use this skill whenever a user has one or more fuzz harness (.c/.cpp) files and wants to create, improve, or evaluate the seed corpus that bootstraps fuzzing. Also trigger when the user asks about "initial seeds", "seed corpus", "corpus generation", "fuzzing inputs", or says something like "the fuzzer isn't finding anything" or "how do I start fuzzing". Works with OSS-Fuzz projects and custom harnesses alike.
|
LibFuzzer Seed Generator
Good initial seeds dramatically accelerate fuzzing: they give the fuzzer diverse, valid
starting points so it spends its time exploring interesting program states rather than
trying to guess the input format from scratch.
This skill walks through five phases:
- Discover — find every harness in the project automatically
- Classify — determine which harnesses already have seeds (OSS-Fuzz native) vs. which need them (custom)
- Analyze — read each custom harness to understand its input format
- Generate — create seeds matched to the format: reuse existing corpus where compatible, hand-craft otherwise
- Validate — run seeds under AddressSanitizer and measure edge coverage with LibFuzzer's built-in
-runs=0 mode
Phase 1 — Discover Harnesses
Run the bundled discovery script to find every harness and detect which are OSS-Fuzz native:
python3 <skill_dir>/scripts/discover_fuzzers.py <project_fuzzing_dir>
The script:
- Globs for
*fuzzer*.c, *fuzzer*.cpp, *fuzz*.c, *fuzz*.cpp
- Reads
ossfuzz.sh (and CMakeLists.txt, build.sh) to find which harnesses are compiled and shipped to OSS-Fuzz
- Prints a classified list:
[OSS-Fuzz native] vs [custom — needs seeds]
Do not skip this step. Projects grow; new harnesses are added without updating seed corpora. Always discover first.
Phase 2 — Classify
After running the discovery script, you'll have two groups:
| Class | Action |
|---|
OSS-Fuzz native and a seed corpus already exists in inputs/ | Verify seeds are valid (Phase 5), then skip generation |
| OSS-Fuzz native but no local seeds | Download corpus from OSS-Fuzz GCS (gs://oss-fuzz-corpus/<project>/) if available; otherwise treat as custom |
| Custom | Proceed to Phase 3 |
Phase 3 — Analyze Input Format
Read the harness source carefully. The goal is to answer: "What byte sequence does LLVMFuzzerTestOneInput(data, size) expect?"
Common format patterns
Type A — Raw content (no framing)
The harness passes data directly to a parse/decode function with no header.
json = cJSON_ParseWithLength((const char*)data, size);
Seeds = valid content bytes (JSON, XML, image bytes, etc.)
Type B — Fixed ASCII header + content + NUL
The first N bytes are ASCII flag characters ('0'/'1', 'b'/'f', etc.); the remainder is
content; the last byte must be '\0'.
if (data[0] != '0' && data[0] != '1') return 0;
...
json = cJSON_ParseWithOpts((const char*)data + 4, NULL, require_termination);
Seeds = [flag bytes][content]\0
Type C — Binary fractional header + Consumer regions
A short binary header encodes how to split the payload into independent regions, each consumed
by a different sub-function.
size_t t = ((size_t)data[0] * plen) / 256;
Seeds = binary header bytes + concatenated region payloads. Design each region's bytes to
exercise the target sub-function (scalars, arrays, objects, typed arrays, etc.).
Type D — Op-record array + content payload
The first byte is n_ops; the next n_ops × RECORD_SIZE bytes are fixed-size operation
records; the remainder is content parsed normally.
n_ops = data[0] % (MAX_OPS + 1);
ops_bytes = n_ops * OP_RECORD_SIZE;
json_off = 1 + ops_bytes;
root = cJSON_ParseWithLength((const char*)data + json_off, size - json_off);
Seeds = [n_ops u8][op records][content bytes]. Each op record exercises a specific
mutation/query operation.
Type E — Custom binary protocol (Consumer struct)
The harness uses a stateful Consumer / FuzzedDataProvider to pull typed fields
(u8, u16, float, length-prefixed strings) in a defined order.
Seeds must follow the exact consumption order documented in the harness header comments.
Seed reuse decision
Compare the custom harness's byte-stream consumer against the OSS-Fuzz native harness:
- Same consumer (e.g., both call
cJSON_ParseWithLength(data, size) on raw bytes) →
existing seeds are compatible; strip any header bytes the native harness adds and reuse.
- Different consumer → seeds are not directly compatible; generate from scratch.
Phase 4 — Generate Seeds
Create a seeds_<harness_name>/ directory alongside inputs/.
Seed generation principles
-
Cover the happy path first — valid, well-formed inputs that parse successfully.
These let the harness reach the interesting post-parse code.
-
Cover boundary conditions — empty input, single-element containers, max-depth nesting,
zero-length strings, extreme numeric values (0, -1, MAX, NaN, Inf).
-
Cover failure paths — deliberately malformed inputs so the harness exercises error
handling and GetErrorPtr-style APIs.
-
Cover all opcode / operation variants — for Type D harnesses, include seeds for every
opcode value so each branch is reachable from day one.
-
Keep seeds small — LibFuzzer mutates seeds byte by byte; smaller seeds mutate faster.
Aim for < 256 bytes per seed unless the format genuinely requires more.
-
Name seeds descriptively — use names like array_3_items, op_detach_reinsert,
deep_nested_object so the corpus is self-documenting.
Reusing existing corpus seeds
When seeds are compatible (same byte-stream consumer), copy or transform them:
for f in Path("inputs").iterdir():
raw = f.read_bytes()
if len(raw) > 2 and raw[0] in (ord('b'), ord('u')):
json_bytes = raw[2:]
if json_bytes[:1] in (b'{', b'[', b'"', b't', b'f', b'n'):
(out_dir / f.name).write_bytes(json_bytes)
Binary seed helpers
For Types C, D, E, build seeds programmatically. Use struct.pack('<d', v) for doubles,
struct.pack('<H', v) for uint16, and length-prefixed strings bytes([len(s)]) + s.encode().
See scripts/discover_fuzzers.py for helper utilities you can import.
Phase 5 — Validate Seeds
Run the bundled validation script. Use the pre-built ASan+fuzzer-no-link archive from
libfuzzer-lib-builder if available — seed validation does not require MSan:
bash <skill_dir>/scripts/measure_coverage.sh \
<harness.c> build_harness/asan/lib<name>.a \
<seeds_dir> build_harness/asan/include
bash <skill_dir>/scripts/measure_coverage.sh \
<harness.c> <library_source_dir> \
<seeds_dir> <harness_include_dir>
The script runs two phases:
Phase A — Crash validation
Compiles the harness as a full LibFuzzer binary (-fsanitize=fuzzer,address) and runs
every seed individually. Any non-zero exit or ASan report is a failure.
Why -fsanitize=fuzzer and not just -fsanitize=address?
When the pre-built library .a was compiled with -fsanitize=fuzzer-no-link,address
(the standard libfuzzer-lib-builder output), the object files contain SanitizerCoverage
and ASan references that require the LibFuzzer+ASan runtime to resolve at link time.
Using plain -fsanitize=address without fuzzer would cause linker errors like
undefined reference to '__sanitizer_cov_8bit_counters_init'.
The correct pairing is always: library = fuzzer-no-link + harness = fuzzer.
Phase B — Edge coverage measurement
Runs the corpus against the fuzzer binary with -runs=0, which makes LibFuzzer process
every seed once (no mutation) and report the edge/feature coverage of the seed set.
LibFuzzer's built-in SanitizerCoverage instrumentation (already present in the pre-built
.a) is used directly — no need to recompile the library with llvm-cov flags.
Interpreting coverage
The -runs=0 output line looks like:
#42 DONE cov: 312 ft: 489 corp: 38/2.1kb ...
| Field | Meaning | Healthy range for initial seeds |
|---|
cov: | Unique edges (basic block transitions) covered | Higher is better; compare across seed set iterations |
ft: | Unique features (edge × hit-count buckets) covered | Should be ≥ cov: — more features means richer hit-count diversity |
corp: | Seeds kept after deduplication | Close to total seed count = low redundancy; much lower = many seeds cover the same paths |
Signs of healthy seeds:
corp: ≈ total seed count (each seed contributes unique coverage)
cov: grows noticeably when new seeds are added (diminishing returns is normal later)
Signs of a problem:
corp: is much smaller than total seeds → seeds are redundant; redesign to cover more distinct paths
cov: barely changes when adding seeds for new format variants → seeds are not reaching the target code; re-read the harness entry-point guards (wrong header bytes, missing NUL terminator, etc.)
If you want source-level line/branch coverage (which lines of library.c were hit),
you must rebuild the library from source with -fprofile-instr-generate -fcoverage-mapping
in addition to the sanitizer flags, then use llvm-cov report. This is optional —
the cov: metric is sufficient to judge whether seeds are reaching deep library logic.
Deliverables
After completing all phases, provide:
seeds_<harness>/ directory with named seed files
- A brief summary table: harness name | format type | # seeds |
cov: edges | ft: features | corp: kept
- Any seeds that required special handling (binary format, op-records, etc.) — explain the
encoding so the next developer can add more seeds without reading the harness source again