Skia 模糊测试(Fuzzing)完全指南:从 fuzz 复现到 OSS-Fuzz 持续集成
Skia 模糊测试(Fuzzing)完全指南:从 fuzz 复现到 OSS-Fuzz 持续集成
导读
本文以 fuzz/README.md 为骨架,系统讲解 Skia 图形库的模糊测试体系:什么是 API fuzzer 与 binary fuzzer、如何用 fuzz 可执行文件复现 ClusterFuzz/OSS-Fuzz 报告的崩溃、如何基于 libFuzzer 编写并运行新的 fuzzer,以及如何将 fuzzer 接入 OSS-Fuzz 持续运行。读完本文,你将掌握从"拿到一个崩溃测试用例"到"定位根因",再到"新增一个 fuzzer 并上线到自动化平台"的完整工作流。
一、认识 Skia 的 Fuzzers:两类输入模型
在 fuzz/ 目录中,Skia 维护了一批 fuzzers——即"接收随机输入、集中针对特定 API 执行代码"的小型程序。根据输入数据的语义,它们被划分为两类:
- Binary fuzzer(二进制 fuzzer):输入数据本身就是某种格式的文件内容,fuzzer 直接消费这份数据。典型代表是 codec fuzzer——将一个被变异过的 PNG/JPEG 等文件尝试解码为
SkImage;以及 SkSL 解析类 fuzzer(把随机字节当作着色器源码字符串)。这类 fuzzer 对随机数据的格式高度敏感。 - API fuzzer(接口 fuzzer):输入是一段随机字节流,fuzzer 将其"翻译"成对某个 API 的连续调用序列。典型代表是 canvas fuzzer——把随机字节解析成一系列
SkCanvas绘制调用。
这两种分类在 fuzz/FuzzMain.cpp 中清晰可见:fuzz_api 通过 sk_tools::Registry<Fuzzable> 按名称查找 API fuzzer(见 fuzz/Fuzz.h 中 DEF_FUZZ 宏的注册机制),而 binary fuzzer 则直接读取 SkData 调用对应的 Fuzz* 入口函数。
二、两种可执行文件打包方式
根据根目录的 BUILD.gn,这些 fuzzer 被打包成两种形态:
| 形态 | 用途 | 构建方式 |
|---|---|---|
fuzz 可执行文件 |
包含所有 fuzzer,是复现 fuzzer 上报 bug 的便捷工具 | GN 目标 fuzz |
| 单 fuzzer 可执行文件 | 恰好包含一个 fuzzer,适合用 libFuzzer 驱动持续变异输入 | GN 目标 libfuzzer_app |
fuzz 聚合二进制的入口在 fuzz/FuzzMain.cpp:它解析 -b/-t/-n/--loops 等命令行参数,判断输入是文件还是目录,然后对每个输入执行指定(或自动推断出的)fuzzer 类型。
三、使用 fuzz 复现崩溃:从 ClusterFuzz 到本地重现
更完整的构建与运行说明见 site/docs/dev/testing/fuzz.md。
3.1 前置条件:构建 Skia
假设你已经能够构建 Skia。许多 fuzz 只有在 ASAN 或 MSAN 构建下才能复现(详见 site/docs/dev/testing/xsan.md)。为保证复现结果不依赖具体机器与平台,构建时建议在 BUILD.gn 中追加以下参数:
skia_use_fontconfig=false
skia_use_freetype=true
skia_use_system_freetype2=false
skia_use_wuffs=true
skia_enable_skottie=true
skia_enable_fontmgr_custom_directory=false
skia_enable_fontmgr_custom_embedded=false
skia_enable_fontmgr_custom_empty=true
3.2 最简复现命令
从 ClusterFuzz 或 oss-fuzz 下载一个 fuzz 测试用例后,只需:
out/ASAN/fuzz -b /path/to/downloaded/testcase
fuzz 会根据测试用例的文件名尽力猜测对应的 fuzzer 类型和名称(自动推断逻辑见 fuzz/FuzzMain.cpp 的 try_auto_detect,它用正则匹配 clusterfuzz-testcase(-minimized)?-<type>-<id> 与 api-<name>-<hex> 等命名模式)。你也可以手动指定类型和名称:
out/ASAN/fuzz -t filter_fuzz -b /path/to/downloaded/testcase
out/ASAN/fuzz -t api -n RasterN32Canvas -b /path/to/downloaded/testcase
3.3 枚举所有类型与名称
out/ASAN/fuzz --help # 列出所有 type
out/ASAN/fuzz -t api # 列出所有 api name
--help 中列举的完整 type 清单(对应 fuzz/FuzzMain.cpp 的 g_type_message)包括:android_codec、animated_image_decode、api、color_deserialize、colrv1、filter_fuzz(等价于 Chrome 的 filter_fuzz_stub)、hdr_agtm、image_decode、image_decode_incremental、image_mode、image_scale、json、path_deserialize、region_deserialize、region_set_path、skdescriptor_deserialize、skmeshspecification、skottie_json(需 SK_ENABLE_SKOTTIE)、skp、skruntimeblender、skruntimecolorfilter、skruntimeeffect、sksl2glsl、sksl2metal、sksl2pipeline、sksl2spirv、sksl2wgsl、svg_dom(需 SK_ENABLE_SVG)、textblob。
3.4 复现失败时的兜底:--loops
如果崩溃没有立即出现(例如竞态或需要多次迭代才触发的路径),可以加大运行次数:
out/ASAN/fuzz -b /path/to/downloaded/testcase --loops <times-to-run>
在 fuzz/FuzzMain.cpp 中,loopCount 被强制 max(FLAGS_loops, 1),且无论输入是单个文件还是目录,每个输入都会重复执行 loopCount 次。
3.5 其他实用命令行参数
fuzz 还支持(见 fuzz/FuzzMain.cpp):
-d, --dump <name>:非空时,将image*或skp类型的结果以 PNG 形式导出(dump_png通过SkPngEncoder/SkPngRustEncoder实现,取决于构建时的SK_CODEC_DECODES_PNG_WITH_RUST宏)。-v, --verbose:打印更多 fuzzing 过程信息。-b, --bytes:指向文件或目录;目录时逐个文件依次作为输入喂给 fuzzer。
四、用 libFuzzer 编写新的 fuzzer
libFuzzer 是编写新 fuzzer 最便捷的方式,也是 OSS-Fuzz 驱动 Skia fuzzer 的运行方式。你的 fuzzer 入口必须实现如下 API:
extern "C" int LLVMFuzzerTestOneInput(const uint8_t*, size_t);
4.1 环境准备
安装带 libFuzzer 的 Clang(示例为 clang-19):
sudo apt install clang-19
在 BUILD.gn 中配置:
cc="clang-19"
cxx="clang++-19"
skia_build_fuzzers=true
其中 skia_build_fuzzers 开关在 gn/skia.gni 定义,默认值为 false。随后在 BUILD.gn 中使用 libfuzzer_app 目标添加新 fuzzer 条目。
4.2 构建并运行
ninja -C out/fuzz my_new_fuzzer # 构建单个目标
out/fuzz/my_new_fuzzer # 直接运行,libFuzzer 开始变异输入
4.3 与 fuzz/FuzzMain.cpp 对接
新增 fuzzer 后,还需在 fuzz/FuzzMain.cpp 中登记,才能被聚合版 fuzz 二进制调用,然后再同步更新 OSS-Fuzz 配置使其自动运行(见第五节)。
API fuzzer 的接入方式:在 fuzz/Fuzz.h 中 DEF_FUZZ(name, f) 宏通过 sk_tools::Registry<Fuzzable> 注册一个可发现条目;fuzz_api(fuzz/FuzzMain.cpp)遍历该注册表,按 -n 指定的名字调用 Fuzzable::fn(&fuzz)。一个典型的 libFuzzer 包装(以 ImageFilter 为例,见 fuzz/oss_fuzz/FuzzAPIImageFilter.cpp):
#include "fuzz/Fuzz.h"
void fuzz_ImageFilter(Fuzz* f);
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
if (size > 4000) { // 限制输入长度,防止超大输入拖慢 fuzzing
return 0;
}
auto fuzz = Fuzz(data, size);
fuzz_ImageFilter(&fuzz);
return 0;
}
binary fuzzer 的接入方式:直接实现 FuzzXxx(const uint8_t* data, size_t size) 并同样在 SK_BUILD_FOR_LIBFUZZER 下暴露 LLVMFuzzerTestOneInput。例如图片解码 fuzzer fuzz/oss_fuzz/FuzzImage.cpp 将输入字节交给 SkImages::DeferredFromEncodedData 解码后绘制到 128×128 光栅 surface;SkSL→GLSL 编译 fuzzer fuzz/oss_fuzz/FuzzSKSL2GLSL.cpp 则把输入当作 fragment shader 源码交给 SkSL::Compiler 编译并转译。
五、Fuzzing 专用编译宏(Defines)
以下宏能引导 fuzzer 更高效地运行(例如避免 OOM、避免落入不必要地慢的代码路径):
// 使用 afl-fuzz 时必须定义,防止 OOM 给结果引入噪声
SK_BUILD_FOR_AFL_FUZZ
// 使用 libFuzzer 时必须定义
SK_BUILD_FOR_LIBFUZZER
// 添加守卫,在判定某条代码路径将耗时过长或占用大量内存时中止执行;
// 当上面两个宏任一被定义时,该宏默认同时生效
SK_BUILD_FOR_FUZZER
从源码可见,SK_BUILD_FOR_LIBFUZZER 还承担着条件编译职责:各单 fuzzer 目标只有在它被定义时才会编译出 LLVMFuzzerTestOneInput 入口(见 fuzz/oss_fuzz/FuzzImage.cpp 与 fuzz/oss_fuzz/FuzzSKSL2GLSL.cpp 的 #if defined(SK_BUILD_FOR_LIBFUZZER) 代码块),确保同一份源码既能用于聚合的 fuzz 二进制,又能用于 libFuzzer 单目标。
六、Fuzz 数据源与 Fuzz 类:API fuzzer 的输入语义
API fuzzer 通过 fuzz/Fuzz.h 中的 Fuzz 类消费随机字节流,它本质上是一个"字节游标":
next(T* t):按sizeof(T)取下一段字节填充变量。类注释特别强调用指针传参而非返回值,是为了避免不同编译器(GCC 与 Clang)对函数参数求值顺序不同导致的平台相关行为。nextRange(T*, Min, Max):将值钳制在[min, max]区间内(内部对枚举做sk_strip_enum剥离后比较,规避 UBSAN 对越界枚举的告警)。nextEnum(T*, T max):枚举安全取值,同样规避 UBSAN。nextN(T* ptr, int n):连续填充 n 个元素。nextBool()、next(SkRegion*):针对布尔与SkRegion的特化版本。signalBug():调用raise(SIGSEGV)显式向 fuzzer 报告"这些输入触发了 bug"。size()/exhausted()/remainingSize()/remainingData()/deplete():游标状态管理。
一个典型的 API fuzzer 定义(fuzz/FuzzCanvas.cpp):
DEF_FUZZ(RasterN32Canvas, fuzz) {
auto surface = SkSurfaces::Raster(
SkImageInfo::MakeN32Premul(kCanvasSize.width(), kCanvasSize.height()));
if (!surface || !surface->getCanvas()) { fuzz->signalBug(); }
FuzzCanvas(fuzz, surface->getCanvas());
}
FuzzCanvas 会把 Fuzz 游标吐出的字节流解析为对 SkCanvas 的绘制调用序列,从而覆盖画布 API 的各类输入组合。
另外值得注意 fuzz/FuzzMain.cpp 中的 calculate_option:它把输入前 1024 个字节求和得到一个 8 位整数,客户端用取模将其映射为不同选项(如缩放倍数、解码模式),从而让 image_scale/image_mode 等单个 fuzzer 就能覆盖多个配置维度,无需为每个选项各建一个 fuzzer。
七、接入 OSS-Fuzz:持续模糊测试与自动提 bug
Skia 的 fuzzer 运行在 OSS-Fuzz 之上,由它驱动 libfuzzer、afl-fuzz、hong-fuzz 等引擎持续执行,并在发现问题时自动向 OSS-Fuzz issue 跟踪器提交并关闭 bug。Skia 相关的构建配置存放在 OSS-Fuzz 仓库的 projects/skia 目录中,增减/修改自动运行的 fuzzer 时都改这里。
7.1 启用新 fuzzer 的典型步骤
带
*的步骤仅针对强依赖随机数据格式的 fuzzer(即 binary fuzzer,如图像解码、SkSL 解析)。
- *添加种子语料(seed corpus)到
gs://skia-cdn/oss-fuzz/(位于 Skia Buildbots 项目下);上传该 bucket 需要 "breakglass" 权限。 - *更新 OSS-Fuzz 的 Dockerfile,将种子语料下载到构建镜像中。
- 更新 build.sh:构建目标 fuzzer 并把产物移入
$OUT;若有种子语料,移入$OUT并命名为"fuzzer 可执行文件名 +_seed_corpus.zip"后缀。
可参考的添加 fuzzer 的示例 PR:binary、API。
7.2 本地测试 OSS-Fuzz 构建
OSS-Fuzz 新项目指南描述了如何用 Docker 在本地验证 OSS-Fuzz 构建与 fuzzer 运行。另有针对 skcms 仓库的 OSS-Fuzz 配置(projects/skcms),其构建流程类似,区别在于 build.sh 直接编译 fuzz 可执行文件而非使用 GN 目标。
7.3 OSS-Fuzz 仪表盘与覆盖率
- oss-fuzz.com/fuzzer-stats 可查看各 fuzzer 的指标:每秒执行次数(越高越好)、每个 fuzzer 的边缘覆盖率百分比、fuzzing 运行中 OOM/超时/崩溃的占比、完整输入语料(corpus_backup)等。查看仪表盘需要相应权限。
- OSS-Fuzz 还提供全部 Skia fuzzer 的合并覆盖率报告及示例报告。
正常情况下,被 fuzzing 的 Skia 版本约每天更新 2 次;如遇 OSS-Fuzz 问题,可向对应 component 提交 issue(googler 专有)。
八、当前仓库中的 fuzzer 全貌
fuzz/oss_fuzz/ 目录收录了约 50 个与 OSS-Fuzz 对应的单 fuzzer 目标,覆盖多个子系统:
- 编解码:
FuzzAndroidCodec、FuzzAnimatedImage、FuzzImage、FuzzIncrementalImage、FuzzBMPRustDecoder、FuzzJPEGRustDecoder、FuzzPNGRustEncoder、FuzzWEBPEncoder等; - SkSL 编译器:
FuzzSKSL2GLSL、FuzzSKSL2Metal、FuzzSKSL2Pipeline、FuzzSKSL2SPIRV、FuzzSKSL2WGSL、FuzzSkRuntimeEffect、FuzzSkRuntimeBlender、FuzzSkRuntimeColorFilter; - 序列化/反序列化:
FuzzPathDeserialize、FuzzImageFilterDeserialize、FuzzRegionDeserialize、FuzzSkDescriptorDeserialize、FuzzTextBlobDeserialize; - API 级绘制:
FuzzNullCanvas、FuzzRasterN32Canvas、FuzzAPISVGCanvas、FuzzMockGPUCanvas、FuzzDrawFunctions、FuzzAPICreateDDL、FuzzDDLThreading、FuzzPrecompile; - 路径/几何运算:
FuzzParsePath、FuzzPathop、FuzzPathMeasure、FuzzPolyUtils、FuzzQuadRoots、FuzzCubicRoots、FuzzTriangulation; - 其他:
FuzzCOLRv1、FuzzColorspace、FuzzJSON、FuzzSkParagraph、FuzzSVG、FuzzHdrAgtm、FuzzSkMeshSpecification、FuzzRegionOp、FuzzRegionSetPath等。
fuzz/FuzzMain.cpp 中的 cf_api_map 与 cf_map 两张映射表,将 ClusterFuzz/OSS-Fuzz 的测试用例命名映射回 Skia 内部的 type/name(例如 api_raster_n32_canvas → api 类型 + RasterN32Canvas 名称、image_filter_deserialize → filter_fuzz),这正是 try_auto_detect 能自动猜中类型的基础。
九、进阶参考资料
- 创建 binary fuzzer
- 创建 API fuzzer
- 仓库内更完整的构建与运行说明:site/docs/dev/testing/fuzz.md;Sanitizer(ASAN/MSAN)构建细节见 site/docs/dev/testing/xsan.md。
小结:Skia 的 fuzzing 体系是一条完整链路——fuzz/ 下的 Fuzz 类与 DEF_FUZZ 注册机制是 API fuzzer 的通用底座,fuzz/FuzzMain.cpp 是聚合复现的指挥中枢,fuzz/oss_fuzz/ 则是面向 libFuzzer/OSS-Fuzz 的标准化入口集合。理解这条链路,你就能既快速复现线上报告的崩溃,又能为 Skia 贡献新的模糊测试目标,把漏洞消灭在发布之前。