Bun 的实验性 Rust 移植近期引发技术社区对内存安全边界的大规模讨论。官方审计报告显示,该移植代码库包含 13,365 个 unsafe 语法点,分布于 774 个 Rust 文件、51 个子系统中。这一数字与同为 Rust 实现的 Python 包管理器 uv 的 73 个 unsafe 调用形成鲜明对比,但深入分析后发现,Bun 的 unsafe 分布呈现明显的结构性特征 —— 其中约 70% 可通过工程化手段安全化,剩余 30% 则集中于 FFI 边界等不可规避场景。
数字背后的结构性来源
Bun 的 unsafe 块并非随机散布,而是高度集中于三类根源:Zig 遗留代码模式(4,530 处,占 33.9%)、FFI 边界调用(3,986 处,占 29.8%)、以及事件循环回调(1,413 处,占 10.6%)。这三类合计占据总量的三分之二以上。
Zig 遗留代码是最大单一来源。Bun 最初以 Zig 实现,而 Zig 的内存模型与 Rust 的所有权系统存在本质差异。移植过程中,大量 *mut Self 状态机、父对象反向指针、手动引用计数等模式被直接平移,导致 Rust 编译器无法验证其安全性。例如,异步对象以 *mut Self 寻址是因为嵌套调用可能释放对象,或 JavaScript 重入会违反 &mut 唯一性规则 —— 这在 Zig 和 C++ 中合法,但在 Rust 中必须通过 unsafe 块显式标注。
FFI 边界调用是第二大来源。Bun 与 JavaScriptCore、uWebSockets、uSockets、libuv、BoringSSL、c-ares、zlib、libarchive、lol-html、mimalloc 等十个 C/C++ 库存在深度集成。根据 Rust 的语义,调用任何 C 函数天然是 unsafe 的,因为编译器无法证明指针生命周期跨越调用边界,或保证 C 代码不会重入。这 3,986 处 FFI 调用并非代码质量问题,而是跨语言互操作的固有成本。
事件循环回调(1,413 处)则反映了异步运行时与 JavaScript 引擎交互的复杂性。当 uWS、libuv 或 c-ares 的回调通过 void* userdata 回传时,代码需将裸指针恢复为 &mut Self,这一过程天然跨越安全边界。
安全化路径:从 13,365 到约 4,000
官方审计将 unsafe 块按消除成本分为五类:无性能损耗即可安全化(6,274 处,46.9%)、需增加一次检查(2,184 处,16.3%)、需重新设计类型(822 处,6.2%)、可集中封装保留(3,097 处,23.2%)、以及作为正确性保留(988 处,7.4%)。综合计算,约 9,300 处(69.4%)具备安全化路径。
Cell 替换裸指针:对于 2,455 处 *mut Self 状态机模式,审计建议采用 Rc<T> 或 RefPtr<T> 配合 Cell<T> 进行重构。对于 Copy 类型的标量字段,此转换无运行时开销,编译器生成的机器码与裸指针版本完全一致。对于非 Copy 类型,可采用 JsCell::with_mut 将 &mut 借用窗口收缩至单个闭包内,以可审计的方式管理别名风险。
safe fn 重声明:针对 3,107 处纯函数式 C 调用(如长度计算、哈希更新),可通过 Rust 2024 的 unsafe extern { pub safe fn ... } 语法重新声明为安全函数,使调用点自动脱离 unsafe 块。审计识别出约 1,829 处此类调用可在无性能损耗下完成转换。
RAII 包装与所有权句柄:对于状态式 FFI 调用(如文件句柄、SSL 上下文),审计建议为每个句柄类型创建 RAII 包装(如 OwnedX509、JpegHandle),将 unsafe 集中至构造函数与析构函数,而调用点使用安全 API。这可将 1,170 处分散的 unsafe 调用集中至约 200 处库级封装。
回调类型化注册:针对 1,538 处回调 userdata 恢复模式,可通过泛型化注册机制(如 uws::ExtSlot<T>、bun_threading::ScheduledTask<C>)在注册与恢复时保持类型一致性,使回调体以安全 &self 接收参数,消除 954 处 unsafe 块。
必须保留的 unsafe:FFI 与性能边界
约 4,000 处(30.6%)unsafe 块被判定为必须保留,主要集中在以下场景:
FFI 边界不可消除:调用 C 函数本质上是 unsafe 的,因为 Rust 无法验证 C 端的契约。审计计划将这部分代码集中至 *_sys 辅助 crate,与 C 文档并列,形成单一可信边界。预计 1,700–1,800 处 FFI 调用将以这种集中形式保留。
GC 交互语义:JavaScriptCore 的 GC 管理对象不遵循 Rust 的所有权模型。JSString 等对象的存活由标记 - 清除机制决定,而非借用检查器。这 408 处 GC 相关 unsafe 反映了托管运行时与 Rust 所有权系统的根本张力。
性能关键路径:仅 412 处(3.1%)unsafe 纯粹为了性能,包括 get_unchecked、跳过 UTF-8 验证的转换、SIMD 内联汇编等。审计建议保留这些代码,并在关键路径添加容量断言作为防御性检查。
所有权转移点:from_raw_parts、assume_init、所有权转移的 Send 实现等标准库原语,其 unsafe 是 API 设计的一部分,约 988 处此类代码被判定为 "正确即保留"。
工程实践启示
Bun 的审计报告为大规模 Rust 迁移提供了可复用的方法论框架:
模式优先于计数:unsafe 块的绝对数量并非安全性的直接度量,关键在于其分布模式。Bun 的 13.7 处 / 千行 unsafe 密度在审计后预计降至 4.2 处 / 千行,与 Deno(5.8)和 Wasmtime(5.5)处于同一量级,显著低于纯绑定层项目如 rusty_v8(38.4)。
声明式安全注释:Bun 强制要求每个 unsafe 块携带 // SAFETY: 注释,并由 deny 级 lint 强制执行。这一实践使审计团队能够快速追溯每个 unsafe 块的契约依据,是规模化代码审查的基础设施。
分阶段收敛策略:审计将安全化工作划分为八个阶段,从修复 unsound 安全函数(零 unsafe 计数变化)到逐步引入 Cell、safe fn 重声明、RAII 包装等。这种渐进式路径避免了 "大爆炸" 重构的风险,允许在保持功能的同时逐步收敛安全边界。
跨语言边界的集中封装:FFI 代码的安全化不在于消除 unsafe,而在于将其集中至最小可信边界。通过 *_sys crate 将 C 调用与 Rust 业务逻辑隔离,可在不牺牲性能的前提下实现安全性的可审计性。
资料来源
- Bun 官方 unsafe 审计报告:https://bun.com/bun-unsafe-audit
- Fenado AI 对比分析:https://fenado.ai/articles/buns-experimental-rust-port-shows-13000-unsafe-calls-dwarfing-uvs-73
内容声明:本文无广告投放、无付费植入。
如有事实性问题,欢迎发送勘误至 i@hotdrydog.com。