
connectedhomeip 模糊测试实战libFuzzer 与 Google FuzzTest 编写与集成指南【免费下载链接】connectedhomeipMatter (formerly Project CHIP) creates more connections between more objects, simplifying development for manufacturers and increasing compatibility for consumers, guided by the Connectivity Standards Alliance.项目地址: https://gitcode.com/GitHub_Trending/co/connectedhomeip本文以 docs/testing/fuzz_testing.md 为骨架结合 Matterconnectedhomeip开源仓库中的真实 fuzzer 实现、GN 构建模板与覆盖率脚本系统讲解两代模糊测试技术栈传统 libFuzzer 目标LLVMFuzzerTestOneInput与新一代 Google FuzzTestFUZZ_TEST属性测试。读完本文你将掌握在 Matter 仓库中编写 fuzz target、接入构建系统chip_fuzz_target/chip_pw_fuzz_target、配置种子语料、运行持续模糊与生成覆盖率报告、并利用覆盖率定位“Fuzz Blocker”的完整实战方法。为什么 Matter 需要模糊测试模糊测试Fuzz Testing的核心思路是向函数和方法注入随机且非预期的数据以发现 Bug、安全漏洞或判断软件是否会崩溃。它有以下几个关键特征持续性被测函数function under test被以迭代方式调用数千次每次喂入不同的输入配合 Sanitizer通常启用 ASanAddressSanitizer等内存检测工具以捕捉内存错误如缓冲区溢出、use-after-free和未定义行为undefined behavior两类主流框架C/C 生态最常用的是 libFuzzer 和 AFLGoogle 的 FuzzTest 则是更新的框架提供用户友好的 API 和更强的输入生成控制能力并与 Google TestGTest无缝集成。对于 Matter 这类承载设备配网、证书解析、TLV 编解码、mDNS/DNS-SD 报文解析、安全握手协议PASE/CASE/BDX等大量“面向恶意输入”代码的协议栈而言模糊测试是验证解析器健壮性的关键防线。仓库中已有大量真实 fuzzer 分散在各模块的tests/目录下例如src/setup_payload/tests/FuzzBase38.cppBase38 编码src/lib/core/tests/FuzzTlvReader.cppTLV 读取器src/lib/dnssd/minimal_mdns/tests/FuzzPacketParsing.cppmDNS 报文解析src/credentials/tests/FuzzChipCert.cpp证书解析它们分别对应 libFuzzer 与 FuzzTest 两种写法是本文后续示例的活教材。使用 libFuzzer 编写简单 FuzzerlibFuzzer 要求每个 fuzz target 实现统一的入口函数extern C int LLVMFuzzerTestOneInput(const uint8_t * data, size_t len)该函数会被 fuzzing 引擎反复调用每次传入一组随机字节。Fuzzer 源码必须位于某个测试目录形如src/some_directory/tests/这是构建系统扫描 fuzz target 的约定。仓库中最简单的示例是 src/setup_payload/tests/FuzzBase38Decode.cpp它用 fuzz 输入构造 Base38 编码字符串并调用解码器#include cstddef #include cstdint #include iostream #include setup_payload/Base38Decode.h using namespace chip; extern C int LLVMFuzzerTestOneInput(const uint8_t * data, size_t len) { std::string base38EncodedString(reinterpret_castconst char *(data), len); std::vectoruint8_t decodedData; // 忽略返回值一般来说输入是垃圾数据无法正确解码。 // 我们只测试解码器在 fuzzer 生成的输入上不会崩溃。 RETURN_SAFELY_IGNORED chip::base38Decode(base38EncodedString, decodedData); return 0; }注意其中两个细节用RETURN_SAFELY_IGNORED包裹返回值明确告知读者“此处有意忽略错误码”防止编译器-Wunused-result告警注释说明了 fuzzer 的语义不关心解码成功与否只关心“不崩溃”。这是健壮性robustnessfuzzer 的典型写法。接入构建系统fuzz_test.gni要在 GN 构建中注册 fuzz target需要在对应测试目录的BUILD.gn中引入build/chip/fuzz_test.gni并使用模板chip_fuzz_target。以 src/setup_payload/tests/BUILD.gn 为真实范例import(${chip_root}/build/chip/fuzz_test.gni) if (enable_fuzz_test_targets) { chip_fuzz_target(fuzz-setup-payload-base38) { sources [ FuzzBase38.cpp ] public_deps [ ${chip_root}/src/platform/logging:stdio, ${chip_root}/src/setup_payload, ] } chip_fuzz_target(fuzz-setup-payload-base38-decode) { sources [ FuzzBase38Decode.cpp ] public_deps [ ${chip_root}/src/platform/logging:stdio, ${chip_root}/src/setup_payload, ] } }模板的三个关键字段字段含义CHIP_FUZZ_TARGETtarget 名fuzz target 的名字会直接成为可执行文件名的一部分SOURCES测试目录中包含 fuzzer 实现的源文件即LLVMFuzzerTestOneInput所在文件PUBLIC_DEPS构建 fuzzer 所需的代码依赖必须列出被测代码所在的 target再看模板本身 build/chip/fuzz_test.gni 的实现它揭示了几个重要的环境约束declare_args() { # 使用 libFuzzer 的模糊测试 enable_fuzz_test_targets (is_libfuzzer || oss_fuzz) is_clang chip_build_tests (current_os linux || current_os mac) # 使用 pw_fuzzer 和 FuzzTest 框架的模糊测试 pw_enable_fuzz_test_targets false }fuzz target 只在 Linux / macOS Clang 环境下生效本地构建时模板自动追加//build/config/compiler:libfuzzer_fuzzing和 ASansanitize_address配置若启用 MSAN 则跳过 ASan二者互斥在 OSS-Fuzz 环境下则改用oss_fuzz配置二进制默认输出到${root_out_dir}/tests目录。汇总 fuzz 目标并一键构建在仓库根目录 BUILD.gn 中维护一个fuzz_testsgroup把所有 fuzz target 汇总起来并注释标明“ADD HERE YOUR FUZZING TARGET”以供新增if (enable_fuzz_test_targets) { group(fuzz_tests) { deps [ ${chip_root}/src/credentials/tests:fuzz-chip-cert, ${chip_root}/src/lib/core/tests:fuzz-tlv-reader, ${chip_root}/src/lib/dnssd/minimal_mdns/tests:fuzz-minmdns-packet-parsing, ${chip_root}/src/lib/format/tests:fuzz-payload-decoder, ${chip_root}/src/setup_payload/tests:fuzz-setup-payload-base38, ${chip_root}/src/setup_payload/tests:fuzz-setup-payload-base38-decode, // 在这里添加你自己的 fuzz target ${chip_root}/some_directory/tests:FuzzTargetName ] } }构建全部 fuzzer 的命令模板为./scripts/build/build_examples.py --target host-compiler-tests-asan-libfuzzer-clang build例如注意 host 与 compiler 必须替换为实际环境./scripts/build/build_examples.py --target darwin-arm64-tests-asan-libfuzzer-clang build编译产物位于out/host-compiler-tests-asan-libfuzzer-clang/tests目录。运行 fuzzer 并指定语料库corpus目录path_to_fuzzer_in_test_folder path_to_corpuslibFuzzer 会从该目录加载已有语料作为变异起点并将发现的新输入回写其中实现“语料库持续进化”。Google FuzzTest属性测试驱动的新一代模糊测试Google FuzzTest 通过 Pigweed 的pw_fuzzer集成进 Matter。与 libFuzzer 的“裸字节输入”不同FuzzTest 以属性测试property-based testing的方式组织测试你声明一个属性函数Property Function接收有类型的参数并为每个参数指定一个输入域Input DomainFuzzTest 引擎负责在域内生成海量取值并反复调用属性函数。典型使用场景用 Sanitizer 发现未定义行为持续运行 fuzz test 并检查是否触发崩溃或 sanitizer 报错从而捕捉缓冲区溢出、use-after-free 等细微内存问题用断言发现正确性 Bug典型例子是Round Trip Fuzzing往返模糊测试——先把模糊输入编码、再解码、最后验证结果与原始输入一致。仓库中的 src/setup_payload/tests/FuzzBase38PW.cpp 就是范例。核心概念FUZZ_TEST 宏FuzzTest 通过FUZZ_TEST宏实例化测试。宏的第一个参数是测试套件名suite第二个参数是属性函数名.WithDomains()为参数指定输入域FUZZ_TEST(TLVReader, FuzzTlvReader).WithDomains(fuzztest::Arbitrarystd::vectorstd::uint8_t());上面这个宏会调用属性函数FuzzTlvReader并让 FuzzTest 生成“各种各样的字节向量”来彻底测试它。文档中的关键字对应关系Property Function属性函数即被模糊测试的目标函数必须返回voidInput Domain输入域定义属性函数在模糊测试期间将接收的输入的范围和类型通过.WithDomains()子句指定。属性函数Property Function属性函数是 FUZZ_TEST 的核心签名示例// 属性函数 void FuzzTlvRead(const std::vectorstd::uint8_t bytes) { TLVReader reader; reader.Init(bytes.data(), bytes.size()); chip::TLV::Utilities::Iterate(reader, FuzzIterator, nullptr); }属性函数的约定必须返回void会在同一进程内被以大量不同输入调用目标是触发崩溃可以包含断言Assertion例如 Round-Trip Fuzzing 中的相等性校验。仓库中 src/setup_payload/tests/FuzzBase38PW.cpp 提供了一个完整的往返模糊测试属性函数它把 fuzz 输入 Base38 编码、再解码、再断言与原始输入一致void Base38RoundTripFuzz(const std::vectoruint8_t bytes) { size_t outputSizeNeeded base38EncodedLength(bytes.size()); const size_t kMaxOutputSize 512; ASSERT_LT(outputSizeNeeded, kMaxOutputSize); ByteSpan span(bytes.data(), bytes.size()); char encodedBuf[kMaxOutputSize]; MutableCharSpan encodedSpan(encodedBuf); CHIP_ERROR encodingError base38Encode(span, encodedSpan); ASSERT_EQ(encodingError, CHIP_NO_ERROR); std::string base38EncodedString(encodedSpan.data(), encodedSpan.size()); std::vectoruint8_t decodedData; CHIP_ERROR decodingError base38Decode(base38EncodedString, decodedData); ASSERT_EQ(decodingError, CHIP_NO_ERROR); // 确保解码后的数据与原始模糊输入一致 ASSERT_EQ(decodedData, bytes); } // 最大长度取 306此时 outputSizeNeeded 为 511小于 kMaxOutputSize FUZZ_TEST(Base38Decoder, Base38RoundTripFuzz).WithDomains(Arbitrarystd::vectoruint8_t().WithMaxSize(306));注意这里用WithMaxSize(306)限定输入向量最大长度其取值是有依据的Base38 编码后长度必须小于kMaxOutputSize 512306 字节输入恰好编码为 511 字节是安全上限。输入域Input DomainsFuzzTest 提供了丰富的输入域全部位于fuzztest::命名空间所有原生 C 类型都可以通过ArbitraryT()使用例如字符串FUZZ_TEST(Base38Decoder, FuzzQRCodeSetupPayloadParser).WithDomains(Arbitrarystd::string());容器/向量域是最常用的输入域之一可用.WithMaxSize()/.WithMinSize()限制输入规模FUZZ_TEST(MinimalmDNS, TxtResponderFuzz).WithDomains(Arbitraryvectoruint8_t().WithMaxSize(254));ElementOf非常实用通过显式枚举值集合来定义域。仓库 src/credentials/tests/FuzzChipCertPW.cpp 用ElementOf枚举协议 ID让 fuzzer 在真实协议标识中挑选auto AnyProtocolID() { return ElementOf({ chip::Protocols::SecureChannel::Id, chip::Protocols::InteractionModel::Id, chip::Protocols::BDX::Id, chip::Protocols::UserDirectedCommissioning::Id }); } FUZZ_TEST(PayloadDecoder, RunDecodeFuzz).WithDomains(Arbitrarystd::vectorstd::uint8_t(), AnyProtocolID(), Arbitraryuint8_t());同一文件中还用ElementOf枚举CertDecodeFlags的三种合法取值空标志、生成 TBS 哈希、信任锚点见AnyCertDecodeFlag()src/credentials/tests/FuzzChipCertPW.cpp。域组合器Domain Combinators当需要用已有输入域构造另一个域例如构造一个对象再传给属性函数时可以使用域组合器。典型例子是Map把多个输入域送入映射函数输出一个单一的域。仓库中的实例是AnyValidationContext()——它被用在FUZZ_TEST(FuzzCASE, HandleSigma3b)中负责基于多个基础域构造 CASE 协议 Sigma3 阶段的校验上下文对象。种子与语料库Seeds and Corpus种子Seeds的价值当被模糊测试的函数接收复杂输入如大型字节数组时提供初始种子非常有用。fuzzing 引擎会从这些初始种子开始变异而不是生成完全随机的输入从而更快地探索更真实、更有意义的代码路径提高发现问题概率。用法一用变量作为输入。FuzzCASE.cpp中的 lambdaSeededEncodedSigma1()把一段从单元测试提取的真实编码 Sigma1 报文作为种子auto SeededEncodedSigma1() { std::vectoruint8_t dataVec(std::begin(FuzzSeed_EncodedSigma1_Node01_02_Chip), std::end(FuzzSeed_EncodedSigma1_Node01_02_Chip)); return Arbitraryvectoruint8_t().WithSeeds({ dataVec }); } FUZZ_TEST(FuzzCASE, ParseSigma1_RawPayload) .WithDomains(SeededEncodedSigma1());见 src/protocols/secure_channel/tests/FuzzCASE_PW.cpp。用真实报文做种子fuzzer 会围绕“格式合法”的报文做变异更容易触达解析器深处。用法二用文件作为输入。通过fuzztest::ReadFilesFromDirectory()从目录批量读取文件作为种子。它返回“单元素元组的向量”每个元组包含文件内容字符串。仓库 src/credentials/tests/FuzzChipCertPW.cpp 中的seedProviderlambda 解包元组并把内容提取为std::vectorstd::string配合std::string域使用FUZZ_TEST(FuzzChipCert, ConvertX509CertToChipCertFuzz).WithDomains(Arbitrarystd::string().WithSeeds(seedProvider(isDerFile)));注意seedProvider接收一个过滤函数如isChipFile、isDerFile、isChipRCACFile从而把种子语料按角色chip 证书 / DER 证书 / RCAC / NOC分类喂给不同 fuzzer。种子目录是仓库中的credentials/test/operational-certificates-error-cases/含 172 个.chip与 100 个.der错误用例向量并且文件底部还有一个TEST(FuzzChipCertSeeds, SeedCorpusIsPresent)单元测试作为种子语料守卫当种子目录存在时若实际读到的种子数量低于下限kMinChipSeeds 50、kMinDerSeeds 20CI 会直接失败防止 CWD 错误或过滤器失效导致“静默无种子”的虚假覆盖src/credentials/tests/FuzzChipCertPW.cpp。关于.WithSeeds()位置的警告必须把.WithSeeds()挂在.WithDomains(...)内部的输入域上。FuzzTest 虽然也允许把.WithSeeds()链在FUZZ_TEST注册本身.WithDomains(...)之后但在 libFuzzer 兼容模式即 OSS-Fuzz 运行这些测试的方式下这种挂在注册上的种子会被静默丢弃只有挂在输入域上的种子才会被真正送达。多参数属性函数的整输入种子当需要给多个参数“捆绑”种子例如让消息序列与使其有意义的会话建立步骤保持配对时把所有参数域包进一个TupleOf(...)并把.WithSeeds()挂在它上面——.WithDomains()接受一个元组域来代替多个独立参数域。仓库实例FUZZ_TEST(FuzzBdxTransferSessionPW, BdxSessionSequenceDoesNotCrash)正是这么做的src/protocols/bdx/tests/FuzzBdxTransferSessionPW.cppFUZZ_TEST(MySuite, MyFuzzTest) .WithDomains(TupleOf(Arbitrarybool(), Arbitraryuint16_t(), Arbitrarystd::vectoruint8_t()) .WithSeeds(WholeInputSeeds()));该文件中的BdxWholeInputSeeds()构建了 12 组完整会话序列发起/响应的接收方/发送方 × 驱动模式 × 正常/EOF/跳过/多块传输路径把 BDX TransferSession 状态机的每一条“受理 → 查询 → 块传输 → 确认”链路都钉在种子中使变异器从kTransferInProgress内部状态起步而不是从随机字节重新发现整条消息序列。运行 FuzzTestFuzzTest 集成chip_pw_fuzz_target编译出的二进制如fuzz-chip-cert-pw支持多种运行模式单元测试模式每个输入只模糊一秒钟./fuzz-chip-cert-pw持续模糊模式先列出测试再指定要运行的 FuzzTestCase$ ./fuzz-chip-cert-pw --list_fuzz_tests [.] Sanitizer coverage enabled. Counter map size: 11134, Cmp map size: 262144 [*] Fuzz test: ChipCert.ChipCertFuzzer [*] Fuzz test: ChipCert.DecodeChipCertFuzzer $ ./fuzz-chip-cert-pw --fuzzChipCert.DecodeChipCertFuzzer按时间运行某个 TestSuite 的全部测试例如各跑 10 分钟# 两个 FuzzTest 将各运行 10 分钟 ./fuzz-chip-cert-pw --fuzz_for10m查看帮助# FuzzTest 相关帮助 ./fuzz-chip-cert-pw --helpfull # gtest 相关帮助 ./fuzz-chip-cert-pw --help构建与覆盖率FuzzTest 目标通过chip_pw_fuzz_target模板接入构建见 src/setup_payload/tests/BUILD.gnif (pw_enable_fuzz_test_targets) { chip_pw_fuzz_target(fuzz-setup-payload-base38-pw) { test_source [ FuzzBase38PW.cpp ] public_deps [ ${chip_root}/src/platform/logging:stdio, ${chip_root}/src/setup_payload, ] } }模板实现build/chip/fuzz_test.gni的关键点它通过pw_test生成可执行文件强制依赖$dir_pw_fuzzer:fuzztest并把enable_if true置位——注释明确指出这是必要的否则 FuzzTest 会被编译进FuzzTarget.DISABLED.ninja而无法产出可执行文件。启用use_coverage时还会追加coverage编译配置。[!TIP]编写 FuzzTest 时善用覆盖率报告Coverage Reports来获得更多洞察。覆盖率生成流程用覆盖率插桩构建 FuzzTest构建方法见 docs/guides/BUILDING.md 中关于 pw_fuzzer FuzzTests 的章节用 scripts/tests/run_fuzztest_coverage.py 运行这些测试——在持续模糊模式下运行得越久覆盖越好HTML 覆盖率报告路径会在生成后输出。run_fuzztest_coverage.py脚本内部会定位构建产物目录out/profiling_fuzztest按chip_pw_fuzztest/tests/*模式扫描 fuzz test 二进制并将报告输出到out/coverage_fuzztest。用覆盖率报告排查 Fuzz Blocker覆盖率报告能帮助 FuzzTest 开发者识别Fuzz Blocker——即阻止 fuzz test 探索代码中某一部分的东西例如某个永远失败的校验导致其后的函数永远执行不到。实战案例一个真实的 Fuzz Blocker 分析下图展示了一个 Fuzz Blocker 的排查过程。覆盖率报告中展示了每行的执行次数第 #2159 行在至少 129,452 次执行中从未被触达而其上方的第 #2156 行极可能就是 Fuzz Blocker——该处的data.fabricId校验永远失败堵住了其后函数的执行路径。解决方案调整 FuzzTest使其能够通过该校验。一个可行的方法是给被模糊测试的NOC注入一个合法的 NOC 证书作为种子单独模糊FabricId字段使用与该合法 NOC 证书中一致的合法FabricId作为FabricId的种子。改造后第 #2159 行终于被触达见下图覆盖率显著提升说明 fuzz test 更有效了。这一方法被实际应用于FUZZ_TEST(FuzzCASE, HandleSigma3b)。从源码看FuzzCASE_PW.cpp中HandleSigma3b所在的模糊测试套件正是依赖“种子化合法证书”这一思路FuzzSeed_EncodedSigma1_Node01_02_Chip等种子全部取自 OpCred 身份Root01:Node01_02的真实测试向量src/protocols/secure_channel/tests/FuzzCASE_PW.cpp保证安全握手链路中的证书校验能从“合法基线”出发探索变异空间。仓库中的 FuzzTest 全景与 FAQ覆盖面一览截至当前仓库src/下共有 40 余个 fuzz 源文件覆盖了 Matter 协议栈的关键攻击面安全握手src/protocols/secure_channel/tests/FuzzCASE_PW.cpp、src/protocols/secure_channel/tests/FuzzPASE_PW.cpp证书与认证src/credentials/tests/FuzzChipCertPW.cpp、src/credentials/tests/FuzzAttestationElementsPW.cppTLV 与 OTA 头部src/lib/core/tests/FuzzTlvReaderPW.cpp、src/lib/core/tests/FuzzOTAImageHeaderPW.cppmDNS/DNS-SDsrc/lib/dnssd/minimal_mdns/tests/FuzzPacketParsingPW.cpp、src/lib/dnssd/tests/FuzzMdnsResolverPW.cppBDX 文件传输src/protocols/bdx/tests/FuzzBdxTransferSessionPW.cpp、src/protocols/bdx/tests/FuzzBdxUriPW.cppBLE/BTPsrc/ble/tests/FuzzBtpEnginePW.cppWiFi PAF / 会话管理 / ICDsrc/wifipaf/tests/FuzzWiFiPAFTPPW.cpp、src/transport/tests/FuzzSessionManagerGroupCounterPW.cpp、src/app/icd/client/tests/FuzzICDCheckInPW.cpp常见问题FuzzTest 与 Abseil 子模块的版本要求Q运行pw_fuzzer FuzzTest 时FuzzTest 和 Abseil 子模块应处于什么修订版本Google FuzzTest 通过pw_fuzzer集成进 Matter而pw_fuzzer有若干依赖。Matter 将这些依赖包括 Google FuzzTest 与 Abseil作为子模块引入。由于 FuzzTest 与 Abseil 只支持bazel和CMake两种构建系统、不支持 GNPigweed 维护者使用脚本为这些依赖生成 GN 文件。因此Matter 中 FuzzTest 和 Abseil 子模块的修订版本应匹配或至少不早于生成这些 GN 文件时使用的特定版本SHA1生成 GN 文件所用的版本可在 Pigweed 官方 FuzzTest Version 与 Abseil Version 页面查询。后续演进方向原文档标注待补充原文档明确列出的后续补充项包括Test Fixtures 的更多信息待相关问题解决后、如何将 FuzzTests 添加到构建系统的完整教程以及 OSS-Fuzz 的更多集成细节。读者在动手实践时可对照 build/chip/fuzz_test.gni 与 scripts/tests/run_fuzztest_coverage.py 先行探索这两条路径。小结与上手路线在 connectedhomeip 中编写模糊测试可以归纳为三步libFuzzer 路线在src/xxx/tests/下新建FuzzXxx.cpp实现LLVMFuzzerTestOneInput在 src/setup_payload/tests/BUILD.gn 这种测试目录的BUILD.gn中用chip_fuzz_target注册在根 BUILD.gn 的fuzz_testsgroup 中登记用build_examples.py的-asan-libfuzzer-clangtarget 构建后运行。FuzzTest 路线在测试目录中编写FuzzXxxPW.cpp用FUZZ_TEST声明属性函数与输入域用chip_pw_fuzz_target注册通过--fuzz/--fuzz_for持续运行用run_fuzztest_coverage.py生成覆盖率报告。持续优化为复杂输入提供种子.WithSeeds()、ReadFilesFromDirectory、TupleOf整输入种子用覆盖率报告定位 Fuzz Blocker并通过“合法种子 局部字段模糊”的方式打通被堵塞的代码路径。模糊测试不是“写一次就完事”的静态工作而是与覆盖率报告配合、持续迭代的工程实践——这正是 Matter 这样面向网络协议的代码库保障解析器与安全握手实现健壮性的核心手段之一。【免费下载链接】connectedhomeipMatter (formerly Project CHIP) creates more connections between more objects, simplifying development for manufacturers and increasing compatibility for consumers, guided by the Connectivity Standards Alliance.项目地址: https://gitcode.com/GitHub_Trending/co/connectedhomeip创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考