WinAFL fuzz open-source via LLVM instrumentation
0. Intro
Hello! I’m D0C70R, and I’m writing this blog post on open-source fuzzing with WinAFL using LLVM instrumentation. This content is based on a paper submitted to the 2026 Summer Conference of the Korea Institute of Information Security and Cryptology, and I ask for your understanding as there are still many shortcomings.
The AFL fuzzer is one that many of you have probably used at least once. AFL and AFL++ are mainly used in Linux environments, and in particular they provide afl-clang, which is primarily intended to insert instrumentation for coverage measurement in open-source targets.
In contrast, WinAFL is used in Windows environments to fuzz exe binaries. For coverage measurement, WinAFL provides DynamoRIO and TinyInst, which belong to the Dynamic Binary Instrumentation (hereafter DBI) family; IntelPT, which uses tracing features in Intel CPU environments; and Syzygy, which measures coverage through static binary instrumentation in x86 environments.
However, in the case of open source, DBI is no different from closed-source fuzzing, IntelPT cannot be used if there is no Intel CPU, and Syzygy applies only to x86 binaries, which means the advantages of open source cannot be fully utilized.
In this post, I describe how to overcome the limitations of open-source fuzzing in WinAFL by applying Linux-style afl-clang to the Windows environment and measuring coverage by inserting instrumentation when building open-source binaries.
1. Implementation and Application of LLVM Instrumentation
1-1. Design of the instrumentation hook insertion module
The clang wrapper designed in this paper injects an LLVM pass plugin during the C/C++ compilation process, and that pass inserts an instrumentation hook called __afl_trace at the beginning of each Basic Block in the intermediate representation (IR) stage.
Below is the main part of the code that iterates over all functions and Basic Blocks and inserts __afl_trace using the LLVM pass plugin. It is built as coverage-pass.dll.
// coverage-pass.dll
FunctionCallee hook = M.getOrInsertFunction("__afl_trace", hookTy); // Add __afl_trace to the LLVM module if it does not exist
for (Function& F : M) {
unsigned bb_index = 0;
if (F.isDeclaration()) continue;
if (F.getName().starts_with("__afl_trace")) continue; // Exclude __afl_trace from instrumentation hook insertion
for (BasicBlock& BB : F) { // Iterate over the function's Basic Blocks
auto insertPt = BB.getFirstInsertionPt();
if (insertPt == BB.end()) continue;
builder.CreateCall(hook, { builder.getInt32(cur_loc) }); // Insert __afl_trace at the beginning of the Basic Block
}
}
Below is the internal structure of the __afl_trace function. It takes the current Basic Block ID as an argument and calls coverage_standby() to load a temporary shared memory region (shared memory created separately from WinAFL).
After that, it calculates Edge coverage by XORing the previous Basic Block and the current Basic Block ID, applies it to the coverage map with an AND operation, and increments the bitmap.
// coverage-rt.lib
void __cdecl __afl_trace(uint32_t cur_loc) {
uint32_t idx;
coverage_standby();
idx = (cur_loc ^ g_prev_loc) & (MAP_SIZE - 1); // Calculate Edgce coverage using the previous Basic Block ID value and the current Basic Block ID value
g_state.area[idx]++; // Increase the coverage bitmap when an Edge is executed
g_prev_loc = (cur_loc >> 1); // Swap the current Basic Block ID into the previous Basic Block ID
}
1-2. Design of the clang wrapper
In 1-1, I explained the modules used to insert instrumentation hooks and reflect them in the coverage bitmap. The modules we have are coverage-pass.dll and coverage-rt.lib.
Now, if we design only a clang-based wrapper to apply these two libraries, we can compile binaries with instrumentation hooks inserted.
The functionality of the clang wrapper is very simple. During the IR stage in the clang compilation flow, it loads coverage-pass.dll, inserts instrumentation hooks, and generates object files. During the linking stage, it calls coverage-rt.lib, connects the actual implementation of __afl_trace, and links in the functionality that reflects coverage and the bitmap.
// afl-clang.c main function
strcpy(tool_dir, sizeof(tool_dir), self);
dirname_inplace(tool_dir);
// Load modules
snprintf(pass_dll, sizeof(pass_dll), _TRUNCATE, "%s\\coverage-pass.dll", tool_dir);
snprintf(rt_lib, sizeof(rt_lib), _TRUNCATE, "%s\\coverage-rt.lib", tool_dir);
The libraries are loaded relative to the folder containing afl-clang.exe, and in the end target.exe is generated with the instrumentation hook inserted.
1-3. Integration with WinAFL
At this point, afl-clang has been implemented on Windows in a fairly plausible way, but WinAFL supports only the four methods mentioned in the Intro, so coverage measurement using LLVM clang is not supported. However, because WinAFL provides its source code, fuzzing in the way intended by this paper becomes possible with some modifications.
A dll is created to let the target directly record coverage into WinAFL's shared memory. During fuzzing, simply adding the -l option loads this library, and it performs the function of reflecting the shared memory inside coverage-rt.lib into WinAFL.
First, the code below creates the shared memory for the coverage bitmap to be shared with the child process. The shared memory (64KB) is stored in g_map_handle, and MapViewOfFile maps it into the process address space so it can be accessed through a pointer.
// llvm_runner.dll
static int create_map(void) {
...
// Shared memory area
g_map_handle = CreateFileMappingA(
INVALID_HANDLE_VALUE,
NULL,
PAGE_READWRITE,
0,
MAP_SIZE,
g_map_name);
g_map_view = (uint8_t*)MapViewOfFile(g_map_handle, FILE_MAP_ALL_ACCESS, 0, 0, MAP_SIZE); // Map it so the current process can access the shared memory
memset(g_map_view, 0, MAP_SIZE); // Initialize the coverage map
return 1;
}
In the code above, g_map_name is stored in the shared memory environment variable AFL_COVERAGE_MAP of the coverage-rt.lib module created above. This makes it possible to access and write to a separate shared memory region, and after one cycle ends, it is reflected into WinAFL shared memory.
// Part of the dll_run_target function in llvm_runner.dll
if (!SetEnvironmentVariableA("AFL_COVERAGE_MAP", g_map_name) || // Store g_map_name in the AFL_COVERAGE_MAP environment variable
!SetEnvironmentVariableA("AFL_COVERAGE_CLEAR", "1") ||
!SetEnvironmentVariableA("AFL_COVERAGE_FILE", NULL) ||
!SetEnvironmentVariableA("AFL_STATIC_CONFIG", NULL)) {
return FAULT_ERROR;
}
...
memcpy(trace_bits, g_map_view, copy_size); // Copy the temporary shared memory g_map_handle into WinAFL shared memory
Now only a small change to WinAFL remains. As mentioned above, below is part of the code that adds an option to fuzz using the instrumentation designed for the -l option and adds the functionality to load llvm_runner.dll.
void load_custom_library(const char *libname){
HMODULE hLib = LoadLibraryEx(libname, NULL, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS);
// Call the coverage map reflection functions in llvm_runner.dll
dll_init_ptr = (dll_init)GetProcAddress(hLib, "dll_init");
dll_run_ptr = (dll_run)GetProcAddress(hLib, "dll_run");
dll_run_target_ptr = (dll_run_target)GetProcAddress(hLib, "dll_run_target");
}
int main(int argc, char **argv){
// Add -l option
case 'l':
custom_dll_defined = 1;
load_custom_library(optarg); // Load llvm_runner.dll
break;
}
All preparations are complete! We now have afl-clang.exe, coverage-pass.dll, and coverage-rt.lib for LLVM instrumentation insertion; llvm_runner.dll for WinAFL fuzzing; and the customized afl-fuzz.exe.
2. 1-Day Vulnerability Triggering Experiment
The operating system of the PC used for fuzzing was Windows 11 Education 25H2, and the CPU was an AMD Ryzen 5 5600 6-Core Processor, an environment in which IntelPT cannot be used. Therefore, the instrumentations tested were DynamoRIO, TinyInst, Syzygy, and LLVM instrumentation via afl-clang.
The selected 1-Day vulnerabilities were CVE-2016-9297(tiffinfo), CVE-2017-9048(xmllint), CVE-2017-13028(TCPdump), CVE-2019-13109(Exiv2), and CVE-2019-13288(Xpdf). To include a comparison with Syzygy, the targets were built in an x86 environment.
All targets were fuzzed for 3 hours under the same conditions, and we compare coverage expansion, binary execution speed, whether the 1-Day vulnerability was triggered, and the time required to trigger it.
Final coverage expansion
| CVE(Target) |
afl-clang(Ours) |
DynamoRIO |
TinyInst |
Syzygy |
| CVE-2016-9297(tiffinfo) |
36% |
14% |
13% |
10.5% |
| CVE-2017-9048(xmllint) |
35% |
18.5% |
17.5% |
0.3% |
| CVE-2017-13028(TCPdump) |
54% |
38.5% |
10.5% |
6.5% |
| CVE-2019-13109(Exiv2) |
2.45% |
1.55% |
0.35% |
0.08% |
| CVE-2019-13288(Xpdf) |
13.5% |
13.0% |
8.5% |
9.3% |
Binary execution speed (exec/s)
| CVE(Target) |
afl-clang(Ours) |
DynamoRIO |
TinyInst |
Syzygy |
| CVE-2016-9297(tiffinfo) |
91.2792 |
3.4808 |
1.0883 |
21.9930 |
| CVE-2017-9048(xmllint) |
42.2399 |
1.2308 |
1.2798 |
123.2742 |
| CVE-2017-13028(TCPdump) |
27.5253 |
1.3911 |
0.669 |
0.4461 |
| CVE-2019-13109(Exiv2) |
17.8563 |
55.0731 |
11.469 |
55.2034 |
| CVE-2019-13288(Xpdf) |
12.4583 |
30.9091 |
15.7029 |
33.0855 |
1-Day vulnerability trigger time (min)
| CVE(Target) |
afl-clang(Ours) |
DynamoRIO |
TinyInst |
Syzygy |
| CVE-2016-9297(tiffinfo) |
57.846 |
N/A |
N/A |
N/A |
| CVE-2017-9048(xmllint) |
124.836 |
N/A |
N/A |
N/A |
| CVE-2017-13028(TCPdump) |
136.698 |
N/A |
N/A |
N/A |
| CVE-2019-13109(Exiv2) |
N/A |
N/A |
N/A |
N/A |
| CVE-2019-13288(Xpdf) |
2.5 |
5.516 |
N/A |
N/A |
Overall, afl-clang showed the best coverage expansion. Exiv2 is relatively complex and only reached 2.45% in the end, but TCPdump reached 54%. Xpdf showed similar performance between afl-clang and DynamoRIO.
On the other hand, in terms of execution speed, afl-clang was not unconditionally superior. Rather, Syzygy was fast on xmllint at 123.2742 per second, and on Exiv2 and Xpdf it was slower than DynamoRIO and showed execution speeds almost similar to TinyInst.
For the 1-Day vulnerability triggering results, afl-clang succeeded on every target except Exiv2, and excluding afl-clang, DynamoRIO was the only one that successfully triggered the vulnerability on Xpdf.
3. Conclusion and Future Research Directions
This paper conducted an experiment to apply the LLVM clang instrumentation method of Linux AFL to the Windows WinAFL environment. Compared with WinAFL’s existing instrumentation methods, LLVM instrumentation showed a higher coverage growth rate, reasonable execution speed on the same targets, and reasonable vulnerability trigger success rates.
The implemented afl-clang proved that it has less overhead than the control groups and has the advantage of being able to accurately measure coverage for all source code of the fuzzing target by inserting __afl_trace directly at the beginning of Basic Blocks in the compiler’s intermediate representation stage.
In future research, I plan to implement the designed Clang wrapper in LTO mode, build it by linking it with the fuzzing target, and conduct experiments targeting the discovery of 0-Day vulnerabilities in open source.
Thank you for reading this long post!
4. Reference
Fioraldi, A., Mantovani, A., Maier, D., & Balzarotti, D. (2023). Dissecting american fuzzy lop: a fuzzbench evaluation. ACM transactions on software engineering and methodology, 32(2), 1-26.
Fioraldi, A., Maier, D., Eißfeldt, H., & Heuse, M. (2020). {AFL++}: Combining incremental steps of fuzzing research. In 14th USENIX workshop on offensive technologies (WOOT 20).
Bruening, D., & Amarasinghe, S. (2004). Efficient, transparent, and comprehensive runtime code manipulation.
Chen, Y., Mu, D., Xu, J., Sun, Z., Shen, W., Xing, X., ... & Mao, B. (2019, July). Ptrix: Efficient hardware-assisted fuzzing for cots binary. In Proceedings of the 2019 ACM Asia Conference on Computer and Communications Security (pp. 633-645).
winafl, https://github.com/googleprojectzero/winafl
TinyInst, https://github.com/googleprojectzero/TinyInst
Syzygy, https://github.com/google/syzygy
Fuzzing101, https://github.com/antonio-morales/fuzzing101