Hitchhiker's Guide — Running a Safe Fuzzing Campaign
The operational companion to the Phase 08 WARMUP. The WARMUP builds the research method (fuzzing families, harness quality, crash-to-impact, root cause, variants, disclosure); this guide is the campaign playbook — the build, the corpus strategy, the campaign design document, harness acceptance tests, the triage record, patch-review questions, and the research package. Owned/CTF/authorized targets only; stop at minimum proof; keep corpora and crashes private until coordinated disclosure permits release.
Table of Contents
- The Campaign Design Document
- Build
- Corpus Strategy
- Harness Acceptance Tests
- Triage Procedure
- The Triage Record
- Patch Review Questions
- Stateful and Concurrent Targets
- Common Failure Modes
- The Research Package
- Worked Examples
- Interview Drills
- References
The Campaign Design Document
Write this before you fuzz a single byte — it keeps a fast technical experiment from outrunning its ethical and operational boundary (Phase 00):
scope/authorization · target version + build · exposed API · attacker input
security boundary · excluded actions · stop conditions · sanitizer set
resource limits · corpus handling · disclosure contact · cleanup
Build
clang -g -O1 -fsanitize=fuzzer,address,undefined \
-fno-omit-frame-pointer target.c harness.c -o fuzz_target
mkdir -p corpus crashes
./fuzz_target corpus -artifact_prefix=crashes/ -timeout=2 -rss_limit_mb=2048 -dict=fmt.dict
Use only owned code or a project whose policy permits testing.
Corpus Strategy
- the smallest valid example for every format feature (start the search deep);
- boundary values for lengths/counts/nesting;
- a dictionary for magic bytes and keywords (helps form valid structure);
- invalid-but-near-valid examples;
- state sequences for protocols (not isolated packets);
- regression inputs retained forever.
Harness Acceptance Tests
A harness must pass these before you trust a campaign (Phase 08 WARMUP, Chapter 4):
- the same input produces the same state and result (deterministic);
- state is reset between iterations;
- valid seeds reach the intended features (coverage proves it);
- malformed-input rejection does not crash the harness (no false oracle);
- leaks/timeouts have a deliberate policy;
- external I/O and logs are bounded;
- a seeded defect is found, then disappears after the patch;
- coverage maps to the expected parser/state-machine functions.
Triage Procedure
- reproduce with the exact build and sanitizer;
- minimize while preserving root cause;
- symbolicate and inspect source;
- determine reachability and privilege (crash → boundary → reachability → controllability → impact, Phase 08 WARMUP, Chapter 6);
- patch the root cause;
- run the full corpus and tests;
- search variants (Phase 08 WARMUP, Chapter 10);
- prepare a maintainer-ready report.
The Triage Record
For every unique root cause (after dedup) preserve: build ID, flags, sanitizer, minimized-input hash, exact reproduction, the first invalid operation, allocation/free context, attacker-controlled values, reachability, privilege, mitigations, demonstrated effect vs inferred impact (separated), and duplicates. Keep raw sensitive inputs out of public artifacts.
Patch Review Questions
- Does validation happen before arithmetic, allocation, copy, or side effect?
- Are types wide and signedness explicit?
- Does the patch cap work as well as memory (DoS)?
- Does it reject a class rather than one sample (Phase 08 WARMUP, Chapter 9)?
- Can another parser/caller bypass it (variant)?
- Are error and cleanup paths safe?
- Is any retained compatibility intentional?
- Does the regression fail on the old revision and pass on the new?
Stateful and Concurrent Targets
Represent protocol state explicitly and mutate sequences, not isolated packets; bound sequence length and reset deterministically. For concurrency: separate race detection (TSan) from coverage fuzzing, control scheduling where possible, repeat suspected races, and never claim a race's impact from one timing-dependent report.
Common Failure Modes
Harness aborts mistaken for target crashes; OOM/timeouts ignored; nondeterminism creating flaky findings; corpus containing secrets; or a patch that rejects one input instead of repairing bounds and ownership. The flagship triage lab deliberately separates vulnerable and patched parser behavior and tests exact-length, pixel limits, checksum, minimization, and stable crash signatures.
The Research Package
Deliver: a reproducible container/toolchain lock, the harness, seeds, dictionary, coverage report, minimized cases, root-cause notes, the patch, regressions, a variant checklist, affected-version analysis, a coordinated-disclosure timeline, and a public-safe report distinguishing fact from inference (Phase 08 WARMUP, Chapter 12). If no original defect was found, a rigorous coverage-gap analysis and a harness contribution is a valid result — fabricating novelty is failure.
Worked Examples
Reading a libFuzzer run and minimizing
#1048576 NEW cov: 412 ft: 980 corp: 41/2150b exec/s: 21000 rss: 180Mb
#2097152 NEW cov: 488 ft: 1203 corp: 58/3400b exec/s: 20500 rss: 190Mb
==31kk== ERROR: AddressSanitizer: heap-buffer-overflow ...
artifact_prefix='crashes/'; ... Test unit written to crashes/crash-3f9a
Read the dashboard: cov (edges) and ft (features) climbing means the search is making progress;
if they flatline, that's a plateau (harness/corpus problem, not runtime). exec/s of ~20k means the
harness is in-process and fast — a CLI-subprocess harness would be ~100×, slower and noisier. Then
minimize and confirm the bug reproduces without the fuzzer:
./fuzz_target -minimize_crash=1 -runs=200000 -exact_artifact_path=min crashes/crash-3f9a
./target_repro min # standalone reproducer — proves it's the target, not the harness
Variant analysis with CodeQL/grep
You found memcpy(dst, src, count * size) with no overflow check. Abstract the pattern and hunt
for siblings — the author likely repeated it (Phase 08 WARMUP, Chapter 10):
# cheap first pass
rg -n 'memcpy\([^,]+,[^,]+,\s*\w+\s*\*\s*\w+\)' # multiply inside a copy length
// CodeQL: a multiplication flowing to an allocation/copy size with no bound on the operands
from MulExpr m, FunctionCall c
where c.getTarget().hasName(["memcpy","malloc"]) and c.getAnArgument() = m
select c, "size from unchecked multiplication"
The point: a single finding becomes a patch that closes the class. When you receive a vendor patch, diff it the same way — did they fix all instances or just the reported one?
Patch-diff intuition
- if (len > buf_size) return -1;
+ if (len > buf_size || len > remaining) return -1; // the fix tells you the bug
A patch adding len > remaining reveals the original bug was an OOB read where a field claimed a
legal size but ran past the buffer — and tells you affected versions are everything before this
commit. You'd use this to plan backports and write a detection, without publishing a weaponized
PoC before users patch (Chapter 12).
Interview Drills
- "Design a harness for a stateful protocol parser." Fuzz message sequences against the state machine, in-process, structure-aware so it reaches deep states; seed valid exchanges; dictionary of tokens; deterministic (control time/randomness/threads, reset globals); bounded timeouts; ASan/UBSan; plus a differential second implementation to catch state divergence without a crash.
- "Triage a heap use-after-free." Reproduce under ASan (free site + use site). Root-cause the premature free (lifetime/ownership), distinguishing it from the use where it crashed. Assess controllability (is the reallocated region attacker-controlled?), reachability, boundary. Patch the lifetime, regression-test from the minimized input, variant-search the pattern.
- "Why did coverage plateau?" Almost never runtime. A structure the fuzzer can't synthesize (dictionary/structure-aware), a checksum gate (recompute in harness), a harness not reaching deep code (expand), or a thin corpus. Inspect the coverage map for the stuck frontier.
- "Is this crash security-relevant?" Walk crash→boundary→reachability→controllability→demonstrated impact. Local-only non-attacker input or a fixed null deref with no boundary crossed = low. Attacker-reachable across a trust boundary with a controllable value = serious. State proven vs inferred; never overclaim.
- "You fuzzed for two weeks and found nothing. Did you fail?" No — a rigorous coverage-gap analysis and a reusable harness contribution is a valid result. Fabricating novelty is the actual failure.
References
- The Fuzzing Book (Zeller et al.); libFuzzer, AFL++, and OSS-Fuzz documentation.
- LLVM sanitizer docs (ASan/UBSan/MSan/TSan); structure-aware fuzzing (libprotobuf-mutator).
- Dennis Andriesse, Practical Binary Analysis; Ghidra / GDB / LLDB docs.
- Google Project Zero blog (variant analysis, root-cause/disclosure norms).
- ISO/IEC 29147 / 30111 (coordinated disclosure); the phase CVE Casebook.