libFuzzer要求入口函数为extern "C" int LLVMFuzzerTestOneInput(const uint8_t*, size_t),需禁用C++名字修饰、捕获异常、避免exit/abort,并配合ASan/UBSan编译,使用seed/dict提升结构化输入覆盖率,清除全局状态确保crash可复现。

libFuzzer 要求的入口函数必须叫 FuzzTarget,且签名固定为 extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size)
libFuzzer 不接受任意函数名或参数类型——它在链接期硬编码查找这个 C 风格符号。C++ 项目里容易因命名空间、重载或 std::vector 参数误用导致链接失败,报错类似 undefined reference to `LLVMFuzzerTestOneInput'。
实操建议:
- 必须用
extern "C"消除 C++ 名字修饰(mangling),否则符号找不到 -
Data是 raw buffer,别直接传给std::string(Data, Size)——若数据含\0会截断;应改用std::string(reinterpret_cast(Data), Size) - 函数体里要捕获所有异常(
try { ... } catch (...) { return 0; }),否则未处理异常会终止 fuzzing 进程 - 不要在
LLVMFuzzerTestOneInput里调用exit()或abort(),libFuzzer 依赖函数返回值判断是否 crash(非零即 crash)
解析器崩溃前没触发 sanitizer 报告?检查是否启用了 -fsanitize=fuzzer,address,undefined 且未被覆盖
libFuzzer 本身不检测内存错误,全靠 sanitizer 协同工作。常见坑是只加了 -fsanitize=fuzzer 却漏掉 address(ASan)或 undefined(UBSan),导致越界读、空指针解引用等漏洞静默跳过,fuzzer 只当普通输入处理。
实操建议:
- 编译时必须同时启用:
clang++ -fsanitize=fuzzer,address,undefined -g -O2;三者缺一不可 - 避免与
-fno-exceptions共用——UBSan 的部分检查依赖异常机制,关掉后可能漏报 - 若解析器内部用了
mmap或自定义分配器,ASan 可能无法拦截,需配合__sanitizer_annotate_contiguous_container手动标记 - UBSan 对整数溢出默认不 abort,需加
-fsanitize-trap=signed-integer-overflow让其 crash,libFuzzer 才能捕获
如何让 libFuzzer 快速覆盖到深层解析逻辑?用 LLVMFuzzerMutate 或字典(dictionary)引导变异
纯随机比特翻转对结构化输入(如 JSON/XML/PE 文件)效率极低,fuzzer 很难自发拼出合法 magic bytes 或嵌套语法,导致卡在解析器前端校验就退出,深层逻辑零覆盖。
实操建议:
- 提供最小合法输入作为 seed corpus:比如一个 4 字节的合法 header,放在
seeds/目录下,启动时加-runs=1000000 seeds/ - 用 dictionary 显式注入关键词:
-dict=parsers.dict,内容形如key: "Content-Type"、magic: "\x7fELF",每行一个 token - 慎用
LLVMFuzzerMutate自定义变异——它绕过 libFuzzer 内置的覆盖率反馈,仅适合已知格式边界时做定向 fuzz;多数情况优先用 dictionary + seed - 若解析器有状态机,可在关键分支插入
__attribute__((no_sanitize("fuzzer"))) __builtin_trap()强制生成新 coverage edge,辅助 fuzzer 发现路径
解析器依赖全局状态(如单例、静态缓冲区)会导致 crash 无法复现
libFuzzer 默认并发运行多个实例(-workers=4),若解析器读写同一块静态内存(例如全局 std::map 缓存或 static char buf[4096]),不同线程会互相污染,crash 输入在单线程下无法复现,调试时陷入死循环。
实操建议:
- 测试前清空所有全局状态:在
LLVMFuzzerTestOneInput开头调用reset_parser_state()(需你实现) - 禁用多线程 fuzz:
-workers=1 -jobs=1,先确保单线程下 crash 稳定可复现 - 避免在解析过程中修改全局日志开关、信号 handler 或 locale 设置——这些变更会跨 input 持久化
- 若必须用静态缓存,改为 thread_local 存储,或在每次输入结束时显式销毁
实际跑起来后,最常被忽略的是 sanitizer 与 libFuzzer 的协同粒度:ASan 报告的堆栈未必对应 crash 点,它可能发生在前一个 input 的残留状态里;务必结合 -trace-cmp 和 -print_final_stats=1 观察 coverage 增长是否真实进入目标函数。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











