研究

Rust 借用检查器曝出存活 11 年的 soundness 漏洞

rust-lang/rust #164250:safe 代码绕过带 Drop 结构体的生命周期约束触发 use-after-scope,已存活 11 年。

礁石与青苔之间一条锈蚀的铁链

2026 年 10 月 13 日,jsnell 在 rust-lang/rust 仓库提交 #164250 号 issue:一段不含任何 unsafe 的 Rust 代码通过了类型检查和借用检查,用 AddressSanitizer 运行却报 stack-use-after-scope。根因是借用检查器的一个盲区——「带 Drop 实现的结构体」上声明的生命周期约束,在协变字段赋值时被整个跳过。issue 开启当天一度标为 P-critical,Types 团队的 lcnr 复核后改定 P-high,理由写在置顶说明里:这个 bug 已经 11 年了。

要点

  • 纯 safe 代码即可绕过 struct 的生命周期约束:Q<T: 'static> 带手写 Drop 时,给协变字段赋值不会触发 T: 'static 检查,drop 里可把非 'static 引用当 &'static 用,ASan 报 stack-use-after-scope。
  • 漏洞很老:issue 正文确认 1.85.0 至当前 nightly 均可复现;theemathas 的精简版自 1.18.0(2017)起可编译,maxdexh 的 closure 变体自 1.4(2015)起段错误或打印已释放内存,lcnr 称它已存在 11 年。
  • 修复未落地:PR #164277 已挂出尚未合入;issue 开启当天被锁定;HN 讨论约 700 分、386 条评论(2026-10-14 读取)。

背景

Rust 的核心承诺是编译期的:不写 unsafe,就不会内存不安全。打破这条承诺的编译器缺陷在 rust-lang/rust 里有专门的 I-unsound 标签,归 T-types 团队处理。weinzierl 在 Hacker News 数过:仓库里挂着 100 多个 I-unsound issue,一些已存在多年,这次的事件「引人注目,但并不稀奇」。他还提醒了两条参照系:C++ 里同型代码本来就是未定义行为,Rust 的差别在于把它变成「编译器的 bug」而非「使用者的错」。编程语言基础设施这段时间并不平静,本月初 Zig 因 CI 问题宣布迁出 GitHub 并禁止 AI 贡献;Rust 这次的不同在于,被动摇的是安全承诺本身。

事实部分

漏洞的形状不复杂。struct Q<T: 'static>(Option<T>) 声明字段类型必须满足 'static,并为它手写了一个 Drop 实现——报告者的版本把值推进一个 static 存储,再读出来。问题出在赋值那一刻:&'a String 对生命周期 'a 协变,q.0 = Some(&l)(l 是局部 String)会悄悄把字段的生命周期缩到局部作用域,编译器没有按 struct 上的 T: 'static 重新检查。等 q 离开作用域,drop 里拿到的已经是一个非 'static 引用,代码却按 &'static String 用——栈上早已销毁的对象被读了出来。theemathas 的精简复现把这一点摆得更直接:drop 里拿到的是非 'static 的 &String,类型系统却放行你当 'static 用。

maxdexh 随后证明问题比字段赋值更宽:他的 mem::swap 经函数指针强转、交换两个数组元素的变体说明类型统一在更多路径上同样不查类型的生命周期 bound,他并认为这类类型本应判为 ill-formed(此变体不带版本主张);另一个 FnMut closure 变体则「自 1.4 起」段错误、打印已释放内存。版本考古汇总如下,想自己动手验证可以用 issue 正文附带的 Godbolt 链接:

复现变体最早可复现版本证据
jsnell 原始报告1.85.0 至当前 nightlyissue 正文,附 Godbolt 链接
theemathas 精简版1.18.0(2017)起可编译贡献者在 issue 中自测
maxdexh closure 变体1.4(2015)起段错误或打印已释放内存贡献者在 issue 中自测

优先级的处理过程写在 issue 置顶说明里:lcnr 确认这类问题本应定 P-critical,之所以「只是 P-high,是因为它已经 11 年了」——团队判断一个存活这么久的 soundness 缺陷大概率没在实际代码里造成过事故,触发它需要一个「故意写坏的 Drop 实现」。issue 已于 10 月 13 日被管理员锁定,理由是防止垃圾评论;挂出的修复 PR #164277 尚未合入,theemathas 同时标出疑似同源的 #128225。截至发稿,官方渠道没有出现 CVE 编号——个别第三方博客用 CVE 字眼描述此事,与 issue 现状不符。

各方说法

HN 讨论截至 2026-10-14 约 700 分、386 条评论,争的是定性:这是编译器 bug,还是类型系统设计缺陷。GrantMoyer 认为编译器「本应根据生命周期规则拒绝这段代码」,是实现层的问题,修起来不难;foldr 给出机制解释——编译器没有对代码块末尾隐式插入的 drop 调用做类型检查,而正确的代码不会依赖这个缺失的检查。反方也存在:csande17 等人指出 Rust 没有正式规范,所谓「类型系统的规则」只存在于共识里,严格说是整个实现未经形式化验证。nextaccountic 说得更直白:Rust 不是定理证明器,编译器 bug 就是普通的逻辑 bug——本站报道过用 Lean 证明器逐步核对推理的 AlphaProof Nexus,我们对这种「验证过的保证」并不陌生。修复的代价也有争论:bluGill 的「依赖该 bug 的代码本来就无效」一说他自己承认是反讽;afdbcreid 援引 Rice 定理反驳说完美编译器不存在,并补充:就其他一些已知的 soundness 问题而言,可能的出路确实包括拒绝当前能编译的程序,但未必是这一个。Rust 的现行规则是这类修复可在任意 semver 版本落地,配合 crater run 评估生态影响。

编辑判断

这件事对两类人最要紧:审计安全关键 Rust 代码的人,和手写 Drop 的作者。审计时,「生命周期 bound + 手写 Drop」是一个值得多看一眼的组合;写 Drop 时,别把字段里的引用当成满足 struct bound 的引用来用——这次证明了编译器不会替你查。更值得记录的是 triage 逻辑本身:一段纯 safe 代码即可触发的内存不安全,因为「活了 11 年」而定为 P-high。这是把「多年没出事」当作「不太会出事」的证据。weinzierl 的说法算是对这种折中最平实的辩护:safe Rust 的承诺真实存在,实现并不完美,所有已知漏洞都可修。诚实的另一面是:目前没有证据显示真实代码受影响,而修复本身也可能拒编译当前能通过的程序。在 PR #164277 的 crater 结果出来之前,谁也说不准生态要付多大代价。

接下来

  • PR #164277 的合入与 crater run 结果:修复会不会打破下游 crate,答案会出现在 PR 讨论和 rust-lang/rust 的 crater 通知里。
  • issue 解锁后的后续:#128225 是否确认为同一根因,优先级是否再调整。