zstd 自定义 Sequence Producer 插件模糊测试实战:基于 seq_prod_fuzz_example 的完整指南
2026/9/18 8:13:36 网站建设 项目流程

zstd 自定义 Sequence Producer 插件模糊测试实战:基于 seq_prod_fuzz_example 的完整指南

【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo

本指南以 zstd 仓库中 tests/fuzz/seq_prod_fuzz_example 目录为核心,讲解如何为自定义外部 Sequence Producer 插件接入 zstd 官方模糊测试(fuzzing)框架,将插件编译进 libFuzzer 目标并通过 ASan/UBSan 进行健壮性验证。读完本文,你将掌握插件接口的五个核心回调、fuzz.py--custom-seq-prod构建流程、回退(fallback)机制的测试方法,以及如何把这套流程迁移到自己的序列生成器上。

背景:为什么需要模糊测试外部 Sequence Producer

zstd 的压缩核心依赖"序列生成器"(sequence producer)来发现输入中的重复模式并生成(offset, litLength, matchLength)序列。zstd 本身自带一套经过高度优化的内部实现,同时也通过ZSTD_registerSequenceProducer()暴露了外部序列生成器 API,允许第三方插件(如硬件加速设备、专用匹配算法)接管序列生成环节。这类插件往往运行在压缩热路径上,任何越界读写、窗口越界、序列校验失败都可能导致崩溃或生成非法压缩帧。

zstd 官方早在 PR #3437 就为外部 Sequence Producer API 引入了模糊测试基础设施,但那一版只能覆盖 zstd 核心库中"与插件交互的那部分代码",插件作者自己的实现并不在被测范围内。为此,zstd 在 fuzz_third_party_seq_prod.h 中定义了一套插件侧符号接口:插件作者只需提供实现这些符号的目标文件(.o),并让该目标文件替换默认实现,zstd 的 fuzzer 就会自动把插件代码纳入覆盖范围,直接对插件的序列生成逻辑做压力测试。而 seq_prod_fuzz_example 正是这一机制的最小可运行示例。

目录结构与示例内容

seq_prod_fuzz_example目录只包含三个文件,结构极其精简:

文件作用
README.md使用说明:四条命令完成"拉取语料 → 编译插件 → 构建 fuzzer → 运行"
example_seq_prod.c插件示例实现:实现全部五个回调符号
Makefile用 clang 编译插件对象文件的构建脚本

示例插件的定位是"最小占位实现":它并不真正做匹配,而是返回ZSTD_SEQUENCE_PRODUCER_ERROR强制走回退路径,专门用于验证"插件被调用、出错、zstd 回退到默认序列生成器"这条链路是否按预期工作。

快速开始:四条命令跑通整个流程

README 给出的完整流程如下(均在 tests/fuzz 目录下执行):

# 1. 下载/解压每个 fuzz target 的官方种子语料到 ./corpora/TARGET $ make corpora # 2. 用 clang 编译示例插件,产出 example_seq_prod.o $ make -C seq_prod_fuzz_example/ # 3. 构建全部 fuzzer 目标,并链接自定义插件对象文件 $ python3 ./fuzz.py build all --enable-fuzzer --enable-asan --enable-ubsan --cc clang --cxx clang++ --custom-seq-prod=seq_prod_fuzz_example/example_seq_prod.o # 4. 以 libFuzzer 引擎运行 simple_round_trip 目标开始模糊测试 $ python3 ./fuzz.py libfuzzer simple_round_trip

下面对每一步展开说明其底层机制。

第 1 步:make corpora 拉取种子语料

fuzz/Makefile 中的corpora目标会为 fuzz.py 中注册的 19 个 target(simple_round_tripstream_round_tripblock_round_tripsimple_decompress等)分别下载corpora/%_seed_corpus.zip并解压到./corpora/TARGET。这些种子样本是 libFuzzer 变异起点,直接影响覆盖率爬升速度,建议保留。

第 2 步:编译插件对象文件

seq_prod_fuzz_example/Makefile 的完整内容如下:

CC = clang CFLAGS = -g -fno-omit-frame-pointer -fsanitize=undefined,address,fuzzer -I../ -I../../../lib/ .PHONY: default default: example_seq_prod.o example_seq_prod.o: example_seq_prod.c $(CC) -c $(CFLAGS) $^ -o $@

关键点:

  • 必须使用 clangfuzz_third_party_seq_prod.h明确说明用 gcc 构建插件不受支持。原因在于-fsanitize=fuzzer-fsanitize-coverage是 clang 专属能力,且 libFuzzer 引擎本身依赖 clang 的插桩。
  • sanitizer 与 fuzz 插桩必须在编译插件时开启-fsanitize=undefined,address,fuzzer让插件的读写错误能被 ASan/UBSan 捕获,-fsanitize=fuzzer使插件代码也参与覆盖率反馈。
  • 包含路径-I../指向tests/fuzz(为找到fuzz_third_party_seq_prod.h),-I../../../lib/指向 zstd 库头文件(为找到zstd.h)。

第 3 步:构建 fuzzer 并链接插件

fuzz.py build命令中的--custom-seq-prod参数是整个机制的开关。查看 fuzz.py 源码 可以看到它做了两件事:

if args.third_party_seq_prod_obj: cppflags += ['-DFUZZ_THIRD_PARTY_SEQ_PROD'] mflags += ['THIRD_PARTY_SEQ_PROD_OBJ={}'.format(args.third_party_seq_prod_obj)]
  • 定义预处理宏FUZZ_THIRD_PARTY_SEQ_PROD,让 fuzzer 源码中"插件模式"分支生效;
  • 通过THIRD_PARTY_SEQ_PROD_OBJ把插件对象文件传入 make,最终被追加进每个 fuzz target 的链接输入(见 fuzz/Makefile 中的FUZZ_D_OBJ10/FUZZ_RT_OBJ10变量)。

因此插件中的FUZZ_thirdPartySeqProd等符号会在链接期覆盖默认实现,而默认的simpleSequenceProducer(位于 contrib/externalSequenceProducer/sequence_producer.c)则被取代。

其余构建参数含义:

参数作用
--enable-fuzzer启用 clang 内置 libFuzzer 引擎(-fsanitize=fuzzer),此时LIB_FUZZING_ENGINE被忽略
--enable-asan启用 AddressSanitizer(-fsanitize=address
--enable-ubsan启用 UndefinedBehaviorSanitizer(-fsanitize=undefined),并默认追加-fno-sanitize=pointer-overflow等抑制项
--cc clang --cxx clang++显式指定编译器;也可通过环境变量CC=clang CXX=clang++等价传入

注意fuzz.py会校验 sanitizer 组合:MSAN 不能与其他 sanitizer 混用(见build_parser中的--enable-msan检查逻辑)。

第 4 步:运行 libFuzzer

fuzz.py libfuzzer simple_round_trip会执行编译出的simple_round_trip二进制,并自动构造三个目录:

  • corpora/simple_round_trip:主语料目录(不存在则创建);
  • corpora/simple_round_trip-crash:崩溃产物目录,通过-artifact_prefix=传入 libFuzzer;
  • corpora/simple_round_trip-seed:种子目录,存在则作为额外输入。

命令行中未被fuzz.py解析的参数会原样透传给 libFuzzer,例如可以追加-jobs=10 -workers=10 -max_total_time=1000进行多核长时间跑测。此外 fuzz/README.md 还提供了跨 target 的批量运行方式,例如用for target in $(./fuzz.py list)循环逐个跑,或用xargs -P并行跑。

插件接口:五个必须实现的符号

插件需要实现的接口全部定义在 fuzz_third_party_seq_prod.h 中。该头文件要求先定义ZSTD_STATIC_LINKING_ONLY再包含zstd.h,以获得ZSTD_Sequence等静态 API 声明。

生命周期回调

符号调用时机与约定
size_t FUZZ_seqProdSetup(void)每个测试用例(test-case)开始前调用,用于执行初始化(如启动硬件设备)。fuzzer 会assert()返回值必须为 0,非零表示出错
size_t FUZZ_seqProdTearDown(void)每个测试用例结束后调用,释放Setup获取的资源,防止跨用例泄漏;同样要求返回 0
void* FUZZ_createSeqProdState(void)每个测试用例开始时、且在Setup之后调用,返回可传入ZSTD_registerSequenceProducer()的序列生成器状态对象。fuzzer 会assert()返回值非 NULL
size_t FUZZ_freeSeqProdState(void* sequenceProducerState)每个测试用例结束后释放状态对象,要求返回 0

一个值得注意的设计约束:同一个测试用例内的所有压缩操作共享同一个状态对象。头文件注释明确解释了原因——当前 fuzzer 不涉及多线程场景,因此共享状态是安全的;未来若要支持多线程 fuzzing,需要新的方案。

核心被测函数

size_t FUZZ_thirdPartySeqProd(void* sequenceProducerState, ZSTD_Sequence* outSeqs, size_t outSeqsCapacity, const void* src, size_t srcSize, const void* dict, size_t dictSize, int compressionLevel, size_t windowSize);

这个函数签名与ZSTD_registerSequenceProducer()的回调签名完全一致,它就是插件作者真正想被 fuzz 的对象。每次调用都会收到FUZZ_createSeqProdState()返回的状态指针。windowSize参数尤其重要——插件生成序列时偏移必须落在窗口范围内(见下文示例实现)。

内部宏

头文件还定义了两个内部辅助宏,fuzzer 源码在#ifdef FUZZ_THIRD_PARTY_SEQ_PROD分支下通过它们完成生命周期管理:

#define FUZZ_SEQ_PROD_SETUP() \ do { \ FUZZ_ASSERT(FUZZ_seqProdSetup() == 0); \ FUZZ_seqProdState = FUZZ_createSeqProdState(); \ FUZZ_ASSERT(FUZZ_seqProdState != NULL); \ } while (0) #define FUZZ_SEQ_PROD_TEARDOWN() \ do { \ FUZZ_ASSERT(FUZZ_freeSeqProdState(FUZZ_seqProdState) == 0); \ FUZZ_ASSERT(FUZZ_seqProdTearDown() == 0); \ } while (0)

当未定义FUZZ_THIRD_PARTY_SEQ_PROD时,这两个宏会被展开为空操作,fuzzer 行为与常规模式完全一致。

示例实现逐行解读

example_seq_prod.c 是一个刻意简化的"契约自检"插件:

_Thread_local size_t threadLocalState; size_t FUZZ_seqProdSetup(void) { threadLocalState = 0; return 0; } size_t FUZZ_seqProdTearDown(void) { return 0; } void* FUZZ_createSeqProdState(void) { return calloc(1, sizeof(size_t)); } size_t FUZZ_freeSeqProdState(void* state) { free(state); return 0; } size_t FUZZ_thirdPartySeqProd( void* sequenceProducerState, ZSTD_Sequence* outSeqs, size_t outSeqsCapacity, const void* src, size_t srcSize, const void* dict, size_t dictSize, int compressionLevel, size_t windowSize ) { /* Try to catch unsafe use of the shared state */ size_t* const sharedStatePtr = (size_t*)sequenceProducerState; assert(*sharedStatePtr == threadLocalState); (*sharedStatePtr)++; threadLocalState++; /* Check that fallback is enabled when FUZZ_THIRD_PARTY_SEQ_PROD is defined */ return ZSTD_SEQUENCE_PRODUCER_ERROR; }

它演示了两个测试要点:

  1. 共享状态一致性自检:状态对象每次calloc分配、free释放;每次回调都把共享状态与线程局部副本比对并同步递增。若 fuzzer 中存在并发或生命周期管理缺陷(例如状态被二次释放、跨用例复用),assert会立刻暴露问题。
  2. 强制回退路径ZSTD_SEQUENCE_PRODUCER_ERROR在 lib/zstd.h 中定义为((size_t)(-1)),返回它表示"序列生成失败,请 zstd 内部回退"。这样每次压缩都会走"调用外部插件 → 失败 → 回退默认实现"的完整链路,专门验证插件模式下回退机制始终可用。

真实插件不会永远返回错误——它会像 contrib/externalSequenceProducer/sequence_producer.c 中的simpleSequenceProducer那样用哈希表做匹配,并把offset <= windowSize作为硬约束(注释明确提示"必须保持在窗口大小内"),然后返回生成的序列数量。

fuzzer 如何接入插件:从源码看调用链

理解插件如何被调用,需要看 fuzzer 公共辅助代码 zstd_helpers.c:

void* FUZZ_seqProdState = NULL; // 全局状态,由 FUZZ_SEQ_PROD_SETUP() 填充 static void setSequenceProducerParams(ZSTD_CCtx *cctx, FUZZ_dataProducer_t *producer) { #ifdef FUZZ_THIRD_PARTY_SEQ_PROD ZSTD_registerSequenceProducer( cctx, FUZZ_seqProdState, FUZZ_thirdPartySeqProd ); #else ZSTD_registerSequenceProducer( cctx, NULL, simpleSequenceProducer ); #endif #ifdef FUZZ_THIRD_PARTY_SEQ_PROD FUZZ_ZASSERT(ZSTD_CCtx_setParameter(cctx, ZSTD_c_enableSeqProducerFallback, 1)); #else setRand(cctx, ZSTD_c_enableSeqProducerFallback, 0, 1, producer); #endif ... }

两条关键事实:

  • 定义FUZZ_THIRD_PARTY_SEQ_PROD(即传入了--custom-seq-prod)时,每个压缩上下文都会注册你的FUZZ_thirdPartySeqProd,且强制把ZSTD_c_enableSeqProducerFallback设为 1——这意味着 fuzzer 会系统性测试"外部插件失败 → 内部回退"的路径;
  • 未定义该宏时,fuzzer 默认注册内置的simpleSequenceProducer,仅在约 1/11 的概率下启用回退。两种模式在同一套 target 源码下共存,插件模式不会影响常规 fuzz 目标的行为。

同时,fuzz/Makefile 默认把contrib/externalSequenceProducer/sequence_producer.c编译进所有 target($(DEFAULT_SEQ_PROD_SRC)),因此未指定插件时 fuzzer 依然有合法的序列生成器可用。每个测试用例的开始/结束由FUZZ_SEQ_PROD_SETUP()/FUZZ_SEQ_PROD_TEARDOWN()宏在 fuzzer 入口处驱动,保证状态对象与初始化动作与用例一一对应。

迁移到自己的插件

要把示例流程套用到自己的序列生成器上,只需四步:

  1. 在插件源码中包含tests/fuzz/fuzz_third_party_seq_prod.h,实现上面五个符号;核心逻辑放在FUZZ_thirdPartySeqProd中。
  2. 参照seq_prod_fuzz_example/Makefile,用 clang 以-g -fno-omit-frame-pointer -fsanitize=undefined,address,fuzzer(并根据需要调整 sanitizer 组合)编译出.o文件。
  3. 构建时把--custom-seq-prod指向你的.o文件,其余构建参数保持一致(--enable-fuzzer --enable-asan --enable-ubsan --cc clang --cxx clang++)。
  4. 运行任意 round-trip 类 target(如simple_round_tripstream_round_tripdictionary_round_trip),这类 target 会校验"压缩后再解压得到原始输入",插件生成非法序列时必然暴露。

建议先用./fuzz.py libfuzzer simple_round_trip -runs=10000做冒烟验证,再按需加-jobs/-max_total_time做大规模跑测;崩溃样本会落在corpora/<target>-crash,可用-artifact_prefix指定路径并通过哈希复现。

常见问题与注意事项

  • 为什么必须用 clang?插件编译与 fuzzer 构建都需要 clang 的-fsanitize=fuzzer/ 覆盖率插桩,fuzz_third_party_seq_prod.h明确说明不支持 gcc。
  • FUZZ_THIRD_PARTY_SEQ_PROD是谁定义的?它由fuzz.py在检测到--custom-seq-prod时自动注入CPPFLAGS,无需手动定义;只要构建命令带了该参数,插件模式即生效。
  • 插件返回错误是否算 bug?不算。ZSTD_SEQUENCE_PRODUCER_ERROR是合法的失败信号,zstd 会启用回退;被 fuzz 的正是"错误处理 + 回退"的鲁棒性。
  • 状态对象为什么可以跨用例内多次压缩共享?这是当前 fuzzer 架构的既定假设(单线程),头文件注明未来多线程场景需要新方案;你自己的插件不应假设每次回调拿到独立状态。

延伸阅读

  • tests/fuzz/README.md:zstd fuzzing 总览,涵盖语料生成(make -C ../tests decodecorpus+./fuzz.py gen TARGET)、AFL、MSAN 与回归测试用法。
  • tests/fuzz/fuzz_third_party_seq_prod.h:插件接口的权威定义(即本指南引用的接口来源)。
  • tests/fuzz/fuzz.py:构建/运行脚本,buildlibfuzzeraflregressiongenminimizeziplist八个子命令的完整实现。
  • tests/fuzz/zstd_helpers.c:fuzzer 与插件对接的核心注册逻辑。
  • contrib/externalSequenceProducer/sequence_producer.c:一个真实的simpleSequenceProducer参考实现(哈希匹配 + 窗口约束),可作为编写自己插件的起点。
  • lib/zstd.h:ZSTD_registerSequenceProducer()ZSTD_SequenceZSTD_SEQUENCE_PRODUCER_ERROR的 API 定义。

【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo

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

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

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

立即咨询