使用 Fuzzilli 对 workerd 进行 JavaScript 模糊测试:REPRL 集成与配置实战
【免费下载链接】workerdThe JavaScript / Wasm runtime that powers Cloudflare Workers项目地址: https://gitcode.com/GitHub_Trending/wo/workerd
导读
本文基于 workerd 仓库中的 fuzzilli/README.md,系统讲解如何将 Google 出品的 JavaScript 引擎模糊测试工具 Fuzzilli 接入 workerd(Cloudflare Workers 的 JavaScript/Wasm 运行时),通过 REPRL(REad-PRint-Loop,读-执行循环)协议批量执行由 Fuzzilli 生成的测试脚本,从而发现 V8 引擎、内建 API 与 Node.js 兼容层中的崩溃与缺陷。读完本文,你将掌握 fuzzilli 目录中 capnp 配置与 JS mock 的组织方式、REPRL 握手与执行协议的工作原理、验证 REPRL 接口是否可用的测试命令,以及使用 FuzzilliCli 配合语料库进行大规模模糊测试的完整实战方案。
目录结构与角色定位
fuzzilli 目录是 workerd 为 Fuzzilli 模糊测试专门准备的接入层,它并不包含模糊测试引擎本身,而是提供两类关键资产:
- capnp 配置:声明 workerd 需要以何种 Worker 形态启动、加载哪个入口脚本、注入哪些 binding 与 compatibility flags,用于在 REPRL 模式下把 workerd 变成一个"一次启动、反复执行外部脚本"的测试宿主。
- JavaScript mock 与入口脚本:用于模拟 Cloudflare 平台 API(KV、D1、R2、Analytics、Queue)以及把 Node.js 模块暴露到全局作用域,供模糊测试脚本直接调用。
目录内文件均由 fuzzilli/BUILD.bazel 中的exports_files(glob(["*.capnp", "*.js"]))导出,因此这些配置和脚本可以被 Bazel 构建系统引用。核心文件对应关系如下:
| 文件 | 角色 |
|---|---|
| config.capnp | 基础 REPRL 配置,加载worker.js,仅含极简 binding |
| config-full.capnp | 完整 REPRL 配置,加载worker-full.js,注入服务绑定与多个 mock 服务 |
| worker.js | 基础入口,仅导入workerd:stdin并调用Stdin.reprl() |
| worker-full.js | 完整入口,把 Web 标准、流、Node.js 模块与测试断言全部暴露到全局 |
| kv-mock.js / d1-mock.js / r2-mock.js / analytics-mock.js / queue-mock.js | 对应 Cloudflare 平台 API 的 mock 实现 |
| worker-consume-request.js | 供 service binding 消费请求的辅助 Worker |
基础 REPRL 配置:config.capnp 逐段拆解
基础配置 config.capnp 是理解整个接入流程的最小样例,其结构如下:
using Workerd = import "/workerd/workerd.capnp"; const reprl :Workerd.Config = ( services = [ (name = "main", worker = .replServer) ], sockets = [ ( name = "http", address = "*:8080", http = (), service = "main" ) ] ); const replServer :Workerd.Worker = ( modules = [ (name = "worker", esModule = embed "worker.js") ], bindings = [ ( name = "secret", text = "thisisasecret" ), ( name = "CACHE", memoryCache = ( id = "abc123", limits = ( maxKeys = 10, maxValueSize = 1024, maxTotalValueSize = 1024, ), ) ) ], compatibilityDate = "2023-02-28", compatibilityFlags = ["nodejs_compat", "experimental", "unsafe_module"] );关键点说明:
using Workerd = import "/workerd/workerd.capnp":workerd 的 capnp 模式定义,Workerd.Config声明一个可运行的 workerd 实例,Workerd.Worker声明具体的 Worker。services:声明服务main,其 Worker 指向下面的replServer。sockets:声明 HTTP 监听*:8080。注释# We don't need sockets for REPRL mode as it uses direct file descriptors(见完整版配置)明确指出:REPRL 模式并不依赖 socket,而是直接使用文件描述符通信,因此完整版配置直接省略了 sockets 块。modules:通过embed "worker.js"把入口脚本内嵌进二进制,模块名worker即 REPRL 模式下的宿主脚本。bindings:注入两个 binding——字符串类型的secret(值为thisisasecret)与内存缓存CACHE(maxKeys = 10、maxValueSize = 1024、maxTotalValueSize = 1024),用于让模糊测试脚本能够触达 cache 相关 API 路径。compatibilityDate:配置兼容日期,基础版为2023-02-28,完整版已更新到2025-05-01。compatibilityFlags:启用nodejs_compat(Node.js 兼容层)、experimental(实验特性)与unsafe_module(允许导入内部模块,如workerd:stdin)。
完整 REPRL 配置:config-full.capnp 的 API 覆盖策略
fuzzilli/config-full.capnp 是"到目前被模糊测试过的 API 全集"的配置,README 明确要求"若要测试某个 API,把它导入到所使用的.js文件中"。相比基础版,它做了三件事的扩展:
1. 扩展 services,为平台 API 提供 mock 后端:除main外,新增consumer(消费请求)以及test-kv、test-d1-mock、test-r2、test-analytics、test-queue五个 mock 服务,分别对应 kv-mock.js、d1-mock.js、r2-mock.js、analytics-mock.js、queue-mock.js。
2. 扩展 bindings,注入全套 Cloudflare API 绑定:
bindings = [ (name = "CONSUMER", service = "consumer"), (name = "volatileCache", memoryCache = ( id = "abc123", limits = ( maxKeys = 100, maxValueSize = 1024, maxTotalValueSize = 102400, ), )), (name = "MY_KV", kvNamespace = "test-kv"), (name = "MY_D1", wrapped = ( moduleName = "cloudflare-internal:d1-api", innerBindings = [(name = "fetcher", service = "test-d1-mock")], )), (name = "MY_R2", r2Bucket = "test-r2"), (name = "ANALYTICS", analyticsEngine = "test-analytics"), (name = "MY_QUEUE", queue = "test-queue"), ]注意 D1 使用wrapped方式包装内部模块cloudflare-internal:d1-api,并通过innerBindings把 fetcher 指向 mock 服务——这与 src/cloudflare/internal/d1-api.ts 等内部 API 的注入方式一致。
3. 扩展 compatibilityFlags:在基础三项之上追加enable_nodejs_fs_module、html_rewriter_treats_esi_include_as_void_tag、durable_object_rename、service_binding_extra_handlers、expose_global_message_channel、enable_web_file_system,尽量扩大 API 覆盖范围。
以 kv-mock.js 为例,mock 实现还注入了"混沌"行为:约 12% 概率返回 429/503/408 状态码、畸形 JSON 或错误 Content-Type,从而让模糊测试脚本也能覆盖错误处理与类型断言分支。
Worker 入口脚本:worker.js 与 worker-full.js
基础入口 worker.js 极简到只有三行核心逻辑:
import { default as Stdin } from 'workerd:stdin'; export default { async test() { Stdin.reprl(); }, };它通过unsafe_module兼容标志导入workerd:stdin,在test()处理器中调用Stdin.reprl()进入 REPRL 主循环。
完整入口 worker-full.js 则在进入 REPRL 循环前,把待测 API 全部挂到globalThis:
- Web 标准:
Response、Request、Headers、URL、URLPattern、URLSearchParams、TextEncoder/Decoder、atob/btoa、Blob、File、FormData、fetch、WebSocket、EventSource、HTMLRewriter、MessageChannel/Port、AbortController/Signal; - 流 API:
ReadableStream、WritableStream、TransformStream、CompressionStream、DecompressionStream、TextEncoderStream/DecoderStream、IdentityTransformStream、FixedLengthStream; - Node.js 模块:
node:fs、node:buffer的Buffer、node:stream的读写/转换流、node:string_decoder、node:events、node:process的env、node:zlib、node:async_hooks的AsyncLocalStorage、node:diagnostics_channel、node:dns、node:path、node:crypto; - 测试与断言:
node:assert的ok/match/rejects/throws/strictEqual/deepStrictEqual/notStrictEqual与node:test的mock,让模糊测试脚本可以直接书写断言; - Cloudflare API 内存 mock:
MOCK_KV、MOCK_D1、MOCK_R2,直接以 Promise 返回构造好的数据。
值得注意的细节:脚本显式执行process = undefined; globalThis.process = undefined;以移除 process 全局,模拟 Workers 运行时环境;同时把scheduler挂到globalThis.scheduler供scheduler.await使用。这些 global 的暴露清单本身就是一份"当前已纳入模糊测试的 API 范围"索引——新增 API 时只需在test()中追加globalThis.xxx = xxx即可。
REPRL 协议原理:从握手到脚本执行
README 将 REPRL 主执行流程归纳为 5 个步骤,与源码 src/workerd/api/unsafe.c++ 中Stdin::reprl()(受#ifdef WORKERD_FUZZILLI保护,仅在 Fuzzilli 构建下编译)的实现完全对应:
- Fuzzilli 启动 workerd,并把要执行的测试脚本交给它;
- workerd 读取 capnp 配置,配置指向导入了自定义 API 端点的脚本(例如
config.capnp+worker.js); - workerd 导入依赖并调用
Stdin.reprl(); - 握手阶段:workerd 打开管道与共享内存,向 Fuzzilli 写入 4 字节
HELO,Fuzzilli 回写HELO(源码中write(REPRL_CWFD, helo, 4)与read(REPRL_CRFD, helo, 4),并校验内容是否为"HELO"); - 执行循环:Fuzzilli 发送
exec命令(action 值0x63657865),随后发送 8 字节脚本长度,再通过数据管道发送脚本本身;workerd 以{ script }形式包装脚本(kj::str("{", script_, "}")),用jsg::NonModuleScript::compile编译后在独立作用域内执行——与eval命令的语义类似。
执行结果的处理逻辑同样清晰:脚本正常执行完毕返回 0;抛出JsExceptionThrown时返回 11(对应退出码11 << 8写回状态管道);每轮结束后调用__sanitizer_cov_reset_edgeguard重置 SanitizerCoverage 的边守卫计数器,供 Fuzzilli 收集覆盖率反馈。
REPRL 的协议规范来自 src/workerd/tests/libreprl/libreprl.h(源自 V8 项目、Google 开源):单次可传输脚本上限REPRL_MAX_DATA_SIZE = 16 << 20(16 MB);32 位退出状态格式为[ did_timeout | exit_code | terminating_signal ],分别用RIFTIMEDOUT、REXITSTATUS、RTERMSIG提取。该库同时提供了reprl_initialize_context、reprl_execute、reprl_fetch_stdout、reprl_fetch_stderr、reprl_fetch_fuzzout等接口,供测试程序驱动 workerd 子进程。
验证 REPRL 是否可用
1. Bazel 单元测试
README 给出第一条验证路径——运行仓库内自带的 REPRL 集成测试:
bazel test --config=fuzzilli //src/workerd/tests:test-reprl-kj --repo_env=CC=/usr/bin/clang-19 --test_timeout=5 --test_output=all--config=fuzzilli:启用 Fuzzilli 专属构建配置(对应上面的WORKERD_FUZZILLI编译开关);//src/workerd/tests:test-reprl-kj:目标测试,源码位于 src/workerd/tests/test-reprl.c++,在 src/workerd/tests/BUILD.bazel 中声明;--repo_env=CC=/usr/bin/clang-19:指定 clang-19 作为编译器(SanitizerCoverage 与 Fuzzilli 通常依赖较新的 clang);--test_timeout=5:测试超时 5 秒,--test_output=all打印完整输出。
2. Fuzzilli 侧 REPRLRun 冒烟测试
若已具备 Fuzzilli 工程(Swift 编写),在 Fuzzilli 目录下执行:
swift run REPRLRun <path-to-workerd> fuzzilli <path-to-capnp-config> --experimental其中<path-to-workerd>指向编译出的 workerd 二进制(README 建议位于bazel-bin/src/workerd/server/workerd),fuzzilli为 profile 名称,第三个参数为 capnp 配置路径。REPRLRun会模拟 Fuzzilli 的 REPRL 驱动行为,向 workerd 发送脚本并检查返回状态,是确认"握手-执行-回报"链路是否打通的快速手段。
使用语料库进行大规模模糊测试
验证通过后即可启动正式模糊测试:
swift run -c release FuzzilliCli --inspect=all --profile=workerd <path-to-workerd-root>/bazel-bin/src/workerd/server/workerd --additionalArguments=<path-to-workerd-root>/samples/reprl/config-full.capnp,--experimental --storagePath=fs-new --jobs=30 --importCorpus=<path-to-corpus> --corpusImportMode=full --staticCorpus参数逐一拆解:
swift run -c release:以 release 模式运行 FuzzilliCli,保证模糊测试吞吐;--inspect=all:对所有匹配的 worker 进行状态检查与诊断输出;--profile=workerd:使用 workerd profile(与上面的 REPRLRun 冒烟测试保持一致的 profile 名);<path-to-workerd-root>/bazel-bin/src/workerd/server/workerd:被测试的 workerd 二进制路径;--additionalArguments=<capnp 配置>,--experimental:追加传给 workerd 的命令行参数。注意:README 中此路径写作samples/reprl/config-full.capnp,但当前仓库中该配置实际位于 fuzzilli/config-full.capnp,使用时应替换为正确的仓库相对路径;--storagePath=fs-new:Fuzzilli 状态存储目录,用于持久化语料库与崩溃样本;--jobs=30:并发执行 30 个 workerd 实例,最大化吞吐;--importCorpus=<path-to-corpus>+--corpusImportMode=full:全量导入已有语料库作为种子;--staticCorpus:语料库按静态方式维护,配合fs-new存储路径实现可复现的模糊会话。
扩展测试范围:如何把新 API 纳入模糊测试
README 给出了明确的扩展路径:"要测试某个 API,把它 import 到所使用的.js文件中"。完整操作流程如下:
- 修改入口脚本:在 worker-full.js 顶部
import目标 API(或内部模块),并在test()处理器中通过globalThis.xxx = xxx暴露到全局作用域,使 Fuzzilli 生成的脚本可以直接引用; - 若涉及平台绑定:在 config-full.capnp 的
bindings中新增对应 binding(如kvNamespace、r2Bucket、queue),必要时同步新增 mock 服务与 mock 脚本,遵循现有test-kv/kv-mock.js的范式; - 重新构建并冒烟测试:用
bazel test --config=fuzzilli //src/workerd/tests:test-reprl-kj与REPRLRun验证新 API 在 REPRL 模式下可用; - 运行模糊测试:使用 FuzzilliCli 命令,观察新 API 路径上的覆盖率与崩溃样本。
相关源码与测试索引
- 入口与配置:fuzzilli/config.capnp、fuzzilli/config-full.capnp、fuzzilli/worker.js、fuzzilli/worker-full.js
- mock 实现:fuzzilli/kv-mock.js、fuzzilli/d1-mock.js、fuzzilli/r2-mock.js、fuzzilli/analytics-mock.js、fuzzilli/queue-mock.js、fuzzilli/worker-consume-request.js
- REPRL 运行时实现:src/workerd/api/unsafe.c++(
Stdin::reprl,#ifdef WORKERD_FUZZILLI) - REPRL 协议库:src/workerd/tests/libreprl/libreprl.h、src/workerd/tests/libreprl/libreprl.c
- 集成测试:src/workerd/tests/test-reprl.c++、src/workerd/tests/BUILD.bazel
- 内部平台 API(D1 包装的来源):src/cloudflare/internal/d1-api.ts
小结
fuzzilli 目录为 workerd 提供了一条完整的"引擎级模糊测试"通道:capnp 配置决定 Worker 形态与 binding 注入,JS 入口脚本决定哪些 API 暴露给模糊器,Stdin.reprl()通过 REPRL 协议与 Fuzzilli 完成握手、脚本收发与状态回报。借助test-reprl-kj集成测试与REPRLRun冒烟命令可以快速验证接入正确性,而 FuzzilliCli 的--jobs、--importCorpus、--staticCorpus等参数则支撑起大规模、可复现的持续模糊测试流程。对于任何希望扩展模糊覆盖面的开发者而言,只需在worker-full.js与config-full.capnp中按既有范式追加 API 与绑定,即可把新的运行时特性纳入自动化漏洞挖掘体系。
【免费下载链接】workerdThe JavaScript / Wasm runtime that powers Cloudflare Workers项目地址: https://gitcode.com/GitHub_Trending/wo/workerd
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考