What is fuzzing? Coverage-guided testing that finds real bugs
Fuzzing means generating inputs a program was not expecting and watching what breaks. How coverage-guided fuzzers work, why sanitizers matter more than the fuzzer, and how to run one.

Fuzzing is automated testing with hostile inputs: generate data the program was not expecting, feed it in, and watch for crashes. The technique dates to 1988, when Barton Miller's students at Wisconsin fed random characters to Unix utilities and crashed roughly a third of them. It is still finding bugs today for the same reason — programs are written against the input the author imagined.
Dumb fuzzing versus coverage-guided fuzzing
The distinction is what separates a curiosity from a practical tool.
Dumb fuzzing generates random bytes. Against a program expecting a PNG, almost every input is rejected in the first few bytes by the header check, so the fuzzer spends its time exercising the same rejection path forever.
Coverage-guided fuzzing instruments the binary so the fuzzer can see which branches an input reached. When a mutated input reaches a branch nothing has reached before, that input is kept and mutated further. The fuzzer effectively hill-climbs through the program's structure, and will discover a valid PNG header on its own given a starting example.
This is why coverage-guided fuzzers such as AFL++, libFuzzer and honggfuzz dominate. They turn an impossible search into a tractable one.
Sanitizers matter more than the fuzzer
A fuzzer detects a bug when the program does something observable — usually a crash. The problem is that most memory-safety bugs do not crash. Reading four bytes past the end of a heap buffer normally returns whatever was next in memory and the program carries on, so the fuzzer sees nothing.
Sanitizers fix that by making the bug loud:
- ASan (AddressSanitizer) — buffer overflows, use-after-free, double-free. Roughly 2× slowdown, and worth it.
- UBSan — undefined behaviour: signed overflow, misaligned access, invalid shifts.
- MSan — reads of uninitialised memory.
- TSan — data races.
Fuzzing a binary built without sanitizers finds only the bugs that happen to segfault, which is a small fraction of the bugs that are there. The sanitizer, not the fuzzer, is what usually determines the yield.
A worked example with libFuzzer
libFuzzer is built into Clang. You write an entry point that takes a buffer and passes it to the code under test:
// fuzz_parse.c
#include <stdint.h>
#include <stddef.h>
#include "parser.h"
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
if (size < 4) return 0; // reject inputs too small to be meaningful
parse_packet(data, size); // the function under test
return 0; // non-zero is reserved; always return 0
}Build it with coverage instrumentation and AddressSanitizer, then run:
clang -g -O1 -fsanitize=fuzzer,address,undefined \
fuzz_parse.c parser.c -o fuzz_parse
mkdir -p corpus
cp samples/*.bin corpus/ # seed with real, valid inputs
./fuzz_parse corpus/ -max_len=4096 -jobs=8Seeding matters. A fuzzer starting from an empty corpus must rediscover your input format from nothing; a handful of real files gets it past the parsing preamble immediately.
Reading the output
libFuzzer prints a line per significant event:
#2048 NEW cov: 412 ft: 891 corp: 37/1204b exec/s: 9182
#4096 REDUCE cov: 412 ft: 891 corp: 37/1198b exec/s: 9044
==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60300000eff4cov— basic blocks covered. If this stops rising for a long time, the fuzzer is stuck and needs better seeds or a dictionary.corp— how many inputs are being kept as interesting.exec/s— executions per second. Below a few hundred, something is wrong; check for logging or disk I/O in the harness.
On a crash the input is written to crash-<sha1>, and rerunning the binary against that single file reproduces it deterministically.
When the fuzzer stops finding things
A plateau in coverage is the normal state after a few hours. The usual remedies, in order of effectiveness:
- Add a dictionary. Magic values and keywords are hard to discover by mutation.
-dict=http.dictcontaining tokens such as"GET"and"Content-Length"can unlock large areas. - Remove checksum checks. A CRC at the start of the format rejects every mutation. Compile the harness with verification disabled.
- Write more harnesses. One entry point covering one function usually beats a single harness trying to cover everything.
- Minimise the corpus with
-merge=1, so cycles are not spent on redundant inputs.
What fuzzing will not find
Fuzzing finds crashes and sanitizer violations. It does not find logic errors, because it has no idea what the correct answer is — a parser that returns the wrong value without crashing looks like success. It does not find authorisation flaws, business-logic mistakes or anything requiring a valid multi-step session. And it will not find a bug in code the harness never reaches, which is why coverage reports matter as much as crash counts.
For continuous coverage of open-source projects, Google's OSS-Fuzz runs this infrastructure for free and has found tens of thousands of bugs across widely used libraries.


