Bun 团队于 5 月 21 日发布了一份关于其尚未发布的 Rust 重写版本的全面安全审计报告。这份由 AI 辅助生成的审计报告显示,Bun 的 Rust 代码库中共有 13,365 个 unsafe 语法节点,分布在 774 个文件、51 个子系统之中。经过逐项分类评估,团队认为其中约 69.4%(约 9,300 个)最终可以转换为安全代码,仅约 30.6%(约 4,000 个)必须保留为 unsafe。

这份审计的核心发现可以从两个维度来理解。从来源分布看,unsafe 块主要集中于三个根因:
- 33.9% 来自 Zig 时代的ownership惯用法(如原始指针状态机、父节点回指指针、手动引用计数),这些模式随着从 Zig 到 Rust 的移植而被直接带入了新代码库;
- 29.8% 来自 FFI 边界,即调用 JavaScriptCore、uWebSockets、libuv、BoringSSL、zlib 等 C/C++ 库时天然需要 unsafe;10.6% 来自事件循环回调机制中同线程重入导致的别名化问题。
- 只有约 3%(412 个站点)的 unsafe 用于真正的性能优化场景——跳过边界检查、未初始化缓冲区、SIMD intrinsic 等。
从处置策略看,这 13,365 个 unsafe 块被归为五种结局:无需降速即可安全化(46.9%)、添加一处检查后安全化(16.3%)、需要重新设计相关类型才能安全化(6.2%)、保留 unsafe 但在内部统一封装(23.2%)、以及本就正确、无需改动(7.4%)。
审计还发现了五个实际存在的声超(soundness)漏洞——即无需借助任何 unsafe 代码,仅从 safe Rust 即可触发未定义行为。典型案例包括 RacyCell<t> 上的无条件 Sync 实现,该实现将"单线程或外部同步"这一契约仅托付于注释而在 safe 代码中产生数据竞争;以及 VirtualMachine::as_mut(&self) -> &mut VirtualMachine 这类自引用访问器约定,在 safe 代码中会导致引用别名化。
在与业界同类项目的横向对比中,Bun 当前每千行 Rust 约含 13.7 个 unsafe,而 Deno 主仓库(仅 TypeScript 运行时部分)为 5.8、Deno 加上 rusty_v8 引擎绑定为 8.4、Tokio 异步运行时为 6.5、Wasmtime 为 5.5。值得注意的是,Bun 将所有 bindings 和 runtime 均置于同一 workspace,而 Deno 将引擎绑定拆分为独立的 rusty_v8 且大量 runtime 逻辑用 TypeScript 编写,二者并不完全可比。按照审计报告的清理计划执行完毕后,Bun 的 unsafe 密度预计可降至每千行 4.2,接近 Wasmtime 水平。
整个审计采用了一套严格的方法论:先用 ripgrep 枚举所有 unsafe 语法作为 ground truth,再由两轮独立的人工分类加对抗审查,最后由仲裁者逐条裁定分歧,最终四种独立划分方式的总和均精确等于 13,365。审计本身由语言模型 agent 管道执行,但所有 46 处 file:line 引用均经人工核实。
团队提出的清理路线图包含八个步骤,依序推进:第一步为 RefPtr<t> 补充拥有所有权的 RAII 形式 OwnedRef,解除对剩余清理工作的阻塞;第二步引入检查型 Cell 替代 JS 可重入状态的原始指针,使 2,192 个 unsafe 块变得声音;第三步对持有跨重入 borrow 的约 16 种核心类型实施四类重新设计(提前拷贝、拆分结构体、延迟回调、外部化状态),清除 822 个 unsafe;第四步为纯函数型 C 接口重新声明为 safe fn,清除 1,829 个;第五步以引用计数替代手动父子指针,清除 1,107 个;第六步引入类型化回调注册机制,清除 954 个;第七步以标准库惯用法(Box/Once/Span 等)处理剩余模式,清除 2,376 个。全部完成后,unsafe 块将从 13,365 降至约 4,000。
此次审计是 Bun 从 Zig 实现向全面 Rust 重写进程中的一次重要技术复盘。团队选择将所有 13,365 个 unsafe 逐条公开分类而非简单压制,既是对开源社区的问责,也是一份关于大型代码库渐进式 Rust 化及 unsafe 代码管理的工程教材。
参考来源:https://bun.com/bun-unsafe-audit