首页
/ Ladybird 浏览器模糊测试实战:从 libFuzzer 本地构建到 OSS-Fuzz 持续运行的完整指南

Ladybird 浏览器模糊测试实战:从 libFuzzer 本地构建到 OSS-Fuzz 持续运行的完整指南

2026-09-04 17:42:43作者:翟江哲Frasier

Lagom(Ladybird 的独立构建体系)内置了一套完整的模糊测试(Fuzzing)基础设施:18+ 个基于 LLVM libFuzzer 的 Fuzzer 覆盖 JS 引擎、图像解码器、CSS/JSON/XML 解析器、Wasm 与正则表达式等浏览器核心组件,并且同一套构建配置可以无缝接入 OSS-Fuzz 持续运行。读完本文,你将掌握 Ladybird 中模糊测试的三种构建模式(libFuzzer 本地构建、standalone 无插桩构建、OSS-Fuzz 构建)的完整操作命令,理解 CMake 构建配置中每种模式的链接与插桩差异,并能独立复现、调试和符号化 Fuzzer 捕获的崩溃。

一、Fuzzer 全景:目标清单与构建系统

1.1 Fuzzer 目标清单

所有 Fuzzer 目标集中声明在 Meta/Fuzzers/fuzzers.cmake 中。当前仓库注册了以下 18 个基础 Fuzzer(另有一个条件编译的 CSSParser):

Fuzzer 目标 被测库(依赖) 覆盖能力
FuzzASN1 LibCrypto、LibTLS ASN.1 证书/密钥解析
FuzzBase64Roundtrip Base64 编码往返
FuzzBMPLoader LibGfx、LibImageDecoders BMP 图像解码
FuzzGIFLoader LibGfx、LibImageDecoders GIF 图像解码
FuzzICOLoader LibGfx、LibImageDecoders ICO 图像解码
FuzzJs LibJS、LibGC JavaScript 解析与执行
FuzzJsonParser JSON 解析
FuzzMatroskaReader LibMedia Matroska/WebM 容器解析
FuzzPEM LibCrypto PEM 密钥解析
FuzzPNGLoader LibGfx、LibImageDecoders PNG 图像解码
FuzzRegexECMA262 LibRegex ECMA-262 正则表达式
FuzzRSAKeyParsing LibCrypto RSA 密钥解析
FuzzTextDecoder LibTextCodec 文本编码解码
FuzzURL LibURL URL 解析
FuzzWasmParser LibWasm Wasm 字节码解析
FuzzWOFF LibGfx WOFF 字体解码
FuzzXML LibXML XML 解析
FuzzCSSParser(条件) LibWeb CSS 解析(仅当 LibWeb 目标存在时追加)

CSSParser 的注册方式是按需追加的,fuzzers.cmake 中的逻辑是:

if (TARGET LibWeb)
    list(APPEND FUZZER_TARGETS CSSParser)
endif()

也就是说,如果构建配置没有编译 LibWeb,这个 Fuzzer 会自动缺席,其余 18 个仍然可用。

1.2 三种构建模式在 CMake 中的实现

Meta/Fuzzers/CMakeLists.txt 定义了一个 add_simple_fuzzer() 函数,它是所有 Fuzzer 的统一入口,内部按 CMake 选项分三个分支:

function(add_simple_fuzzer name)
  add_executable(${name} "${name}.cpp")

  if (ENABLE_FUZZERS_OSSFUZZ)
    # OSS-Fuzz:引擎由外部 -DFUZZER_DICTIONARY_DIRECTORY 与 $LIB_FUZZING_ENGINE 提供
    if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${name}.dict")
      configure_file("${name}.dict" "${FUZZER_DICTIONARY_DIRECTORY}/${name}.dict" COPYONLY)
    endif()
    target_link_libraries(${name} PUBLIC ${ARGN} AK LibCore)
  elseif (ENABLE_FUZZERS_LIBFUZZER)
    # 本地 libFuzzer:-g -O1 -fsanitize=fuzzer
    target_compile_options(${name} PRIVATE $<$<CXX_COMPILER_ID:Clang>:-g -O1 -fsanitize=fuzzer>)
    target_link_libraries(${name}
      PUBLIC ${ARGN} AK LibCore
      PRIVATE $<$<CXX_COMPILER_ID:Clang>:-fsanitize=fuzzer>)
  else()
    # standalone:链接 EntryShim.cpp 提供普通 main()
    target_sources(${name} PRIVATE "EntryShim.cpp")
    target_link_libraries(${name} PUBLIC ${ARGN} AK LibCore)
  endif()
  # ...
endfunction()

三个分支对应三种使用场景:

  1. ENABLE_FUZZERS_OSSFUZZ:面向 OSS-Fuzz 容器环境。注意这里会把同目录的 .dict 词典文件(如 FuzzJs.dict)原样拷贝到 FUZZER_DICTIONARY_DIRECTORY,供 libFuzzer 的种子词典机制使用;实际的 fuzzing 引擎由 OSS-Fuzz 通过 $LIB_FUZZING_ENGINE 链接器标志注入。
  2. ENABLE_FUZZERS_LIBFUZZER:本地开发常用路径。编译时加 -g -O1 -fsanitize=fuzzer,链接时同样加 -fsanitize=fuzzer,得到自带 main()、可无限循环运行并自动落盘 crash 输入的完整 libFuzzer 可执行文件。
  3. 默认(standalone):不注入任何插桩,改为把 EntryShim.cpp 编入目标,提供一个简单的 main()

此外,在 ENABLE_FUZZERS_LIBFUZZER 下,链接阶段还会统一追加 Address Sanitizer:

if (ENABLE_FUZZERS_LIBFUZZER)
set(CMAKE_EXE_LINKER_FLAGS "${ORIGINAL_CMAKE_EXE_LINKER_FLAGS} -fsanitize=address")
set(CMAKE_SHARED_LINKER_FLAGS "${ORIGINAL_CMAKE_SHARED_LINKER_FLAGS} -fsanitize=address")
set(CMAKE_MODULE_LINKER_FLAGS "${ORIGINAL_CMAKE_MODULE_LINKER_FLAGS} -fsanitize=address")

这正是 Meta/Fuzzers/README.md 所说“Fuzzers work best with Address Sanitizer enabled”的落地实现。同文件末尾还单独构建了一个 FuzzilliJs 目标——它不链接完整 libFuzzer 引擎,而是用 -fsanitize-coverage=trace-pc-guard 生成覆盖率插桩,作为 Fuzzilli 覆盖引导 fuzzer 的被测二进制(配套 FuzzilliJs.dockerfileFuzzilliJsInstructions.md)。

1.3 顶层 CMake 的开关与前置校验

CMakeLists.txt(仓库根目录)中的关键逻辑:

if (ENABLE_FUZZERS_LIBFUZZER OR ENABLE_FUZZERS_OSSFUZZ)
    set(ENABLE_FUZZERS ON)
endif()

if (CMAKE_CXX_COMPILER_ID MATCHES "Clang$")
    if (ENABLE_FUZZERS_LIBFUZZER)
        add_cxx_compile_options(-fsanitize=fuzzer)
        set(LINKER_FLAGS "${LINKER_FLAGS} -fsanitize=fuzzer")
    endif()
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
    if (ENABLE_FUZZERS_LIBFUZZER)
        message(FATAL_ERROR
            "Fuzzer Sanitizer (-fsanitize=fuzzer) is only supported for Fuzzer targets with LLVM. ...")
    endif()
endif()
# ...
if (ENABLE_FUZZERS)
    add_subdirectory(Meta/Fuzzers)
endif()

由此可以得到三条构建约束:

  • 只要打开 ENABLE_FUZZERS_LIBFUZZERENABLE_FUZZERS_OSSFUZZ 任一开关,就会自动置位 ENABLE_FUZZERS,进而把 Meta/Fuzzers 子目录加入构建;
  • 使用 GCC 工具链却打开 ENABLE_FUZZERS_LIBFUZZER 会被 message(FATAL_ERROR ...) 直接拒绝,因为 -fsanitize=fuzzer 仅 LLVM 支持——这解释了为什么本地 fuzz 构建必须用 clang;
  • 打开 libFuzzer 开关后,-fsanitize=fuzzer 会通过 add_cxx_compile_options 全局生效,这也是 README 强调“fuzzer build requires code generators to be pre-built without fuzzing in a two stage build”的原因:代码生成阶段与 fuzz 阶段需要隔离,避免生成器被 fuzzer 插桩污染。

本地构建的 CMake preset 定义在 Meta/CMake/presets/CMakeBasePresets.json,名为 Fuzzers,其缓存变量为:

"binaryDir": "$env{LADYBIRD_SOURCE_DIR}/Build/fuzzers",
"cacheVariables": {
    "BUILD_SHARED_LIBS": "OFF",
    "CMAKE_BUILD_TYPE": "RelWithDebInfo",
    "VCPKG_OVERLAY_TRIPLETS": "...distribution-triplets",
    "ENABLE_FUZZERS_LIBFUZZER": "ON",
    "ENABLE_ADDRESS_SANITIZER": "ON"
}

二、本地运行 Fuzzer(libFuzzer 模式)

2.1 标准构建流程

按 README 的指引,本地 fuzzing 需要 clang(仓库要求 clang >= 14),并建议使用独立的构建目录。完整命令:

./BuildFuzzers.sh
./Build/lagom-fuzzers/FuzzSomething # The full list can be found in Fuzzers/CMakeLists.txt

BuildFuzzers.sh 的默认分支实际执行:

cmake -S "$LADYBIRD_SOURCE_DIR" -GNinja --preset Fuzzers -B "$LADYBIRD_SOURCE_DIR"/Build/lagom-fuzzers \
    -DCMAKE_C_COMPILER="${CC}" \
    -DCMAKE_CXX_COMPILER="${CXX}"
ninja -C "$LADYBIRD_SOURCE_DIR"/Build/lagom-fuzzers

也就是说:Ninja 生成器 + Fuzzers preset(即上一节列出的缓存变量),产物落在 Build/lagom-fuzzers/。脚本开头通过 Meta/Utils/find_compiler.shpick_host_compiler --clang-only 挑选 clang 工具链(README 中提到的 pick_clang() 在仓库演进中已改为这个 find_compiler 工具函数,作用一致:从系统预定义路径中寻找满足版本要求的 clang),并把选中的 CC/CXX 显式传给 CMake,确保 fuzz 目标由 clang 编译。

2.2 一个 Fuzzer 长什么样

以最简单的 Meta/Fuzzers/FuzzBMPLoader.cpp 为例,整个文件只有 19 行:

#include <LibGfx/ImageFormats/BMPLoader.h>
#include <stdio.h>

extern "C" int LLVMFuzzerTestOneInput(uint8_t const* data, size_t size)
{
    AK::set_debug_enabled(false);
    auto decoder_or_error = Gfx::BMPImageDecoderPlugin::create({ data, size });
    if (decoder_or_error.is_error())
        return 0;
    auto decoder = decoder_or_error.release_value();
    (void)decoder->frame(0);
    return 0;
}

这体现了仓库中所有 Fuzzer 的统一模式:

  • 导出 C 符号 LLVMFuzzerTestOneInput(uint8_t const* data, size_t size) 作为唯一入口;
  • 首行 AK::set_debug_enabled(false) 关闭 AK 的调试断言噪声,让 sanitizer 报告成为唯一的崩溃信号;
  • 用原始字节构造解码器并强制解码第 0 帧,任何越界读、非法分配、UB 都会被 ASan 捕获。

Meta/Fuzzers/FuzzJs.cpp 为例则是“解析 + 执行”的更重模式:先 Utf8View(js).validate() 校验 UTF-8,再 JS::VM::create() 创建虚拟机、解析 JS::Scriptvm->run(...) 执行,从而用随机字节驱动整个 JS 引擎链路。

2.3 运行时的实用技巧(README 全部要点)

以下均为 Meta/Fuzzers/README.md 给出的可操作建议,逐条说明:

  • fuzzing 结果落盘在当前目录:crash、slow 输入等结果文件(如 crash-27480a...slow-...)会直接写进你运行 Fuzzer 的工作目录。
  • 喂语料(corpus):给 Fuzzer 一个初始语料能显著提升覆盖率: ./Fuzzers/FuzzBMPLoader ../Base/res/html/misc/bmpsuite_files/rgba32-61754.bmp。需要说明的是,README 引用的 Base/res/html/misc 图像套件在仓库当前结构中已不存在;同类测试输入现在集中在 Tests/LibGfx/test-inputs/ 目录下,例如 Tests/LibGfx/test-inputs/bmp/ 中就有 bitmap.bmptoo-many-palette-colors.bmp 以及来自 OSS-Fuzz 的 oss-fuzz-testcase-62541.bmp,可直接作为语料来源。
  • LLVM 引擎也会创建新文件(如 crash-*slow-*、语料库文件),README 特别提醒不要盲目提交它们。
  • 并行-jobs=24 -workers=24 让 libFuzzer 分叉 24 个 worker 并行探索。
  • 降噪-close_fd_mask=3 同时关闭 stdout/stderr 以减少日志输出,但会连断言信息一起隐藏;-close_fd_mask=1 只关 stdout,是折中方案。README 同时建议把话痨式的日志输出挪到 FOO_DEBUG 宏后面,从源头降噪。

2.4 无插桩构建:--standalone

如果只想“对单个输入文件跑一遍被测代码”(例如 CI 中对某个复现文件做回归验证),可以用:

./BuildFuzzers.sh --standalone

# 从给定文件(或 stdin)读取单个输入并执行后退出
./Build/lagom-fuzzers-standalone/Fuzzers/FuzzSomething

对应脚本分支:

cmake -S "$LADYBIRD_SOURCE_DIR" -GNinja -B "$LADYBIRD_SOURCE_DIR"/Build/lagom-fuzzers-standalone \
    -DENABLE_FUZZERS=ON

注意这里只打开 ENABLE_FUZZERS,不打开 ENABLE_FUZZERS_LIBFUZZER,于是 add_simple_fuzzer() 走第三个分支,把 EntryShim.cpp 编进目标。EntryShim 的实现很直白:

  • argc > 1 时调用 fuzz_from_file(argv[1])stat 取文件大小 → malloc 缓冲 → read 读满 → LLVMFuzzerTestOneInput
  • 无参数时调用 fuzz_from_stdin():以 4096 字节为块循环 realloc + read 直到 EOF,再交给同一个入口。

这就是 README 所说“read a single test input from a given filename (or, if no filename is given, from stdin) and exit”的全部实现。

2.5 微调 fuzz 构建的 CMake 缓存

README 允许直接操纵 fuzzing 构建的 CMake 缓存,例如:

cmake -B Build/fuzzers -S . -DENABLE_LAGOM_CCACHE=OFF

因为 preset 的 binaryDir 就是 Build/fuzzers,这条命令改的是与 BuildFuzzers.sh 相同的构建目录(ccache 对某些 sanitizer 组合不友好时尤其有用)。同理,你也能在这个目录上追加任意 -D 选项来调整 fuzz 构建。另外 README 提到:想换用其他 fuzzing 引擎(OSS-Fuzz 构建即为示范),大概率需要在第二阶段 CMake 构建或环境变量中显式设置 CFLAGS/CXXFLAGS

三、管理有价值的测试用例

README 专门用一节讨论“如何跟踪那些命中大量边界条件的怪异文件”,其结论和现状值得完整梳理:

  • 仓库中确实存在一批历史积累的图像测试套件(bmp suite、jpg suite 等),但它们带有 GPL 许可,与仓库其余部分的许可兼容性不佳,因此团队选择把“自产的”有趣测试用例与 GPL 套件分离;
  • 由于 fuzzing 会不断产生更多更大的文件,README 的结论是:不要把实际测试用例堆在主仓库里,而是放进独立的 fuzz corpora 仓库(README 原文引用的是 SerenityOS 时代的 serenity-fuzz-corpora),鼓励往那里“upload lots and lots files”;
  • 从当前仓库的结构可以印证这一策略:崩溃复现输入以 oss-fuzz-testcase-*.bmp 等命名保存在 Tests/LibGfx/test-inputs/ 这类测试输入目录中,作为回归测试资产,而非膨胀主代码树。

对贡献者的实操含义:当你本地 fuzz 出一个 crash 输入文件后,合理做法是把它压缩/最小化后提交为测试输入或上传到 corpora 仓库,同时按项目流程报告问题——而不是把几十 MB 的语料直接 commit 进来。

四、在 OSS-Fuzz 上持续运行 Fuzzer

4.1 OSS-Fuzz 侧的机制

按 README 说明:oss-fuzz.com 会自动运行 Fuzzers/ 子目录下所有名字以 Fuzz 开头、且注册进了构建(即进入 FUZZER_TARGETS 清单)的 Fuzzer——前提是配置了 ENABLE_FUZZERS_OSSFUZZ。OSS-Fuzz 上的项目配置会调用 BuildFuzzers.sh 并带上 --oss-fuzz 参数,在官方 Docker 容器内构建。

--oss-fuzz 分支的完整 cmake 调用(摘自 BuildFuzzers.sh)值得逐参数解读:

cmake -S "$LADYBIRD_SOURCE_DIR" -GNinja -B "$LADYBIRD_SOURCE_DIR"/Build/fuzzers \
    -DBUILD_SHARED_LIBS=OFF \
    -DENABLE_FUZZERS_OSSFUZZ=ON \
    -DFUZZER_DICTIONARY_DIRECTORY="$OUT" \
    -DCMAKE_C_COMPILER="${CC}" \
    -DCMAKE_CXX_COMPILER="${CXX}" \
    -DCMAKE_CXX_FLAGS="$CXXFLAGS -DOSS_FUZZ=ON" \
    -DLINKER_FLAGS="$LIB_FUZZING_ENGINE"
ninja -C "$LADYBIRD_SOURCE_DIR"/Build/fuzzers
cp "$LADYBIRD_SOURCE_DIR"/Build/fuzzers/bin/Fuzz* "$OUT"/
  • -DENABLE_FUZZERS_OSSFUZZ=ON:走 add_simple_fuzzer() 的第一分支——不自己加 -fsanitize=fuzzer,把引擎留给 $LIB_FUZZING_ENGINE(OSS-Fuzz 容器注入的 libFuzzer 静态库,通过 -DLINKER_FLAGS="$LIB_FUZZING_ENGINE" 接入链接阶段);
  • -DFUZZER_DICTIONARY_DIRECTORY="$OUT"$OUT 是 OSS-Fuzz 约定的产物输出目录,.dict 词典会被拷贝到那里与二进制并排,libFuzzer 运行时即可自动发现;
  • -DOSS_FUZZ=ON:全局宏定义,供源码在 OSS-Fuzz 环境下调整行为;
  • 最后 cp .../bin/Fuzz* "$OUT"/ 把所有 Fuzzer 二进制收集到 OSS-Fuzz 要求的位置。

4.2 在本地复现 OSS-Fuzz 构建

README 给出的完整命令序列(需先获取 google/oss-fuzz 仓库,此处不展开外部地址):

cd oss-fuzz
python3 infra/helper.py build_image serenity
python3 infra/helper.py build_fuzzers serenity

产物位于 oss-fuzz 仓库的 build/out/serenity,可逐个手动运行,或直接:

python3 infra/helper.py run_fuzzer serenity FUZZER_NAME

两个进阶用法:

  • 用 OSS-Fuzz 的构建流程、但对着本地 checkout 构建(调试本地改动时非常有用): python3 infra/helper.py build_fuzzers serenity $HOME/src/serenity/(把路径替换为你本地的 Ladybird 检出目录);
  • 直接进容器手动折腾:docker run -it gcr.io/oss-fuzz/serenity bash

五、分析一个 crash:复现、gdb 与符号化

README 的“Analyzing a crash”一节包含几个非常实用的坑位提示,全部继承如下:

5.1 libFuzzer 的怪异接口

LLVM fuzzer 的帮助需要用 -help=1 查看,--help-help 都会被忽略。

5.2 复现与 gdb 调试

复现某个 crash 输入,直接把文件作为参数传给 Fuzzer:

MyFuzzer crash-27480a219572aa5a11b285968a3632a4cf25388e

在 gdb 中复现时,需要关闭 libFuzzer 自带的信号处理器,否则 gdb 看到的是信号处理后的现场而不是真正崩溃点。README 给出的完整示例(-handle_abrt=0 -handle_segv=0):

$ gdb ./Fuzzers/FuzzBMP
<... SNIP some output ...>
(gdb) run -handle_abrt=0 -handle_segv=0 crash-27480a219572aa5a11b285968a3632a4cf25388e
<... SNIP some output ...>
FuzzBMP: ../../Libraries/LibGfx/Bitmap.cpp:84: Gfx::Bitmap::Bitmap(...): Assertion `m_data && m_data != (void*)-1' failed.

Thread 1 "FuzzBMP" received signal SIGABRT, Aborted.
__GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
(gdb)

要点:加两个 -handle_* 参数后,断言/段错误会以原始信号形式到达 gdb,backtrace 才能给出真实栈。

5.3 UBSan 与符号化两个常见坑

  • UBSan 默认输出经常没用:设置 export UBSAN_OPTIONS=print_stacktrace=1 让每个 UBSan 报告都带完整堆栈;
  • external symbolizer 报错:如果你看到 WARNING: invalid path to external symbolizer! / Failed to use and restart external symbolizer!, 意味着 sanitizer 找不到 llvm-symbolizer 可执行文件。它通常随系统的 llvm 软件包提供;注意带版本号后缀的 llvm-symbolizer-11 这类名字不会被 sanitizer 识别,需要用符号链接或 PATH 调整让裸名 llvm-symbolizer 可被找到。

六、速查:常用命令汇总

场景 命令
本地 libFuzzer 构建 ./BuildFuzzers.sh
运行某个 Fuzzer ./Build/lagom-fuzzers/FuzzSomething
带语料运行 ./Fuzzers/FuzzBMPLoader <corpus-file.bmp>
无插桩 standalone 构建 ./BuildFuzzers.sh --standalone
单文件复现(standalone) ./Build/lagom-fuzzers-standalone/Fuzzers/FuzzSomething input.bin(或从 stdin 读入)
调整 fuzz 构建缓存 cmake -B Build/fuzzers -S . -DENABLE_LAGOM_CCACHE=OFF
并行 fuzz 追加 -jobs=24 -workers=24
降低日志输出 追加 -close_fd_mask=3(会隐藏断言信息)或 -close_fd_mask=1
复现 crash MyFuzzer crash-<hash>
gdb 中复现 run -handle_abrt=0 -handle_segv=0 crash-<hash>
UBSan 打印堆栈 export UBSAN_OPTIONS=print_stacktrace=1

适用前提与限制:本地 libFuzzer 构建要求 clang(>= 14)工具链与 Ninja,且因 -fsanitize=fuzzer 仅 LLVM 支持,GCC 构建该模式会被顶层 CMake 直接报错拒绝;standalone 模式则对编译器无此限制。Fuzzer 清单以 Meta/Fuzzers/fuzzers.cmake 为准,OSS-Fuzz 侧只会运行其中注册的 Fuzz* 目标。

登录后查看全文
热门项目推荐
相关项目推荐