5 月 21 日,Bun 团队公布了一份针对其尚未发布的 Rust 重写版本的安全审计报告。这份借助 AI 辅助完成的审计,把整个代码库翻了个底朝天——结果令人既振奋又警醒:13,365 个 unsafe 语法节点散布在 774 个文件、51 个子系统里,但经过逐项评估,约 69.4%(约 9,300 个)最终可以转为安全代码,真正必须保留的只有约 30.6%。
这意味着什么?一个追求极致性能的运行时项目,在用 Rust 重写时大量使用了 unsafe,但其中近七成并非出于真正的底层需求——它们是习惯、便利或迁移遗留的产物。
为什么 unsafe 这么多
Bun 的核心定位是高速 JavaScript 运行时,底层需要直接操作内存、调用系统 API、与 C 库交互。这些场景确实需要绕过 Rust 的安全检查。但审计揭示的现实是:大量 unsafe 块并非出现在 FFI 边界或裸指针操作中,而是被用在完全可以由编译器守护的地方。
常见的原因包括:
- 迁移惯性:从 C/Zig 转向 Rust 时,开发者习惯性地用
unsafe复制原有逻辑,而非重新用安全抽象表达 - 性能焦虑:担心安全代码有额外开销,提前用
unsafe"优化",但实际并未测量差异 - API 不熟悉:Rust 标准库和生态提供了很多安全替代(如
slice::swap、Cell、RefCell),但开发者没找到就直接写了裸操作
审计的分类评估把这些原因逐个拆开了。
七成可移除:具体怎么分类的
审计不是简单数个数。团队对每个 unsafe 块做了分类,核心判断标准是:这个块是否在做只有 unsafe 才能做的事?
Rust 中真正需要 unsafe 的操作只有五类:
- 解引用裸指针
- 调用 unsafe 函数或方法
- 访问或修改可变静态变量
- 实现 unsafe trait
- 读写
Union字段
如果一个 unsafe 块内部只做了安全操作(比如普通的数组索引、结构体赋值),那这个 unsafe 标注就是多余的——它既没有解锁任何额外能力,也没有让编译器帮你检查这块代码的安全性。
审计发现,约 9,300 个 unsafe 块属于这种情况:它们包裹的代码完全可以用安全 Rust 表达,移除 unsafe 后功能不变,且获得了编译器的安全保障。
剩下约 4,000 个确实涉及裸指针、FFI 调用或内存布局操作,这些是 Bun 作为运行时无法回避的底层需求。
AI 辅助审计怎么做的
这份报告特别标注了"AI 辅助生成"。面对 13,000+ 个 unsafe 块,纯人工逐个审查几乎不可行。团队的做法是:
- 用工具自动提取所有
unsafe语法节点及其上下文 - 让 AI 模型根据上下文分类:该块是否包含真正的 unsafe 操作、是否有安全替代方案
- 人工复核 AI 分类结果中的关键项和边界情况
这不是让 AI 替你做安全决策,而是让 AI 做初筛和分类,人类做最终判断。对于这种大规模重复性分析任务,AI 的效率优势很明显。
实践:在自己的 Rust 项目中审计 unsafe
不管你是否在写运行时,如果你维护的 Rust 项目中有 unsafe,值得做一次类似的自查。下面是一个可以直接跑的流程。
第一步:统计 unsafe 块
用 cargo-geiger 扫描项目中所有 unsafe 使用情况:
# 安装 cargo-geiger
cargo install cargo-geiger
# 在项目根目录运行扫描
cargo geiger --all-features
输出会列出每个包中 unsafe 函数数、unsafe 块数、unsafe 表达式数。如果没装 cargo-geiger,也可以用更简单的方式快速统计:
# 统计自己代码(不含依赖)中的 unsafe 块数量
find src -name '*.rs' -exec grep -n 'unsafe' {} + | wc -l
# 列出每个文件中的 unsafe 行及行号
find src -name '*.rs' -exec grep -Hn 'unsafe' {} +
第二步:逐块判断是否必要
对每个 unsafe 块,检查其内部是否包含上述五类真正的 unsafe 操作。这里是一个自动化初筛脚本的思路:
"""
unsafe_audit.py — 初筛 Rust 源码中的 unsafe 块,标记可能可移除的项
用法:
python unsafe_audit.py /path/to/rust/project/src
输出每个 unsafe 块的位置和内部操作分类,标记"无真正 unsafe 操作"的块。
"""
import os, re, sys
TRUE_UNSAFE_PATTERNS = [
r'\*\w+', # 裸指针解引用,如 *ptr
r'as\s+\*\w+', # 转裸指针,如 as *const u8
r'extern\s*\{', # FFI 声明
r'static\s+mut', # 可变静态变量
r'union\s+\w+', # Union 定义
r'unsafe\s+trait', # unsafe trait
r'unsafe\s+fn', # unsafe 函数定义
]
def find_unsafe_blocks(content):
"""粗略提取 unsafe { ... } 块的内容"""
pattern = r'unsafe\s*\{([^}]*)\}'
return re.findall(pattern, content, re.DOTALL)
def classify_block(block_body):
"""判断块内是否包含真正的 unsafe 操作"""
has_true_unsafe = False
for p in TRUE_UNSAFE_PATTERNS:
if re.search(p, block_body):
has_true_unsafe = True
break
return has_true_unsafe
def audit_dir(src_dir):
results = []
for root, _, files in os.walk(src_dir):
for f in files:
if not f.endswith('.rs'):
continue
path = os.path.join(root, f)
with open(path) as fh:
content = fh.read()
blocks = find_unsafe_blocks(content)
for i, body in enumerate(blocks):
is_necessary = classify_block(body)
results.append({
'file': path,
'block_index': i,
'body_preview': body.strip()[:80],
'necessary': is_necessary,
})
return results
if __name__ == '__main__':
src = sys.argv[1] if len(sys.argv) > 1 else 'src'
results = audit_dir(src)
removable = [r for r in results if not r['necessary']]
necessary = [r for r in results if r['necessary']]
print(f"总 unsafe 块: {len(results)}")
print(f"可能可移除: {len(removable)} ({len(removable)/max(len(results),1)*100:.1f}%)")
print(f"可能需保留: {len(necessary)} ({len(necessary)/max(len(results),1)*100:.1f}%)")
print("\n--- 可能可移除的块 ---")
for r in removable[:20]:
print(f" {r['file']} #{r['block_index']}: {r['body_preview']}")
运行方式:
python unsafe_audit.py ./src
注意:这个脚本用正则做粗筛,无法处理嵌套大括号和宏展开等复杂情况。生产级审计需要用 Rust 的 AST 工具(如 syn crate)做精确解析。但作为第一步快速摸底,它足够用了。
第三步:移除不必要的 unsafe
确认某个 unsafe 块内部没有真正的 unsafe 操作后,直接删除 unsafe 关键字,保留块内代码。然后用常规测试验证行为不变。
对于确实需要 unsafe 的块,缩小其范围——只把真正需要 unsafe 的那一行包在 unsafe 里,而不是把整个函数或大段逻辑标为 unsafe:
// 之前:整块标 unsafe,范围过大
unsafe fn process_buffer(buf: *const u8, len: usize) -> Vec<u8> {
let slice = unsafe { std::slice::from_raw_parts(buf, len) }; // 这行才需要 unsafe
let mut result = Vec::with_capacity(len); // 这行不需要
result.extend_from_slice(slice); // 这行不需要
result
}
// 之后:unsafe 只包裹真正需要的操作
fn process_buffer(buf: *const u8, len: usize) -> Vec<u8> {
let slice = unsafe { std::slice::from_raw_parts(buf, len) };
let mut result = Vec::with_capacity(len);
result.extend_from_slice(slice);
result
}
Bun 审计的更深层启示
这份报告的价值不只是数字。它揭示了一个 Rust 生态的普遍问题:高性能项目在用 Rust 重写时,倾向于过度使用 unsafe。
Bun 原本用 Zig 编写,Zig 的安全模型与 Rust 不同——Zig 默认允许裸指针操作,只在显式标记时启用安全检查。迁移到 Rust 时,开发者自然会把原来的裸指针逻辑直接搬到 unsafe 块里。这能跑,但丢失了 Rust 的核心价值。
69.4% 可移除的比例说明:大部分 unsafe 不是性能必需,而是迁移残留和认知惯性。真正需要 unsafe 的底层操作(FFI、内存布局控制、SIMD 等)只占三成左右。
这对其他正在考虑 Rust 重写的项目是个重要参考:先写安全版本,测量性能,只在瓶颈处引入 unsafe。而不是一开始就大面积 unsafe,再回头审计。
给维护者的检查清单
如果你维护的 Rust 项目中有 unsafe,建议按以下步骤行动:
- 统计现状:用
cargo geiger或脚本统计 unsafe 块数量和分布 - 初筛分类:对每个块判断是否包含真正的 unsafe 操作,标记可疑的多余块
- 逐个移除:从最简单的块开始,删除不必要的
unsafe,跑测试确认 - 缩小范围:对必须保留的
unsafe,把块缩小到只包含真正需要的操作 - 添加安全注释:每个保留的
unsafe块上方写// SAFETY: ...注释,说明为什么这行代码是安全的——这是 Rust 社区的标准实践,也是后续审计的基础 - 定期复查:把 unsafe 审计纳入 CI 或发布流程,防止新增不必要的 unsafe
Bun 的审计证明了一件事:unsafe 不是性能的代名词,大部分时候它只是还没来得及重构的安全债务。把七成 unsafe 变成安全代码,既不牺牲功能,又让编译器帮你守住边界——这笔账,任何项目都该算一算。