Fuzz Testing Guide: Finding Bugs Computers Are Better at Finding Than Humans
Fuzz testing (fuzzing) automatically generates random, malformed, or unexpected inputs and feeds them to your program looking for crashes, hangs, memory corruption, and assertion violations. Modern coverage-guided fuzzers like AFL++ and LibFuzzer generate millions of test cases per second guided by code coverage feedback and have found thousands of CVEs in widely-used software.
What Fuzz Testing Is
A fuzzer runs in a tight loop:
- Generate an input (random or based on a corpus)
- Feed it to the target program
- Observe the result: crash? hang? memory error? assertion fail?
- If interesting output: save the input to the corpus
- Repeat at millions of iterations per second
Coverage-guided fuzzing tracks which code paths each input exercises. When an input reaches a new code path, it's added to the corpus and used as a mutation seed — the fuzzer explores outward from what works.
What Fuzzing Finds
Fuzzing excels at finding:
Memory safety issues (C/C++):
- Buffer overflows
- Use-after-free
- Heap corruption
- Integer overflows leading to buffer over-reads
Logic errors:
- Crashes on unexpected input
- Assertion failures
- Infinite loops on certain inputs
Security vulnerabilities:
- XML/JSON parser bombs
- Format string vulnerabilities
- Path traversal via unusual encodings
Go Fuzzing (Built-in since Go 1.18)
func FuzzParseConfig(f *testing.F) {
f.Add("key=value")
f.Add("")
f.Fuzz(func(t *testing.T, input string) {
result, err := ParseConfig(input)
if err == nil {
encoded := result.Encode()
_, err2 := ParseConfig(encoded)
if err2 != nil {
t.Errorf("Round-trip failed: %q -> %q -> %v", input, encoded, err2)
}
}
})
}go test -fuzz=FuzzParseConfig -fuzztime=60sPython Fuzzing with Atheris
import atheris
import sys
import json
def TestOneInput(data: bytes):
fdp = atheris.FuzzedDataProvider(data)
text = fdp.ConsumeUnicodeNoSurrogates(len(data))
try:
result = json.loads(text)
re_encoded = json.dumps(result)
json.loads(re_encoded)
except (json.JSONDecodeError, ValueError):
pass
atheris.Setup(sys.argv, TestOneInput)
atheris.Fuzz()Java Fuzzing with Jazzer
public class FuzzTarget {
public static void fuzzerTestOneInput(FuzzedDataProvider data) {
String input = data.consumeRemainingAsString();
try {
MyXmlParser.parse(input);
} catch (MyParseException e) {
// Expected
}
}
}Differential Fuzzing
Test two implementations of the same spec and flag disagreements:
func FuzzHTTPParsers(f *testing.F) {
f.Fuzz(func(t *testing.T, input []byte) {
result1, err1 := parser1.Parse(input)
result2, err2 := parser2.Parse(input)
if (err1 == nil) != (err2 == nil) {
t.Errorf("Parsers disagree: parser1=%v parser2=%v on %q", err1, err2, input)
}
})
}Integrating Fuzzing into CI
Seed corpus regression (every build):
go test -run=FuzzParseConfig # fast: only runs seed corpusTime-boxed fuzzing on PRs:
- name: Fuzz test (5 minutes)
run: go test -fuzz=FuzzParseConfig -fuzztime=300sExtended fuzzing (weekly):
on:
schedule:
- cron: '0 3 * * 0'
jobs:
fuzz:
steps:
- run: go test -fuzz=FuzzParseConfig -fuzztime=3600sAlways Use Sanitizers
gcc -fsanitize=address,undefined -g -O1 target.c -o targetFuzzing without AddressSanitizer means memory errors go undetected.
Tools by Language
| Tool | Languages | Type |
|---|---|---|
| AFL++ | C/C++ | Coverage-guided |
| LibFuzzer | C/C++ | Coverage-guided |
| go-fuzz / native | Go | Coverage-guided |
| Atheris | Python | Coverage-guided |
| Jazzer | Java/JVM | Coverage-guided |
| OWASP ZAP | Web apps | Black-box |