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, and API targets. Compiled from 23 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)
- Web Fuzzing (ffuf, wfuzz, feroxbuster, Burp Intruder)
- API Fuzzing (REST, GraphQL, Protobuf)
- Kernel & OS Fuzzing
- Directed & Grammar-Based Fuzzing
- AI-Augmented Fuzzing
- Crash Triage & Minimization
- CI/CD Integration
- 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.
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;
}
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.
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)#
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___
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 |
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. Reportedly added coverage to dozens of projects with minimal human effort.
- MALF — multi-agent LLM framework for fuzzing industrial control protocols; agents specialize in packet structure, state modeling, and mutation selection.
- 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. 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 |
15. 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.
16. 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.
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.
- USB stack fuzzing (syzkaller’s
usb-fuzzer) — dozens of Linux USB driver UAFs.
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.
17. 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 |
| 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 |
| 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 / BooFuzz / SPIKE | Network protocols |
| Radamsa | Black-box mutation of structured text |
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 |
Kernel / OS#
| Tool | Target |
|---|---|
| Syzkaller / syzbot | Linux / BSD / Fuchsia / Windows kernels |
| Trinity | Linux syscall fuzzer |
| kAFL / Nyx | Snapshot-based full-system fuzzing |
| 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 |
18. 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 |
19. 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 |
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.