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_fuzzer、parser_fuzzer、scalar_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_fuzzer、parser_fuzzer、verifier_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::IDLOptions的strict_json、skip_unexpected_fields_in_json、allow_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,它是三者中最复杂的,用**正则表达式作为"参照实现"**与真实解析器做对拍。其逻辑分四步:
输入预处理:用
BreakSequence把输入中的//、/*替换为@/、@*,避免 JSON 注释干扰正则匹配;再把deg、rad、sin、cos、tan、asin、acos、atan等标量函数名替换为_,保持输入为纯数值文本。类型分派:输入第 1 字节的低 4 位(
flags_scalar_type = 0x0F)编码标量类型,第 5 位(flags_quotes_kind = 0x10)决定用双引号还是单引号包裹。ScalarReferenceResult::Check将编码映射到 11 种类型:double、float、int8、int16、int32、int64、uint8、uint16、uint32、uint64、bool,并由对应正则类判定输入是否合法:IntegerRegex:十进制[-+]?[0-9]+或十六进制[-+]?0[xX][0-9a-fA-F]+;UIntegerRegex:额外接受-0(^-?0+$);BooleanRegex:接受true/false或整数字面量;FloatRegex:接受十六进制浮点(0x...p...)、十进制浮点(含.与e/E指数)以及大小写不敏感的nan、inf、infinity。
动态 schema 解析对拍:程序用
"table X { Y: <type>; } root_type X;"现场构造 schema,然后分三个阶段验证:- Stage 1:把原始输入包进
{ "Y" : <input> }解析,断言"正则判定"与"真实解析器判定"一致;若不一致但属于"数值越界"(错误信息含does not fit或out of range)则放行——这是唯一允许的偏差; - Stage 2:把输入包上引号(
'...'或"...")再解析,断言带引号与不带引号的解析结果一致; - Stage 3:把输入作为 schema 默认值(
table X { Y: <type> = <input>; })配合空 JSON{}解析,再用GenerateText生成文本,断言与常规解析结果完全一致。
- Stage 1:把原始输入包进
重复执行:与
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),仅供参考