Fuzzing and Generated Input
Feeding a program input nobody would write, on purpose. Coverage-guided fuzzing, corpora, sanitisers, and turning a crash into a permanent regression test.
Feeding a program input nobody would write, on purpose. Coverage-guided fuzzing, corpora, sanitisers, and turning a crash into a permanent regression test.
Property-based testing generates inputs from a domain you described. Fuzzing generates input from nothing — bytes, malformed structures, truncated files, deeply nested nonsense — and asks a narrower question: does the program crash, hang, or corrupt memory?
That narrower question turns out to be enormously valuable. Fuzzing has found tens of thousands of defects in widely used software, and it is the standard technique for any code that parses input from outside your system. By the end of this lesson you will know how coverage-guided fuzzing works, what to point it at, and how a crash becomes a permanent regression test.
The property under test is deliberately weak: for any input, the program does something defined. It returns a result, or it raises a declared error. It does not crash, hang, exhaust memory, or produce a security-relevant failure.
Well-formed examples.
The documents in the specification, the payloads the other team sends, the files that opened correctly last week.
All of them structurally valid, because that is what you have to hand while building.
Everything else.
An attacker probing deliberately. A corrupted upload. A truncated network read. A file written by a version of some tool nobody has any more.
That is a low bar to fail, and code handling untrusted input fails it constantly.
Where fuzzing has the highest hit rate:
parsers JSON, XML, CSV, YAML, dates, URLs, query strings
decoders image, audio, video, compression, character sets
deserialisers anything reconstructing objects from bytes
protocol handlers HTTP, custom binary protocols, message framing
file uploads whatever your application accepts
regular expressions catastrophic backtracking on crafted inputThe common factor: input crosses a trust boundary and gets structurally interpreted. That combination is where fuzzing belongs, and it is exactly where a crash becomes a denial-of-service or worse.
Random bytes find shallow bugs. A parser that requires a two-byte magic number rejects 99.998% of random input immediately, so a naive fuzzer never reaches any real code.
Coverage-guided fuzzing solves this, and the idea is elegant. The fuzzer instruments the program to observe which code paths an input reaches. Then:
The result is an evolutionary search. Inputs that get deeper into the program survive and breed, so the fuzzer teaches itself the input format — it will discover the magic number, then a valid header, then a valid record, and go looking past it.
generation 1 random bytes rejected at byte 1
generation 40 the magic number, by chance reaches the header parser
generation 200 a plausible header reaches the record loop
generation 900 a valid record, length wrong CRASH in the record loopNobody described the format. The coverage signal was enough.
For Python, Atheris brings coverage-guided fuzzing from libFuzzer:
# fuzz_parse_date.py
import atheris
import sys
with atheris.instrument_imports():
from myapp.dates import parse_date, InvalidDateError
def one_input(data: bytes) -> None:
fdp = atheris.FuzzedDataProvider(data)
value = fdp.ConsumeUnicodeNoSurrogates(200)
try:
parse_date(value)
except InvalidDateError:
pass # a declared failure is correct
# anything else propagates and is reported as a crash
atheris.Setup(sys.argv, one_input)
atheris.Fuzz()python fuzz_parse_date.py corpus/ -max_total_time=300Hypothesis also has a fuzzing mode, which is the shortest path if you already use it — the "no crash" property from the previous lesson, run for a long time with a coverage signal:
pip install hypofuzz
hypothesis fuzz -- -k test_parsing_never_raisesFor Go and Rust it is built in, which makes it the cheapest to adopt:
func FuzzParseDate(f *testing.F) {
f.Add("2026-03-15") // seed the corpus
f.Add("15/03/2026")
f.Fuzz(func(t *testing.T, s string) {
_, err := ParseDate(s) // must not panic
_ = err
})
}go test -fuzz=FuzzParseDate -fuzztime=5mcargo fuzz run parse_date -- -max_total_time=300The corpus is the set of inputs the fuzzer keeps because each reaches something new. It is the most valuable artefact the exercise produces, and treating it as disposable is the most common mistake.
Seed it with real examples. Valid files, real API payloads, the awkward cases from your test data. A fuzzer starting from good seeds reaches interesting code in minutes rather than hours.
corpus/
├── valid-minimal.csv
├── valid-10k-rows.csv
├── utf8-bom.csv
├── missing-header.csv
├── quoted-commas.csv
└── crash-2026-03-15-truncated-record.csvCommit it. Then every future run starts from everything previous runs learned, and the fuzzer makes progress across months rather than restarting each time.
Minimise it periodically. Tools can reduce a corpus to the smallest set covering the same paths, which keeps runs fast.
In C, C++ or Rust with unsafe code, many serious bugs do not crash immediately. A buffer overread returns adjacent memory and carries on — which is how a parser bug becomes a data-disclosure vulnerability.
Sanitisers make those failures loud, and fuzzing without them finds a fraction of what is there:
clang -fsanitize=address,undefined -fsanitize=fuzzer parse.c -o fuzz_parse
./fuzz_parse corpus/ -max_total_time=600AddressSanitizer out-of-bounds reads and writes, use-after-free
UndefinedBehavior integer overflow, invalid casts, bad shifts
MemorySanitizer reads of uninitialised memory
ThreadSanitizer data races
LeakSanitizer memory leaksIn memory-safe languages the equivalent concern is different and still real: unbounded resource use. A fuzzer will find the input that allocates two gigabytes or takes four minutes, and those are denial-of-service defects even though nothing is corrupted.
Bad — the crash is fixed and the input is discarded:
Fuzzer found: crash-a3f2c1 (truncated record, 14 bytes)
Fixed the index check in parse_record().
Deleted the crash file.Good — the input becomes a committed regression test and a corpus entry:
# tests/regression/test_parse_date_fuzz.py
def test_a_truncated_record_raises_rather_than_crashing():
"""From fuzz crash a3f2c1: a 14-byte truncated record read past
the end of the buffer."""
with pytest.raises(InvalidDateError):
parse_date("2026-03-\x00\x00\x00\x00\x00")corpus/crash-a3f2c1-truncated-record.bin # committedThe bad version fixes one instance and keeps nothing. The input is gone, so nothing prevents the same class returning after a refactor — and the corpus lost the one entry that reached the deepest path anybody had found.
The good version does three things: the regression test fails against the old code, the corpus keeps the input so every future run re-checks it, and the docstring records where it came from so the next person understands why a string of null bytes is in the test suite.
Fuzzers usually minimise a crashing input for you before reporting it —
-minimize_crash=1 in libFuzzer — which is the same shrinking idea from
property-based testing, and it is what makes the committed test readable.
A practical sequence for adopting it, in order:
1. Pick the highest-risk parser or decoder. One target, not five.
2. Write the harness: bytes in, call the function, allow only the
declared exceptions.
3. Seed the corpus with real, valid examples and your awkward
test data.
4. Run for an hour locally. Fix what it finds.
5. Commit the corpus and the regression tests.
6. Add a 5-minute run to CI against the committed corpus.
7. Add a long run nightly or weekly, and read its output.Steps 5 and 6 are what turn it from an exercise into a mechanism. A fuzzer run once is an afternoon of bug-fixing; a fuzzer with a committed corpus and a scheduled run is a permanent capability.
# The property: for ANY input, the program does something DEFINED.
# a result, or a declared error. Never a crash, hang, or memory bug.
# Where it pays — input crosses a trust boundary and is
# structurally interpreted
parsers (JSON, XML, CSV, dates, URLs) / decoders (image, audio,
compression) / deserialisers / protocol handlers / file uploads /
regexes vulnerable to catastrophic backtracking
# Coverage-guided fuzzing: an evolutionary search
# run an input, record the paths it reached
# reached something new? KEEP it in the corpus
# mutate corpus entries to make candidates; repeat millions of times
# the fuzzer teaches ITSELF the input format# Getting one running
python fuzz_parse.py corpus/ -max_total_time=300 # Atheris
hypothesis fuzz -- -k test_parsing_never_raises # HypoFuzz
go test -fuzz=FuzzParseDate -fuzztime=5m # built in
cargo fuzz run parse_date -- -max_total_time=300 # built in
# C / C++ / unsafe Rust: sanitisers are not optional
clang -fsanitize=address,undefined -fsanitize=fuzzer parse.c
# ASan (out-of-bounds, use-after-free), UBSan (overflow, bad casts),
# MSan (uninitialised reads), TSan (races), LSan (leaks)
# without them, a buffer overread returns adjacent memory silently# The corpus is the asset
seed it with real valid examples + your awkward test data
COMMIT it — future runs start from everything already learned
minimise it periodically to keep runs fast
crash inputs go in it, named for what they were
# A crash becomes a permanent test
# 1. let the fuzzer minimise it (-minimize_crash=1)
# 2. write a regression test that FAILS against the old code
# 3. commit the input to the corpus
# 4. note in the test where it came from
# fixing and deleting the input keeps nothing
# The arrangement that works
5 minutes in CI against the committed corpus -> regression check
a long run nightly or weekly -> finds new defects
# either alone is much weaker than both
# Authorisation
# fuzz code you own, in an environment you own. Pointing a fuzzer
# at someone else's service is an attack, whatever the intent.The next lesson changes scale from one program to several: contract testing, which verifies that two services still agree about their interface without running both — the answer to end-to-end tests that do not scale past a handful of services.
Before that, write one fuzz harness for the riskiest parser you own, seed it with five real inputs, and run it for an hour. The hit rate on first-ever-fuzzed input handling code is high enough that this is rarely a wasted hour.