People treat AFL++ like a magic box: aim it at a binary, wait for crashes. The real leverage is upstream of that — a harness that drives the interesting code fast, with instrumentation so the fuzzer can see coverage and a sanitizer so shallow corruption actually crashes instead of silently limping on. Get the harness right and the fuzzer does the rest.
- goal
- reach crashes in the parser
- strategy
- persistent-mode harness + afl-clang-fast + ASAN + a seed corpus
the harness is the target#
Don't fuzz the whole program through argv and a fresh process per input —
that's slow and mostly exercises startup. Wrap the function under test in a
persistent-mode loop so one process handles thousands of inputs:
|
__AFL_LOOP resets and re-runs without a fresh execve each time — often a
10–100× throughput win. Build it instrumented, with AddressSanitizer so
out-of-bounds and use-after-free turn into loud aborts the fuzzer counts as
crashes:
feed it well, then watch the right number#
- Seed corpus. Start from valid inputs — real config files, sample packets.
The fuzzer mutates from these; garbage seeds waste hours rediscovering basic
structure. Then minimise:
afl-cmin -i corpus -o corpus_min -- ./harnessdrops seeds that add no coverage. - Dictionary. Hand AFL++ the grammar's keywords (
-x config.dict) so it splices in tokens likeinclude/{/;it would take ages to mutate into existence. - The number that matters is coverage, not exec/s. A fast fuzzer stuck at flat
map densityis finding nothing. If coverage plateaus, your harness isn't reaching deep code — add a-d(deterministic-off) run, more seeds, or split a monster function so the magic-byte checks stop gating everything.
triage what falls out#
Crashes land in out/default/crashes/. They're raw inputs; reproduce and
minimise before you read them:
afl-tmin shrinks a 4KB crashing input down to the handful of bytes that
actually trip it — usually the difference between "no idea" and "oh, an unbounded
memcpy on a length field." ASAN's stack trace points at the exact line.
the bug afl-tmin handed me
A length field read from the input, trusted, and used straight as a memcpy
size — three bytes of input, one heap overflow:
segfault{the_harness_did_the_hard_part}
The fuzzer is the cheap part — it's a mutation loop. The work is the harness that drives the right code at speed, the corpus that gives it a running start, and the triage that turns a folder of crashing blobs into one root cause. Do those three well and AFL++ finds bugs in an afternoon that a week of manual review would walk straight past.