Instrumentation Overview
Relevant source files
The following files were used as context for generating this wiki page:Instrumentation is the core mechanism that enables AFL++ to perform coverage-guided fuzzing. It involves inserting code into a target program to record its execution paths. This data, typically edge coverage, is stored in a shared memory region that the afl-fuzz engine analyzes to understand which inputs exercise new code paths. This feedback loop allows the fuzzer to intelligently evolve its test cases to explore the target program more effectively.
AFL++ offers a rich ecosystem of instrumentation methods, broadly categorized into compile-time instrumentation for targets with available source code, and binary-only instrumentation for closed-source targets. Compile-time methods, such as those using LLVM or GCC plugins, are generally faster and more precise. Binary-only methods, like QEMU mode, Frida mode, and Unicorn mode, use emulation or dynamic instrumentation to gather coverage from binaries without needing to recompile them.
Sources: instrumentation/README.llvm.md, instrumentation/README.gcc_plugin.md, qemu_mode/README.md, frida_mode/src/instrument/instrument.c
Core Concepts
At its heart, AFL++ instrumentation tracks transitions between basic blocks, known as "edges". Each edge is assigned a hash, and when traversed, a corresponding byte in a shared memory map is incremented.
Shared Memory Map
The communication between the fuzzer and the instrumented target happens via a shared memory map. The ID of this shared memory region is passed to the target process via the __AFL_SHM_ID environment variable. The size of this map is defined by MAP_SIZE, which is (1U << MAP_SIZE_POW2).
The location of an edge in the map is calculated as (current_location ^ previous_location). This allows the fuzzer to track not just which blocks are hit, but in what sequence.
Sources: config.h:237-251, src/afl-cc.c:2154-2158
Fork Server
To improve performance, most instrumentation modes use a "fork server". The target program is started, initializes, and then pauses just before processing the fuzzer's input. The fuzzer communicates with this paused process to repeatedly fork() new children, each of which processes a single input. This avoids the overhead of re-executing the program's initialization code for every test case.
Sources: src/afl-fuzz-init.c:2038-2047
afl-cc: The Compiler Wrapper
afl-cc is the universal compiler wrapper for AFL++. It acts as a drop-in replacement for gcc or clang and is responsible for selecting the appropriate compiler and injecting the necessary instrumentation passes and runtime objects.
The selection of the compiler and instrumentation mode follows a specific order of precedence:
- Command-line arguments (e.g.,
--afl-llvm). - Environment variables (e.g.,
AFL_CC_COMPILER). - The name of the compiler binary (e.g.,
afl-clang-lto). - Auto-detection of the best available toolchain.
The following diagram illustrates the decision logic within afl-cc.
This diagram shows how afl-cc determines which instrumentation mode to use.
Sources: src/afl-cc.c:489-644
Compiler Mode Selection
afl-cc can operate in several modes, determining which backend compiler and instrumentation technique to use.
| Mode | Description | How to select |
|---|---|---|
| LTO | LLVM Link-Time Optimization mode. Provides collision-free coverage and is highly recommended. | afl-clang-lto, AFL_CC_COMPILER=LTO |
| LLVM | LLVM mode using passes for instrumentation. The default and most feature-rich mode. | afl-clang-fast, AFL_CC_COMPILER=LLVM |
| GCC_PLUGIN | Uses a GCC plugin for instrumentation. | afl-gcc-fast, AFL_CC_COMPILER=GCC_PLUGIN |
| CLANG | Legacy Clang mode using afl-as. | afl-clang |
| GCC | Legacy GCC mode using afl-as. | afl-gcc |
Sources: src/afl-cc.c:113-132, 489-644
Compile-Time Instrumentation
When source code is available, compile-time instrumentation is the preferred method due to its high performance and accuracy.
LLVM Mode
This is the most advanced and feature-rich instrumentation backend, used by afl-clang-fast and afl-clang-lto. It supports multiple coverage strategies.
| LLVM Instrument Mode | Environment Variable | Description |
|---|---|---|
| PCGUARD | AFL_LLVM_INSTRUMENT=PCGUARD (Default) | A fast, dominator-tree-based instrumentation. |
| CLASSIC | AFL_LLVM_INSTRUMENT=CLASSIC | The original AFL-style edge coverage instrumentation. |
| LTO | Use afl-clang-lto | Link-Time Optimization mode for collision-free coverage. |
| CTX | AFL_LLVM_INSTRUMENT=CTX | Context-sensitive coverage, hashing the callstack into the edge ID. |
| CALLER | AFL_LLVM_INSTRUMENT=CALLER | A lighter form of context sensitivity, using only the immediate caller's ID. |
| NGRAM-N | AFL_LLVM_INSTRUMENT=NGRAM-{N} | N-gram coverage, combining the current edge with the N-1 previous locations. |
| LLVMNATIVE | AFL_LLVM_INSTRUMENT=NATIVE | Uses LLVM's native SanitizerCoverage (trace-pc-guard). |
| CODECOV | AFL_LLVM_INSTRUMENT=llvm-codecov | Native trace-pc-guard instrumentation for source code coverage analysis. |
Sources: instrumentation/README.llvm.md, src/afl-cc.c:595-758
LTO Mode (afl-clang-lto)
LTO mode instruments the code at link-time, which allows it to assign unique, non-colliding edge IDs across the entire program. This eliminates map collisions, a problem that can hide paths from the fuzzer. It also automatically creates a dictionary from string comparisons found in the code.
Key features:
- Guaranteed collision-free coverage.
- 10-25% speed gain over
afl-clang-fast. - Automatic dictionary generation.
To use LTO mode, you must use the afl-clang-lto wrapper and often need to specify llvm-ar and llvm-ranlib.
CC=afl-clang-lto CXX=afl-clang-lto++ RANLIB=llvm-ranlib AR=llvm-ar ./configure
Sources: instrumentation/README.lto.md
GCC Plugin Mode
For projects that must be compiled with GCC, AFL++ provides afl-gcc-fast. This uses a GCC plugin to perform compiler-level instrumentation, which is significantly faster than the old assembly-rewriting approach. It supports GCC versions 4.5 and newer.
To use it, you need to install the gcc-VERSION-plugin-dev package and then use the afl-gcc-fast wrapper. It also supports features like CmpLog.
Sources: instrumentation/README.gcc_plugin.md
Binary-Only Instrumentation
When source code is not available, AFL++ provides several modes to fuzz binaries directly.
QEMU Mode
QEMU mode uses QEMU's user-mode emulation to instrument black-box binaries. It translates basic blocks at runtime and inserts coverage-tracking code. While it incurs a performance penalty (2-5x slowdown), it is a powerful tool for binary-only targets.
Key features:
- Persistent Mode: For x86, ARM, and AArch64, this mode can dramatically increase speed by avoiding repeated forking and re-emulation.
- Snapshot Mode: An extension of persistent mode that can snapshot and restore memory state, enabling fuzzing of more complex targets.
- Partial Instrumentation:
AFL_QEMU_INST_RANGESandAFL_QEMU_EXCLUDE_RANGESallow focusing instrumentation on specific modules or address ranges. - CmpLog: Supported for x86, ARM, and AArch64 to help solve complex comparisons.
Sources: qemu_mode/README.md
Frida Mode
Frida mode uses the Frida dynamic instrumentation toolkit to instrument targets. It works by injecting a JavaScript payload that uses Frida's Stalker to rewrite basic blocks on-the-fly and insert coverage collection callbacks.
The core logic is in instrument_basic_block, which iterates over instructions in a basic block and injects callouts. The on_basic_block function is called for each block to calculate and record the edge coverage.
// frida_mode/src/instrument/instrument.c
__attribute__((hot)) static void on_basic_block(GumCpuContext *context,
gpointer user_data) {
// ...
guint64 current_pc = instrument_get_offset_hash(current_rip);
guint64 edge;
// ...
edge = current_pc ^ *instrument_previous_pc_addr;
instrument_increment_map(edge);
// ...
*instrument_previous_pc_addr = util_rotate(current_pc, 1, map_size_pow2);
}
This snippet shows the edge coverage calculation in Frida mode.
Sources: frida_mode/src/instrument/instrument.c:111-147, frida_mode/src/instrument/instrument_coverage.c
Unicorn Mode
Unicorn mode uses the Unicorn Engine, a lightweight multi-platform, multi-architecture CPU emulator framework. It is designed for emulating and instrumenting small, self-contained code snippets rather than full processes. The user must write a harness that loads the binary code, sets up the memory map and CPU state, and then invokes the fuzzer via uc.afl_fuzz().
Sources: unicorn_mode/README.md
Selective Instrumentation
To focus fuzzing on a specific part of a complex program, AFL++ provides two mechanisms for selective instrumentation.
1. Source Code Directives
You can control instrumentation directly from the source code using preprocessor macros. This requires adding __AFL_COVERAGE(); after the includes.
__AFL_COVERAGE_ON();: Enable coverage from this point.__AFL_COVERAGE_OFF();: Disable coverage from this point.__AFL_COVERAGE_DISCARD();: Reset all coverage gathered so far.__AFL_COVERAGE_SKIP();: Mark the current test case as unimportant.
These macros are defined in afl-cc.c and passed to the compiler via -D flags.
Sources: instrumentation/README.instrument_list.md:14-35, src/afl-cc.c:1001-1020
2. Allow/Deny Lists
A more common method is to provide a file listing the source files or function names to be instrumented (allowlist) or to be excluded from instrumentation (denylist). This is controlled by the AFL_LLVM_ALLOWLIST and AFL_LLVM_DENYLIST environment variables. This feature is supported by both LLVM and GCC Plugin modes.
The file format supports comments (#) and simple fnmatch patterns. You can specify files and functions using src: and fun: prefixes.
Example allowlist file:
# Instrument only the parser
src: src/parser.c
fun: parse_packet
Sources: instrumentation/README.instrument_list.md:37-96
Advanced Features
CmpLog / Redqueen
CmpLog is a feature inspired by the Redqueen project that helps solve "magic byte" comparisons. It instruments comparison instructions (cmp, strcmp, etc.) to log the constant values being compared against the fuzzer's input. afl-fuzz then uses these logged values to generate new inputs that are more likely to pass these checks. This is available in LLVM, GCC Plugin, and QEMU modes.
Sources: instrumentation/README.llvm.md, instrumentation/README.gcc_plugin.md, qemu_mode/README.md
Persistent Mode
Persistent mode is a high-performance feature that avoids the overhead of fork() for each new input. The target is modified to loop internally, receiving new inputs from the fuzzer without restarting the process. This can provide a 10-20x speedup but requires manual modification of the target's source code.
It is enabled by wrapping the target function in the __AFL_LOOP() macro.
while (__AFL_LOOP(1000)) {
// Code to be fuzzed
}
The __AFL_LOOP macro is defined by afl-cc and handles communication with the fork server.
Sources: src/afl-cc.c:1052-1077, instrumentation/README.llvm.md
IJON (I'm Just a Number)
IJON allows the fuzzer to track not just code coverage but also the values of specific variables. By instrumenting code to report changes in important state variables (e.g., sizes, counters, flags), the fuzzer can gain a more fine-grained understanding of the program's state and potentially uncover deeper bugs. This is enabled with the AFL_LLVM_IJON environment variable during compilation.
The instrumentation pass looks for special macros like IJON_MAX, IJON_SET, IJON_INC, and IJON_STATE in the source code to determine which variables to track.
Sources: src/afl-cc.c:2020-2057, test/test-ijon-complete.sh:10-14