Skip to content

Fuzz Testing

Fuzz testing throws vast amounts of malformed, random or adversarially mutated input at a program and watches for crashes, hangs, panics and memory errors. Unlike property-based testing, the goal is not a stated invariant — it is simply does it survive?

A coverage-guided fuzzer keeps input that reaches new code paths and mutates it further, so it explores deeper over time. When it finds a crash it saves the exact input as a crash reproducer.

seed corpus ──► mutate ──► run target
▲ │
│ new coverage? │
└──────── keep ──────────┤
▼
crash / hang / sanitizer
│
▼
save reproducer + stack trace

Modern fuzzers pair with sanitizers — AddressSanitizer, MemorySanitizer, UndefinedBehaviorSanitizer — to catch silent memory misuse, not just segfaults.

Use it for

  • Parsers and decoders for untrusted input: JSON, images, protocols, archives
  • Memory-unsafe code (C/C++/Rust unsafe), where a crash may be exploitable
  • Security-sensitive surfaces: auth, crypto glue, deserialisation
  • Continuous fuzzing in CI (OSS-Fuzz style) once a target exists

Avoid it when

  • The input space is tiny and enumerable — table-driven tests cover it
  • There is no clear crash oracle and no sanitizer — nothing will be flagged
  • The code is pure and already exhaustively tested with properties
  • You expect it to prove correctness; fuzzing finds crashes, not wrong answers
Tool Ecosystem Notes
libFuzzer C/C++ (LLVM) In-process, coverage-guided; part of LLVM
AFL++ C/C++ Instrumentation, forkserver; afl-fuzz
Atheris Python Coverage-guided fuzzing for CPython
Jazzer Java/JVM JVM fuzzing, integrates with JUnit 5
cargo-fuzz Rust libFuzzer front-end for Rust, needs nightly
go-fuzz / native fuzzing Go go test -fuzz built in since Go 1.18
OSS-Fuzz Multi Google’s continuous fuzzing service
ClusterFuzzLite Multi Run OSS-Fuzz in your own CI
Terminal window
# JS has no in-process coverage fuzzer; drive a native target
npm install -D jazzer.js
npx jazzer fuzz/parse_target.js -- -runs=100000
const { FuzzedDataProvider } = require('@jazzer.js/core');
module.exports.fuzz = function (data) {
const provider = new FuzzedDataProvider(data);
const input = provider.consumeRemainingAsString();
try {
parseConfig(input);
} catch (e) {
if (!(e instanceof ParseError)) throw e;
}
};
  • Commit your corpus and reproducers. A crash input is a regression test; keep it so the fix never silently regresses.
  • Fuzz the target, not the wrapper. Instrument the deepest parser, and let the harness only marshal bytes.
  • Build with sanitizers. ASan/UBSan/TSan catch the memory and undefined-behaviour bugs that do not segfault on their own.
  • Bound time and memory. Set -timeout, -rss_limit_mb and a run budget so one pathological input does not hang CI.
  • Treat any crash as a security bug first. Even a parser panic can be a denial of service; triage before dismissing.
  • Run continuously, not once. Fuzzing rewards time; wire a short run into CI and a longer OSS-Fuzz job optionally.

Self-assessment quiz

Pick one answer per question, then press Check answers.

  1. What is the primary goal of fuzz testing?
  2. What makes a fuzzer coverage-guided?
  3. Why pair a fuzzer with a sanitizer like AddressSanitizer?
  4. What should you do with a crash reproducer?
  5. Which tool provides built-in coverage-guided fuzzing for Go since 1.18?