Testcase Minimization (afl-tmin)
Relevant source files
The following files were used as context for generating this wiki page:afl-tmin is a utility included with AFL++ designed to reduce the size of a test case while preserving its essential behavior. Given an input file that causes a crash or exhibits a specific instrumentation signature, afl-tmin attempts to remove as much data as possible without altering that behavior. This process is crucial for simplifying complex, fuzzer-generated inputs, making them easier to analyze, debug, and report.
The tool operates in one of two primary modes, which it selects automatically based on the initial behavior of the target binary with the provided input. It can run in a crash-centric mode, where it ensures the test case still causes a crash, or an instrumentation-based mode, where it ensures the execution path remains consistent. A special "hang mode" is also available to minimize inputs that cause the target to time out.
Sources: src/afl-tmin.c:19-27
Core Functionality and Modes
afl-tmin's primary goal is to shrink an input file. It achieves this by systematically removing or altering parts of the file and re-running the target binary to see if the "interesting" behavior is preserved.
Sources: src/afl-tmin.c:21-27
Operating Modes
The tool performs an initial "dry run" to determine the operating mode.
- Crash Mode: If the initial input causes the target to crash,
afl-tminwill work to ensure that any minimized version also causes a crash. By default, any crash is acceptable. WithAFL_TMIN_EXACTset, it will require the execution path to match exactly as well. - Instrumentation Mode: If the initial input executes normally,
afl-tmincalculates a checksum of the coverage bitmap. It then ensures that any minimized version produces the exact same checksum, meaning the execution path is identical. - Hang Mode: Activated with the
-Hflag, this mode is used for inputs that cause the target to time out.afl-tminwill ensure the minimized version also results in a timeout.
The following diagram illustrates the mode selection and validation logic in tmin_run_target.
This diagram shows how afl-tmin decides whether to keep a modification based on the execution result and the current operating mode.
Sources: src/afl-tmin.c:401-482
Command-Line Options
afl-tmin is configured via command-line arguments. The most important ones are listed below.
| Option | Argument | Description |
|---|---|---|
-i | file | Specifies the input test case to be minimized. |
-o | file | Specifies the final output file for the minimized data. |
-f | file | Location to write the test case to, if not using stdin. |
-t | msec | Timeout for each run in milliseconds (default: 1000). |
-m | megs | Memory limit for the child process in megabytes. |
-e | Edge coverage only mode; ignores hit counts. | |
-x | Treat non-zero exit codes as crashes. | |
-H | Minimize a hang instead of a crash or specific path. | |
-l | bytes | Set the minimum block deletion length to speed up minimization. |
-Q / -U / -O | Use QEMU, Unicorn, or Frida mode for binary-only targets. |
Sources: src/afl-tmin.c:1008-1029, test/test-basic.sh:100-101
Environment Variables
afl-tmin behavior can be further refined using environment variables.
| Variable | Description |
|---|---|
AFL_TMIN_EXACT | In crash mode, requires that the execution path of the minimized crashing input matches the original path. |
AFL_NO_FORKSRV | Disables the forkserver and runs the target via execve for each run. |
AFL_CRASH_EXITCODE | Specifies a child exit code to be interpreted as a crash. |
AFL_CUSTOM_MUTATOR_LIBRARY | Path to a custom mutator library, which can provide a custom trimming implementation. |
AFL_PYTHON_MODULE | Python module to use as a custom mutator. Note: Python custom mutators are not fully supported in afl-tmin. |
AFL_MAP_SIZE | Sets the size of the shared memory map for coverage. |
Sources: src/afl-tmin.c:1031-1052, src/afl-tmin.c:1164-1169
Minimization Process
The minimization process is a multi-stage effort to reduce the file size. It starts with an optional custom trimming stage, followed by several built-in reduction passes.
Sources: src/afl-tmin.c:490-843
This diagram shows the sequence of minimization stages performed by afl-tmin.
Custom Mutator Trimming
If a custom mutator is provided via AFL_CUSTOM_MUTATOR_LIBRARY and it exports the necessary trimming functions (afl_custom_init_trim, afl_custom_trim, afl_custom_post_trim), afl-tmin will first attempt to use this custom logic.
afl_custom_init_trimis called to initialize the process and get the total number of trimming steps.afl-tminthen enters a loop, callingafl_custom_trimto get a trimmed buffer.- The trimmed buffer is tested using
tmin_run_target. afl_custom_post_trimis called with the result (success or failure), which then returns the next step index.
If the custom trimmer successfully reduces the file size, the standard minimization stages may be skipped.
Sources: src/afl-tmin.c:520-625, include/afl-fuzz.h:1344-1393
Stage 0: Block Normalization
This is a one-time pass that attempts to "normalize" the input by replacing blocks of data with the character '0'. It iterates through the file in chunks and, for each chunk, tries replacing it with zeros. If tmin_run_target confirms that the behavior is preserved, the change is kept. This helps simplify the input before more aggressive block deletion begins.
- Chunk size is determined by
next_pow2(in_len / TMIN_SET_STEPS), with a minimum ofTMIN_SET_MIN_SIZE.
Sources: src/afl-tmin.c:641-682, include/config.h:210-211
Stage 1: Block Deletion
This is the most significant reduction stage. It iteratively attempts to remove blocks of data from the input file.
- The process starts with a large block size, typically
next_pow2(in_len / TRIM_START_STEPS). - It walks through the file, deleting one block at a time and testing the result with
tmin_run_target. - If a deletion is successful, the change is committed, and the process continues from the current position.
- If unsuccessful, it moves to the next block position.
- After a full pass with a given block size, the block size is halved, and the process repeats.
- This continues until the block size reaches a minimum limit (
del_len_limit, configurable via-l).
Sources: src/afl-tmin.c:693-764, include/config.h:191-193
Stage 2: Symbol Minimization
After block deletion, afl-tmin tries to reduce the variety of byte values (symbols) in the file.
- It first builds a frequency map (
alpha_map) of all byte values present in the current test case. - It then iterates through all possible byte values from 0 to 255 (skipping
'0'). - For each byte value found in the file, it creates a temporary buffer where all occurrences of that byte are replaced with
'0'. - If
tmin_run_targetconfirms the behavior is unchanged, the modification is saved to the main buffer.
This stage simplifies the character set of the input, for example, by replacing all instances of 'A' with '0' if it has no effect on the execution path.
Sources: src/afl-tmin.c:780-820
Stage 3: Character Minimization
This is the final and most granular pass. It is similar to symbol minimization but operates on a per-byte basis.
- It iterates through every single byte of the input file.
- If a byte is not already
'0', it temporarily changes it to'0'. - It runs the target with
tmin_run_target. If the behavior is preserved, the change is made permanent. Otherwise, the original byte is restored.
After this stage, if any changes were made in any of the stages during the pass, the entire process loops back to Stage 1 (Block Deletion) to see if further reductions are now possible. If a full pass completes with no changes, the minimization is considered complete.
Sources: src/afl-tmin.c:827-854
Target Interaction and Validation
The core of afl-tmin's decision-making process lies in the tmin_run_target function, which executes the target and validates the outcome.
This sequence diagram shows a single validation step within the minimization loop.
Sources: src/afl-tmin.c:401-482
The validation logic is as follows:
- Write Testcase: The modified buffer is written to the target's input source (a file or shared memory) via
pre_afl_fsrv_write_to_testcase, which also handles custom mutator post-processing. - Run Target:
afl_fsrv_run_targetexecutes the binary through the forkserver, monitoring for crashes and timeouts. - Classify Counts: The raw coverage data in
trace_bitsis "classified" to bucket hit counts (e.g., 4-7 hits becomes 8). If-eis used, all non-zero counts become 1. This normalization ensures that minor variations in loop counts don't break the minimization. - Validate Behavior:
- In hang mode, a
FSRV_RUN_TMOUTresult means success. - In crash mode, a
FSRV_RUN_CRASHresult means success. - In instrumentation mode, the
hash64checksum of the classifiedtrace_bitsmust match theorig_cksumcalculated from the initial dry run.
- In hang mode, a
If validation passes, the modification is kept; otherwise, it is reverted.
Sources: src/afl-tmin.c:283-315, src/afl-tmin.c:401-482
Conclusion
afl-tmin is an essential utility for making the results of a fuzzing campaign actionable. By systematically and aggressively reducing test cases to their minimal form, it saves developers significant time and effort in diagnosing the root cause of crashes or other interesting behaviors discovered by AFL++. Its multi-stage approach, combining coarse block removal with fine-grained byte-level simplification, is highly effective at producing small, clean test cases. The integration with custom mutators further extends its capabilities, allowing for domain-specific trimming logic.