Random Chaos at the Byte Level
Fuzzing (or Fuzz Testing) is a cousin to Property-Based Testing, but it operates at a lower level and with less structure. Where PBT generates semantically valid data of the right type (e.g., "a list of integers"), a fuzzer often generates raw, malformed, or mutated byte sequences and hurls them at a program's input interfaces.
Think of it this way: PBT is a clever QA engineer who generates edge-case inputs within the rules. A fuzzer is someone who takes a valid JPEG file and randomly flips bits in it, then tries to open it in your image viewer to see if it crashes.
Why Fuzzing Matters
Fuzzing is primarily used for security research and finding crashes in code that parses complex, untrusted inputs — file format parsers, network protocol handlers, image decoders, archive extractors, and compilers.
If your program accepts a user-uploaded image and resizes it, a fuzzer will feed it:
- Corrupted image headers that claim the image is 100GB but the file is 1KB.
- Files with valid headers but garbage pixel data.
- Files containing null bytes in unexpected positions.
- Inputs that trigger integer overflows in dimension calculations.
These inputs can expose memory safety bugs — buffer overflows, use-after-free errors, null pointer dereferences — that a human tester would never think to construct manually, but that a real attacker absolutely would.
Coverage-Guided Fuzzing
Modern fuzzers like AFL++ (American Fuzzy Lop) and libFuzzer (built into LLVM) are not purely random. They are coverage-guided: they instrument the binary to track which code paths each input exercises. When a mutated input causes the program to execute a new code path, the fuzzer keeps that input as a seed for further mutation. This means the fuzzer actively explores the program's logic rather than blindly spraying random bytes.
Go has built-in fuzzing support since Go 1.18:
func FuzzParseDate(f *testing.F) {
// Seed corpus — known valid inputs
f.Add("2024-01-15")
f.Add("1999-12-31")
f.Fuzz(func(t *testing.T, s string) {
// The fuzzer mutates 's' and calls this function thousands of times.
// We just check it doesn't panic — no crash is the property.
result, err := ParseDate(s)
if err == nil && result.IsZero() {
t.Error("parsed successfully but returned zero time")
}
})
}
Run with: go test -fuzz=FuzzParseDate -fuzztime=30s
PBT vs. Fuzzing: When to Use Which
| Dimension | Property-Based Testing | Fuzzing |
|---|---|---|
| Input structure | Semantically valid, typed | Raw bytes, often malformed |
| Primary target | Business logic, algorithms | Parsers, security, crash safety |
| Properties checked | Explicit invariants you define | Implicit: "does not crash/panic" |
| Typical language | Application code | C, C++, Go, Rust, security-critical code |
| Tooling | Hypothesis, jqwik, rapid | AFL++, libFuzzer, go-fuzz |