Core Architecture
Relevant source files
The following files were used as context for generating this wiki page:The core architecture of AFL++ is a highly optimized system designed for efficient, coverage-guided fuzzing. It revolves around three main components: a compiler wrapper for instrumentation, a high-performance forkserver for target execution, and a shared memory region for tracking code coverage. This design minimizes the overhead of repeatedly launching the target application, allowing for millions of executions per second on fast targets.
The main fuzzer process, afl-fuzz, orchestrates the entire operation. It manages the input queue, performs mutations, and analyzes the coverage feedback from each run to discover new and interesting execution paths. Communication between the fuzzer and the instrumented target is handled through a control pipe and a status pipe, which are integral to the forkserver mechanism.
Key Components
The architecture is composed of several interconnected parts that work in concert.
| Component | Description | Primary Source Files |
|---|---|---|
| Instrumentation | Compile-time process that injects code into the target binary to observe its execution path. | src/afl-cc.c, instrumentation/afl-compiler-rt.o.c |
| Shared Memory | A memory region shared between afl-fuzz and the target, used to store the coverage bitmap. | src/afl-sharedmem.c, include/config.h |
| Forkserver | A mechanism within the instrumented binary that clones itself on demand, avoiding costly execve() calls for each fuzzing iteration. | src/afl-forkserver.c, src/afl-fuzz.c |
Fuzzer (afl-fuzz) | The main process that drives fuzzing, manages the queue, mutates inputs, and analyzes results. | src/afl-fuzz.c, src/afl-fuzz-init.c |
High-Level Interaction
The following diagram illustrates the interaction between the main components of AFL++.
This diagram shows the main fuzzing loop where afl-fuzz selects an input, mutates it, and sends it to the forkserver. The forkserver creates a new process to run the instrumented code, which in turn updates the shared coverage bitmap. The fuzzer analyzes this bitmap to guide future mutations.
Sources: src/afl-fuzz.c, src/afl-forkserver.c, src/afl-sharedmem.c
Instrumentation
Instrumentation is the process of adding code to the target binary at compile time. This is primarily handled by the afl-cc compiler wrapper. The injected code is responsible for communicating with the fuzzer and reporting code coverage.
Compile-Time Instrumentation
afl-cc acts as a drop-in replacement for gcc or clang. It adds parameters to the compiler command line to link the AFL++ runtime (afl-compiler-rt.o) and inject the instrumentation passes.
Sources: src/afl-cc.c:2010-2097
The runtime code in afl-compiler-rt.o.c contains the logic for the forkserver and for communicating with afl-fuzz. When the instrumented program starts, a constructor function __afl_auto_init is executed, which in turn calls __afl_maybe_log. This function is the entry point to setting up the forkserver and shared memory.
Sources: instrumentation/afl-compiler-rt.o.c:840-850
Coverage Tracking
The instrumentation works by assigning a random location ID to each basic block in the program. It then generates code that, for each edge transition, calculates an index into the shared memory map and increments a counter at that index. The index is typically computed as (current_location ^ previous_location) >> 1. This provides path-level granularity, not just block-level. The global pointer __afl_area_ptr points to the shared memory bitmap used for this purpose.
Sources: instrumentation/afl-compiler-rt.o.c:59-71, instrumentation/afl-compiler-rt.o.c:753-761
Shared Memory
AFL++ uses a shared memory segment as the primary communication channel for coverage information between the fuzzer and the target process.
Initialization
The afl-fuzz process initializes the shared memory segment using afl_shm_init(). It determines the size of the map, which defaults to MAP_SIZE (typically 64KB, defined by MAP_SIZE_POW2 in config.h), but can be auto-scaled. The ID of this shared memory segment is passed to the target process via the __AFL_SHM_ID environment variable.
Sources: src/afl-fuzz.c:4895-4900, src/afl-sharedmem.c:64-97, include/config.h:383-389
Target-Side Attachment
The instrumented binary, upon startup, reads the __AFL_SHM_ID environment variable and uses shmat() to attach to the existing shared memory segment. The global pointer __afl_area_ptr is then set to point to this mapped region. If this process fails, the program will abort, indicating a setup issue.
Sources: instrumentation/afl-compiler-rt.o.c:138-161
Structure
The shared memory is a simple byte array, referred to as trace_bits in afl-fuzz and __afl_area_ptr in the target. Each byte in the map acts as a hit counter for a specific edge in the target's control flow graph. afl-fuzz analyzes the values in this map to infer not only which paths were taken but also, to some extent, how many times.
Sources: src/afl-fuzz.c:4896, instrumentation/afl-compiler-rt.o.c:69-71
Forkserver
The forkserver is a key performance optimization in AFL++. It eliminates the overhead of execve() and dynamic linking for each fuzzing iteration. An instrumented binary contains the forkserver, which is activated on the first run.
Handshake and Lifecycle
The interaction between afl-fuzz and the forkserver follows a strict protocol managed via two pipes: a control pipe and a status pipe.
This sequence shows the one-time setup and the per-fuzz-iteration loop.
Sources: src/afl-fuzz-init.c:4270-4300, src/afl-forkserver.c:110-180, instrumentation/afl-compiler-rt.o.c:181-245
Communication Protocol
Communication is managed through file descriptors FORKSRV_FD (198) and FORKSRV_FD + 1 (199).
| File Descriptor | Name | Direction | Purpose |
|---|---|---|---|
FORKSRV_FD (198) | Control Pipe (write end) | afl-fuzz -> Target | Fuzzer sends commands to the forkserver to request a new child. |
FORKSRV_FD + 1 (199) | Status Pipe (read end) | Target -> afl-fuzz | Forkserver sends back the child's PID and exit status. |
Sources: include/config.h:424-427, src/afl-forkserver.c:110-180
The forkserver waits for a 4-byte value from the control pipe. Upon receiving it, it calls fork(). The child process continues execution, while the parent waits for the child to terminate and then loops back to wait for the next command. This simple but effective design is a cornerstone of AFL++'s performance.
Sources: src/afl-forkserver.c:157-179
Fuzzing Process
The main logic resides in afl-fuzz, which orchestrates the entire fuzzing campaign.
Initialization and Dry Run
Upon starting, afl-fuzz performs these steps:
- Setup: Creates output directories and sets up file descriptors.
- Load Testcases: Reads initial seed files from the input directory (
-i). - Dry Run: Executes each initial seed file once to calibrate the target. This initial run populates the coverage map, identifies timeouts and crashes in the seed corpus, and determines execution speed. The
calibrate_casefunction is central to this phase.
Sources: src/afl-fuzz-init.c:3095-3100, src/afl-fuzz-init.c:3323-3680
The Main Fuzzing Loop
After initialization, afl-fuzz enters its main while loop, which continuously executes the fuzz_one function.
The following diagram outlines the process for a single input from the queue.
This flow shows the progression from deterministic stages to more chaotic "havoc" and "splicing" stages, with the goal of discovering new paths at each step.
Sources: src/afl-fuzz.c:5334-5490
A key function in this loop is save_if_interesting(), which is called after a target run. It compares the coverage bitmap in shared memory against the global virgin_bits map to see if any new edges have been discovered. If so, the input is considered "interesting" and is added to the queue for future fuzzing.
Sources: src/afl-fuzz-init.c:978-984
Conclusion
AFL++'s core architecture is a finely tuned system that balances performance and effectiveness. The use of compile-time instrumentation provides detailed coverage information with low runtime overhead. The forkserver model dramatically speeds up execution by avoiding the costs associated with process creation. Finally, the shared memory bitmap offers a simple and fast channel for the fuzzer to receive feedback from the target. Together, these components create a powerful and efficient foundation for modern coverage-guided fuzzing.