MNN 依赖库 FlatBuffers 的 libFuzzer 模糊测试套件:从 verifier 到 parser 的稳定性保障
2026/9/14 9:59:43 网站建设 项目流程

MNN 依赖库 FlatBuffers 的 libFuzzer 模糊测试套件:从 verifier 到 parser 的稳定性保障

【免费下载链接】MNNMNN: A blazing-fast, lightweight inference engine battle-tested by Alibaba, powering high-performance on-device LLMs and Edge AI.项目地址: https://gitcode.com/GitHub_Trending/mn/MNN

本指南完整讲解 MNN 仓库内第三方依赖 FlatBuffers 自带的 fuzzer 测试套件:它如何基于 LLVM 的 libFuzzer 引擎,通过verifier_fuzzerparser_fuzzerscalar_fuzzer三个目标分别对反序列化校验、schema/JSON 解析和数值标量解析进行覆盖引导的模糊测试,并验证解析器在任意 C-locale 下的行为一致性。读完本文,你将掌握该套件的构建环境、每个 fuzzer 目标的源码级工作原理、完整的运行命令与 libFuzzer 参数含义,以及如何通过-merge合并语料库和规避已知的 LLVM std::regex 栈溢出限制。

FlatBuffers 测试体系中的 fuzzer 定位

FlatBuffers 是 MNN 序列化 schema(见 schema/current 下的 *_generated.h)所依赖的底层库。其官方测试套件位于 3rd_party/flatbuffers/tests,除常规的单元测试(test.cpp)外,专门在tests/fuzzer目录下维护了一套基于libFuzzer的模糊测试。

libFuzzer 是一个进程内、覆盖引导、进化式的模糊测试引擎:它直接与被测库链接,通过特定的入口函数(即"目标函数")把模糊输入喂给被测代码;随后追踪代码中哪些区域被执行,并对输入语料进行变异,以最大化代码覆盖率。覆盖率信息由 LLVM 的SanitizerCoverage插桩提供。

这套测试的动机很明确:FlatBuffers 的解析与校验代码直接面对不可信的二进制 buffer 与文本输入(schema、JSON),一旦存在越界读、栈溢出、未定义行为或 locale 相关的隐藏状态,就可能在真实业务中触发崩溃或数据损坏。fuzzer 正是用自动化变异的方式在编译期插桩(ASan/MSan/UBSan)的保护下主动寻找这类缺陷。

关于 libFuzzer 的完整文档可参考 LLVM 官方手册(https://llvm.org/docs/LibFuzzer.html),本文聚焦于本仓库中这套测试的实际实现与用法。

构建前提:LLVM 编译器 + CMake

要构建并运行这些 fuzzer,前置条件是安装LLVM 编译器(含 clang 前端)CMake,因为-fsanitize=fuzzer是 clang 提供的编译与链接支持。

构建脚本 CMakeLists.txt 展示了完整的构建配置思路,其中关键点包括:

  • 编译标准与告警-std=c++14 -Wall -pedantic -Werror -Wextra -Wno-unused-parameter -fsigned-char,把告警升级为错误;
  • 调试信息-g -fno-omit-frame-pointer,保证崩溃时能回溯调用栈;
  • Sanitizer 组合:默认启用-fsanitize=fuzzer,address,undefined(ASan + UBSan + libFuzzer),脚本中保留了切换 MemorySanitizer 的开关(-fsanitize=fuzzer,memory,undefined -fsanitize-memory-track-origins=2,注意 MSan 与 ASan 不能共存,且 MSan 通常带来约 3 倍性能开销);
  • 覆盖率插桩-fsanitize-coverage=edge,trace-cmp
  • 链接器-fuse-ld=lld
  • 解析深度强制收紧FLATBUFFERS_MAX_PARSING_DEPTH=8,刻意用更浅的递归深度上限来暴露潜在的递归栈问题;
  • 断言宏重定向:通过target_compile_definitions(flatbuffers PUBLIC FLATBUFFERS_ASSERT=fuzzer_assert_impl)把库内所有FLATBUFFERS_ASSERT重定向到 fuzzer_assert.h 中的fuzzer_assert_impl——该宏在断言失败时直接调用__builtin_trap()终止程序,从而保证Debug 与 Release 下断言都会生效,任何内部不变量被破坏都会立即被 fuzzer 捕获。

最终构建出三个可执行文件:scalar_fuzzerparser_fuzzerverifier_fuzzer,分别对应三个 fuzzer 源文件。

三个 fuzzer 目标:源码级原理

套件包含三个测试目标,各有分工:

  • verifier_fuzzer:校验Monster schema的反序列化(Verifier)引擎在任意二进制输入下的稳定性;
  • parser_fuzzer:校验schema 与 JSON 解析器在各类输入下的稳定性;
  • scalar_fuzzer:聚焦校验解析器在解析schema/JSON 中的数值标量时的行为正确性。

verifier_fuzzer:反序列化校验器

入口实现见 flatbuffers_verifier_fuzzer.cc,整个目标函数非常精简:

extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { flatbuffers::Verifier verifier(data, size); MyGame::Example::VerifyMonsterBuffer(verifier); return 0; }

它把 fuzzer 产出的任意字节流直接交给flatbuffers::Verifier,并调用由 monster_test.fbs 生成的VerifyMonsterBuffer(生成的校验代码位于 monster_test_generated.h)做完整结构校验。Monster schema 覆盖了嵌套 table、struct、union、vector、string、enum、key 等多种结构,能充分锻炼校验器的边界处理。该目标的目的是证明:任何内存中的二进制数据,无论多么畸形,Verifier 都只应返回失败而绝不产生崩溃或越界访问

parser_fuzzer:schema 与 JSON 解析器

入口实现见 flatbuffers_parser_fuzzer.cc。其输入布局为:第 1 字节作为 Parser 选项标志位,第 2 字节作为重复执行次数,其余为实际解析输入:

static constexpr uint8_t flags_strict_json = 0x01; static constexpr uint8_t flags_skip_unexpected_fields_in_json = 0x02; static constexpr uint8_t flags_allow_non_utf8 = 0x04;

这三个标志位分别映射到flatbuffers::IDLOptionsstrict_jsonskip_unexpected_fields_in_jsonallow_non_utf8,让 fuzzer 能在不同解析模式下探索同一段输入(源码中另保留了 0x08–0x80 的扩展位,供后续增加选项)。extra_rep_number取自输入第 2 字节并归一化到 0–9,决定同一输入重复解析多少次。

该目标最重要的设计是locale 无关性验证:每次解析至少执行两次,偶数轮(0、2、4…)会先通过setlocale(LC_ALL, ...)切换到测试 locale,解析完再切回"C"。这是为了确保解析器不存在依赖运行环境 locale 的隐藏状态——同一个输入在任意 locale 下都应得到一致结果。locale 值通过OneTimeTestInit(见 test_init.h)从环境变量FLATBUFFERS_TEST_LOCALE读取,并在程序启动时打印。

scalar_fuzzer:数值标量专项

入口实现见 flatbuffers_scalar_fuzzer.cc,它是三者中最复杂的,用**正则表达式作为"参照实现"**与真实解析器做对拍。其逻辑分四步:

  1. 输入预处理:用BreakSequence把输入中的///*替换为@/@*,避免 JSON 注释干扰正则匹配;再把degradsincostanasinacosatan等标量函数名替换为_,保持输入为纯数值文本。

  2. 类型分派:输入第 1 字节的低 4 位(flags_scalar_type = 0x0F)编码标量类型,第 5 位(flags_quotes_kind = 0x10)决定用双引号还是单引号包裹。ScalarReferenceResult::Check将编码映射到 11 种类型:doublefloatint8int16int32int64uint8uint16uint32uint64bool,并由对应正则类判定输入是否合法:

    • IntegerRegex:十进制[-+]?[0-9]+或十六进制[-+]?0[xX][0-9a-fA-F]+
    • UIntegerRegex:额外接受-0^-?0+$);
    • BooleanRegex:接受true/false或整数字面量;
    • FloatRegex:接受十六进制浮点(0x...p...)、十进制浮点(含.e/E指数)以及大小写不敏感的naninfinfinity
  3. 动态 schema 解析对拍:程序用"table X { Y: <type>; } root_type X;"现场构造 schema,然后分三个阶段验证:

    • Stage 1:把原始输入包进{ "Y" : <input> }解析,断言"正则判定"与"真实解析器判定"一致;若不一致但属于"数值越界"(错误信息含does not fitout of range)则放行——这是唯一允许的偏差;
    • Stage 2:把输入包上引号('...'"...")再解析,断言带引号与不带引号的解析结果一致;
    • Stage 3:把输入作为 schema 默认值(table X { Y: <type> = <input>; })配合空 JSON{}解析,再用GenerateText生成文本,断言与常规解析结果完全一致。
  4. 重复执行:与parser_fuzzer相同,通过extra_rep_number控制重复次数,偶数轮切换 locale 验证 locale 无关性。

所有对拍断言都通过重定向后的FLATBUFFERS_ASSERT__builtin_trap)终止,配合测试引擎的失败监听器(见 test_assert.h 的TestFailEventListener机制),任何不一致都会立即触发 fuzzer 报告崩溃。

运行 fuzzer:命令与参数详解

默认运行方式(在tests/fuzzer目录下,把语料目录作为命令行参数传入):

./verifier_fuzzer -reduce_depth=1 -use_value_profile=1 -shrink=1 ../.corpus_verifier/ ./parser_fuzzer -reduce_depth=1 -use_value_profile=1 -shrink=1 ../.corpus_parser/ ./scalar_fuzzer -reduce_depth=1 -use_value_profile=1 -shrink=1 -max_len=3000 ../.corpus_parser/ ../.seed_parser/

各参数含义(具体可用./parser_fuzzer -help=1查看当前 libFuzzer 版本的完整说明,不同 libFuzzer 版本参数可能略有差异):

  • -reduce_depth=1:修剪过深的执行栈,抑制无意义的深层探索;
  • -use_value_profile=1:开启基于值(比较指令)的覆盖率反馈,能显著增强对数值解析这类"比较密集型"代码的变异引导;
  • -shrink=1:崩溃复现时尽量收缩输入长度;
  • -max_len=3000:限制单条输入最大长度(见下文已知限制);
  • -timeout=10:单条输入超时阈值(秒),防止正则匹配极端输入时卡死;
  • -rss_limit_mb=2048:单进程内存上限;
  • -jobs=2:并行任务数;
  • -only_ascii=1:仅生成 ASCII 输入,用于快速做数值兼容性检查(对scalar_fuzzer尤其高效,因为其输入本质是数字文本)。

带 locale 的典型运行(Windows 命令行风格同样适用):

FLATBUFFERS_TEST_LOCALE="" ./scalar_parser FLATBUFFERS_TEST_LOCALE="ru_RU.CP1251" ./parser_fuzzer FLATBUFFERS_TEST_LOCALE="ru_RU.CP1251" ./scalar_fuzzer -reduce_depth=1 -use_value_profile=1 -shrink=1 -max_len=3000 -timeout=10 -rss_limit_mb=2048 ../.corpus_parser/ ../.seed_parser/

设置FLATBUFFERS_TEST_LOCALE后,程序会在启动时打印The environment variable FLATBUFFERS_TEST_LOCALE=...(见 test_init.h),之后每个 fuzzer 目标在偶数轮解析中切换到该 locale,从而覆盖ru_RU.CP1251这类非 C locale 下小数点、数字分组、字符分类等行为差异。

设计要点:FlatBuffers 的语法基于可打印 ASCII 字符,库本身在设计上应与最终用户应用的全局或线程 locale 无关。fuzzer 的目标就是用FLATBUFFERS_TEST_LOCALE主动"进攻"这一性质——若解析结果随 locale 改变,即视为缺陷。

合并与最小化语料库

libFuzzer 的-merge标志用于过滤(最小化)语料库:当-merge=1时,第 2 个及之后语料目录中凡是触发了新代码覆盖的输入,都会被合并进第 1 个语料目录(默认值为 0)。这既能沉淀有价值的输入,也能剔除冗余样本:

./scalar_fuzzer -merge=1 ../.seed_parser/ ../.corpus_parser/

例如把新收集的语料.corpus_parser/合并进种子集合.seed_parser/,只保留能带来新覆盖的输入,让语料库持续保持精简高效。

已知限制与规避

  • LLVM 7.0 的std::regex存在栈溢出问题scalar_fuzzer内部大量使用std::regex做数值匹配,过长的输入可能导致正则匹配时栈溢出,因此单条输入长度必须限制在 3000 以内,即运行scalar_fuzzer时必须携带-max_len=3000(源码 flatbuffers_scalar_fuzzer.cc 也以注释形式固化了这一约定)。
  • Sanitizer 组合互斥:ASan 与 MSan 不能同时启用,选择 MemorySanitizer 模式需接受约 3 倍的性能开销(见 CMake 脚本中的YES分支注释)。
  • 参数随版本变化:libFuzzer 的各 flag 在不同 LLVM 版本间可能有差异,遇到不识别参数时以-help=1输出的当前版本说明为准。

小结

这套 fuzzer 套件从三个层面为 FlatBuffers 提供保障:verifier_fuzzer保证畸形二进制不会击穿反序列化校验,parser_fuzzer保证 schema/JSON 解析在多种选项组合与任意 locale 下稳定且无隐藏状态,scalar_fuzzer则以正则对拍 + 三阶段验证的方式把数值解析的语义正确性做到了可证明的程度。对于在 MNN 中直接使用 FlatBuffers 生成的 schema 代码(schema/current)的开发者而言,理解这套测试的设计,也能更清楚地知道解析层可能存在的边界条件,并在集成或升级 FlatBuffers 版本时,第一时间用同样的方式回归验证解析与校验的稳定性。

【免费下载链接】MNNMNN: A blazing-fast, lightweight inference engine battle-tested by Alibaba, powering high-performance on-device LLMs and Edge AI.项目地址: https://gitcode.com/GitHub_Trending/mn/MNN

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询