SerenityOS 中使用 Fuzzilli 对 LibJS 进行 JavaScript 引擎模糊测试:FuzzilliJs 完整构建与运行指南
2026/9/11 22:23:00 网站建设 项目流程

SerenityOS 中使用 Fuzzilli 对 LibJS 进行 JavaScript 引擎模糊测试:FuzzilliJs 完整构建与运行指南

【免费下载链接】serenityThe Serenity Operating System 🐞项目地址: https://gitcode.com/GitHub_Trending/se/serenity

导读

FuzzilliJs 是 SerenityOS 为自家 JavaScript 引擎 LibJS 打造的 Fuzzilli 桥接模糊测试目标:它通过 REPRL(REad-Parse-Run-Loop)协议与 Google Project Zero 的 Fuzzilli 模糊器协同工作,用结构感知的程序生成策略持续挖掘 LibJS 解释器与字节码执行器中的崩溃、断言失败与内存安全缺陷。本文将基于仓库内 FuzzilliJsInstructions.md 的官方步骤,结合 FuzzilliJs.cpp、FuzzilliJs.dockerfile 与 CMakeLists.txt 的源码细节,从原理到实操完整讲解如何下载并构建 Fuzzilli、编译 FuzzilliJs 目标、启动模糊测试会话,以及如何用 Docker/Podman 一键搭建整套环境。读完本文,你将具备在本仓库 Lagom 构建体系下独立运行针对 LibJS 的 Fuzzilli 模糊测试、并分析其结果的能力。

FuzzilliJs 在 SerenityOS 模糊测试体系中的定位

SerenityOS 的模糊测试基础设施集中在 Meta/Lagom/Fuzzers 目录。其中绝大部分目标(如 FuzzJs、FuzzRegexECMA262、FuzzWasmParser 等)都是标准 libFuzzer 风格:以LLVMFuzzerTestOneInput为入口,由 Clang 的 libFuzzer 负责变异与调度,目标清单与依赖关系登记在 fuzzers.cmake 中。

而 FuzzilliJs 走的是另一条技术路线。它不依赖 libFuzzer 的变异引擎,而是实现了一套 Fuzzilli 定义的REPRL(REad-Parse-Run-Loop)进程内通信协议,让 Fuzzilli 这个"程序生成器"(program generator)能以极低的进程开销持续向 LibJS 喂入由语法感知模板拼接而成的 JavaScript 程序。二者的分工可以从源码对比中看得很清楚:

  • FuzzJs.cpp 每次输入都新建JS::VM与执行上下文,属于经典的"一个输入一个进程/一次调用"模型;
  • FuzzilliJs.cpp 则常驻进程,在while (true)循环里反复接收脚本、解析并执行,配合共享内存与 edge guard 复位机制实现高吞吐的进程内复用。

正是这种"常驻 + 协议驱动"的设计,使 FuzzilliJs 成为仓库中唯一一个为外部结构化模糊器量身定制的目标,也解释了为什么它的构建方式(-fsanitize-coverage=trace-pc-guard)与其余 fuzzer(-fsanitize=fuzzer)截然不同。

原理剖析:FuzzilliJs 如何与 Fuzzilli 通信

要正确使用 FuzzilliJs,有必要先理解它内部发生了什么。核心逻辑全部位于 FuzzilliJs.cpp 这一个文件中,大致可分为三层。

REPRL 协议与共享内存通道

文件顶部定义了一组固定的文件描述符常量:

#define REPRL_CRFD 100 // 控制读:接收 Fuzzilli 发来的指令 #define REPRL_CWFD 101 // 控制写:向 Fuzzilli 回报执行结果 #define REPRL_DRFD 102 // 数据读:共享内存中的脚本输入 #define REPRL_DWFD 103 // 数据写:FUZZILLI_PRINT 的输出通道 #define REPRL_MAX_DATA_SIZE (16 * 1024 * 1024) // 单个脚本上限 16 MiB

main()启动后首先通过write(REPRL_CWFD, helo, 4)read(REPRL_CRFD, helo, 4)完成 "HELO" 握手,随后将REPRL_DRFD指向的共享内存区域映射为输入缓冲区:

reprl_input = (char*)mmap(0, REPRL_MAX_DATA_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, REPRL_DRFD, 0);

主循环每轮读取 4 字节的 action(必须为'cexe',即 "execute")与 8 字节的脚本长度,再从共享内存中拷贝脚本交给 LibJS 处理,最后把以(result & 0xff) << 8编码的退出状态写回REPRL_CWFD。Fuzzilli 因此可以在不重建进程的前提下,以接近函数调用的开销执行成千上万个测试用例。

sanitizer coverage:trace-pc-guard 与共享边表

为了让 Fuzzilli 获得 LibJS 内部的代码覆盖率反馈,FuzzilliJs 手工实现了两个 sanitizer coverage 回调:

extern "C" void __sanitizer_cov_trace_pc_guard_init(uint32_t* start, uint32_t* stop); extern "C" void __sanitizer_cov_trace_pc_guard(uint32_t* guard);

初始化时它从环境变量SHM_ID读取共享内存键名,通过shm_open+mmap映射一块0x100000(1 MiB)的位图;每条被插桩的 edge 都有一个 guard 值,执行时__sanitizer_cov_trace_pc_guardindex / 81 << (index % 8)在位图中置位,随后将该 edge 清零,避免重复计数。每一轮脚本执行完毕后调用__sanitizer_cov_reset_edgeguards()复位全部 guard,为下一轮覆盖率统计做好准备。这套机制对应 CMake 中的编译选项:

target_compile_options(FuzzilliJs PRIVATE $<$<CXX_COMPILER_ID:Clang>:-g -O1 -fsanitize-coverage=trace-pc-guard> )

见 Meta/Lagom/Fuzzers/CMakeLists.txt。值得注意的是,该目标只在ENABLE_FUZZERS_LIBFUZZER开启时才会被加入构建,同时整个 fuzzer 构建还会附加 AddressSanitizer(见同文件的-fsanitize=address链接标志)。

暴露给被测脚本的fuzzilli()原生函数

FuzzilliJs 自定义了一个TestRunnerGlobalObject(继承自JS::GlobalObject),并向 JS 环境注册名为fuzzilli的原生函数(FuzzilliJs.cpp)。它支持两类操作:

  • fuzzilli("FUZZILLI_CRASH", type):按类型主动触发崩溃(type 0 为写入固定地址0x41414141),用于验证崩溃复现链路;
  • fuzzilli("FUZZILLI_PRINT", str):把字符串写到REPRL_DWFD(数据写描述符)对应的输出流,供 Fuzzilli 采集程序输出。

测试程序执行路径为:校验脚本是否为合法 UTF-8 →JS::Script::parse解析 →vm->bytecode_interpreter().run()执行(FuzzilliJs.cpp)。解析或执行失败均以结果码 1 上报,但不会终止进程,这正是 REPRL 协议保证模糊吞吐的关键。

环境准备:获取 Fuzzilli 与安装 Swift

开始之前,请确保满足以下前置条件:

  1. Fuzzilli 源码:从 Fuzzilli 官方仓库(googleprojectzero/fuzzilli)克隆一份拷贝。Fuzzilli 是 Google Project Zero 团队用 Swift 编写的 JavaScript 引擎模糊测试框架。
  2. Swift 工具链:安装 Swift 并确保swift命令已加入你的PATH环境变量,因为 Fuzzilli 本体及其 CLI 均由 Swift 构建。不同平台的 Swift 安装方式不同,请以官方安装包或发行版软件源为准。
  3. SerenityOS 侧构建工具链:FuzzilliJs 是一个 C++ 目标,需要 Clang 编译器(构建脚本要求 Clang 15 或更高版本,BuildFuzzers.sh 中的pick_clang()会依次尝试clangclang-17clang-18等候选并拒绝 Apple Clang,因为 Xcode 未随附 libFuzzer),以及 CMake、Ninja 等常规构建工具。

第一步:像构建其他 fuzzer 一样构建 FuzzilliJs

FuzzilliJs 的构建完全复用 Lagom 的通用 fuzzer 构建流程,详见 Meta/Lagom/ReadMe.md 的 "Fuzzing locally" 一节。由于 fuzzer 构建需要先用无插桩的工具链生成代码生成器,Lagom 采用两阶段构建:

cd Meta/Lagom ./BuildFuzzers.sh

脚本 BuildFuzzers.sh 的行为如下:

  1. 先构建Build/toolsBUILD_LAGOM=OFF的 LagomTools),用于生成 fuzzer 构建阶段所需的代码生成器,避免对工具自身进行插桩;
  2. 挑选可用的 Clang,然后以-DENABLE_FUZZERS_LIBFUZZER=ON -DENABLE_ADDRESS_SANITIZER=ON -DENABLE_UNDEFINED_SANITIZER=ON配置Build/lagom-fuzzers目录并执行ninja

构建完成后,FuzzilliJs 二进制位于Build/lagom-fuzzers/bin/FuzzilliJs。这里有两个关键点需要理解:

  • 为什么必须用脚本而非直接 cmake?因为两阶段构建是硬性要求:fuzzer 目标(包括 FuzzilliJs 使用的-fsanitize-coverage=trace-pc-guard插桩)不能作用于构建工具本身,否则会导致递归插桩问题。
  • 与普通 fuzzer 的差异:其他 fuzzer 通过add_simple_fuzzer函数统一注册(编译选项为-fsanitize=fuzzer),而 FuzzilliJs 在 CMakeLists.txt 中被单独add_executable,链接AK LibCore LibJS,编译选项为-fsanitize-coverage=trace-pc-guard——它不需要 libFuzzer 的入口函数LLVMFuzzerTestOneInput,而是自带main()实现 REPRL 循环,因此不能作为普通 libFuzzer 目标直接运行。

如果你只想单测 FuzzilliJs 的输入处理路径(不跑完整 Fuzzilli),可以使用./BuildFuzzers.sh --standalone,它会生成无插桩的独立二进制(Build/lagom-fuzzers-standalone),配合 EntryShim.cpp 从文件或 stdin 读取单个输入并退出——不过要注意,FuzzilliJs 的 REPRL 握手依赖 Fuzzilli 主动连接,单独运行它只会卡在 HELO 阶段,所以 standalone 模式更适合验证常规 fuzzer,FuzzilliJs 请始终配合 Fuzzilli 使用。

第二步:构建 Fuzzilli(Swift 构建)

进入克隆下来的 Fuzzilli 仓库根目录,以 release 配置构建:

swift build -c release

该命令会编译 Fuzzilli 的全部模块,包括核心引擎与FuzzilliCli命令行工具。构建产物会输出到 Fuzzilli 仓库内的.build/release/目录(具体路径随平台略有差异,Linux 上通常为.build/x86_64-unknown-linux-gnu/release/)。请确保这一步成功完成再进入下一步,因为运行阶段依赖 release 构建产物。

第三步:启动模糊测试会话

以 release 模式运行 Fuzzilli CLI,并传入 SerenityOS 专用 profile 与 FuzzilliJs 二进制路径:

swift run -c release FuzzilliCli --profile=serenity /path/to/FuzzilliJs

参数含义:

  • -c release:以 release 配置运行(与构建步骤保持一致,避免重新编译 debug 版本);
  • --profile=serenity:选择 serenity 专属的 fuzzing profile。Fuzzilli 通过 profile 决定生成程序的 JavaScript 语言特性集、内置函数库与默认配置;
  • /path/to/FuzzilliJs:上一步构建出的目标二进制绝对(或相对)路径,例如Meta/Lagom/Build/lagom-fuzzers/bin/FuzzilliJs

Fuzzilli 还提供丰富的运行选项,可用以下命令查看全部参数:

swift run FuzzilliCli --help

常见的有用选项包括工作线程数(--workers)、超时控制、存储路径(--storagePath)、以及崩溃/超时用例的保存目录等,建议根据机器配置与测试时长按需调整。默认情况下 Fuzzilli 会把发现的崩溃、超时与 interesting 用例写入其运行目录,便于事后复现分析。

备选方案:用 Docker/Podman 一键构建与运行

如果不想在本机手动装配 Swift 与 Clang 工具链,仓库提供了现成的容器化方案 FuzzilliJs.dockerfile。它是一个多阶段构建(multi-stage build),共三个阶段:

  1. serenity-build 阶段(基础镜像fedora:39):安装clang cmake git-core ninja-build,克隆 SerenityOS 仓库(--depth=1浅克隆),然后在Meta/Lagom下执行./BuildFuzzers.sh构建全部 fuzzer;
  2. fuzzilli-build 阶段:安装git-core patch swift-lang,克隆 Fuzzilli 仓库并执行swift build -c release
  3. runtime 阶段:安装swift-lang procps-ng(运行时需要libswiftCore.so等 Swift 运行时库),从前两个阶段分别拷贝Build/lagom-fuzzers/binlib64与编译好的FuzzilliCli,创建fuzzilli-storage目录,并以如下命令启动:
./FuzzilliCli --profile=serenity --storagePath=fuzzilli-storage ${FUZZILLI_CLI_OPTIONS} ./bin/FuzzilliJs

使用 Podman 的构建与运行命令(Docker 用法基本一致):

# 构建镜像 podman build \ --tag fuzzillijs \ -f ./FuzzilliJs.dockerfile # 运行容器(挂载持久化存储目录) podman run \ -it --rm \ -v ./path/to/fuzzilli-storage:/home/fuzzilli-storage:Z \ localhost/fuzzillijs

需要向 Fuzzilli 传递额外 CLI 选项(例如断点续跑--resume,完整列表见--help)时,通过环境变量注入:

podman run \ -it --rm \ -v ./path/to/fuzzilli-storage:/home/fuzzilli-storage:Z \ -e FUZZILLI_CLI_OPTIONS='--resume' \ localhost/fuzzillijs

值得注意的一点:Fuzzilli 上游虽然为多个受支持的 JS 引擎提供了现成的 Dockerfile,但 SerenityOS 没有直接复用那种方式。正如 dockerfile 注释所解释的,那需要相当程度的补丁改造,除非计划把 LibJS 支持合入 Fuzzilli 上游,否则性价比不高,因此这里选择在 SerenityOS 侧维护独立的容器方案。这也说明 FuzzilliJs 目前是 SerenityOS 维护的桥接实现,而非 Fuzzilli 官方原生支持的引擎。

结果分析:复现崩溃与排障要点

Fuzzilli 发现崩溃后,通常会在运行目录留下可复现的用例文件。结合 Meta/Lagom/ReadMe.md 中 "Analyzing a crash" 一节的通用经验,可以从以下角度排查:

  • 崩溃用例回放:将 Fuzzilli 保存的用例喂给调试器,观察崩溃位置是否落在 LibJS 的解析器(JS::Script::parse)或字节码解释器(vm->bytecode_interpreter().run())路径上;
  • ASan/UBSan 输出:构建时已默认开启 AddressSanitizer 与 UndefinedBehaviorSanitizer(-DENABLE_ADDRESS_SANITIZER=ON -DENABLE_UNDEFINED_SANITIZER=ON),崩溃时应优先阅读 sanitizer 的报错栈;若 UBSan 信息不直观,可设置export UBSAN_OPTIONS=print_stacktrace=1强制打印堆栈;
  • 覆盖率相关告警:若日志出现invalid path to external symbolizer之类提示,说明系统缺少llvm-symbolizer(通常随 LLVM 工具链提供),补装对应包即可获得符号化堆栈。

此外,FuzzilliJs 的测试路径本身有几处已知边界:脚本若非法 UTF-8 会被直接以结果码 1 拒绝(对应仓库中记录的 FIXME 问题,见 FuzzilliJs.cpp);单个脚本超过REPRL_MAX_DATA_SIZE(16 MiB)也会被拒绝执行。了解这些边界有助于判断"用例被拒"究竟是 Fuzzilli 生成策略所致,还是被测目标的固有约束。

小结:从构建到持续模糊的完整链路

回顾整条链路:FuzzilliJs 通过 REPRL 协议与共享内存位图,把 LibJS 的解析、字节码编译与执行暴露给 Fuzzilli 的结构化程序生成器;构建侧依赖 Lagom 的两阶段 Clang 构建(BuildFuzzers.sh)产出带trace-pc-guard插桩与 ASan/UBSan 的目标;运行侧则由FuzzilliCli --profile=serenity驱动,也可通过 FuzzilliJs.dockerfile 容器化一键完成。这套方案与 OSS-Fuzz 上持续运行的常规 fuzzer 形成互补:libFuzzer 家族(如 FuzzJs.cpp)负责无差别变异,而 Fuzzilli 负责语法感知的程序合成,二者共同覆盖 LibJS 的不同缺陷面。掌握了上述步骤后,你既可以临时搭建一次性的模糊测试会话,也可以将其接入自己的持续集成流程,为 LibJS 的健壮性提供长期保障。

【免费下载链接】serenityThe Serenity Operating System 🐞项目地址: https://gitcode.com/GitHub_Trending/se/serenity

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

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

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

立即咨询