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

发布时间:2026/9/5 19:01:55
Ladybird 浏览器模糊测试实战:从 libFuzzer 本地构建到 OSS-Fuzz 持续运行的完整指南 Ladybird 浏览器模糊测试实战从 libFuzzer 本地构建到 OSS-Fuzz 持续运行的完整指南【免费下载链接】ladybirdTruly independent web browser项目地址: https://gitcode.com/GitHub_Trending/la/ladybirdLagomLadybird 的独立构建体系内置了一套完整的模糊测试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另有一个条件编译的CSSParserFuzzer 目标被测库依赖覆盖能力FuzzASN1LibCrypto、LibTLSASN.1 证书/密钥解析FuzzBase64Roundtrip—Base64 编码往返FuzzBMPLoaderLibGfx、LibImageDecodersBMP 图像解码FuzzGIFLoaderLibGfx、LibImageDecodersGIF 图像解码FuzzICOLoaderLibGfx、LibImageDecodersICO 图像解码FuzzJsLibJS、LibGCJavaScript 解析与执行FuzzJsonParser—JSON 解析FuzzMatroskaReaderLibMediaMatroska/WebM 容器解析FuzzPEMLibCryptoPEM 密钥解析FuzzPNGLoaderLibGfx、LibImageDecodersPNG 图像解码FuzzRegexECMA262LibRegexECMA-262 正则表达式FuzzRSAKeyParsingLibCryptoRSA 密钥解析FuzzTextDecoderLibTextCodec文本编码解码FuzzURLLibURLURL 解析FuzzWasmParserLibWasmWasm 字节码解析FuzzWOFFLibGfxWOFF 字体解码FuzzXMLLibXMLXML 解析FuzzCSSParser条件LibWebCSS 解析仅当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 -fsanitizefuzzer target_compile_options(${name} PRIVATE $$CXX_COMPILER_ID:Clang:-g -O1 -fsanitizefuzzer) target_link_libraries(${name} PUBLIC ${ARGN} AK LibCore PRIVATE $$CXX_COMPILER_ID:Clang:-fsanitizefuzzer) else() # standalone链接 EntryShim.cpp 提供普通 main() target_sources(${name} PRIVATE EntryShim.cpp) target_link_libraries(${name} PUBLIC ${ARGN} AK LibCore) endif() # ... endfunction()三个分支对应三种使用场景ENABLE_FUZZERS_OSSFUZZ面向 OSS-Fuzz 容器环境。注意这里会把同目录的.dict词典文件如 FuzzJs.dict原样拷贝到FUZZER_DICTIONARY_DIRECTORY供 libFuzzer 的种子词典机制使用实际的 fuzzing 引擎由 OSS-Fuzz 通过$LIB_FUZZING_ENGINE链接器标志注入。ENABLE_FUZZERS_LIBFUZZER本地开发常用路径。编译时加-g -O1 -fsanitizefuzzer链接时同样加-fsanitizefuzzer得到自带main()、可无限循环运行并自动落盘 crash 输入的完整 libFuzzer 可执行文件。默认standalone不注入任何插桩改为把 EntryShim.cpp 编入目标提供一个简单的main()。此外在ENABLE_FUZZERS_LIBFUZZER下链接阶段还会统一追加 Address Sanitizerif (ENABLE_FUZZERS_LIBFUZZER) set(CMAKE_EXE_LINKER_FLAGS ${ORIGINAL_CMAKE_EXE_LINKER_FLAGS} -fsanitizeaddress) set(CMAKE_SHARED_LINKER_FLAGS ${ORIGINAL_CMAKE_SHARED_LINKER_FLAGS} -fsanitizeaddress) set(CMAKE_MODULE_LINKER_FLAGS ${ORIGINAL_CMAKE_MODULE_LINKER_FLAGS} -fsanitizeaddress)这正是 Meta/Fuzzers/README.md 所说“Fuzzers work best with Address Sanitizer enabled”的落地实现。同文件末尾还单独构建了一个FuzzilliJs目标——它不链接完整 libFuzzer 引擎而是用-fsanitize-coveragetrace-pc-guard生成覆盖率插桩作为 Fuzzilli 覆盖引导 fuzzer 的被测二进制配套 FuzzilliJs.dockerfile 与 FuzzilliJsInstructions.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(-fsanitizefuzzer) set(LINKER_FLAGS ${LINKER_FLAGS} -fsanitizefuzzer) endif() elseif (CMAKE_CXX_COMPILER_ID STREQUAL GNU) if (ENABLE_FUZZERS_LIBFUZZER) message(FATAL_ERROR Fuzzer Sanitizer (-fsanitizefuzzer) is only supported for Fuzzer targets with LLVM. ...) endif() endif() # ... if (ENABLE_FUZZERS) add_subdirectory(Meta/Fuzzers) endif()由此可以得到三条构建约束只要打开ENABLE_FUZZERS_LIBFUZZER或ENABLE_FUZZERS_OSSFUZZ任一开关就会自动置位ENABLE_FUZZERS进而把Meta/Fuzzers子目录加入构建使用 GCC 工具链却打开ENABLE_FUZZERS_LIBFUZZER会被message(FATAL_ERROR ...)直接拒绝因为-fsanitizefuzzer仅 LLVM 支持——这解释了为什么本地 fuzz 构建必须用 clang打开 libFuzzer 开关后-fsanitizefuzzer会通过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 }二、本地运行 FuzzerlibFuzzer 模式2.1 标准构建流程按 README 的指引本地 fuzzing 需要 clang仓库要求 clang 14并建议使用独立的构建目录。完整命令./BuildFuzzers.sh ./Build/lagom-fuzzers/FuzzSomething # The full list can be found in Fuzzers/CMakeLists.txtBuildFuzzers.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 生成器 Fuzzerspreset即上一节列出的缓存变量产物落在Build/lagom-fuzzers/。脚本开头通过Meta/Utils/find_compiler.sh的pick_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::Script并vm-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.bmp、too-many-palette-colors.bmp以及来自 OSS-Fuzz 的oss-fuzz-testcase-62541.bmp可直接作为语料来源。LLVM 引擎也会创建新文件如crash-*、slow-*、语料库文件README 特别提醒不要盲目提交它们。并行-jobs24 -workers24让 libFuzzer 分叉 24 个 worker 并行探索。降噪-close_fd_mask3同时关闭 stdout/stderr 以减少日志输出但会连断言信息一起隐藏-close_fd_mask1只关 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_FUZZERSON注意这里只打开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 字节为块循环reallocread直到 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_CCACHEOFF因为 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 上持续运行 Fuzzer4.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_LIBSOFF \ -DENABLE_FUZZERS_OSSFUZZON \ -DFUZZER_DICTIONARY_DIRECTORY$OUT \ -DCMAKE_C_COMPILER${CC} \ -DCMAKE_CXX_COMPILER${CXX} \ -DCMAKE_CXX_FLAGS$CXXFLAGS -DOSS_FUZZON \ -DLINKER_FLAGS$LIB_FUZZING_ENGINE ninja -C $LADYBIRD_SOURCE_DIR/Build/fuzzers cp $LADYBIRD_SOURCE_DIR/Build/fuzzers/bin/Fuzz* $OUT/-DENABLE_FUZZERS_OSSFUZZON走add_simple_fuzzer()的第一分支——不自己加-fsanitizefuzzer把引擎留给$LIB_FUZZING_ENGINEOSS-Fuzz 容器注入的 libFuzzer 静态库通过-DLINKER_FLAGS$LIB_FUZZING_ENGINE接入链接阶段-DFUZZER_DICTIONARY_DIRECTORY$OUT$OUT是 OSS-Fuzz 约定的产物输出目录.dict词典会被拷贝到那里与二进制并排libFuzzer 运行时即可自动发现-DOSS_FUZZON全局宏定义供源码在 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 的帮助需要用-help1查看--help和-help都会被忽略。5.2 复现与 gdb 调试复现某个 crash 输入直接把文件作为参数传给 FuzzerMyFuzzer crash-27480a219572aa5a11b285968a3632a4cf25388e在 gdb 中复现时需要关闭 libFuzzer 自带的信号处理器否则 gdb 看到的是信号处理后的现场而不是真正崩溃点。README 给出的完整示例-handle_abrt0 -handle_segv0$ gdb ./Fuzzers/FuzzBMP ... SNIP some output ... (gdb) run -handle_abrt0 -handle_segv0 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 (sigsigentry6) at ../sysdeps/unix/sysv/linux/raise.c:50 (gdb)要点加两个-handle_*参数后断言/段错误会以原始信号形式到达 gdbbacktrace才能给出真实栈。5.3 UBSan 与符号化两个常见坑UBSan 默认输出经常没用设置export UBSAN_OPTIONSprint_stacktrace1让每个 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_CCACHEOFF并行 fuzz追加-jobs24 -workers24降低日志输出追加-close_fd_mask3会隐藏断言信息或-close_fd_mask1复现 crashMyFuzzer crash-hashgdb 中复现run -handle_abrt0 -handle_segv0 crash-hashUBSan 打印堆栈export UBSAN_OPTIONSprint_stacktrace1适用前提与限制本地 libFuzzer 构建要求 clang 14工具链与 Ninja且因-fsanitizefuzzer仅 LLVM 支持GCC 构建该模式会被顶层 CMake 直接报错拒绝standalone 模式则对编译器无此限制。Fuzzer 清单以 Meta/Fuzzers/fuzzers.cmake 为准OSS-Fuzz 侧只会运行其中注册的Fuzz*目标。【免费下载链接】ladybirdTruly independent web browser项目地址: https://gitcode.com/GitHub_Trending/la/ladybird创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考