Discover vulnerabilities and crashes by feeding programs random, malformed, or unexpected inputs at scale. Covers coverage-guided fuzzing, grammar-based fuzzing, differential fuzzing, integration with CI/CD, and the patterns that find the bugs traditional testing misses.
Fuzz testing, or fuzzing, is an automated testing technique that feeds invalid, unexpected, or randomly generated data as input to a program to uncover implementation bugs, crashes, hangs, or security vulnerabilities. It matters when you need to exercise edge cases that manual or example-based testing cannot reach—particularly in input parsing, protocol handling, memory management, or any code path where the program trusts its input. Unlike unit tests that verify correct behavior with known-good inputs, fuzzing deliberately seeks the boundary between valid and invalid, often revealing crashes that would otherwise remain hidden until a user reports them in production.
The most common entry point for binary fuzzing is American Fuzzy Lop (afl), an open-source coverage-guided fuzzer. To begin, you prepare a seed corpus—a directory of inputs that the fuzzer will mutate and evolve. Even a single empty file or a minimal valid input is sufficient to start; the fuzzer will quickly diverge from there.
mkdir -p seeds output
echo "" > seeds/minimal
You then invoke afl-fuzz, providing the seed directory, an output directory, and the target binary after a -- separator:
afl-fuzz -i seeds -o output -- ./target_binary --port 8080
| Flag | Meaning |
|---|---|
-i seeds | Directory containing seed inputs. afl reads these initially and mutates them. |
-o output | Where afl writes crash artifacts, unique inputs, and state files. |
-t 5000 | Timeout in microseconds for each individual input. If the target does not exit within this window, afl kills it and moves on. |
-m 1024 | Hard memory limit in kilobytes for the target process. Exceeding this triggers a kill and is counted as a "interesting" input. |
-n | Run without the afl forkserver. Useful for targets that refuse to run under the forkserver or for debugging. |
-d | Dump the "interesting" input that triggered a new coverage path to the output directory, useful for manual inspection. |
The fuzzer runs in a loop: it takes a seed input, mutates it (via splicing, byte flipping, arithmetic increments, and other strategies), runs the target binary with the mutated input, and monitors coverage via the AFL instrumentation compiled into the target. If the new input triggers a previously unseen code path, it’s added to the corpus. If it crashes the target, the crash file is saved to the output directory for later analysis.
afl requires the target to be compiled with its instrumentation. For C/C++ compilers, this is typically done via the provided afl-gcc or afl-clang wrappers, which insert the necessary instrumentation hooks at compile time:
afl-gcc -o target_binary source.c
./target_binary # runs briefly to establish the forkserver
afl-fuzz -i seeds -o output -- ./target_binary
For languages with runtimes (Go, Rust, Python), the approach varies. Go has a fuzzing story integrated into its test framework since Go 1.18, but that uses a different mechanism than afl. For traditional compiled targets where you control the build, afl-gcc/afl-clang are the standard path.
When afl-fuzz stops (whether by timeout, manual interrupt, or a crash), the output directory contains structured artifacts. The most important are crash files—inputs that caused the target to fail. These are typically stored in crashes/ subdirectory with filenames like crash-<timestamp>-<hash>.
Opening a crash file often reveals the last few hundred bytes of input that provoked the failure. If the target is a network service, the crash file might be an HTTP request; if it’s a file parser, it’s a malformed file segment. The goal now is minimization: producing the smallest possible input that still triggers the crash, which makes root-cause analysis faster.
afl provides afl-minimize for this purpose:
afl-minimize -i crashes/ -o minimized/ -- ./target_binary
This iteratively removes bytes from the crash input while preserving the crash, producing a minimal reproduction case. For very complex crashes (e.g., those requiring specific state or network conditions), manual trimming may be necessary.
afl-showmap lets you visualize which code paths were exercised during a fuzzing run. This is useful for answering questions like "am I covering the error-handling branch?" or "which function bodies have never been reached?"
afl-showmap -i seeds -o showmap_output -- ./target_binary < input_file
The output is a directory of files representing coverage snapshots. Comparing two snapshots reveals which new paths were added by a given input—a useful feedback loop when tuning mutation strategies.
Not every project can or wants to recompile a binary with afl instrumentation. Many modern language runtimes ship with their own fuzzing frameworks that integrate with the language’s type system and test infrastructure.
libFuzzer is a coverage-guided fuzzer built into the LLVM compiler infrastructure. It does not require a separate wrapper; you compile your fuzz target with the -fsanitize=fuzzer flag, and the runtime handles the fuzzing loop.
A minimal libFuzzer target in C looks like this:
#include <stdint.h>
#include <stddef.h>
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
// Your fuzz target logic here
// Return 0 on success
return 0;
}
Compile and run:
clang -fsanitize=fuzzer target.c -o target_fuzzer
./target_fuzzer
The fuzzer will start mutating the empty input (or a provided seed) and exercise code paths based on coverage feedback. Like afl, it terminates inputs that exceed a default timeout (5 seconds, adjustable via the MAX_TOTAL_TIME environment variable or -max_total_time flag).
Go 1.18+ includes go test -fuzz. A fuzz test is a regular Go test function name starting with Fuzz:
func FuzzFlag(f *testing.F) {
f.Fuzz(func(t *testing.T, arg string) {
// exercise code with arbitrary string input
_ = arg
})
}
Run with:
go test -fuzz=.
Go’s fuzzer uses a seed corpus (files placed in fuzz/ directory) and mutates them using a combination of byte-level and structure-aware strategies. It stops after a configurable number of iterations or when a coverage plateau is reached.
Not all targets are local binaries. Web applications, APIs, and protocol implementations often need fuzzing too. The pattern typically involves a "dumb" fuzzer like radamsa that generates random byte sequences, which are then fed to the target via some transport (HTTP, file, socket).
radamsa is a simple, fast fuzzer that takes an input file and produces variants, applying mutations such as bit flipping, byte insertion/deletion, and range adjustments. It has no coverage feedback; its purpose is purely to generate surprising inputs.
echo "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n" > seeds/request.txt
radamsa seeds/request.txt -o fuzz_request.txt
A common pattern for API fuzzing combines radamsa with curl in a loop:
while true; do
radamsa seeds/base_request.txt -o /tmp/fuzz_input
curl -s -o /dev/null -w "%{http_code}" -X POST http://localhost:8080/api/data --data-binary @/tmp/fuzz_input
done
This spins forever, posting a newly fuzzed JSON or form body on each iteration. You redirect stdout to /dev/null and inspect stderr or exit behavior for signs of crashes, panics, or unexpected status codes. For automated runs, you’d typically bound the iteration count and capture any non-2xx response or process crash.
When the target expects structured protocol data (e.g., TLS handshakes, DNS queries, WebSocket frames), a dumb mutator quickly produces invalid inputs that the target rejects before reaching interesting code. In those cases, fuzzers that understand the protocol structure are preferred. While many commercial products offer protocol-aware fuzzing, the underlying pattern is generic: generate messages that respect the protocol’s ABNF or grammar rules, but fuzz specific fields, lengths, and type boundaries. The fuzzer may be configured with a dictionary of valid tokens, a set of known valid message shapes, or a state machine that tracks expected next steps in the protocol exchange.
Fuzzers are fundamentally stateless—they treat each input as an independent invocation. If your target relies on session state, request ordering, or incremental data processing, a standard fuzzer will likely fail to exercise the relevant code paths. The fuzzer may send a "logout" request before any "login," or it may replay the same input twice without updating internal counters. Designing a fuzzable entry point often requires wrapping the target in a test harness that resets state between invocations, or providing a "seed" sequence of inputs that brings the system to a known state before the fuzzer takes over.
Not every input that triggers undefined behavior produces an instant segfault or abort. Some fuzzed inputs corrupt the heap in subtle ways that only manifest later—perhaps during a subsequent allocation, a cleanup function, or even in a different process entirely. afl and libFuzzer both detect immediate crashes (signal delivery, exit code non-zero), but silent corruption may go unnoticed. Tools like AddressSanitizer (ASan) and MemorySanitizer (MSan) are often layered alongside the fuzzer to catch out-of-bounds accesses, use-after-free, and uninitialized memory reads that would otherwise remain hidden.
Coverage-guided fuzzers rely on new inputs triggering previously unseen code paths. After a certain period, the fuzzer may stop finding new coverage, even though it’s still running. This is not necessarily a sign of failure—it may mean the exercise set is complete—but it can feel like stagnation. At this point, practitioners often introduce seed corpora crafted from real-world inputs ( captured traffic, log files, user submissions) to jump-start the fuzzer into new branches. Alternatively, adjusting the timeout, mutation rate, or enabling “rare” mutation strategies (bit flips on specific byte ranges) can coax new paths out.
A fuzzer may produce dozens of crash files, but not all are actionable. A crash in a rarely used error-handling branch, one that requires a specific signal handler state, or one that only manifests under resource pressure (OOM, thread contention) may not represent a security vulnerability or a user-visible bug. Triaging crashes often requires correlating them with build information, sanitizer reports, and an understanding of whether the crashed path is reachable under normal operating conditions. Conversely, some crashes may be dismissed as “expected” if the target is designed to handle malformed input gracefully—but what “gracefully” means is often application-specific and easy to misjudge without context.
Fuzzers that run for hours or days can consume significant CPU, memory, and disk space. afl’s -m flag limits per-input memory, but the fuzzer process itself may grow in memory usage over time due to corpus expansion. Disk usage can also balloon as the corpus grows to thousands of inputs. Regular cleanup of the output directory, pruning of duplicate or uninteresting inputs, and setting a hard runtime limit (e.g., via systemd timer, cron, or CI timeout) are necessary practices to prevent runaway fuzzing campaigns.
Fuzzing in continuous integration is typically time-boxed. A common pattern is to run the fuzzer for a fixed duration (e.g., 30 minutes) and fail the build if any new crash files are produced. This prevents regressions—if a previously fuzzed code path now crashes under the same inputs, the CI run catches it before merge. The crash artifacts are usually archived as build artifacts for later manual review.
# Pseudocode for a GitHub Actions step
- name: Run fuzzer
run: afl-fuzz -i seeds -o /tmp/fuzz_output -- ./target_binary -t 1800000
- name: Check for new crashes
run: if [ -n "$(ls /tmp/fuzz_output/crashes/ 2>/dev/null)" ]; then echo "::error::New crashes found"; exit 1; fi
If the fuzzer runs without finding crashes, the step passes. If it finds crashes, the build fails and the crash files are available for the developer to investigate.
A project’s seed corpus is often treated as a living artifact that evolves alongside the codebase. When a new input format is introduced (e.g., a new request header, a binary configuration format), a representative sample is added to the seeds directory. Over time, the corpus accumulates inputs that reflect real-world usage patterns, making the fuzzer more effective at finding relevant bugs. Some projects version-control their seed corpus alongside the source code, treating it as part of the test suite.
For projects that use garbage-collected or managed runtimes (Java, JavaScript, Python), dedicated fuzzing frameworks exist that understand the runtime’s memory model. Java has AFL-Java and JQF; JavaScript has JS-Fuzz and AFL-based wrappers. These tools instrument the runtime rather than the individual binary, allowing them to track object allocations, prototype chain walks, and GC-induced state changes. The patterns are analogous to the binary case—mutate inputs, track coverage, collect crashes—but the instrumentation targets the interpreter or virtual machine instead of a native executable.
When fuzzing a protocol endpoint, the typical workflow is:
This approach sits between "dumb" byte-level fuzzing and full protocol validation. It requires some understanding of the protocol’s syntax, but less than building a custom fuzzer from scratch.
Fuzz testing is a feedback-driven technique for finding crashes and security issues by automatically generating and evolving inputs. Whether you use a coverage-guided fuzzer like afl or libFuzzer, a language-integrated framework, or a dumber mutator like radamsa paired with curl, the core loop is the same: mutate inputs, execute the target, monitor for crashes or new coverage, and iterate. The sharp edges—state management, silent heap corruption, coverage plateaus, crash relevance, and resource exhaustion—are where fuzzing projects often fall short if not planned for. Integrating fuzzing into a CI/CD pipeline as a time-bounded gating step, maintaining a living seed corpus, and pairing the fuzzer with sanitizers are practical patterns that increase the odds of finding real bugs without derailing development velocity.
This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy