AFL++ Guide: Coverage-Guided Fuzzing for C, C++, and More
AFL++ (American Fuzzy Lop++) is the most widely used coverage-guided fuzzer for C and C++ programs. It instruments your binary at compile time to track code coverage, then mutates inputs guided by that coverage to explore your program's logic exhaustively. Used by Google, Mozilla, and security researchers to find thousands of CVEs. This guide covers installation, instrumentation, running a fuzzing campaign, and reading results.
How AFL++ Works
AFL++ uses compile-time instrumentation to insert coverage counters at every branch point in your program. When you run a test input, AFL++ knows exactly which branches were taken. This information guides the mutation engine: inputs that trigger new branches are kept as seeds; inputs that cover old ground are discarded.
The mutation pipeline:
- Pick a seed from the corpus
- Mutate it (bit flips, byte insertions, interesting values, splicing with other seeds)
- Run the instrumented binary
- New coverage? Add to corpus
- Crash/hang? Save as finding
- Repeat — millions of times per second
AFL++ averages 1,000–5,000 executions per second for most programs. Over a 24-hour run, that's 86–430 million test cases.
Installation
# Ubuntu/Debian
sudo apt-get update
sudo apt-get install -y afl++
# Or build from source (latest features)
git clone https://github.com/AFLplusplus/AFLplusplus
cd AFLplusplus
make distrib
sudo make installVerify:
afl-fuzz --version
# afl-fuzz++4.21a by Michal ZalewskiInstrumenting Your Target
AFL++ works through instrumentation — modifying the binary to track coverage.
Option 1: Compile-time instrumentation (fastest)
Replace gcc/g++ with afl-cc/afl-c++:
# Before: normal compilation
gcc -O2 -o my_parser src/parser.c
# After: AFL++ instrumented compilation
afl-cc -O2 -o my_parser src/parser.c
afl-c++ -O2 -o my_parser src/parser.cppFor autotools projects:
CC=afl-cc CXX=afl-c++ ./configure
makeFor CMake:
cmake -DCMAKE_C_COMPILER=afl-cc -DCMAKE_CXX_COMPILER=afl-c++ ..
makeOption 2: QEMU mode (no source code needed)
If you can't instrument the binary (closed source, proprietary):
afl-fuzz -Q -i corpus/ -o output/ -- ./target @@QEMU mode is 2-5x slower than compile-time instrumentation.
Writing a Fuzz Target
AFL++ needs a target that reads input from a file or stdin.
File input target:
// parse_target.c
#include <stdio.h>
#include <stdlib.h>
#include "my_parser.h"
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <input_file>\n", argv[0]);
return 1;
}
FILE *f = fopen(argv[1], "rb");
if (!f) return 1;
fseek(f, 0, SEEK_END);
size_t len = ftell(f);
fseek(f, 0, SEEK_SET);
char *buf = malloc(len + 1);
fread(buf, 1, len, f);
fclose(f);
buf[len] = '\0';
// Your parser under test
parse_document(buf, len);
free(buf);
return 0;
}afl-cc -O2 -g -fsanitize=address -o target parse_target.c -lmy_parserPersistent mode (10-100x faster):
Instead of forking a new process per test, use the persistent mode loop:
#include "afl-fuzz.h" // Included with AFL++
int main() {
// One-time initialization
my_parser_init();
// Persistent mode loop
while (__AFL_LOOP(10000)) {
// Read input
size_t len = 0;
uint8_t *buf = NULL;
ssize_t n = getline((char**)&buf, &len, stdin);
if (n > 0) {
parse_document(buf, n);
}
free(buf);
}
return 0;
}afl-cc -O2 -g -fsanitize=address -o target_persistent target.cSetting Up the Corpus
The initial corpus is seeds — valid inputs that your parser accepts. Good seeds:
- Cover different valid input formats
- Are small (AFL++ mutates them byte by byte)
- Are diverse (different features of the format)
mkdir corpus/
# Add some valid examples
echo '{"key": "value"}' > corpus/simple.json
echo '{"nested": {"a": 1, "b": [1,2,3]}}' > corpus/nested.json
echo '{}' > corpus/empty.json
echo '{"long": ' > corpus/truncated.json # Intentionally truncatedMinimize the corpus (removes redundant seeds that don't add coverage):
afl-cmin -i corpus/ -o corpus_min/ -- ./target @@Minimize individual corpus files:
for f in corpus_min/*; do
afl-tmin -i "$f" -o "corpus_minimized/$(basename $f)" -- ./target @@
doneRunning AFL++
Basic run:
afl-fuzz \
-i corpus/ \ # Input corpus directory
-o output/ \ # Output directory (crashes, findings, stats)
-- ./target @@ # @@ is replaced with input file pathFor stdin input:
afl-fuzz -i corpus/ -o output/ -- ./targetParallel fuzzing (one master, N-1 secondary):
# Master (generates new test cases)
afl-fuzz -i corpus/ -o output/ -M fuzzer01 -- ./target @@
# Secondary instances (in separate terminals or processes)
afl-fuzz -i corpus/ -o output/ -S fuzzer02 -- ./target @@
afl-fuzz -i corpus/ -o output/ -S fuzzer03 -- ./target @@
afl-fuzz -i corpus/ -o output/ -S fuzzer04 -- ./target @@AFL++ synchronizes the corpus between instances — each instance sees findings from all others.
Reading the AFL++ Dashboard
american fuzzy lop ++4.21a (target)
+- process timing -------------------------------------------------+
| run time : 0 days, 2 hrs, 13 min, 7 sec |
| last new path : 0 days, 0 hrs, 3 min, 2 sec |
| last uniq crash : none seen yet |
| last uniq hang : 0 days, 0 hrs, 0 min, 8 sec |
+- overall results ------------------------------------------------+
| cycles done : 3 |
| map density : 11.35% / 14.21% |
| count coverage : 4.74 bits/tuple |
| findings in depth : 0/0/0/0/0/0/0 (0) |
| total paths : 2271 |
| uniq crashes : 0 |
| uniq hangs : 1 |
+- fuzzing strategy yields ----------------------------------------+
| bit flips : 247/4.00k |
| byte flips : 30/2.00k |
| arithmetics : 1.65k/31.2k |
| known ints : 94/7.66k |
| dictionary : n/a |
| havoc/splice : 6.75k/73.0k |Key metrics to watch:
- total paths — code paths discovered. Should keep growing; if it plateaus, consider adding more seeds
- uniq crashes — crashes found. Each unique crash is a potential bug
- uniq hangs — infinite loops or timeout conditions
- cycles done — how many times AFL++ has gone through the corpus
- last new path — time since a new code path was discovered. Growing old (> 30 min) suggests you've explored most reachable paths
- map density — percentage of coverage bitmap used. High density (> 70%) may indicate map collisions; increase
MAP_SIZE
Combining with AddressSanitizer
Compile with ASAN for better crash detection:
AFL_USE_ASAN=1 afl-cc -O1 -g -o target_asan target.cOr manually:
afl-cc -O1 -g -fsanitize=address,undefined -o target_asan target.cASAN finds:
- Heap buffer overflows (beyond what causes a crash)
- Use-after-free
- Stack buffer overflows
- Memory leaks (with
ASAN_OPTIONS=detect_leaks=1)
Note: ASAN adds ~2x memory overhead and slows execution by 2x. For long runs, consider using ASAN-only for crash triage rather than initial discovery.
Handling Crashes
When AFL++ finds a crash, it saves the input to output/crashes/:
output/
crashes/
id:000000,sig:11,src:000042,time:12345,op:havoc,rep:4
id:000001,sig:06,src:000087,time:23456,op:flip8,rep:2
hangs/
id:000000,src:000012,time:34567,op:flip1,rep:8
queue/
...Reproduce a crash:
./target output/crashes/id:000000,sig:11,...Minimize the crash (find smallest input that still crashes):
afl-tmin \
-i output/crashes/id:000000,sig:11,... \
-o minimized_crash.bin \
-- ./target @@Analyze the crash with GDB:
gdb --args ./target_asan minimized_crash.bin
run
bt # Backtrace when it crashesDictionaries
A dictionary helps AFL++ generate inputs with meaningful tokens. For JSON:
# dictionaries/json.dict
keyword_true="true"
keyword_false="false"
keyword_null="null"
bracket_open="{"
bracket_close="}"
colon=":"
comma=","Use with AFL++:
afl-fuzz -i corpus/ -o output/ -x dictionaries/json.dict -- ./target @@AFL++ includes dictionaries for common formats:
ls /usr/share/afl/dictionaries/
# html.dict http.dict json.dict sql.dict xml.dict ...Evaluating Fuzzing Coverage
After a campaign, evaluate what AFL++ covered:
# Generate an LCOV report from the AFL++ corpus
afl-showmap -C -i output/queue/ -o coverage.map -- ./target @@
# View stats
cat output/default/fuzzer_stats | grep -E "paths_total|unique_crashes|unique_hangs"For C/C++ with gcov:
# Compile with coverage
gcc --coverage -O0 -g -o target_cov target.c
# Run corpus through coverage-instrumented binary
for f in output/queue/id*; do ./target_cov "$f"; done
# Generate report
gcov target.cOSS-Fuzz Integration
If your project is open source, submit to OSS-Fuzz — Google's free continuous fuzzing service:
- Write a
fuzz_target.cc(LibFuzzer target) - Create a
Dockerfilethat builds your project - Submit a PR to the oss-fuzz repository
OSS-Fuzz runs your fuzzers continuously on Google's infrastructure, reports crashes to you, and verifies fixes. It has found thousands of CVEs in projects like OpenSSL, FFmpeg, and libpng.