
SerenityOS 中使用 Fuzzilli 对 LibJS 进行 JavaScript 引擎模糊测试FuzzilliJs 完整构建与运行指南【免费下载链接】serenityThe Serenity Operating System 项目地址: https://gitcode.com/GitHub_Trending/se/serenity导读FuzzilliJs 是 SerenityOS 为自家 JavaScript 引擎 LibJS 打造的 Fuzzilli 桥接模糊测试目标它通过 REPRLREad-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 定义的REPRLREad-Parse-Run-Loop进程内通信协议让 Fuzzilli 这个程序生成器program generator能以极低的进程开销持续向 LibJS 喂入由语法感知模板拼接而成的 JavaScript 程序。二者的分工可以从源码对比中看得很清楚FuzzJs.cpp 每次输入都新建JS::VM与执行上下文属于经典的一个输入一个进程/一次调用模型FuzzilliJs.cpp 则常驻进程在while (true)循环里反复接收脚本、解析并执行配合共享内存与 edge guard 复位机制实现高吞吐的进程内复用。正是这种常驻 协议驱动的设计使 FuzzilliJs 成为仓库中唯一一个为外部结构化模糊器量身定制的目标也解释了为什么它的构建方式-fsanitize-coveragetrace-pc-guard与其余 fuzzer-fsanitizefuzzer截然不同。原理剖析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 MiBmain()启动后首先通过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 coveragetrace-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_openmmap映射一块0x1000001 MiB的位图每条被插桩的 edge 都有一个 guard 值执行时__sanitizer_cov_trace_pc_guard按index / 8、1 (index % 8)在位图中置位随后将该 edge 清零避免重复计数。每一轮脚本执行完毕后调用__sanitizer_cov_reset_edgeguards()复位全部 guard为下一轮覆盖率统计做好准备。这套机制对应 CMake 中的编译选项target_compile_options(FuzzilliJs PRIVATE $$CXX_COMPILER_ID:Clang:-g -O1 -fsanitize-coveragetrace-pc-guard )见 Meta/Lagom/Fuzzers/CMakeLists.txt。值得注意的是该目标只在ENABLE_FUZZERS_LIBFUZZER开启时才会被加入构建同时整个 fuzzer 构建还会附加 AddressSanitizer见同文件的-fsanitizeaddress链接标志。暴露给被测脚本的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开始之前请确保满足以下前置条件Fuzzilli 源码从 Fuzzilli 官方仓库googleprojectzero/fuzzilli克隆一份拷贝。Fuzzilli 是 Google Project Zero 团队用 Swift 编写的 JavaScript 引擎模糊测试框架。Swift 工具链安装 Swift 并确保swift命令已加入你的PATH环境变量因为 Fuzzilli 本体及其 CLI 均由 Swift 构建。不同平台的 Swift 安装方式不同请以官方安装包或发行版软件源为准。SerenityOS 侧构建工具链FuzzilliJs 是一个 C 目标需要 Clang 编译器构建脚本要求 Clang 15 或更高版本BuildFuzzers.sh 中的pick_clang()会依次尝试clang、clang-17、clang-18等候选并拒绝 Apple Clang因为 Xcode 未随附 libFuzzer以及 CMake、Ninja 等常规构建工具。第一步像构建其他 fuzzer 一样构建 FuzzilliJsFuzzilliJs 的构建完全复用 Lagom 的通用 fuzzer 构建流程详见 Meta/Lagom/ReadMe.md 的 Fuzzing locally 一节。由于 fuzzer 构建需要先用无插桩的工具链生成代码生成器Lagom 采用两阶段构建cd Meta/Lagom ./BuildFuzzers.sh脚本 BuildFuzzers.sh 的行为如下先构建Build/toolsBUILD_LAGOMOFF的 LagomTools用于生成 fuzzer 构建阶段所需的代码生成器避免对工具自身进行插桩挑选可用的 Clang然后以-DENABLE_FUZZERS_LIBFUZZERON -DENABLE_ADDRESS_SANITIZERON -DENABLE_UNDEFINED_SANITIZERON配置Build/lagom-fuzzers目录并执行ninja。构建完成后FuzzilliJs 二进制位于Build/lagom-fuzzers/bin/FuzzilliJs。这里有两个关键点需要理解为什么必须用脚本而非直接 cmake因为两阶段构建是硬性要求fuzzer 目标包括 FuzzilliJs 使用的-fsanitize-coveragetrace-pc-guard插桩不能作用于构建工具本身否则会导致递归插桩问题。与普通 fuzzer 的差异其他 fuzzer 通过add_simple_fuzzer函数统一注册编译选项为-fsanitizefuzzer而 FuzzilliJs 在 CMakeLists.txt 中被单独add_executable链接AK LibCore LibJS编译选项为-fsanitize-coveragetrace-pc-guard——它不需要 libFuzzer 的入口函数LLVMFuzzerTestOneInput而是自带main()实现 REPRL 循环因此不能作为普通 libFuzzer 目标直接运行。如果你只想单测 FuzzilliJs 的输入处理路径不跑完整 Fuzzilli可以使用./BuildFuzzers.sh --standalone它会生成无插桩的独立二进制Build/lagom-fuzzers-standalone配合 EntryShim.cpp 从文件或 stdin 读取单个输入并退出——不过要注意FuzzilliJs 的 REPRL 握手依赖 Fuzzilli 主动连接单独运行它只会卡在 HELO 阶段所以 standalone 模式更适合验证常规 fuzzerFuzzilliJs 请始终配合 Fuzzilli 使用。第二步构建 FuzzilliSwift 构建进入克隆下来的 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 --profileserenity /path/to/FuzzilliJs参数含义-c release以 release 配置运行与构建步骤保持一致避免重新编译 debug 版本--profileserenity选择 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共三个阶段serenity-build 阶段基础镜像fedora:39安装clang cmake git-core ninja-build克隆 SerenityOS 仓库--depth1浅克隆然后在Meta/Lagom下执行./BuildFuzzers.sh构建全部 fuzzerfuzzilli-build 阶段安装git-core patch swift-lang克隆 Fuzzilli 仓库并执行swift build -c releaseruntime 阶段安装swift-lang procps-ng运行时需要libswiftCore.so等 Swift 运行时库从前两个阶段分别拷贝Build/lagom-fuzzers/bin、lib64与编译好的FuzzilliCli创建fuzzilli-storage目录并以如下命令启动./FuzzilliCli --profileserenity --storagePathfuzzilli-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_SANITIZERON -DENABLE_UNDEFINED_SANITIZERON崩溃时应优先阅读 sanitizer 的报错栈若 UBSan 信息不直观可设置export UBSAN_OPTIONSprint_stacktrace1强制打印堆栈覆盖率相关告警若日志出现invalid path to external symbolizer之类提示说明系统缺少llvm-symbolizer通常随 LLVM 工具链提供补装对应包即可获得符号化堆栈。此外FuzzilliJs 的测试路径本身有几处已知边界脚本若非法 UTF-8 会被直接以结果码 1 拒绝对应仓库中记录的 FIXME 问题见 FuzzilliJs.cpp单个脚本超过REPRL_MAX_DATA_SIZE16 MiB也会被拒绝执行。了解这些边界有助于判断用例被拒究竟是 Fuzzilli 生成策略所致还是被测目标的固有约束。小结从构建到持续模糊的完整链路回顾整条链路FuzzilliJs 通过 REPRL 协议与共享内存位图把 LibJS 的解析、字节码编译与执行暴露给 Fuzzilli 的结构化程序生成器构建侧依赖 Lagom 的两阶段 Clang 构建BuildFuzzers.sh产出带trace-pc-guard插桩与 ASan/UBSan 的目标运行侧则由FuzzilliCli --profileserenity驱动也可通过 FuzzilliJs.dockerfile 容器化一键完成。这套方案与 OSS-Fuzz 上持续运行的常规 fuzzer 形成互补libFuzzer 家族如 FuzzJs.cpp负责无差别变异而 Fuzzilli 负责语法感知的程序合成二者共同覆盖 LibJS 的不同缺陷面。掌握了上述步骤后你既可以临时搭建一次性的模糊测试会话也可以将其接入自己的持续集成流程为 LibJS 的健壮性提供长期保障。【免费下载链接】serenityThe Serenity Operating System 项目地址: https://gitcode.com/GitHub_Trending/se/serenity创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考