Comprehensive Fuzzing Guide#
A practitioner’s reference for fuzz testing — fundamentals, coverage feedback, harness construction, corpus strategy, sanitizer usage, and the tool stack for web, binary, kernel, API, and smart-contract targets. Compiled from 46 research sources.
Table of Contents#
- Fundamentals
- Fuzzing Taxonomy
- Coverage-Guided Fuzzing
- Harness Construction
- Corpus Management & Seed Selection
- Dictionaries & Structure-Aware Fuzzing
- Sanitizers
- Binary Fuzzing (AFL++, libFuzzer, honggfuzz, LibAFL)
- Web Fuzzing (ffuf, wfuzz, feroxbuster, Burp Intruder)
- API Fuzzing (REST, GraphQL, Protobuf)
- Kernel & OS Fuzzing
- Directed & Grammar-Based Fuzzing
- AI-Augmented Fuzzing
- JVM Fuzzing (Jazzer, LibAFL)
- Rust & Python Fuzzing
- Snapshot Fuzzing (Nyx, HyperHook)
- Smart Contract Fuzzing
- Protocol & Network Fuzzing (Boofuzz, ICS)
- Crash Triage & Minimization
- CI/CD Integration
- Bugs That Survive Continuous Fuzzing
- Real-World Wins & CVEs
- Tools & Frameworks Reference
- Wordlist & Corpus Resources
- Quick Reference Cheatsheet
1. Fundamentals#
Fuzzing is automated software testing by bombarding a target with a large volume of semi-random, invalid, or unexpected inputs and watching for crashes, hangs, memory errors, or assertion failures. The technique originates with Barton Miller’s 1988 University of Wisconsin-Madison experiment, where random inputs crashed roughly a third of tested Unix utilities.
The core loop:
- Test case generation — synthesize or mutate inputs.
- Test execution — run the target with the input.
- Monitoring — observe crashes, hangs, sanitizer reports, coverage.
- Feedback — prioritize interesting inputs, discard redundant ones.
- Crash analysis — deduplicate, minimize, and root-cause the finding.
Why fuzzing works: It surfaces real execution failures — segfaults, UAF, OOB reads/writes, integer overflows, assertion violations — not theoretical bugs. Unlike static analysis, there are few false positives: if the fuzzer crashed the target, the target crashed.
Ideal targets:
| Category | Examples |
|---|---|
| File parsers | PDF, PNG, JPEG, TIFF, audio/video codecs |
| Network protocols | HTTP, DNS, TLS, QUIC, Bluetooth stacks, Netlink |
| Language runtimes | JavaScript engines, WASM, regex engines |
| Serialization | Protobuf, msgpack, CBOR, ASN.1, BSON |
| Crypto libraries | OpenSSL, BoringSSL, NSS |
| OS kernel surfaces | syscalls, ioctls, filesystem drivers, USB stack |
| Web APIs | REST, GraphQL, gRPC endpoints |
| Databases | SQL parsers, NoSQL query engines |
A good target processes external, attacker-controllable input, has parsing logic, uses low-level memory primitives, or implements complex state machines.
2. Fuzzing Taxonomy#
By input generation strategy#
| Type | Starts with | Best for | Tools |
|---|---|---|---|
| Mutation-based | Valid sample inputs; flips bits, inserts/deletes bytes, splices | Binary formats, legacy CLIs, when you have a corpus | AFL++, honggfuzz, Radamsa |
| Generation-based | A grammar, model, or protocol spec | Structured inputs (JS, HTML, JSON, protocols) | Peach, BooFuzz, SPIKE, Fuzzilli, Domato |
| Hybrid | Both; grammar seeds feed into mutation engine | Language runtimes, parsers | Fuzzilli, Dharma |
By visibility into the target#
| Type | Knowledge | Strength | Weakness |
|---|---|---|---|
| Black-box | None — I/O only | Easy to set up, no build changes | Shallow coverage, misses deep paths |
| Grey-box | Lightweight instrumentation (edge coverage) | Best balance of effort and results — the modern default | Needs recompilation or binary rewriting |
| White-box | Full source + symbolic/concolic execution | Reaches deep constraints | Expensive, brittle, complex tooling |
When to use black-box vs coverage-guided (per ClusterFuzz)#
Coverage-guided works best when:
- Target is self-contained and deterministic.
- Can run hundreds of executions per second.
- Classic example: binary format parsers.
Black-box is preferred when:
- Target is large and slow (full browsers).
- Nondeterministic across runs for the same input.
- Input grammar is extremely structured (JavaScript, HTML DOM).
Differential fuzzing#
Feed the same input to multiple implementations of the same spec and flag divergences. Excellent for:
- Cross-browser parser comparison (HTML, CSS, JSON)
- Crypto library consistency (Project Wycheproof)
- Language spec compliance (LangFuzz)
3. Coverage-Guided Fuzzing#
Coverage-guided (grey-box) fuzzing is the modern default. The fuzzer instruments the target at compile time so every edge (branch transition in the CFG) reports into a shared bitmap. Inputs that reach new edges are kept and mutated; inputs that only retrace existing coverage are discarded.
The feedback loop (AFL / libFuzzer)#
- Pick the most promising test case from the queue.
- Mutate it into many children (bit flips, arithmetic, splicing, havoc).
- Run each child; the instrumented binary updates the coverage bitmap.
- Score each child by new coverage. Promising ones enter the corpus.
- Repeat.
AFL’s coverage bitmap#
AFL allocates a 64K 8-bit array called trace_bits/shared_mem. Each cell is a hit counter for a (branch_src, branch_dst) tuple. Instrumentation pseudocode:
cur_location = <COMPILE_TIME_RANDOM>;
shared_mem[cur_location ^ prev_location]++;
prev_location = cur_location >> 1;
The shift-by-one on prev_location preserves directionality (A→B is distinct from B→A).
Clang’s SanitizerCoverage#
libFuzzer and AFL++ both rely on Clang’s -fsanitize-coverage= instrumentation. Compile with:
clang -fsanitize=address,fuzzer fuzzer.cc -o fuzzer
# Or for AFL++ compatibility:
clang -fsanitize=address -fsanitize-coverage=trace-pc-guard target.c -o target
The runtime callbacks __sanitizer_cov_trace_pc_guard, __sanitizer_cov_trace_cmp*, and __sanitizer_cov_trace_switch let the engine record edges, compare operands (CMPLOG/COMPCOV), and switch-table legs.
Extending instrumentation#
You can hook __sanitizer_cov_trace_pc_guard to capture more than edge hits — for example, the return address via __builtin_return_address(0) to drive directed fuzzing toward known-dangerous functions:
extern "C" void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
void *PC = __builtin_return_address(0);
char desc[1024];
__sanitizer_symbolize_pc(PC, "%p %F %L", desc, sizeof(desc));
// compare PC against a watchlist of vulnerable functions
__shmem->edges[*guard / 8] |= 1 << (*guard % 8);
}
This technique (demonstrated on Fuzzilli + JerryScript) lets the fuzzer prioritize inputs that reach historically buggy files or functions.
Coverage metrics: not all edges are equal#
Research (NDSS “Not All Coverage Measurements Are Equal”) shows that weighting edges by security impact — e.g., edges inside memory allocators, string handlers, or unsafe sinks — outperforms flat edge counting. GRLFuzz goes further and uses reinforcement learning to pick mutation strategies per seed based on historical reward.
Context-sensitive coverage#
Standard edge coverage does not track execution order. Different calling sequences can produce identical edge bitmaps while reaching very different program states. AFL++ addresses this with two options:
- Context-sensitive branch coverage — each function gets a unique ID; the fuzzer hashes the call stack IDs together with the edge identifier, so the same edge reached through different call paths counts as distinct coverage.
- N-Gram branch coverage — combines the current location with the previous N locations (1-gram, 2-gram, 4-gram). Higher N values distinguish more execution orderings but increase bitmap pressure.
Context-sensitive coverage targets above 60% are considered strong (unlike the 90%+ achievable with flat edge coverage), because the state space is combinatorially larger.
Value coverage#
Even 100% edge coverage can miss bugs that depend on specific variable values. A division-by-zero triggered only when r.padding == 4312 will survive millions of edge-guided iterations if no input happens to produce that value.
Value coverage tracks which value ranges a variable takes across executions. By inserting binary-search-like branch trees that map variable values into distinct edges, the fuzzer’s coverage feedback naturally steers toward unexplored value regions. This technique extends standard coverage-guided fuzzing to catch arithmetic bugs, boundary conditions, and magic-value-dependent paths that flat edge coverage misses.
4. Harness Construction#
A harness (or fuzz target) is a small wrapper that hands fuzzer-provided bytes to the code you actually want to test. The libFuzzer-style entry point is the de facto standard, understood by libFuzzer, AFL++, honggfuzz, and Centipede:
// fuzz_target.c
#include <stdint.h>
#include <stddef.h>
extern int parse_thing(const uint8_t *data, size_t len);
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
if (size < 4) return 0;
parse_thing(data, size);
return 0;
}
Build it:
clang -g -O1 -fsanitize=fuzzer,address fuzz_target.c parser.c -o fuzz_target
./fuzz_target corpus/ -max_len=4096
Harness design rules#
- Keep it fast. Aim for thousands of execs/sec. Every millisecond of setup costs orders of magnitude in total coverage.
- Stateless where possible. Reset global state between inputs; if not possible, use
-runs=1or fork mode. - Exercise realistic entry points. Wrap the same functions an attacker can reach — not helper internals.
- Split the input. For multi-argument APIs, carve
datainto pieces with a small prefix header orFuzzedDataProvider. - Avoid nondeterminism. Seed any RNG with a constant; disable timestamps, thread scheduling surprises.
- Check assertions, not output. Let sanitizers do the talking.
- Limit allocations. Cap input size (
-max_len=) to avoid OOM noise.
FuzzedDataProvider (libFuzzer helper)#
#include <fuzzer/FuzzedDataProvider.h>
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
FuzzedDataProvider fdp(data, size);
int mode = fdp.ConsumeIntegralInRange<int>(0, 3);
std::string name = fdp.ConsumeRandomLengthString(64);
auto rest = fdp.ConsumeRemainingBytes<uint8_t>();
target_api(mode, name.c_str(), rest.data(), rest.size());
return 0;
}
Harness scope: narrow vs broad#
A common design decision is how much code the harness exercises. Narrow harnesses (one parser, one function) are fast to write and yield high coverage for isolated components but miss integration-level bugs. Broad harnesses (entire protocol stacks, full API surfaces) give the fuzzer a huge search space, causing it to spend months reaching moderate coverage. The practical sweet spot: one harness per logical subsystem, targeting the same entry points an attacker can reach.
Multi-language harness patterns#
Rust (cargo-fuzz / cargo-libafl):
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
if let Ok(s) = std::str::from_utf8(data) {
let _ = my_crate::parse(s);
}
});
Go (native testing.F):
func FuzzParse(f *testing.F) {
f.Add([]byte("seed"))
f.Fuzz(func(t *testing.T, data []byte) {
_, _ = Parse(data)
})
}
Common harness anti-patterns#
- Calling
exit()orabort()on invalid input — the fuzzer sees these as crashes. - Reading from a file path inside the harness — slow and non-hermetic.
- Leaking memory every call — ASan will flag each run as a “crash.”
- Catching all exceptions and returning silently — hides real bugs.
- Writing to global state that isn’t reset — causes flaky reproducers.
- Reusing input bytes for multiple purposes — e.g., using the same bytes for a control decision and as payload data. This creates conflicting mutation pressure. Use
FuzzedDataProviderto consume bytes independently. - Reinterpreting input — casting the same buffer to different types in different branches. Each interpretation competes for the fuzzer’s mutation energy.
5. Corpus Management & Seed Selection#
A corpus is the set of inputs the fuzzer has deemed “interesting” (reaches unique coverage). Seed corpus is the starting material you hand it.
Seed selection principles#
- Diversity over volume. 50 structurally different PDFs outperform 5,000 near-duplicate PDFs.
- Small is beautiful. Tiny seeds mutate faster and cover more ground. Aim for <1 KB where possible.
- Harvest real inputs. Pull samples from your test suite, public corpora, or real network captures.
- Include pathological cases. Empty files, single bytes, maximum-size inputs, boundary values.
Corpus pruning (minimization)#
Over time the corpus grows unbounded. Pruning keeps only inputs that uniquely cover at least one edge. ClusterFuzz runs CORPUS_PRUNE = True once a day. Locally:
# libFuzzer: merge old corpus into a minimal new one
./fuzz_target -merge=1 corpus_min/ corpus/
# AFL: cmin for corpus, tmin for individual inputs
afl-cmin -i corpus -o corpus_min -- ./target @@
afl-tmin -i crash_input -o crash_min -- ./target @@
Seed corpus conventions#
Tools like OSS-Fuzz and ClusterFuzz expect zipped corpora named <fuzz_target>_seed_corpus.zip placed alongside the binary. Dictionaries go in <fuzz_target>.dict.
Public corpus sources#
- oss-fuzz corpora — public backups of Google OSS-Fuzz targets
- Fuzzer Test Suite — Google’s historical benchmark seeds
- Mozilla fuzzdata — browser-relevant formats
- DARPA CGC — challenge binaries with seeds
- VirusTotal / Malware Bazaar — real-world file samples (handle with care)
6. Dictionaries & Structure-Aware Fuzzing#
Pure byte-level mutation struggles with formats that have magic numbers, keywords, or long tokens. A dictionary is a newline-separated list of interesting byte strings the mutator can splice in.
Dictionary format (libFuzzer / AFL)#
# A comment
"FILE"
"\xff\xd8\xff\xe0"
"JFIF\x00"
kw_function="function"
kw_return="return"
Pass to libFuzzer with -dict=keywords.dict or drop it alongside the target as <target>.dict.
Where dictionaries help the most#
- Language grammars —
function,return,=>,async - Binary magic bytes — PNG
\x89PNG, ELF\x7fELF, PDF%PDF- - HTTP verbs, headers —
GET,POST,Content-Type: - SQL keywords —
SELECT,UNION,WHERE - Protocol framing bytes
Structure-aware fuzzing#
For inputs where structural validity matters (JavaScript, SQL, protobuf, HTTP/2), pure mutation is too destructive. Options:
| Technique | Description | Tools |
|---|---|---|
| Grammar-based generation | Produce inputs from a BNF/EBNF | Dharma, Domato, Grammarinator |
| Intermediate language (IL) mutation | Fuzz an AST/IR, then lower to bytes | Fuzzilli (JS), Token-level fuzzers |
| libprotobuf-mutator | Mutate serialized protobuf messages preserving schema | LPM + libFuzzer |
| Custom mutators | libFuzzer’s LLVMFuzzerCustomMutator hook | Any engine |
| Splicing | Combine fragments from valid corpus entries | Built into AFL++ |
Fuzzilli, for example, generates FuzzIL (its own typed IR for JavaScript), mutates at the IR level, then lowers to JS source — ensuring outputs are mostly syntactically valid and much more semantically meaningful than byte flips on a .js file.
7. Sanitizers#
Sanitizers are Clang/GCC-provided compile-time instrumentation that turn latent memory/undefined-behavior bugs into loud, debuggable crashes. Without a sanitizer, many bugs corrupt memory silently and only crash later — if at all. Always fuzz under a sanitizer.
| Sanitizer | Flag | Detects |
|---|---|---|
| ASan (AddressSanitizer) | -fsanitize=address | Heap/stack/global buffer overflows, UAF, double-free, memory leaks (via LSan) |
| UBSan (UndefinedBehaviorSanitizer) | -fsanitize=undefined | Signed integer overflow, NULL deref, misaligned access, divide-by-zero, OOB shifts |
| MSan (MemorySanitizer) | -fsanitize=memory | Use of uninitialized memory — all transitive deps must also be MSan-built |
| TSan (ThreadSanitizer) | -fsanitize=thread | Data races, deadlocks |
| LSan (LeakSanitizer) | -fsanitize=leak | Memory leaks (bundled into ASan by default) |
| CFI (Control Flow Integrity) | -fsanitize=cfi | Indirect call hijacking |
Typical build incantation#
# libFuzzer + ASan + UBSan combo
clang++ -g -O1 \
-fsanitize=fuzzer,address,undefined \
-fno-sanitize-recover=all \
-fno-omit-frame-pointer \
fuzz_target.cc target.cc -o fuzz_target
Sanitizer pitfalls#
- MSan requires all linked libraries to also be MSan-built, or you’ll drown in false positives.
- ASan roughly doubles memory usage and slows execution ~2x — worth it.
- UBSan defaults to warnings; pair with
-fno-sanitize-recover=allto make them fatal. - Don’t mix ASan and MSan in the same binary (they conflict).
- On OSS-Fuzz, each target is typically built three times: ASan+UBSan, MSan, and undefined-sanitizer-only.
Kernel sanitizers#
The Linux kernel has its own family: KASAN (address), KMSAN (uninit memory), UBSAN, KCSAN (concurrency), KFENCE (low-overhead memory error detection). Syzkaller enables these by default.
8. Binary Fuzzing (AFL++, libFuzzer, honggfuzz, LibAFL)#
AFL / AFL++#
AFL (American Fuzzy Lop), written by Michał Zalewski, pioneered practical coverage-guided fuzzing. AFL++ is the community fork with advanced features: CMPLOG (magic-value solving), COMPCOV (byte-compare splitting), QEMU and Unicorn modes for blackbox binaries, LAF-INTEL transformations, persistent mode, and collision-free coverage.
Install and run:
sudo apt-get install -y afl++ clang llvm
# Compile with AFL's wrapper
AFL_USE_ASAN=1 afl-clang-fast -o target target.c
# Fuzz it
afl-fuzz -i input_corpus -o findings -- ./target @@
The @@ token is replaced by AFL with the path to each generated test case.
AFL++ power features#
| Feature | Purpose |
|---|---|
| CMPLOG | Logs comparison operands so the mutator can solve magic-byte checks |
| LAF-INTEL | Splits multi-byte comparisons into per-byte branches so coverage sees partial progress |
| Persistent mode | Loops the harness N times per fork to amortize startup cost (huge speedup) |
| QEMU mode | Instrumentation-free fuzzing of closed-source binaries |
| FRIDA mode | Dynamic instrumentation for binaries on macOS/Android |
| Nyx | Snapshot-based full-system fuzzing via KVM |
libFuzzer#
libFuzzer is LLVM’s in-process, coverage-guided fuzzer. It lives inside your harness binary — no fork-exec per input — making it the fastest option for library fuzzing.
// fuzz_parser.cc
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
return parse(data, size), 0;
}
clang++ -g -fsanitize=fuzzer,address fuzz_parser.cc parser.cc -o fuzz
./fuzz corpus/ -max_len=4096 -dict=keywords.dict -jobs=8 -workers=8
Key libFuzzer flags:
| Flag | Purpose |
|---|---|
-max_len=N | Cap input length |
-dict=file | Use a dictionary |
-jobs=N -workers=N | Parallel fuzzing processes |
-merge=1 dst src | Merge/minimize corpora |
-runs=N | Run N iterations then exit (CI mode) |
-timeout=N | Per-input timeout in seconds |
-rss_limit_mb=N | Memory cap |
-fork=N | Run N child processes for crash isolation |
honggfuzz#
Robert Swiecki’s honggfuzz supports both feedback-driven and dumb fuzzing, hardware-assisted coverage (Intel PT/BTS), and persistent mode. It has an excellent reputation for finding bugs in crypto libraries and was used for many OpenSSL/BoringSSL discoveries.
honggfuzz -i corpus -- ./target ___FILE___
LibAFL#
LibAFL is a modular fuzzing library written in Rust by the AFL++ team. Unlike monolithic fuzzers, LibAFL provides composable building blocks — observers, feedback, schedulers, executors, mutators — that you assemble into a custom fuzzer. This makes it the tool of choice when you need behavior that no off-the-shelf fuzzer supports.
Two ways to use LibAFL:
- libFuzzer drop-in replacement — compile with
libafl_cc(or the libfuzzer-compatibility layer) and run existingLLVMFuzzerTestOneInputharnesses without code changes. Requires nightly Rust. - Custom Rust fuzzer — write a Rust binary that uses LibAFL’s crates to wire up your own feedback loop, mutator pipeline, and executor.
LibAFL’s key advantages over libFuzzer:
| Feature | LibAFL | libFuzzer |
|---|---|---|
| Active development | Yes | Maintenance mode (Google shifted to Centipede) |
| Custom feedback | Arbitrary feedback types via traits | Fixed edge-coverage model |
| Distributed fuzzing | Built-in multi-node support | Manual via -fork |
| Snapshot support | Nyx integration via libafl_nyx | None |
| Language support | C/C++, Rust, Java (via Jazzer fork), Python | C/C++ only |
# LibAFL as libFuzzer drop-in (after building libafl_libfuzzer)
cargo build --release -p libafl_libfuzzer
clang -fsanitize=address -g target.c \
-L target/release -l afl_libfuzzer -o fuzz_target
./fuzz_target corpus/
WinAFL#
For Windows targets, WinAFL uses DynamoRIO or Intel PT to provide coverage feedback on closed-source binaries:
winafl-fuzz.exe -i in -o out -D path\to\dynamorio\bin64 \
-t 10000 -- -coverage_module target.dll -target_module target.exe \
-target_offset 0x1234 -fuzz_iterations 5000 -nargs 1 -- target.exe @@
target_offset is the RVA of the function you want persistent-looped.
Directed greybox fuzzing on Windows#
Directed fuzzers (AFLGo, Hawkeye, and Windows-specific ports) combine coverage guidance with distance metrics — how close each input gets to a target site in the CFG. Useful for patch testing, reproducing known CVEs, and hunting variants near a known-vulnerable function.
9. Web Fuzzing (ffuf, wfuzz, feroxbuster, Burp Intruder)#
Web fuzzing is less about memory corruption and more about content discovery (hidden endpoints, backup files, parameter names) and input probing (SQLi, XSS, SSRF, path traversal payloads).
ffuf#
Fast, Go-based, the modern default.
# Directory discovery
ffuf -u https://target.com/FUZZ -w raft-medium-directories.txt -t 50
# Subdomain discovery
ffuf -u https://FUZZ.target.com -w subdomains-top1million.txt -H "Host: FUZZ.target.com"
# Parameter discovery
ffuf -u "https://target.com/api?FUZZ=test" -w params.txt -fs 1234
# POST body fuzzing
ffuf -u https://target.com/login -X POST \
-d "username=admin&password=FUZZ" -w rockyou.txt \
-H "Content-Type: application/x-www-form-urlencoded" -mc 200,302
# JSON body
ffuf -u https://target.com/api/v1/users -X POST \
-d '{"name":"FUZZ"}' -H "Content-Type: application/json" -w names.txt
Filter flags (-fc, -fs, -fw, -fl) are essential for noisy targets with custom 404s:
ffuf -u https://target.com/FUZZ -w words.txt -fc 404,403 -fs 1337
feroxbuster#
Rust-based, recursive by default, great for deep directory trees:
feroxbuster -u https://target.com -w raft-medium-directories.txt -x php,bak,zip -d 3
wfuzz#
Older Python tool, still useful for its multi-injection-point syntax and filter language:
wfuzz -c -w users.txt -w pass.txt --hc 401 \
-d "user=FUZZ&pass=FUZ2Z" https://target.com/login
Burp Suite Intruder#
Four attack modes:
| Mode | Use case |
|---|---|
| Sniper | One payload list, one marker at a time — classic fuzz |
| Battering Ram | Same payload into every marker simultaneously |
| Pitchfork | Parallel payload sets, walked in lockstep |
| Cluster Bomb | Cartesian product of payload sets (credential spraying) |
Mark insertion points with §, pick a payload list, hit Start. Community Edition throttles Intruder heavily; Pro is effectively required for serious engagements.
Burp Collaborator#
For blind/out-of-band bugs (blind SSRF, blind XXE, blind command injection, blind SQLi), Collaborator provides DNS+HTTP+SMTP callback endpoints. Inject http://<random>.burpcollaborator.net and watch for hits.
Web fuzzing targets that matter#
- Hidden endpoints (
/admin,/.git/config,/backup.zip,/api/v2/internal) - HTTP parameter names (
debug=1,admin=true, internal flags) - Header values (
X-Forwarded-For,X-Original-URL,Host) - Cookie values and session tokens
- File upload content-type / extension allowlists
- Race condition windows (burst-parallel Intruder or Turbo Intruder)
10. API Fuzzing (REST, GraphQL, Protobuf)#
Modern apps expose most of their attack surface through APIs, so API fuzzing has become its own subdiscipline. It differs from web content fuzzing in that the inputs are structured (JSON, XML, protobuf) and the API contract (OpenAPI, GraphQL schema, proto files) can drive generation.
REST API fuzzers#
| Tool | Approach | Notes |
|---|---|---|
| EvoMaster | Search-based white-box + black-box | Generates JUnit tests; used in production at Volkswagen AG |
| RESTler | Stateful, infers dependencies between endpoints | Microsoft Research |
| Schemathesis | Property-based, OpenAPI/Swagger-driven | Python, CI-friendly |
| Dredd | Contract testing against OpenAPI | Pass/fail per endpoint |
| bBOXRT | Black-box robustness testing | Academic |
| Morest / ARAT-RL / AutoRestTest | RL and model-based | Recent academic tools |
Search-based REST fuzzing (EvoMaster)#
EvoMaster treats test generation as a search problem, using evolutionary algorithms to maximize coverage over time. The Volkswagen AG industrial study (2023-2026) surfaced several practical requirements for fuzzers to be usable outside academic labs:
- Authentication chaining — login flows that produce tokens used by later calls.
- Dependency inference —
POST /usersreturns an id used byGET /users/{id}. - External system mocking — black-box mode where downstream SaaS can’t be hammered.
- Stable, idempotent reruns — tests must survive DB state changes.
- Readable, maintainable generated tests — engineers need to understand what failed and why.
- Oracle beyond 5xx — business-logic violations without a crash.
GraphQL fuzzing#
GraphQL’s introspection query (__schema) gives you a full type graph for free, making grammar-based fuzzing straightforward. Notable tools:
- InQL (Burp plugin) — extracts operations from introspection, generates request templates
- clairvoyance — infers schema even when introspection is disabled
- graphql-cop — lightweight misconfig scanner
- GraphQLmap — schema-driven fuzzer
Common GraphQL fuzz targets: batching DoS, field duplication DoS, alias-based rate-limit bypass, introspection leaks, SQLi/NoSQLi in resolvers, IDOR via object-level authorization gaps.
Protobuf / gRPC fuzzing#
Protobuf schemas give you a perfect generator. Use libprotobuf-mutator with libFuzzer to mutate typed messages:
#include "src/libfuzzer/libfuzzer_macro.h"
#include "my_message.pb.h"
DEFINE_PROTO_FUZZER(const my::Message &msg) {
handle_message(msg);
}
LPM keeps messages schema-valid while mutating individual fields — far more effective than flipping bits in a serialized blob.
11. Kernel & OS Fuzzing#
Kernel fuzzing is harder than userspace: the target is stateful, crashes require VM reboots, and coverage has to cross the syscall boundary.
syzkaller (syzbot)#
Dmitry Vyukov’s syzkaller is the state of the art for Linux (and FreeBSD, NetBSD, OpenBSD, Fuchsia, Windows) kernel fuzzing. It:
- Generates syscall sequences from a declarative description language (
.txtdescriptions). - Uses KCOV for coverage feedback.
- Runs many VMs in parallel, snapshotting and rebooting on crashes.
- Automatically bisects kernel commits to find the introducing change.
- Syzbot files bugs upstream with reproducers attached.
Syzkaller has found hundreds of Linux kernel vulnerabilities and is responsible for a substantial fraction of all kernel CVEs in recent years.
KCOV (Linux kernel coverage)#
Compile the kernel with CONFIG_KCOV=y and (selectively) KCOV_INSTRUMENT := y in the Makefiles of subsystems you care about. From userspace:
int fd = open("/sys/kernel/debug/kcov", O_RDWR);
ioctl(fd, KCOV_INIT_TRACE, COVER_SIZE);
unsigned long *cover = mmap(NULL, COVER_SIZE * sizeof(unsigned long),
PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
ioctl(fd, KCOV_ENABLE, KCOV_TRACE_PC);
// ... run the code you want to profile ...
ioctl(fd, KCOV_DISABLE, 0);
// cover[0] holds count, cover[1..] are %rip values of basic blocks
KCOV coverage is per-task and ring-buffer-based. Combined with KASAN, it turns any kernel subsystem into a fuzzable target.
A custom AFL+KCOV setup#
You can trick AFL into thinking your harness is instrumented by having it fake the trace_bits shared memory: fork from AFL’s forkserver protocol, call your target code while KCOV is enabled, then hash KCOV %rip values into the AFL bitmap before reporting completion. The Cloudflare blog post (“A gentle introduction to Linux Kernel fuzzing”) walks through a netlink fuzzer built this way — you build a KCOV-enabled kernel, run it in virtme/KVM, and expose a shim that AFL drives.
Other kernel fuzzers#
| Tool | Target | Notes |
|---|---|---|
| Trinity | Linux syscalls | Classic, argument-aware but not coverage-guided |
| kAFL | Full kernels via Intel PT | Hypervisor-assisted |
| Syzkaller | Linux + others | The workhorse |
| Nyx | Snapshot-fuzzing full VMs | Extremely fast |
| Digtool | Windows kernel | Academic |
External network fuzzing with syzkaller#
Syzkaller can be extended to fuzz the kernel’s network stack externally — injecting raw packets via TUN/TAP and collecting coverage via KCOV. The approach:
- Create a TUN device in the fuzzer VM to inject packets directly into the kernel’s network stack.
- Enable KCOV with
KCOV_REMOTEto collect coverage from softirq/network processing contexts (not just the syscall thread). - Define syzkaller “pseudo-syscalls” that wrap packet injection as callable operations, with structured descriptions of IP/TCP/UDP/ICMP headers.
- Handle checksums — syzkaller must compute valid IP/TCP checksums or the kernel drops packets before reaching interesting code.
- Establish TCP connections by implementing a minimal TCP handshake in the fuzzer to reach connection-state-dependent code paths.
This technique found multiple remotely-triggerable bugs in the Linux kernel, including a one-shot RCE in a non-public kernel flavor. The approach surfaces bugs in protocol parsers that traditional syscall-based fuzzing cannot reach because those code paths are only exercised by incoming network packets.
False positives in kernel fuzzing#
Not every syzkaller crash is a real bug. A documented class of false positives involves soft lockup warnings in network scheduler (net/sched) fuzzing. These occur when qdisc parameters like quantum=1 combined with large stab overhead values cause the dequeue loop to spin for tens of seconds — long enough to trigger the soft lockup watchdog, but not an actual hang. The root cause is that syzkaller’s executor does not fully reset network namespaces between runs (for performance), so qdisc modifications from previous programs persist and affect subsequent executions.
Diagnosing these requires manual bisection of syzkaller logs, re-running with syz-execprog, and dumping tc state via nsenter into the executor’s network namespace. These are not security bugs but waste triage time.
Bugs in kernel fuzzing are tricky#
Most netlink/syscall bugs don’t have direct security impact because the interface requires privilege, but UAFs, stack OOBs, and race conditions in filesystem/networking code frequently become LPEs. Always pair kernel fuzzing with KASAN + UBSAN + KMSAN.
12. Directed & Grammar-Based Fuzzing#
Directed greybox fuzzing (DGF)#
Standard coverage-guided fuzzers try to cover everything. Directed fuzzers focus effort on specific target sites in the CFG — useful for:
- Patch testing (does my fix hold?)
- CVE reproduction and variant hunting
- Reaching a specific function under a complex path condition
AFLGo assigns each basic block a distance to the target and uses simulated annealing over that distance as the fitness function. Variants include Hawkeye (function-level distance), 1dFuzz (for 1-day patch testing), and directed Windows fuzzers built on WinAFL.
Grammar-based fuzzing#
Pure byte mutation is terrible at generating valid JavaScript, SQL, or HTML. Grammar-based approaches encode the input language and generate valid (or almost-valid) programs:
| Tool | Language | Notes |
|---|---|---|
| Fuzzilli | JavaScript | IL-based; found dozens of V8/JSC/SpiderMonkey bugs |
| Domato | HTML/CSS/JS DOM | Chrome security team |
| Dharma | Any grammar | Mozilla, generation-only |
| Grammarinator | ANTLR grammars | Covers many real-world languages |
| Superion | Structure-aware AFL++ | Injects tree mutations |
| Nautilus | Grammar + coverage feedback | Strong on interpreters |
Hybrid: concolic / symbolic execution#
When mutation stalls at a hard branch (e.g., if (input == 0xdeadbeef)), symbolic execution can solve the constraint. Driller (AFL + angr) and QSYM pair a fast coverage-guided fuzzer with on-demand concolic execution to punch through these walls.
13. AI-Augmented Fuzzing#
The fuzzing community has been integrating ML for nearly a decade, but LLMs changed the game in 2023-2025.
What AI brings#
| Task | Classical approach | AI approach |
|---|---|---|
| Harness generation | Manual, hours per target | LLM generates from API headers |
| Seed synthesis | Collect real samples | Generate grammar-valid seeds via LLM |
| Mutation strategy selection | Hand-tuned schedules | RL agents (GRLFuzz) learn per-target |
| Crash triage | Manual stack analysis | LLM summarizes root cause |
| Reachability guidance | Directed fuzzing + static analysis | LLM proposes inputs to reach targets |
Real systems#
- OSS-Fuzz-Gen (Google) — uses LLMs to auto-generate libFuzzer harnesses for open-source C/C++ projects. The from-scratch pipeline takes a GitHub URL, auto-builds the project using heuristic build templates (Make, CMake, Bazel), runs Fuzz Introspector for static analysis (cyclomatic complexity, call trees, cross-references), then prompts LLMs with that analysis data to synthesize harnesses. Successfully upstreamed integrations for 15+ previously-unfuzzed projects and achieved up to 35% absolute coverage increases across 160+ existing OSS-Fuzz projects, discovering 6+ new vulnerabilities.
- G2Fuzz (USENIX Security 2025) — LLM-synthesized input generators for non-textual formats. Instead of asking the LLM to produce raw binary files, G2Fuzz has the LLM write Python scripts that generate format-compliant inputs (TIFF, MP4, PDF). These scripts are mutated at the generator level (holistic search) while AFL++ mutates the generated outputs (local search). Found 10 unique bugs in real-world software (3 CVE-confirmed), outperforming AFL++, Fuzztruction, and FormatFuzzer on UNIFUZZ, FuzzBench, and MAGMA benchmarks.
- MALF — multi-agent LLM framework for fuzzing industrial control protocols. Uses RAG (Retrieval-Augmented Generation) for domain-specific ICS protocol knowledge and QLoRA fine-tuning for protocol-aware input generation. Agents specialize in seed generation, mutation strategies, and feedback-driven refinement. Achieved 88-92% test case pass rate on Modbus/TCP, S7Comm, and Ethernet/IP. Deployed in a real-world power plant attack-defense range, identifying 3 zero-day flaws (one CNVD-registered).
- GRLFuzz — uses reinforcement learning to pick mutation strategies based on which ones historically yielded new coverage for similar seeds.
- LLamaRestTest — LLM-guided REST API fuzzer.
- Claude / GPT-assisted triage — feed the crashing stack, the source of the suspect function, and the input to an LLM for a human-readable root-cause summary.
Practical AI-fuzzing workflow#
- Point an LLM at the target repo; ask it to list the top 10 functions that parse untrusted input.
- Generate libFuzzer harnesses for each; compile with sanitizers.
- Have the LLM draft a dictionary of keywords/magic bytes from the source.
- Run for an hour. Collect crashes.
- For each unique crash, feed stack + function source + input to the LLM for triage notes.
- Iterate: ask the LLM to suggest harness improvements based on coverage gaps.
14. JVM Fuzzing (Jazzer, LibAFL)#
Java and JVM-language fuzzing has matured significantly. Jazzer (Code Intelligence) is the primary tool: a coverage-guided, in-process fuzzer that bridges the JVM and libFuzzer via JNI.
Jazzer architecture#
- Jazzer Driver — a native binary linking libFuzzer. Calls
LLVMFuzzerRunDriverto start the C++ fuzzing loop. - Jazzer Agent — a Java agent that instruments JVM bytecode at runtime using JaCoCo and ASM. Coverage hooks call
CoverageMap.recordCoverage(int id)usingsun.misc.Unsafe.putByteto write directly into the shared coverage bitmap that libFuzzer reads via__sanitizer_cov_pcs_init. - The harness implements
fuzzerTestOneInput(byte[] input)or usesFuzzedDataProviderfor structured input consumption.
Practical Jazzer tips#
fuzzerInitialize()— runs once before fuzzing begins. Use it to set system properties, load application context, and create mock environments. Avoids expensive per-iteration setup.fuzzerTearDown()— cleanup hook after the fuzzing campaign.- Method hooks — Jazzer’s
@MethodHookannotation lets you detect application-specific vulnerability states (e.g., detecting when user-controlled input reachesRuntime.exec()or SQL query builders) rather than relying solely on crashes. - Classpath management — enterprise Java apps require careful classpath configuration. Use
--cpto include all transitive dependencies; missing classes cause silent failures rather than crashes. - Memory-safe language caveats — Java fuzzing rarely finds memory corruption. Focus on logic bugs, injection vulnerabilities, and denial-of-service conditions detectable via custom sanitizers and method hooks.
Jazzer + LibAFL#
LibFuzzer is in maintenance mode, limiting Jazzer’s evolution. The Team Atlanta AIxCC project created a fork replacing libFuzzer with LibAFL as the fuzzing backend. This brings LibAFL’s superior mutation strategies, scheduling, and extensibility to Java fuzzing while preserving Jazzer’s JVM instrumentation. The fork is open-source and demonstrates that LibAFL’s modular architecture can drive non-C/C++ targets.
15. Rust & Python Fuzzing#
Rust fuzzing#
Rust’s memory safety guarantees reduce but do not eliminate fuzzing value — unsafe blocks, logic bugs, panics, and integer overflows remain valid targets.
| Tool | Backend | Notes |
|---|---|---|
| cargo-fuzz | libFuzzer | The standard Rust fuzzer; wraps libfuzzer-sys |
| cargo-libafl | LibAFL | Drop-in replacement for cargo-fuzz using LibAFL’s engine. Fork of cargo-fuzz with LibAFL performance benefits |
| afl.rs | AFL++ | AFL++ wrapper for Rust targets |
# cargo-fuzz quickstart
cargo install cargo-fuzz
cargo fuzz init
cargo fuzz add my_target
cargo fuzz run my_target -- -max_len=4096
cargo-libafl replaces libFuzzer with LibAFL under the same cargo-fuzz CLI interface. Benefits include active development, custom feedback support, and potentially higher throughput.
Python fuzzing with Atheris#
Atheris is Google’s coverage-guided Python fuzzer, pip-installable and simple to set up. It instruments Python bytecode for coverage feedback and can also instrument native C extensions via libFuzzer.
import atheris
import sys
def test_one_input(data):
fdp = atheris.FuzzedDataProvider(data)
s = fdp.ConsumeUnicode(100)
try:
my_module.parse(s)
except (ValueError, TypeError):
pass # expected exceptions
atheris.Setup(sys.argv, test_one_input)
atheris.Fuzz()
Key Atheris patterns:
- Use
atheris.FuzzedDataProviderto consume typed data (strings, ints, floats) from the raw byte buffer. - Catch expected exceptions explicitly; let unexpected ones propagate as findings.
- For C extensions, compile with
-fsanitize=fuzzer-no-link,addressand Atheris will hook into libFuzzer’s coverage. - Python fuzzing finds logic bugs, unhandled edge cases, and crash-inducing inputs in parsing code — not memory corruption (unless C extensions are involved).
16. Snapshot Fuzzing (Nyx, HyperHook)#
Snapshot fuzzing captures a program’s state (memory, registers, execution context) at a specific point, then restores it after each test case. This eliminates startup overhead and enables fuzzing of deeply-nested, stateful, or long-running targets.
Nyx#
Nyx is a hypervisor-based snapshot fuzzer using a modified QEMU-Nyx and KVM-Nyx. It:
- Creates and restores VM snapshots at near-native speed via KVM.
- Uses Intel Processor Trace (PT) for low-overhead coverage collection.
- Communicates between guest and host via custom hypercalls.
- Supports both Linux and Windows guest targets.
HyperHook#
HyperHook (Neodyme) is a harnessing framework that simplifies building Nyx agents. It abstracts:
- Guest-to-host communication (hypercall wrappers).
- Target function harnessing (setting entry points, placing fuzz input).
- Exception handling setup.
- Cross-platform support (Linux and Windows user-space targets).
A typical HyperHook + Nyx + LibAFL setup:
- LibAFL handles input generation, mutation, and scheduling on the host.
- Nyx manages VM snapshots, coverage via Intel PT, and guest execution.
- HyperHook runs inside the guest, harnessing the target function and signaling the host.
Snapshot fuzzing is particularly effective for:
- Applications with long startup phases (databases, browsers).
- Multithreaded targets where fork-mode fuzzing is unreliable.
- Closed-source binaries where recompilation is not possible.
- Kernel-mode targets (full-system snapshot with KVM).
17. Smart Contract Fuzzing#
Blockchain smart contracts are high-value fuzzing targets — bugs directly translate to financial loss.
Medusa#
Medusa (Trail of Bits) is an open-source EVM-based smart contract fuzzer built on Geth, successor to Echidna. Key features:
- Coverage-guided fuzzing with HTML coverage reports.
- Parallel fuzzing — scales across CPU cores for faster campaigns.
- Smart mutational value generation — leverages runtime values and Slither static analysis to optimize inputs.
- On-chain fuzzing — seeds state with values fetched from live blockchain data.
- Property-based testing — write Solidity invariants that Medusa tries to violate.
brew install medusa # macOS
medusa init # generates medusa.json config
medusa fuzz # start fuzzing
Echidna#
The predecessor to Medusa, written in Haskell. Still maintained for bug fixes but development focus has shifted to Medusa. Echidna pioneered property-based smart contract fuzzing with Solidity assertion checking.
Smart contract fuzzing targets#
- Invariant violations — token balances don’t add up, access control bypassed.
- Reentrancy — external calls that re-enter the contract before state updates.
- Integer overflow/underflow — in pre-Solidity-0.8 contracts without SafeMath.
- Flash loan attacks — price manipulation via large temporary borrows.
- Governance attacks — voting manipulation via proposal parameter fuzzing.
18. Protocol & Network Fuzzing (Boofuzz, ICS)#
Boofuzz#
Boofuzz is the modern successor to the Sulley framework — a Python-based, modular protocol fuzzer for stateful network services. Unlike web fuzzers that target HTTP, Boofuzz targets arbitrary TCP/UDP protocols.
from boofuzz import *
session = Session(
target=Target(connection=SocketConnection("192.168.1.1", 502, proto='tcp'))
)
s_initialize("modbus_read")
s_word(0x0001, name="transaction_id", fuzzable=True)
s_word(0x0000, name="protocol_id")
s_word(0x0006, name="length")
s_byte(0x01, name="unit_id")
s_byte(0x03, name="function_code")
s_word(0x0000, name="start_address", fuzzable=True)
s_word(0x000A, name="quantity", fuzzable=True)
session.connect(s_get("modbus_read"))
session.fuzz()
Boofuzz features:
- Stateful protocol modeling — define multi-step protocol sequences (e.g., login then command).
- Process monitoring — detect target crashes via process monitors, serial port monitors, or custom callbacks.
- Web UI — real-time fuzzing progress dashboard.
- Extensible primitives —
s_string,s_byte,s_word,s_dword,s_group,s_blockfor structured protocol fields.
ICS protocol fuzzing#
Industrial control protocols (Modbus/TCP, S7Comm, Ethernet/IP, DNP3, IEC 61850, OPC UA) present unique challenges:
- Stateful, sequence-dependent — commands must follow specific handshake sequences.
- Timing-sensitive — real-time constraints affect crash detection.
- Limited feedback — many PLCs have no crash reporting; monitoring requires external observation (power draw, LED states, network responses).
- Safety-critical — fuzzing live ICS equipment risks physical damage; use isolated testbeds.
Tools: Boofuzz (protocol modeling), MALF (LLM-guided), ISF (Industrial Security Framework), custom syzkaller descriptions for ICS-relevant kernel interfaces.
19. Crash Triage & Minimization#
A good fuzzing run produces hundreds or thousands of crashes — most are duplicates of a handful of real bugs.
Deduplication#
Group crashes by:
- Top-N stack frame hash (typical: top 3 frames, ignoring libc/sanitizer frames)
- Bug type (UAF vs stack OOB vs integer overflow)
- Crashing instruction address (rough, changes with PIE/ASLR)
Tools: casr, exploitable GDB plugin, AFL’s afl-collect, libFuzzer’s -dedup_token=.
Minimization#
Reduce crashing inputs to their smallest reproducing form so you can actually read them.
# AFL++
afl-tmin -i crash_input -o crash_min -- ./target @@
# libFuzzer
./fuzz -minimize_crash=1 -runs=10000 crash_input
Minimized inputs make root cause analysis dramatically easier and produce smaller, more publishable PoCs.
Crash analysis#
# GDB with the crashing input
gdb ./target -ex "run < crash_min" -ex "bt full" -ex "quit"
# rr for time-travel debugging
rr record ./target crash_min
rr replay
# then (rr) reverse-next, reverse-continue, etc.
ASan reports already give you:
- Bug type and summary line
- Allocation, free, and use stacks (for UAF)
- Shadow memory dump around the faulting address
For exploitability assessment, combine the ASan report with register state and the CFG of the crashing function.
Severity triage rough rubric#
| Signal | Likely severity |
|---|---|
| Heap OOB write, UAF, double-free | High — often exploitable |
| Stack buffer overflow | High — often exploitable without stack cookies |
| Heap OOB read | Medium — info leak |
| Uninitialized memory read | Medium — info leak |
| NULL deref | Low — DoS only, usually |
| Integer overflow without memory effect | Low — unless it sizes an allocation |
| Assertion failure / hang | Low — DoS |
20. CI/CD Integration#
Fuzzing pays off when it runs continuously. A one-shot fuzz campaign finds the easy bugs; continuous fuzzing catches regressions.
Short-run CI fuzzing#
On every PR, run each fuzz target for 60-300 seconds against the current corpus. Fail the build on new crashes. This catches obvious regressions without slowing merges.
# .github/workflows/fuzz.yml (sketch)
name: Fuzz
on: [pull_request]
jobs:
fuzz:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: sudo apt-get install -y clang llvm
- run: clang++ -fsanitize=fuzzer,address fuzz_target.cc target.cc -o fuzz
- uses: actions/cache@v4
with:
path: corpus/
key: fuzz-corpus-${{ github.ref }}
- run: ./fuzz corpus/ -max_total_time=120 -max_len=4096
Continuous fuzzing platforms#
| Platform | Owner | Strengths |
|---|---|---|
| OSS-Fuzz | Free for open-source; runs thousands of projects | |
| ClusterFuzz | Self-hostable infrastructure behind OSS-Fuzz | |
| ClusterFuzzLite | Lightweight CI-focused variant | |
| OneFuzz | Microsoft | Windows-first, cloud-native; archived but usable |
| Mayhem | ForAllSecure | Commercial, strong binary-only support |
| Code Intelligence CI Fuzz | Commercial | Java/Kotlin-friendly, enterprise |
OSS-Fuzz integration requires writing a Dockerfile, build.sh, and one or more fuzz targets per project. Google then runs the targets continuously across ASan, MSan, and UBSan builds, files bugs automatically, and enforces a 90-day disclosure window.
CI fuzzing best practices#
- Cache the corpus between runs — otherwise each CI run starts from scratch.
- Time-box runs with
-max_total_timeso CI doesn’t stall. - Upload crashes as artifacts for offline triage.
- Run longer campaigns nightly/weekly alongside the short PR runs.
- Gate merges on zero new crashes, not on coverage deltas (which are noisy).
- Store a golden corpus in object storage and pull it fresh per run.
21. Bugs That Survive Continuous Fuzzing#
Even years of continuous OSS-Fuzz enrollment does not guarantee security. A GitHub Security Lab study documented three high-profile examples and a systematic five-step workflow to address the gaps.
Why bugs survive#
Insufficient harness coverage (GStreamer): Enrolled in OSS-Fuzz for 7 years with only 2 active fuzzers and 19% code coverage. By comparison, OpenSSL has 139 fuzzers. In December 2024, 29 new vulnerabilities were found manually — including high-risk issues — because nobody had written harnesses for under-covered parsers.
Unfuzzed dependencies (Poppler/DjVuLibre): Poppler’s OSS-Fuzz integration covered Poppler itself at ~60% but did not instrument external dependencies like DjVuLibre. A critical 1-click RCE (CVE-2025-53367) was found in DjVuLibre — a dependency shipped by default with Evince/Papers on millions of Ubuntu systems but never fuzzed at all.
Neglected attack surfaces (Exiv2): Despite 3+ years of OSS-Fuzz enrollment and multiple CVEs found (CVE-2024-39695, CVE-2024-24826, CVE-2023-44398), new vulnerabilities (CVE-2025-26623, CVE-2025-54080) were reported by external researchers because encoding functions received far less fuzzing attention than decoding functions.
The five-step fuzzing workflow#
- Code preparation — remove checksums, reduce randomness, drop unnecessary delays, fix signal handling to make the target fuzzer-friendly.
- Improving code coverage — iterative cycle of running fuzzers, checking LCOV reports for uncovered areas, writing new harnesses or input cases. Target >90% edge coverage before moving on. Fuzz both obvious surfaces (decoders, parsers) and non-obvious ones (encoders, muxers, file writers).
- Context-sensitive coverage — switch from flat edge coverage to context-sensitive or N-gram coverage to distinguish execution paths that share the same edges but differ in calling context. Target >60%.
- Value coverage — instrument key variables with range-splitting branches so the fuzzer explores different value domains, catching arithmetic and boundary bugs missed by edge coverage alone.
- Triaging — systematic crash analysis and deduplication.
Advanced techniques for step 2: fault injection (simulating malloc failures, partial I/O, missing files) and snapshot fuzzing (AFL++ QEMU/Nyx modes) for stateful targets.
22. Real-World Wins & CVEs#
Fuzzing’s track record is staggering. A partial sampling:
Browser engines#
- V8 / Chrome — Fuzzilli has found dozens of JIT bugs, many exploited in the wild by state actors before discovery.
- JavaScriptCore — similar story, frequent fuzzing finds turn into in-the-wild iOS exploits.
- SpiderMonkey — LangFuzz, jsfunfuzz, and Fuzzilli produce a steady stream of bugs.
Multimedia & document parsers#
- GStreamer — 29 new vulnerabilities discovered in December 2024 despite 7 years of OSS-Fuzz enrollment, because only 2 harnesses existed covering 19% of code. Demonstrates that enrollment without harness maintenance is insufficient.
- Poppler / DjVuLibre — CVE-2025-53367: a 1-click RCE in DjVuLibre (the DjVu parser shipped with Ubuntu’s Evince). The dependency was never included in Poppler’s OSS-Fuzz build despite being installed on millions of systems by default.
- Exiv2 — CVE-2024-39695, CVE-2024-24826, CVE-2023-44398 (found by OSS-Fuzz), plus CVE-2025-26623 and CVE-2025-54080 (found by external researchers) — encoding-side bugs that surviving continuous fuzzing focused on decoding.
System libraries#
- libjpeg, libpng, libtiff, libxml2, libxslt, libyaml — hundreds of CVEs from AFL and libFuzzer campaigns; most are now OSS-Fuzz targets.
- OpenSSL / BoringSSL — Heartbleed was not found by fuzzing, but many subsequent parser/ASN.1 bugs were. Honggfuzz and OSS-Fuzz routinely surface new issues.
- ImageMagick — “the tarpit” — an enormous parade of CVEs, many fuzzing-found, many in obscure format parsers.
Language runtimes#
- JerryScript —
CVE-2023-36109, an OOB read inecma_stringbuilder_append_rawreached via regex replace substitution, reproduced and localized using instrumented Fuzzilli with per-edge symbolization. - PHP — regular fuzzing finds in the ZIP, phar, and unserialize paths.
- Python — CPython fuzzing targets routinely turn up bugs in the C extensions.
Network stacks#
- Linux kernel netlink / netfilter / TCP stack — Syzkaller bugs numbering in the hundreds.
- QUIC implementations — differential fuzzing across ngtcp2, quiche, msquic finds protocol-compliance bugs.
- DNS resolvers — dnsmasq, unbound, BIND fuzz finds with structured generators.
Kernel & firmware#
- Linux kernel — Syzbot has filed thousands of bugs; KASAN + KCOV + KMSAN.
- Android / Fuchsia — Syzkaller ports find LPEs and driver bugs.
- Windows kernel — OneFuzz and commercial fuzzers find driver-level CVEs.
Industrial control / embedded#
- ICS protocols (Modbus, DNP3, IEC 61850) — MALF-style LLM frameworks and classical protocol fuzzers have surfaced pre-authentication RCE in multiple PLC firmwares. MALF identified 3 zero-day flaws in a power plant attack-defense range deployment (one CNVD-registered).
- USB stack fuzzing (syzkaller’s
usb-fuzzer) — dozens of Linux USB driver UAFs.
Smart contracts#
- Medusa / Echidna — Trail of Bits’ fuzzers have found invariant violations, reentrancy bugs, and access control bypasses in production DeFi protocols. Medusa’s on-chain seeding mode catches bugs that only manifest with real blockchain state.
- G2Fuzz — 10 unique bugs in latest real-world software using LLM-synthesized input generators, 3 CVE-confirmed.
Web apps#
- Directory/endpoint fuzzing with ffuf/feroxbuster regularly surfaces
/.git/,/.env,/backup.zip,/api/internal, admin panels, and dev endpoints on real bounty targets. - Parameter brute-forcing finds hidden debug flags (
?debug=1,?admin=true). - GraphQL introspection + field fuzzing finds IDOR/BOLA at scale.
23. Tools & Frameworks Reference#
Coverage-guided engines#
| Tool | Language | Strengths |
|---|---|---|
| AFL++ | C/C++/Rust via afl-clang-fast, Python via hooks | CMPLOG, persistent mode, QEMU/Frida modes |
| libFuzzer | C/C++ | In-process, extremely fast, LLVM-integrated (maintenance mode) |
| LibAFL | C/C++/Rust/Java (via Jazzer fork) | Modular Rust fuzzing library; composable feedback, mutators, executors |
| honggfuzz | C/C++ | Hardware-assisted coverage, persistent mode |
| Centipede | C/C++ | Google’s distributed successor to libFuzzer |
| go-fuzz / Go native fuzzing | Go | Built into go test since Go 1.18 |
| cargo-fuzz | Rust | libFuzzer wrapper for Rust crates |
| cargo-libafl | Rust | LibAFL-based drop-in replacement for cargo-fuzz |
| Jazzer | Java/Kotlin | libFuzzer wrapper with JVM bytecode instrumentation |
| Atheris | Python | libFuzzer + Python coverage hooks |
| jsfuzz | JavaScript | Node.js coverage-guided fuzzer |
Grammar / structure-aware#
| Tool | Target |
|---|---|
| Fuzzilli | JavaScript engines (V8, JSC, SpiderMonkey, JerryScript) |
| Domato | HTML/CSS/JS DOM rendering |
| Dharma / Grammarinator | Arbitrary grammars |
| libprotobuf-mutator | Protobuf-shaped inputs |
| Peach / SPIKE | Network protocols |
| Radamsa | Black-box mutation of structured text |
| G2Fuzz | LLM-synthesized generators for non-textual inputs (TIFF, MP4, PDF) |
Web & API#
| Tool | Purpose |
|---|---|
| ffuf | Fast HTTP fuzzer: directories, parameters, subdomains, bodies |
| feroxbuster | Recursive content discovery |
| wfuzz | Multi-point HTTP fuzzer with rich filters |
| Gobuster | Simple, fast directory/subdomain brute-forcing |
| Burp Suite Intruder | Payload-driven parameter fuzzing |
| Turbo Intruder | Burp extension for high-speed, race-condition fuzzing |
| Schemathesis | OpenAPI property-based fuzzer |
| RESTler | Stateful REST fuzzer |
| EvoMaster | Search-based REST/GraphQL fuzzer with JUnit output |
| InQL | GraphQL schema extractor + fuzzer |
| Arjun / ParamMiner | HTTP parameter discovery |
Protocol & ICS#
| Tool | Purpose |
|---|---|
| Boofuzz | Stateful network protocol fuzzer (successor to Sulley) |
| MALF | Multi-agent LLM framework for ICS protocol fuzzing |
| ISF | Industrial Security Framework for SCADA/ICS |
Smart contract#
| Tool | Purpose |
|---|---|
| Medusa | EVM-based coverage-guided fuzzer (Trail of Bits, successor to Echidna) |
| Echidna | Property-based Haskell smart contract fuzzer |
Snapshot & harnessing#
| Tool | Purpose |
|---|---|
| Nyx | Hypervisor-based snapshot fuzzing via KVM + Intel PT |
| HyperHook | Harnessing framework for Nyx (Neodyme) |
Kernel / OS#
| Tool | Target |
|---|---|
| Syzkaller / syzbot | Linux / BSD / Fuchsia / Windows kernels |
| Trinity | Linux syscall fuzzer |
| kAFL / Nyx / HyperHook | Snapshot-based full-system fuzzing + harnessing framework |
| usb-fuzzer | Linux USB subsystem |
| KCOV | Kernel coverage collection API |
Continuous platforms#
| Platform | Notes |
|---|---|
| OSS-Fuzz | Free for open source, run by Google |
| ClusterFuzz / ClusterFuzzLite | Self-hostable |
| OneFuzz | Microsoft’s platform |
| Mayhem | ForAllSecure commercial |
Mutation / test generation#
| Tool | Notes |
|---|---|
| Radamsa | Language-agnostic text mutator |
| zzuf | Transparent stream mutator |
| FuzzedDataProvider | libFuzzer helper for structured harness inputs |
24. Wordlist & Corpus Resources#
Web wordlists#
| Source | Use |
|---|---|
SecLists (danielmiessler/SecLists) | The canonical collection: directories, parameters, subdomains, fuzzing payloads |
Assetnote wordlists (assetnote.io/resources/downloads) | Tech-specific: wordpress, laravel, tomcat, etc. |
| raft-medium/large-directories.txt | Standard directory discovery lists |
| raft-large-words.txt | General word dictionary |
| api-endpoints.txt (SecLists) | REST endpoint guesses |
| graphql.txt | GraphQL operation names |
| subdomains-top1million-110000.txt | Subdomain brute-forcing |
| rockyou.txt | Credential stuffing / parameter value spraying |
| PayloadsAllTheThings | SQLi, XSS, SSRF, SSTI, command injection payloads |
Binary / format corpora#
| Source | Use |
|---|---|
| OSS-Fuzz corpus backups | Public GCS bucket per target |
| Mozilla fuzzdata | Browser formats |
| Fuzzer Test Suite | Historical Google benchmark seeds |
| CERT BFF samples | General format seeds |
| Synthetic PDF/PNG/JPEG suites | Stress-test files |
Dictionaries#
| Source | Use |
|---|---|
AFL++ dictionaries/ | Ships with AFL++; covers XML, SQL, PDF, HTML, JS, and more |
| libFuzzer examples | compiler-rt/lib/fuzzer/dictionaries/ |
| Awesome-Fuzzing | Curated links to everything above |
25. Quick Reference Cheatsheet#
Build a libFuzzer target#
clang++ -g -O1 -fsanitize=fuzzer,address,undefined \
-fno-sanitize-recover=all -fno-omit-frame-pointer \
fuzz_target.cc target.cc -o fuzz
./fuzz corpus/ -max_len=4096 -dict=keywords.dict -jobs=8
Build and run AFL++#
AFL_USE_ASAN=1 afl-clang-fast -o target target.c
afl-fuzz -i corpus -o findings -- ./target @@
afl-fuzz -i corpus -o findings -M master -- ./target @@ # main node
afl-fuzz -i corpus -o findings -S slave1 -- ./target @@ # secondary
Minimize a crash#
afl-tmin -i crash -o crash_min -- ./target @@
./fuzz -minimize_crash=1 -runs=100000 crash
Merge/minimize a corpus#
./fuzz -merge=1 corpus_min/ corpus/
afl-cmin -i corpus -o corpus_min -- ./target @@
ffuf one-liners#
# Directories
ffuf -u https://t/FUZZ -w raft-medium.txt -t 50 -fc 404
# Subdomains
ffuf -u https://FUZZ.t.com -w subs.txt -H "Host: FUZZ.t.com"
# Parameters (GET)
ffuf -u "https://t/api?FUZZ=test" -w params.txt -fs 0
# JSON body
ffuf -u https://t/api -X POST -H "Content-Type: application/json" \
-d '{"id":"FUZZ"}' -w ids.txt -mc 200
# Virtual hosts
ffuf -u https://t -H "Host: FUZZ.t.com" -w subs.txt -fs 1234
libFuzzer flag cheatsheet#
| Flag | Meaning |
|---|---|
-max_len=N | Cap input size |
-dict=f | Load dictionary |
-jobs=N | Run N child processes |
-workers=N | Parallel workers |
-runs=N | Stop after N iterations |
-timeout=N | Per-input timeout (seconds) |
-rss_limit_mb=N | Memory cap |
-fork=N | Fork mode for crash isolation |
-merge=1 dst src | Minimize corpus |
-minimize_crash=1 | Shrink a reproducer |
-seed=N | Deterministic seed |
-print_final_stats=1 | Dump coverage stats on exit |
Sanitizer flag combos#
# Development default
-fsanitize=address,undefined -fno-sanitize-recover=all
# Heavy uninit detection (all deps must be MSan-built)
-fsanitize=memory -fsanitize-memory-track-origins=2
# Data races
-fsanitize=thread
# Integer overflow only
-fsanitize=signed-integer-overflow,unsigned-integer-overflow
Harness template (libFuzzer, C++)#
#include <cstddef>
#include <cstdint>
#include <fuzzer/FuzzedDataProvider.h>
#include "target.h"
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
if (size < 8) return 0;
FuzzedDataProvider fdp(data, size);
int mode = fdp.ConsumeIntegralInRange<int>(0, 3);
auto input = fdp.ConsumeRemainingBytes<uint8_t>();
Target t;
t.process(mode, input.data(), input.size());
return 0;
}
AFL++ environment knobs#
| Variable | Effect |
|---|---|
AFL_USE_ASAN=1 | Build with AddressSanitizer |
AFL_USE_UBSAN=1 | Build with UBSan |
AFL_USE_MSAN=1 | Build with MemorySanitizer |
AFL_LLVM_CMPLOG=1 | Enable CMPLOG magic-value solving |
AFL_LLVM_LAF_ALL=1 | Enable all LAF-INTEL transforms |
AFL_SKIP_CPUFREQ=1 | Skip CPU frequency scaling check |
AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES=1 | Skip core-pattern check |
AFL_PERSISTENT=1 | Hint persistent-mode harness |
AFL_TMPDIR=/dev/shm/afl | Put queue on tmpfs for speed |
Jazzer one-liner#
# Run Jazzer on a Java target
jazzer --cp=target.jar --target_class=com.example.FuzzTarget \
--instrumentation_includes=com.example.** corpus/
Atheris one-liner#
python3 -m atheris.instrument_all -- python3 my_fuzzer.py corpus/
Medusa one-liner#
medusa init && medusa fuzz --workers 8
Boofuzz skeleton#
from boofuzz import *
session = Session(target=Target(connection=SocketConnection("host", 80)))
s_initialize("request")
s_string("FUZZ", fuzzable=True)
session.connect(s_get("request"))
session.fuzz()
Crash triage checklist#
- Is this a duplicate of an existing crash? (Top-3 stack frames.)
- What does the ASan/UBSan report classify it as?
- Does
afl-tminor-minimize_crash=1shrink it to something readable? - Is it deterministic across reruns?
- Is the crashing input reachable from real, attacker-controlled input?
- Can you demonstrate impact beyond DoS (write primitive, info leak, RCE)?
- Patch, add the crashing input as a regression test, rerun.
Closing Notes#
Fuzzing is not a replacement for code review, unit tests, or threat modeling — it’s a force multiplier. A single good libFuzzer harness paired with sanitizers and a weekend of compute will out-find most manual audits on parser code. But the hard part is never running the fuzzer; it’s picking the right target, writing a tight harness, curating the corpus, and triaging crashes that matter.
The modern playbook:
- Identify a parser, protocol implementation, or untrusted-input surface.
- Write a small libFuzzer-style harness with
FuzzedDataProvider. - Build with ASan + UBSan + fuzzer instrumentation.
- Seed it with 20-50 diverse, small, real inputs.
- Drop in a dictionary of magic bytes and keywords.
- Run for hours, not seconds. Parallelize with
-jobs. - Minimize every unique crash. Read the reports.
- Wire it into CI so regressions don’t reintroduce the same bugs.
- For closed-source or kernel targets, reach for AFL++ QEMU mode, WinAFL, or syzkaller.
- For structured languages, invest in grammar-aware generation (Fuzzilli, LPM) rather than fighting mutation.
The bugs are there. The tooling is free. The main cost is harness engineering and compute.