使用Clang+libFuzzer做模糊测试时为何显示代码覆盖率为0?
问题描述
使用LLVM libFuzzer和Clang进行模糊测试时,程序按预期触发崩溃,但生成的覆盖率报告显示所有代码行命中数为0。测试代码为经典的嵌套分支示例:
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { if (size > 0 && data[0] == 'H') { if (size > 1 && data[1] == 'I') { if (size > 2 && data[2] == '!') { int *foo = (int *)44; *foo = 5; } } } return 0; }
编译命令:
clang -g -O1 -fsanitize=fuzzer -fprofile-instr-generate -fcoverage-mapping FuzzingApp.cpp -o FuzzingApp.exe
运行程序崩溃后生成default.profraw,经llvm-profdata merge处理后,llvm-cov show输出所有行覆盖率为0,且在Clang 17.0.3(VS分发版)和18.1.8(官方最新版)中均存在该问题。
根本原因
Clang的-fprofile-instr-generate覆盖率数据默认仅在进程正常退出时才会从内存缓冲区写入到.profraw文件。而libFuzzer在检测到崩溃(如非法内存访问)时,会直接通过信号终止进程,跳过了正常退出流程中的覆盖率数据刷新步骤,导致生成的.profraw文件为空或不包含有效执行数据,最终llvm-cov无法解析出正确的覆盖率。
解决方案
方案1:使用libFuzzer内置参数强制dump覆盖率
运行模糊测试程序时添加-dump_coverage=1参数,该参数会让libFuzzer在崩溃前主动将覆盖率数据写入文件:
FuzzingApp.exe -dump_coverage=1
重新生成.profraw后,再执行后续的llvm-profdata merge和llvm-cov show命令,即可看到正确的覆盖率统计。
方案2:在代码中手动触发覆盖率刷新
在触发崩溃的代码前,调用Clang提供的__llvm_profile_write_file()函数强制写入覆盖率数据:
#include <cstdint> // 声明覆盖率写入函数 extern "C" void __llvm_profile_write_file(void); extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { if (size > 0 && data[0] == 'H') { if (size > 1 && data[1] == 'I') { if (size > 2 && data[2] == '!') { __llvm_profile_write_file(); // 崩溃前手动写入覆盖率 int *foo = (int *)44; *foo = 5; } } } return 0; }
重新编译并运行程序,生成的.profraw将包含完整的执行覆盖数据。
额外验证步骤
处理.profraw前,可通过以下命令确认文件是否包含有效数据:
llvm-profdata show default.profdata
若输出中包含函数LLVMFuzzerTestOneInput的覆盖信息,则说明数据有效。
内容的提问来源于stack exchange,提问作者Ian Brockbank

