ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

MongoDB 中的 FuzzTest 属性测试与模糊测试实战指南

MongoDB 中的 FuzzTest 属性测试与模糊测试实战指南 MongoDB 中的 FuzzTest 属性测试与模糊测试实战指南【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo导读本文基于 MongoDB 官方文档 docs/fuzztest.md系统讲解如何在 MongoDB 服务器代码库中使用 Google 的 FuzzTest 框架编写基于属性的模糊测试property-based fuzzing。你将掌握FUZZ_TEST/FUZZ_TEST_F宏、typed domains、种子seeds、roundtrip 与差分模糊等正确性模式以及 MongoDB 专为 BSON 提供的BSONObjImpl自定义 domain同时会深入mongo_cc_fuzztestBazel 规则的底层实现与--fuzz/--fuzz_for等运行参数最终能在本地以单元测试模式或持续模糊模式运行、调试这类测试。FuzzTest 是一个与 GoogleTest 直接集成的C 覆盖率引导模糊测试框架。与传统的 libFuzzer 风格把输入视为不透明字节流不同FuzzTest 让你描述输入的结构用带类型的domains声明输入形态由框架生成并变异满足约束的值底层使用Centipede作为模糊引擎并通过AUBSAN即 UBSan 地址消毒的配套构建暴露未定义行为。何时使用 FuzzTest在 MongoDB 代码库中以下场景适合使用 FuzzTest 而非传统字节级模糊器被测函数接受结构化输入整数、字符串、自定义类型、BSON 对象等而不是不透明的字节流。MongoDB 大量命令与查询解析路径都符合这一特征。想表达不仅仅是崩溃的正确性属性例如 API 不变量invariant、差分等价differential equivalence、往返对称roundtrip symmetry。典型例子见 ndv_hashing_fuzz_test.cpp 中哈希相等当且仅当woCompare相等的不变量断言。希望模糊测试在普通 CI 中也能作为单元测试干净运行无需专门的 fuzzer 构建变体——每个FUZZ_TEST天然就是一个 GoogleTest 用例这正是本文Unit test mode一节的原理。基本用法属性函数与 FUZZ_TEST 宏一个 FuzzTest 由属性函数property function和注册宏两部分组成。属性函数是普通 C 函数其参数定义了要被模糊的输入框架会反复用生成的值调用它寻找任何一次调用触发断言失败或消毒器错误的输入。#include fuzztest/fuzztest.h #include gtest/gtest.h void MyFunctionFuzzer(const std::string input) { MyFunction(input); // sanitizers catch undefined behavior implicitly } FUZZ_TEST(MyTestSuite, MyFunctionFuzzer);当不提供.WithDomains()子句时每个参数默认使用fuzztest::ArbitraryT()它能覆盖大多数标准库类型整数、浮点、字符串、容器等足以在不应崩溃级别快速发现内存安全与未定义行为问题。指定输入 Domains使用.WithDomains()约束生成输入的形态使模糊集中在业务上合法的取值空间⚠️警告切勿用其他编译单元中初始化的全局对象来初始化输入 domains涉及静态初始化顺序问题详见 FuzzTest 的 FUZZ_TEST 宏文档。void ProcessRequestFuzzer(int opcode, const std::string payload) { ProcessRequest(opcode, payload); } FUZZ_TEST(MyTestSuite, ProcessRequestFuzzer) .WithDomains(/*opcode*/fuzztest::InRange(1, 255), /*payload*/fuzztest::Arbitrarystd::string());FuzzTest 内置了丰富的 domain 集合常见的还包括fuzztest::OneOf(...)、fuzztest::ElementOf(...)、fuzztest::VariantOf(...)、fuzztest::Just(...)、fuzztest::NullOptT()等。在 MongoDB 的实际测试中可以看到这些组合的工程化用法例如 sbe_value_fuzz_test.cpp 为 int32/int64 构造了边界小值 一般区间 全任意 极值枚举的混合 domainauto int32Domain() { return fuzztest::OneOf(fuzztest::InRangeint32_t(-1, 1), fuzztest::InRangeint32_t(-20, 20), fuzztest::Arbitraryint32_t(), fuzztest::ElementOfint32_t({std::numeric_limitsint32_t::min(), std::numeric_limitsint32_t::max()})); }提供 Seeds种子seeds给模糊器一个良好的起点提供已知有意义的输入供其变异避免从零开始随机探索。⚠️警告切勿用其他编译单元初始化的全局对象来初始化 seeds原因同上。FUZZ_TEST(MyTestSuite, ProcessRequestFuzzer) .WithDomains(fuzztest::InRange(1, 255), fuzztest::Arbitrarystd::string()) .WithSeeds({{1, hello}, {255, }});也可以从仓库中检入的目录加载种子语料FUZZ_TEST(MyTestSuite, ProcessRequestFuzzer) .WithSeeds(fuzztest::ReadFilesFromDirectory( absl::StrCat(std::getenv(TEST_SRCDIR), /path/to/corpus)));为何种子在 MongoDB 中尤其重要从源码可以确认真实测试用种子覆盖原始字节 domain 几乎不可能自行拼凑出来的冷门类型。例如 ndv_hashing_fuzz_test.cpp 构造了一个seeds()函数把 DBRef命名空间内嵌 NUL 字节注释明确指出这里曾真实存在过截断 bug、Symbol、CodeWScope、Decimal128、Undefined 等异类BSON 元素编码进种子让变异器在这些值的邻域内探索std::vectorstd::string seeds() { auto toBytes [](const BSONObj obj) { return std::string(obj.objdata(), obj.objsize()); }; // Exotic types the raw domain would practically never assemble from scratch; mutation // explores their neighborhoods from here. The DBRef namespace with an embedded NUL is where // a real truncation bug lived. BSONObjBuilder exotic; exotic.appendDBRef(a, std::string_view(db\0x, 4), OID(507f1f77bcf86cd799439011)); exotic.appendSymbol(b, sym); exotic.appendCodeWScope(c, function() {}, BSON(x 1)); exotic.append(d, Decimal128(0.1)); exotic.appendUndefined(e); ... } FUZZ_TEST(NdvHashingFuzz, HashPropertyOnRawBytes) .WithDomains(fuzztest::Arbitrarystd::string().WithSeeds(seeds()));常见正确性模式FuzzTest 让超越崩溃的高级属性断言变得简单最常见的两类Roundtrip往返对称验证 encode→decode或 serialize→parse是恒等映射。这在 MongoDB 的序列化、文档编解码路径中非常典型void SerializeRoundtrips(const MyMessage msg) { auto serialized Serialize(msg); auto parsed Parse(serialized); EXPECT_EQ(msg, parsed); } FUZZ_TEST(MyTestSuite, SerializeRoundtrips);Differential fuzzing差分模糊对同一操作的两个实现新实现 vs 旧实现或不同等价路径喂相同输入并断言结果一致void ImplementationsAgree(const std::string input) { EXPECT_EQ(NewImpl(input), OldImpl(input)); } FUZZ_TEST(MyTestSuite, ImplementationsAgree);MongoDB 的 ndv_hashing_fuzz_test.cpp 给出了一个更精细的不变量示例对 BSON 元素两两断言哈希相等 ⟺woCompare相等并进一步验证元素对元组的哈希相等 ⟺ 元素两两相等。这种属性把哈希编码是否会混淆不同值这一语义正确性直接转成了可被模糊器搜索的断言其设计还特意规避了真实 murmur 碰撞概率2^-64对搜索的干扰是属性测试工程化的极佳范本。使用 FixturesFUZZ_TEST_F如果测试需要昂贵的一次性初始化例如启动某个服务使用 fixture 搭配FUZZ_TEST_F。任何可默认构造的类都能作为 fixture构造函数与析构函数在整个模糊测试期间只运行一次而不是每次迭代都运行。使用 fixture 时必须格外小心只有初始的 fixture 状态可以被保留。测试执行过程中产生的程序状态绝不能影响后续迭代也不能被后续迭代影响——否则会污染语料、引入虚假的时序依赖。class MyServiceFuzzTest { public: MyServiceFuzzTest() { service_.Start(); } ~MyServiceFuzzTest() { service_.Stop(); } void RequestFuzzer(const std::string input) { service_.Handle(input); } private: MyService service_; }; FUZZ_TEST_F(MyServiceFuzzTest, RequestFuzzer);模糊测试 BSONBSONObjImpl 自定义 DomainMongoDB 为 FuzzTest 提供了一个生成合法 BSON 对象的自定义 domainmongo::bson_mutator::BSONObjImpl。它被注册为fuzztest::ArbitraryConstSharedBuffer的模板特化因此任何接受ConstSharedBuffer参数的模糊测试都会自动收到形态良好的 BSON。这一定义可以在 bson_mutator.h 中确认namespace fuzztest::internal { template class ArbitraryImplmongo::ConstSharedBuffer : public mongo::bson_mutator::BSONObjImpl {}; } // namespace fuzztest::internal基本用法#include mongo/bson/bson_mutator/bson_mutator.h void MyCommandFuzzer(ConstSharedBuffer input) { BSONObj obj(input); MyCommand(obj); } FUZZ_TEST(MyCommandFuzzTest, MyCommandFuzzer);从 bson_mutator.h 的源码注释可以看到该 domain 的几个关键设计事实支持所有BSON 类型包括已废弃的类型生成的对象保证能通过validateBSON()的BSONValidateModeEnum::kDefault模式校验但可能包含在更严格校验级别下失败的值这本身就是在测试宽松校验路径的边界它的值类型是ConstSharedBuffer同时以BSONObj作为语料corpus类型using corpus_type BSONObj; using value_type ConstSharedBuffer;模糊测试既可以把它当原始缓冲区也可以直接转换为BSONObj内部通过BSON_MUTATOR_EXPAND_FIELD_TYPES宏展开出 double、string、object、array、binData、OID、bool、date、regex、DBRef、code、symbol、codeWScope、int、timestamp、long、decimal 等全部字段类型见 bson_mutator.h。用 .WithType() 约束字段默认情况下BSONObjImpl随机生成任意元素。若要约束哪些字段存在及其类型使用.WithType()构造器FUZZ_TEST(MyCommandFuzzTest, MyCommandFuzzer) .WithDomains(fuzztest::Arbitrarymongo::ConstSharedBuffer() .WithInt(count) .WithString(name) .WithLong(limit, fuzztest::InRange(0LL, 1000LL)));每个.WithType()对应BSON_MUTATOR_EXPAND_FIELD_TYPES宏展开出的一个WithCamel重载见 bson_mutator.h例如.WithDouble(name, domain)、.WithString(name)、.WithOID(name)等。关键语义源码注释明确说明通过.WithType()添加的字段并不保证出现在每个生成对象中——字段可能缺失从而顺带覆盖缺字段错误处理代码路径。这也意味着被测代码必须健壮地处理字段不存在的情况。用 .WithVariant() 表达多类型字段当一个字段在合法场景下可能持有多种类型时使用.WithVariant()。它接受一个BSONDomainMapabsl::flat_hash_mapBSONType, BSONElementDomainVariant把 BSON 类型映射到任意 FuzzTest domainfuzztest::Arbitrarymongo::ConstSharedBuffer() .WithVariant(value, { {BSONType::numberInt, fuzztest::InRange(0, 100)}, {BSONType::numberLong, fuzztest::InRange(0LL, 100000LL)}, });用 .WithAny() 声明键必须存在但类型不限当键必须出现、但显式类型信息不易确定或用于校验函数时使用.WithAny()fuzztest::Arbitrarymongo::ConstSharedBuffer().WithAny(filter);真实仓库中的组合用法ndv_hashing_fuzz_test.cpp 展示了如何用宏一次性把全部字段类型加入 domain让生成器覆盖所有 BSON 类型#define ENABLE_FIELD_TYPE(Camel, cpptype, bsontype, defaultdomain) .With##Camel(#Camel) FUZZ_TEST(NdvHashingFuzz, HashPropertyOnStructuredBson) .WithDomains(fuzztest::Arbitrarymongo::ConstSharedBuffer() BSON_MUTATOR_EXPAND_FIELD_TYPES(ENABLE_FIELD_TYPE));该文件同时保留了一个原始字节流版本Arbitrarystd::string 先validateBSON再进入属性断言注释解释了两者的分工结构化 domain 保证每次迭代都能命中属性而原始字节变异能覆盖接近合法性边界的编码两种输入空间互为补充。Bazel 目标mongo_cc_fuzztest在 MongoDB 的 Bazel 构建系统中用mongo_cc_fuzztest定义于 bazel/mongo_src_rules.bzl声明模糊测试目标它会自动链接 FuzzTest 与 GoogleTestmongo_cc_fuzztest( name my_command_fuzztest, srcs [my_command_fuzztest.cpp], deps [ //src/mongo:base, //src/mongo/db/commands:my_command, ], )从 mongo_src_rules.bzl 的实现可以看到该规则的几个实现级细节它是mongo_cc_test的包装器自动追加fuzztest//fuzztest、fuzztest//fuzztest:fuzztest_gtest_main、com_google_googletest//:gtest与fuzztest//centipede:centipede_runner_no_main依赖——这正是每个 FUZZ_TEST 同时也是普通 GoogleTest 测试的构建基础自动添加标签mongo_fuzzer_test便于 Evergreen / resmoke 等测试编排系统识别模糊测试通过select把target_compatible_with与//bazel/config:fsan_enabled绑定见 bazel/config/BUILD.bazel即只有启用了fsan构建配置时该目标才可构建否则标记为平台不兼容追加-fsanitize-coveragecontrol-flow链接选项以提供覆盖率反馈。作为对比旧式的 libFuzzer 风格测试使用mongo_cc_fuzzer_test_deprecatedbazel/mongo_src_rules.bzl其注释明确写着不应用于新测试请使用mongo_cc_fuzztest——新代码一律走 FuzzTest 路线。fsan是 Bazel 中的一个布尔型 build settingbazel/config/configs.bzl中的fsan rule(...)对应sanitize_provider见 configs.bzl控制是否启用模糊消毒构建。这是所有模糊运行命令中的必选参数。运行 FuzzTest单元测试模式Unit test mode每个FUZZ_TEST同时也是一个普通 GoogleTest 用例。在单元测试模式下属性函数只被以最小输入调用少量次数因此模糊测试可以在普通 CI 中与单元测试一起运行无需专门构建变体bazel test --compiler_typeclang --configfuzztest --fsan --optdebug --allocatorsystem my_command_fuzztest注意其中--allocatorsystemMongoDB 的模糊目标要求使用系统分配器而非 tcmallocmongo_src_rules.bzl 中对扩展库的注释也解释了 tcmalloc TLS 空间的问题模糊场景同样遵循这一约束。模糊模式Fuzzing mode模糊模式启用消毒器与覆盖率插桩测试将无限运行或直到发现崩溃。它需要fsan构建配置。可以查看 Evergreen 配置获取当前 bazel 参数或直接运行bazel run --compiler_typeclang --configfuzztest --fsan --optdebug --allocatorsystem my_command_fuzztest -- \ --fuzzMyCommandFuzzTest.MyCommandFuzzer--fuzzSuite.Test指定单个测试无限模糊若要对目标内所有测试模糊固定时长使用--fuzz_forbazel run --compiler_typeclang --configfuzztest --fsan --optdebug --allocatorsystem my_command_fuzztest -- --fuzz_for60sEvergreen 中的持续模糊使用mongo_cc_fuzztest在 Bazel 中定义的模糊测试会在 master 分支上由 Evergreen周期性运行。编译后的测试及其关联语料会保存到 S3可下载用于调试问题语料在每次 Evergreen 运行之间复用以不断累积并提升模糊覆盖率。常用运行参数Flag作用--fuzzSuite.Test无限模糊单个测试--fuzz_forT对所有测试模糊时长T--rss_limit_mbN内存超过 N MB 时中止--time_limit_per_inputT单个输入运行超过时长T即中止--reproduce_findings_as_separate_tests用崩溃输入重新运行测试--helpful描述 FuzzTest 的各 flag 含义调试崩溃当模糊器发现崩溃后用--reproduce_findings_as_separate_tests将崩溃输入作为独立测试用例回放便于在调试器下精确定位bazel run --compiler_typeclang --configfuzztest --fsan --optdebug --allocatorsystem my_command_fuzztest -- --reproduce_findings_as_separate_tests结合 MongoDB 的 Evergreen 机制可以从 S3 下载周期性模糊运行保存的语料与崩溃复现材料在本地以完全相同的输入驱动调试。深入阅读BSON Mutator 自定义 Domain 源码 ——BSONObjImpl完整声明与ArbitraryConstSharedBuffer特化mongo_cc_fuzztest 规则实现 —— Bazel 侧依赖注入与fsan兼容性控制NDV 哈希属性模糊测试示例 —— 结构化 BSON domain 与原始字节双路模糊、种子设计、差分属性断言SBE 值模糊测试示例 ——OneOf/ElementOf/VariantOf组合 domain 的实战fsan 构建配置 与 BUILD.bazel 中的 fsan 定义 —— 模糊消毒构建开关【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表