Bitcoin Core 模糊测试实战:基于 libFuzzer、afl++ 与 Honggfuzz 的完整 Fuzzing 指南
【免费下载链接】bitcoinBitcoin Core integration/staging tree项目地址: https://gitcode.com/GitHub_Trending/bi/bitcoin
本文以 Bitcoin Core 仓库的官方模糊测试文档 doc/fuzzing.md 为主体,结合 CMake 预设、fuzz 框架入口代码与 CI 配置,系统讲解如何用 libFuzzer、afl++、Honggfuzz 对 Bitcoin Core 进行模糊测试:从一键构建 fuzz 二进制、理解 harness 注册机制与引擎输出、管理语料库,到复现 CI 报告的崩溃案例并提交覆盖率改进。读完本文,你可以独立搭建 Bitcoin Core 的 fuzzing 环境、运行与调参各类 fuzz 目标,并理解-DBUILD_FOR_FUZZING三种构建模式背后的确定性设计。
一、Bitcoin Core 模糊测试概览
模糊测试(fuzzing)通过向被测代码投喂大量随机或半随机输入,触发未处理边界、整数溢出、内存错误等缺陷。Bitcoin Core 的 fuzzing 体系围绕以下要素组织:
- Fuzz 框架:所有目标编译进同一个
fuzz二进制,运行时通过FUZZ环境变量选择目标。harness(即 fuzz 目标)源码位于 src/test/fuzz/,例如 process_message.cpp 是 net_processing.cpp 中ProcessMessage(...)函数的 fuzzing harness。 - 语料库:项目维护一套种子语料集合(seed corpora)存放于外部的
bitcoin-core/qa-assets仓库,每个 fuzz 目标对应一个子目录(如fuzz_corpora/process_message/)。 - 多种引擎:官方文档分别给出 libFuzzer(默认推荐)、afl++ 与 Honggfuzz 的接入方式,三者共用同一套 fuzz 目标代码。
- OSS-Fuzz:Bitcoin Core 参与 Google 的 OSS-Fuzz 计划,崩溃案例会通过 CI 与公共漏洞看板持续跟踪。
从源码结构看,fuzz 目标通过 fuzz.h 中的FUZZ_TARGET(name, ...)宏声明:宏会为每个目标生成一个name##_fuzz_target(FuzzBufferType)函数,并在静态初始化阶段调用FuzzFrameworkRegisterTarget将其注册进全局表。输入类型统一为FuzzBufferType(即std::span<const uint8_t>),这一设计使得同一份 harness 代码可以被 libFuzzer、afl++、Honggfuzz 等不同引擎复用。
二、libFuzzer 快速上手
官方推荐的入门路径是 libFuzzer,四步即可跑起第一个 fuzz 目标(摘自 doc/fuzzing.md):
$ git clone https://gitcode.com/GitHub_Trending/bi/bitcoin $ cd bitcoin/ $ cmake --preset=libfuzzer $ cmake --build build_fuzz $ FUZZ=process_message build_fuzz/bin/fuzz # 使用 ctrl-c 中止 fuzz2.1 preset 到底设置了什么
--preset=libfuzzer不是简单的别名,其完整定义见 CMakePresets.json:
| 缓存变量 | 取值 | 作用 |
|---|---|---|
BUILD_FOR_FUZZING | ON | 强制 fuzz 确定性(跳过 PoW 校验、固定随机种子、冻结时钟),并禁用其他可执行文件 |
CMAKE_C_COMPILER/CMAKE_CXX_COMPILER | clang/clang++ | libFuzzer 依赖 clang 插桩 |
CMAKE_C_FLAGS/CMAKE_CXX_FLAGS | -ftrivial-auto-var-init=pattern | 用固定 pattern 初始化未初始化栈变量,帮助 ASan 发现 use-of-uninitialized-value |
SANITIZERS | undefined,address,fuzzer | 同时启用 UBSan、ASan 与 libFuzzer 引擎插桩 |
binaryDir | ${sourceDir}/build_fuzz | 构建输出目录,即命令中的build_fuzz |
CI 中对应的配置脚本是 ci/test/00_setup_env_native_fuzz.sh,它在 Ubuntu 容器内使用clang-22,并比本地 preset 多加了两个 sanitizer:-DSANITIZERS=fuzzer,address,undefined,float-divide-by-zero,integer,同时设置RUN_FUZZ_TESTS=true让 CI 用test_runner.py跑全部目标。
2.2 不带 sanitizer 构建以提升吞吐
使用--preset=libfuzzer-nosan可以关闭常用 sanitizer 再执行同样的流程。注意该 preset 使用不同的构建目录,上述命令中的build_fuzz要替换为build_fuzz_nosan。从 CMakePresets.json 可确认其差异仅在于SANITIZERS=fuzzer(只保留 libFuzzer 引擎插桩,去掉undefined,address)与binaryDir。关于为何值得牺牲 bug 检测能力换吞吐,见下文"无 sanitizer 长跑"一节。
2.3 批量运行所有 fuzz 目标:test_runner.py
仓库提供 runner 脚本 test/fuzz/test_runner.py,可对全部(或指定)fuzz 目标一次性执行语料目录中的所有输入。常用参数(./build_fuzz/test/fuzz/test_runner.py --help查看):
- 位置参数:
corpus_dir(必须包含每个目标的子目录,如qa-assets/fuzz_corpora)与可选的target列表,缺省运行全部目标; -x/--exclude:逗号分隔的排除目标列表;--par/-j:并行目标数,默认 4;--valgrind:在 Valgrind 下运行(作为 MSan 的替代方案);-g/--generate:运行有限次数生成/扩充语料到corpus_dir;--m_dir:把额外目录的输入合并进corpus_dir。
脚本执行前会校验构建配置中ENABLE_FUZZ_BINARY为真,并为每个子进程注入 sanitizer 环境变量(test/fuzz/test_runner.py):ASAN_OPTIONS开启 leak 检测与栈 use-after-return 检查,UBSAN_OPTIONS指向 test/sanitizer_suppressions/ubsan 抑制文件并设置halt_on_error=1。
三、fuzz 二进制的运行机制与输出解读
3.1 FUZZ 环境变量与目标注册
运行build_fuzz/bin/fuzz时必须用FUZZ环境变量指定目标,否则进程报错退出。选择逻辑在 fuzz.cpp 中:先查环境变量,再在注册表中find,找不到即打印No fuzz target compiled for ...退出。两个实用技巧同样来自该文件的initialize():
PRINT_ALL_FUZZ_TARGETS_AND_ABORT=1:打印所有已编译(非 hidden)目标名后退出,便于确认目标是否存在;WRITE_ALL_FUZZ_TARGETS_AND_ABORT=<路径>:把所有目标名写入文件,便于脚本处理。
3.2 给 fuzz 二进制传 bitcoind 参数
fuzz 二进制可以接收bitcoind风格的--参数;具体目标可能忽略或消费它们并改变测试行为。关键是用双短横线区分 bitcoind 参数与 fuzz 引擎自身参数,官方示例:
$ FUZZ=address_deserialize build_fuzz/bin/fuzz -runs=1 fuzz_corpora/address_deserialize --checkaddrman=5 --printtoconsole=1其实现见 fuzz.cpp 的SetArgs():LLVMFuzzerInitialize时只收集以--开头的参数存入g_args(如fuzz -runs=1 fuzz_corpora/... --checkaddrman=5中的-runs=1归 libFuzzer,--checkaddrman=5归 bitcoind 参数),随后部分 harness 通过G_TEST_COMMAND_LINE_ARGUMENTS将其注入BasicTestingSetup::m_node::args。
3.3 语料目录与输出解读
指定语料目录后,所有新增覆盖的输入会保存在其中:
$ mkdir -p process_message-seeded-from-thin-air/ $ FUZZ=process_message build_fuzz/bin/fuzz process_message-seeded-from-thin-air/libFuzzer 输出的关键信息:
NEW:生成了覆盖新代码区域的输入;REDUCE:对已有输入做最小化缩减;cov:边覆盖数、corp: N/Mb当前语料文件数与总字节数、exec/s执行速率;NEW_FUNC[x/y]: 0x... in func src/file.h:line:新覆盖到的函数及位置,例如输出中出现CDataStream::CDataStream ... src/./streams.h:248,说明序列化路径被新覆盖;DE: "block"表示 persistent auto-dictionary 学到了"block"这样的字符串特征。
无种子目录起步时,libFuzzer 会在若干执行后开始产生有效输入——官方示例中,fuzzer 自行构造出一条block消息传入ProcessMessage(...)并提升了覆盖,语料目录里最终留下若干文件,如:
$ ls process_message-seeded-from-thin-air/ 349ac589fc66a09abc0b72bb4ae445a7a19e2cd8 4df479f1f421f2ea64b383cd4919a272604087a7 ... $ cat --show-nonprinting process_message-seeded-from-thin-air/349ac589fc66a09abc0b72bb4ae445a7a19e2cd8 block^@M-^?M-^?M-^?M-^?M-^?nM-^?M-^?可见输入内容就是"block"消息头加一段乱码载荷——libFuzzer 依靠 PCHC/persistent dictionary 猜出了 P2P 消息的命令头格式。
3.4 确定性保障:为什么 fuzz 构建要"冻结时间"
fuzz.cpp 的initialize()展示了 fuzz 确定性的具体做法:
SeedRandomStateForTest(SeedRand::ZEROS):以全零种子固定 RNG(私钥材料等强随机除外);SetMockTime(1231006505s):把时间冻结在创世块时间戳,保证初始化可复现;CreateSock与 DNS 查询被替换为"一旦真正建连/查 DNS 就std::terminate()"的桩,防止 harness 产生网络副作用;- 若构建既未开启
BUILD_FOR_FUZZING也非 Debug 模式,Assume()失败不会中止,fuzz 运行无意义,程序直接拒绝运行(这也是 Release 模式构建"只能编译不能运行"的原因)。
若使用第 3 节的BUILD_FUZZ_BINARY=ON -DCMAKE_BUILD_TYPE=Debug构建,确定性默认开启但可被FUZZ_NONDETERMINISM环境变量(任意取值)关闭,适合调试那些在确定性执行下会被跳过的代码路径;fuzz.cpp 会打印警告提示结果可能与 fuzz 构建不一致。
四、Fuzz 语料库:qa-assets 种子集与贡献流程
项目维护的种子语料集合存放于bitcoin-core/qa-assets仓库,目录结构为fuzz_corpora/<目标名>/。使用process_message的种子语料:
$ git clone --depth=1 <bitcoin-core/qa-assets 仓库> $ FUZZ=process_message build_fuzz/bin/fuzz qa-assets/fuzz_corpora/process_message/ INFO: 991 files found in qa-assets/fuzz_corpora/process_message/ INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes INFO: seed corpus: files: 991 min: 1b max: 1858b total: 288291b rss: 150Mb #993 INITED cov: 7063 ft: 8236 corp: 25/3821b exec/s: 0 rss: 181Mb注意两个引擎参数细节:未指定-max_len时 libFuzzer 不会生成超过 4096 字节的输入;INITED行说明加载种子后已有 7063 的边覆盖起点,远高于"无种子"起步。
提交覆盖率改进:fuzzing 过程中若发现提升覆盖的输入,官方强烈建议提交到 qa-assets 仓库——对 Bitcoin Core 仓库的每一个pull request,CI 都会自动用 qa-assets 里的全部输入回归测试所有 fuzz 目标,因此贡献语料是提高项目健壮性最直接的方式之一。
五、无 sanitizer 长跑以提升覆盖
用-DSANITIZERS=address,fuzzer,undefined编译的 harness 利于发现 bug,但执行极慢,限制了新覆盖的探索能力。文档给出的策略是:
- 定期做长时间无额外 bug 检测器的运行(
--preset=libfuzzer-nosan); - 将长跑产生的新输入按 qa-assets 仓库的 PR 模板合并进语料库;
- 保持耐心:即便吞吐改善,libFuzzer 深入深层/难达目标仍可能需要数天与数千万次执行。
六、复现 CI 报告的 fuzzer 崩溃
CI 发现的崩溃会输出形如Test unit written to ./crash-1bc91feec9fc00b107d97dc225a9f2cdaa078eb6的崩溃文件名。本地复现步骤:
进入
qa-assets目录并git pull更新(若文件不存在,需检出与 CI 完全一致的 commit id);确保用全量 sanitizer 编译(有 sanitizer 时 fuzz 更慢,但从崩溃案例复现只需极短时间);
把崩溃文件名追加到种子语料路径后运行:
FUZZ=process_message build_fuzz/bin/fuzz \ qa-assets/fuzz_corpora/process_message/1bc91feec9fc00b107d97dc225a9f2cdaa078eb6若语料文件中没有该案例,可尝试 CI 日志里的 base64 编码版本:
echo "Nb6Fc/97AACAAAD/ewAAgAAAAIAAAACAAAAAoA==" | \ base64 --decode > qa-assets/fuzz_corpora/process_message/1bc91feec9fc00b107d97dc225a9f2cdaa078eb6
在相同(或相近)输入下,本地通常几分钟内即可复现崩溃。
七、三种 fuzz 构建模式对照
doc/fuzzing.md 明确区分了 fuzz 测试的三种构建方式,理解它们对调试很重要:
| 构建方式 | Assume()失败行为 | 确定性 | 说明 |
|---|---|---|---|
-DBUILD_FOR_FUZZING=ON | 中止 | 硬编码开启(跳过 PoW 校验、禁用随机种子、禁用时钟) | 运行 fuzz 测试与生成新输入的标准方式。因确定性被硬编码,只能构建 fuzz 二进制,其他二进制全部禁用 |
-DBUILD_FUZZ_BINARY=ON -DCMAKE_BUILD_TYPE=Debug | 中止 | 默认开启,可用FUZZ_NONDETERMINISM环境变量关闭 | 与BUILD_FOR_FUZZING不同,它不硬编码确定性,因此非 fuzz 二进制可与 fuzz 二进制共存于同一构建,便于在普通构建中复现 fuzz 失败 |
-DBUILD_FUZZ_BINARY=ON -DCMAKE_BUILD_TYPE=Release | 不中止 | 强制关闭 | fuzz 二进制可构建但拒绝运行(Release 下确定性被强制关闭且Assume()不中止),仅用于确保 fuzz 测试可以编译链接 |
BUILD_FOR_FUZZING与BUILD_FUZZ_BINARY的关系可从 fuzz.cpp 得到印证:程序检查G_FUZZING_BUILD || G_ABORT_ON_FAILED_ASSUME,二者皆不满足时直接报错退出;随后调用EnableFuzzDeterminism(),失败时若设置了FUZZ_NONDETERMINISM则打印警告继续,否则强制开启动态确定性并assert。
八、MemorySanitizer(MSan)与 Valgrind
MSan 要求所有被链接的代码都必须被插桩:通常需要从源码编译clang,再用该 clang 编译插桩版 libc++,然后依次从源码构建 Bitcoin Core 的依赖(参见 depends/README.md 描述的 depends 构建体系)和 fuzz 二进制本身。仓库的 MSan CI 任务(ci/test/00_setup_env_native_fuzz_with_msan.sh)可作为这些步骤的参考示例。
不想搭建整套 MSan 环境时,Valgrind 是不需要自定义 libc++ 的替代方案:test_runner.py --valgrind即可在所有 fuzz 目标上以 Valgrind 内存错误检测器运行(CI 中另有 ci/test/00_setup_env_native_fuzz_with_valgrind.sh 对应任务)。
九、源码级覆盖率报告(source-based coverage)
官方文档指向 developer-notes.md 的 "Compiling for Fuzz Coverage" 一节,其完整流程(doc/developer-notes.md):
用 LLVM source-based code coverage 标志构建:
cmake -B build \ -DCMAKE_C_COMPILER="clang" \ -DCMAKE_CXX_COMPILER="clang++" \ -DCMAKE_C_FLAGS="-fprofile-instr-generate -fcoverage-mapping" \ -DCMAKE_CXXFLAGS="-fprofile-instr-generate -fcoverage-mapping" \ -DBUILD_FOR_FUZZING=ON cmake --build build # 可追加 "-j N" 并行用
test_runner.py跑语料并通过LLVM_PROFILE_FILE收集 profile(单目标用固定文件名,多目标用%m_%p模板):LLVM_PROFILE_FILE="$(pwd)/build/raw_profile_data/txorphan.profraw" \ ./build/test/fuzz/test_runner.py ../qa-assets/fuzz_corpora txorphan # 多目标: LLVM_PROFILE_FILE="$(pwd)/build/raw_profile_data/%m_%p.profraw" \ ./build/test/fuzz/test_runner.py ../qa-assets/fuzz_corpora合并 profile 并生成 HTML 报告,命令中用
--ignore-filename-regex排除src/crc32c/、src/leveldb/、src/minisketch/、src/secp256k1/、src/test/等第三方与测试目录,最终报告在build/coverage_report/index.html。
十、afl++ 接入指南
afl++ 快速上手(原文档完整保留):
$ git clone https://gitcode.com/GitHub_Trending/bi/bitcoin $ cd bitcoin/ $ git clone <AFLplusplus 仓库> $ make -C AFLplusplus/ source-only # 若 afl-clang-lto 不可用,参见 AFLplusplus 官方文档 # "Selecting the best afl compiler for instrumenting the target" $ cmake -B build_fuzz \ -DCMAKE_C_COMPILER="$(pwd)/AFLplusplus/afl-clang-lto" \ -DCMAKE_CXX_COMPILER="$(pwd)/AFLplusplus/afl-clang-lto++" \ -DBUILD_FOR_FUZZING=ON $ cmake --build build_fuzz $ mkdir -p inputs/ outputs/ $ echo A > inputs/thin-air-input $ FUZZ=bech32 ./AFLplusplus/afl-fuzz -i inputs/ -o outputs/ -- build_fuzz/bin/fuzz # 你可能需要调整若干内核参数以获得最佳测试效果—— # 如有问题 afl-fuzz 会打印错误与修改建议。对应地,fuzz.cpp 在定义__AFL_LOOP时进入 AFL 持久化模式:从__AFL_FUZZ_TESTCASE_BUF读取当前测试用例并在__AFL_LOOP(100'000)循环中反复调用test_one_input,避免反复重启进程的开销。未定义__AFL_LOOP时(如 Honggfuzz 或非插桩构建),main()则从命令行参数或 stdin 读取输入文件逐一执行,并以"<target>: succeeded against N files in Xs."形式打印结果。
十一、Honggfuzz 接入指南
$ git clone https://gitcode.com/GitHub_Trending/bi/bitcoin $ cd bitcoin/ $ git clone <google/honggfuzz 仓库> $ cd honggfuzz/ $ make $ cd .. $ cmake -B build_fuzz \ -DCMAKE_C_COMPILER="$(pwd)/honggfuzz/hfuzz_cc/hfuzz-clang" \ -DCMAKE_CXX_COMPILER="$(pwd)/honggfuzz/hfuzz_cc/hfuzz-clang++" \ -DBUILD_FOR_FUZZING=ON \ -DSANITIZERS=address,undefined $ cmake --build build_fuzz $ mkdir -p inputs/ $ FUZZ=process_message ./honggfuzz/honggfuzz -i inputs/ -- build_fuzz/bin/fuzz与 libFuzzer preset 不同,Honggfuzz 构建手动指定hfuzz-clang/hfuzz-clang++编译器与SANITIZERS=address,undefined(不含fuzzer,因为引擎由 honggfuzz 本身提供)。
十二、OSS-Fuzz 集成
Bitcoin Core 参与 Google 的 OSS-Fuzz 计划:
- 崩溃与漏洞通过 OSS-Fuzz 的公共 issue 看板披露(可按 bitcoin-core 过滤开放问题);
- 漏洞披露遵循 Bitcoin Core 自身的安全披露政策,可能与 OSS-Fuzz 标准的 90 天披露窗口不同;
- OSS-Fuzz 同时产出 fuzzing 覆盖率报告,可在其 coverage 页面查看 libFuzzer+ASan 构建的覆盖数据。
十三、macOS 说明
项目在 macOS 上的 fuzzing 支持并非官方维护状态:遇到 macOS 问题时,官方建议在 Linux 上进行 fuzzing 以获得最佳效果(macOS 上可通过 Docker 或虚拟机运行 Linux)。不过,在 macOS 上复现和调试fuzz 测试用例是受支持的,做法是不启用任何特定 fuzzing 引擎构建 fuzz 二进制,直接以命令行参数形式喂入输入文件(即fuzz二进制无__AFL_LOOP、非引擎模式下的文件执行路径)。
十四、小结与关键路径索引
| 内容 | 仓库路径 |
|---|---|
| 官方 fuzzing 文档(本文主体) | doc/fuzzing.md |
| libfuzzer / libfuzzer-nosan 预设 | CMakePresets.json |
| fuzz 框架入口(FUZZ 选择、确定性、双横线参数) | src/test/fuzz/fuzz.cpp |
FUZZ_TARGET宏与目标注册 API | src/test/fuzz/fuzz.h |
| 全部 fuzz harness | src/test/fuzz/ |
ProcessMessage被测函数 | src/net_processing.cpp |
| 批量目标 runner(含 Valgrind 模式) | test/fuzz/test_runner.py |
| CI fuzz 任务环境(clang-22 + 全量 sanitizer) | ci/test/00_setup_env_native_fuzz.sh |
| 源码级 fuzz 覆盖率报告流程 | doc/developer-notes.md |
实践建议:日常开发用--preset=libfuzzer全量 sanitizer 构建;追求覆盖探索时定期切换--preset=libfuzzer-nosan长跑,并把新语料回流 qa-assets;调试崩溃案例时优先用 Debug +BUILD_FUZZ_BINARY的混合构建,让普通调试工具链与 fuzz 二进制共存。
【免费下载链接】bitcoinBitcoin Core integration/staging tree项目地址: https://gitcode.com/GitHub_Trending/bi/bitcoin
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考