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

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

  1. reproduce with the exact build and sanitizer;
  2. minimize while preserving root cause;
  3. symbolicate and inspect source;
  4. determine reachability and privilege (crash → boundary → reachability → controllability → impact, Phase 08 WARMUP, Chapter 6);
  5. patch the root cause;
  6. run the full corpus and tests;
  7. search variants (Phase 08 WARMUP, Chapter 10);
  8. 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

  1. "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.
  2. "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.
  3. "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.
  4. "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.
  5. "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.