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

  1. Fundamentals
  2. Fuzzing Taxonomy
  3. Coverage-Guided Fuzzing
  4. Harness Construction
  5. Corpus Management & Seed Selection
  6. Dictionaries & Structure-Aware Fuzzing
  7. Sanitizers
  8. Binary Fuzzing (AFL++, libFuzzer, honggfuzz)
  9. Web Fuzzing (ffuf, wfuzz, feroxbuster, Burp Intruder)
  10. API Fuzzing (REST, GraphQL, Protobuf)
  11. Kernel & OS Fuzzing
  12. Directed & Grammar-Based Fuzzing
  13. AI-Augmented Fuzzing
  14. Crash Triage & Minimization
  15. CI/CD Integration
  16. Real-World Wins & CVEs
  17. Tools & Frameworks Reference
  18. Wordlist & Corpus Resources
  19. 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:

  1. Test case generation — synthesize or mutate inputs.
  2. Test execution — run the target with the input.
  3. Monitoring — observe crashes, hangs, sanitizer reports, coverage.
  4. Feedback — prioritize interesting inputs, discard redundant ones.
  5. 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:

CategoryExamples
File parsersPDF, PNG, JPEG, TIFF, audio/video codecs
Network protocolsHTTP, DNS, TLS, QUIC, Bluetooth stacks, Netlink
Language runtimesJavaScript engines, WASM, regex engines
SerializationProtobuf, msgpack, CBOR, ASN.1, BSON
Crypto librariesOpenSSL, BoringSSL, NSS
OS kernel surfacessyscalls, ioctls, filesystem drivers, USB stack
Web APIsREST, GraphQL, gRPC endpoints
DatabasesSQL 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

TypeStarts withBest forTools
Mutation-basedValid sample inputs; flips bits, inserts/deletes bytes, splicesBinary formats, legacy CLIs, when you have a corpusAFL++, honggfuzz, Radamsa
Generation-basedA grammar, model, or protocol specStructured inputs (JS, HTML, JSON, protocols)Peach, BooFuzz, SPIKE, Fuzzilli, Domato
HybridBoth; grammar seeds feed into mutation engineLanguage runtimes, parsersFuzzilli, Dharma

By visibility into the target

TypeKnowledgeStrengthWeakness
Black-boxNone — I/O onlyEasy to set up, no build changesShallow coverage, misses deep paths
Grey-boxLightweight instrumentation (edge coverage)Best balance of effort and results — the modern defaultNeeds recompilation or binary rewriting
White-boxFull source + symbolic/concolic executionReaches deep constraintsExpensive, 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)

  1. Pick the most promising test case from the queue.
  2. Mutate it into many children (bit flips, arithmetic, splicing, havoc).
  3. Run each child; the instrumented binary updates the coverage bitmap.
  4. Score each child by new coverage. Promising ones enter the corpus.
  5. 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

  1. Keep it fast. Aim for thousands of execs/sec. Every millisecond of setup costs orders of magnitude in total coverage.
  2. Stateless where possible. Reset global state between inputs; if not possible, use -runs=1 or fork mode.
  3. Exercise realistic entry points. Wrap the same functions an attacker can reach — not helper internals.
  4. Split the input. For multi-argument APIs, carve data into pieces with a small prefix header or FuzzedDataProvider.
  5. Avoid nondeterminism. Seed any RNG with a constant; disable timestamps, thread scheduling surprises.
  6. Check assertions, not output. Let sanitizers do the talking.
  7. 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() or abort() 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 grammarsfunction, return, =>, async
  • Binary magic bytes — PNG \x89PNG, ELF \x7fELF, PDF %PDF-
  • HTTP verbs, headersGET, POST, Content-Type:
  • SQL keywordsSELECT, 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:

TechniqueDescriptionTools
Grammar-based generationProduce inputs from a BNF/EBNFDharma, Domato, Grammarinator
Intermediate language (IL) mutationFuzz an AST/IR, then lower to bytesFuzzilli (JS), Token-level fuzzers
libprotobuf-mutatorMutate serialized protobuf messages preserving schemaLPM + libFuzzer
Custom mutatorslibFuzzer’s LLVMFuzzerCustomMutator hookAny engine
SplicingCombine fragments from valid corpus entriesBuilt 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.

SanitizerFlagDetects
ASan (AddressSanitizer)-fsanitize=addressHeap/stack/global buffer overflows, UAF, double-free, memory leaks (via LSan)
UBSan (UndefinedBehaviorSanitizer)-fsanitize=undefinedSigned integer overflow, NULL deref, misaligned access, divide-by-zero, OOB shifts
MSan (MemorySanitizer)-fsanitize=memoryUse of uninitialized memory — all transitive deps must also be MSan-built
TSan (ThreadSanitizer)-fsanitize=threadData races, deadlocks
LSan (LeakSanitizer)-fsanitize=leakMemory leaks (bundled into ASan by default)
CFI (Control Flow Integrity)-fsanitize=cfiIndirect 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=all to 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

FeaturePurpose
CMPLOGLogs comparison operands so the mutator can solve magic-byte checks
LAF-INTELSplits multi-byte comparisons into per-byte branches so coverage sees partial progress
Persistent modeLoops the harness N times per fork to amortize startup cost (huge speedup)
QEMU modeInstrumentation-free fuzzing of closed-source binaries
FRIDA modeDynamic instrumentation for binaries on macOS/Android
NyxSnapshot-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:

FlagPurpose
-max_len=NCap input length
-dict=fileUse a dictionary
-jobs=N -workers=NParallel fuzzing processes
-merge=1 dst srcMerge/minimize corpora
-runs=NRun N iterations then exit (CI mode)
-timeout=NPer-input timeout in seconds
-rss_limit_mb=NMemory cap
-fork=NRun 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:

ModeUse case
SniperOne payload list, one marker at a time — classic fuzz
Battering RamSame payload into every marker simultaneously
PitchforkParallel payload sets, walked in lockstep
Cluster BombCartesian 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

ToolApproachNotes
EvoMasterSearch-based white-box + black-boxGenerates JUnit tests; used in production at Volkswagen AG
RESTlerStateful, infers dependencies between endpointsMicrosoft Research
SchemathesisProperty-based, OpenAPI/Swagger-drivenPython, CI-friendly
DreddContract testing against OpenAPIPass/fail per endpoint
bBOXRTBlack-box robustness testingAcademic
Morest / ARAT-RL / AutoRestTestRL and model-basedRecent 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 inferencePOST /users returns an id used by GET /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 (.txt descriptions).
  • 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

ToolTargetNotes
TrinityLinux syscallsClassic, argument-aware but not coverage-guided
kAFLFull kernels via Intel PTHypervisor-assisted
SyzkallerLinux + othersThe workhorse
NyxSnapshot-fuzzing full VMsExtremely fast
DigtoolWindows kernelAcademic

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:

ToolLanguageNotes
FuzzilliJavaScriptIL-based; found dozens of V8/JSC/SpiderMonkey bugs
DomatoHTML/CSS/JS DOMChrome security team
DharmaAny grammarMozilla, generation-only
GrammarinatorANTLR grammarsCovers many real-world languages
SuperionStructure-aware AFL++Injects tree mutations
NautilusGrammar + coverage feedbackStrong 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

TaskClassical approachAI approach
Harness generationManual, hours per targetLLM generates from API headers
Seed synthesisCollect real samplesGenerate grammar-valid seeds via LLM
Mutation strategy selectionHand-tuned schedulesRL agents (GRLFuzz) learn per-target
Crash triageManual stack analysisLLM summarizes root cause
Reachability guidanceDirected fuzzing + static analysisLLM 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

  1. Point an LLM at the target repo; ask it to list the top 10 functions that parse untrusted input.
  2. Generate libFuzzer harnesses for each; compile with sanitizers.
  3. Have the LLM draft a dictionary of keywords/magic bytes from the source.
  4. Run for an hour. Collect crashes.
  5. For each unique crash, feed stack + function source + input to the LLM for triage notes.
  6. 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:

  1. Top-N stack frame hash (typical: top 3 frames, ignoring libc/sanitizer frames)
  2. Bug type (UAF vs stack OOB vs integer overflow)
  3. 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

SignalLikely severity
Heap OOB write, UAF, double-freeHigh — often exploitable
Stack buffer overflowHigh — often exploitable without stack cookies
Heap OOB readMedium — info leak
Uninitialized memory readMedium — info leak
NULL derefLow — DoS only, usually
Integer overflow without memory effectLow — unless it sizes an allocation
Assertion failure / hangLow — 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

PlatformOwnerStrengths
OSS-FuzzGoogleFree for open-source; runs thousands of projects
ClusterFuzzGoogleSelf-hostable infrastructure behind OSS-Fuzz
ClusterFuzzLiteGoogleLightweight CI-focused variant
OneFuzzMicrosoftWindows-first, cloud-native; archived but usable
MayhemForAllSecureCommercial, strong binary-only support
Code Intelligence CI FuzzCommercialJava/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

  1. Cache the corpus between runs — otherwise each CI run starts from scratch.
  2. Time-box runs with -max_total_time so CI doesn’t stall.
  3. Upload crashes as artifacts for offline triage.
  4. Run longer campaigns nightly/weekly alongside the short PR runs.
  5. Gate merges on zero new crashes, not on coverage deltas (which are noisy).
  6. 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

  • JerryScriptCVE-2023-36109, an OOB read in ecma_stringbuilder_append_raw reached 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

ToolLanguageStrengths
AFL++C/C++/Rust via afl-clang-fast, Python via hooksCMPLOG, persistent mode, QEMU/Frida modes
libFuzzerC/C++In-process, extremely fast, LLVM-integrated
honggfuzzC/C++Hardware-assisted coverage, persistent mode
CentipedeC/C++Google’s distributed successor to libFuzzer
go-fuzz / Go native fuzzingGoBuilt into go test since Go 1.18
cargo-fuzzRustlibFuzzer wrapper for Rust crates
JazzerJava/KotlinlibFuzzer wrapper with JVM bytecode instrumentation
AtherisPythonlibFuzzer + Python coverage hooks
jsfuzzJavaScriptNode.js coverage-guided fuzzer

Grammar / structure-aware

ToolTarget
FuzzilliJavaScript engines (V8, JSC, SpiderMonkey, JerryScript)
DomatoHTML/CSS/JS DOM rendering
Dharma / GrammarinatorArbitrary grammars
libprotobuf-mutatorProtobuf-shaped inputs
Peach / BooFuzz / SPIKENetwork protocols
RadamsaBlack-box mutation of structured text

Web & API

ToolPurpose
ffufFast HTTP fuzzer: directories, parameters, subdomains, bodies
feroxbusterRecursive content discovery
wfuzzMulti-point HTTP fuzzer with rich filters
GobusterSimple, fast directory/subdomain brute-forcing
Burp Suite IntruderPayload-driven parameter fuzzing
Turbo IntruderBurp extension for high-speed, race-condition fuzzing
SchemathesisOpenAPI property-based fuzzer
RESTlerStateful REST fuzzer
EvoMasterSearch-based REST/GraphQL fuzzer with JUnit output
InQLGraphQL schema extractor + fuzzer
Arjun / ParamMinerHTTP parameter discovery

Kernel / OS

ToolTarget
Syzkaller / syzbotLinux / BSD / Fuchsia / Windows kernels
TrinityLinux syscall fuzzer
kAFL / NyxSnapshot-based full-system fuzzing
usb-fuzzerLinux USB subsystem
KCOVKernel coverage collection API

Continuous platforms

PlatformNotes
OSS-FuzzFree for open source, run by Google
ClusterFuzz / ClusterFuzzLiteSelf-hostable
OneFuzzMicrosoft’s platform
MayhemForAllSecure commercial

Mutation / test generation

ToolNotes
RadamsaLanguage-agnostic text mutator
zzufTransparent stream mutator
FuzzedDataProviderlibFuzzer helper for structured harness inputs

18. Wordlist & Corpus Resources

Web wordlists

SourceUse
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.txtStandard directory discovery lists
raft-large-words.txtGeneral word dictionary
api-endpoints.txt (SecLists)REST endpoint guesses
graphql.txtGraphQL operation names
subdomains-top1million-110000.txtSubdomain brute-forcing
rockyou.txtCredential stuffing / parameter value spraying
PayloadsAllTheThingsSQLi, XSS, SSRF, SSTI, command injection payloads

Binary / format corpora

SourceUse
OSS-Fuzz corpus backupsPublic GCS bucket per target
Mozilla fuzzdataBrowser formats
Fuzzer Test SuiteHistorical Google benchmark seeds
CERT BFF samplesGeneral format seeds
Synthetic PDF/PNG/JPEG suitesStress-test files

Dictionaries

SourceUse
AFL++ dictionaries/Ships with AFL++; covers XML, SQL, PDF, HTML, JS, and more
libFuzzer examplescompiler-rt/lib/fuzzer/dictionaries/
Awesome-FuzzingCurated 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

FlagMeaning
-max_len=NCap input size
-dict=fLoad dictionary
-jobs=NRun N child processes
-workers=NParallel workers
-runs=NStop after N iterations
-timeout=NPer-input timeout (seconds)
-rss_limit_mb=NMemory cap
-fork=NFork mode for crash isolation
-merge=1 dst srcMinimize corpus
-minimize_crash=1Shrink a reproducer
-seed=NDeterministic seed
-print_final_stats=1Dump 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

VariableEffect
AFL_USE_ASAN=1Build with AddressSanitizer
AFL_USE_UBSAN=1Build with UBSan
AFL_USE_MSAN=1Build with MemorySanitizer
AFL_LLVM_CMPLOG=1Enable CMPLOG magic-value solving
AFL_LLVM_LAF_ALL=1Enable all LAF-INTEL transforms
AFL_SKIP_CPUFREQ=1Skip CPU frequency scaling check
AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES=1Skip core-pattern check
AFL_PERSISTENT=1Hint persistent-mode harness
AFL_TMPDIR=/dev/shm/aflPut queue on tmpfs for speed

Crash triage checklist

  1. Is this a duplicate of an existing crash? (Top-3 stack frames.)
  2. What does the ASan/UBSan report classify it as?
  3. Does afl-tmin or -minimize_crash=1 shrink it to something readable?
  4. Is it deterministic across reruns?
  5. Is the crashing input reachable from real, attacker-controlled input?
  6. Can you demonstrate impact beyond DoS (write primitive, info leak, RCE)?
  7. 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:

  1. Identify a parser, protocol implementation, or untrusted-input surface.
  2. Write a small libFuzzer-style harness with FuzzedDataProvider.
  3. Build with ASan + UBSan + fuzzer instrumentation.
  4. Seed it with 20-50 diverse, small, real inputs.
  5. Drop in a dictionary of magic bytes and keywords.
  6. Run for hours, not seconds. Parallelize with -jobs.
  7. Minimize every unique crash. Read the reports.
  8. Wire it into CI so regressions don’t reintroduce the same bugs.
  9. For closed-source or kernel targets, reach for AFL++ QEMU mode, WinAFL, or syzkaller.
  10. 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.