Rust 多年来主推的核心卖点是内存安全。但近期有开发者公开了一个能让这一承诺出现裂缝的类型系统缺陷,并且它已在编译器中潜伏了十一年之久。
一个被隐藏十一年的类型系统缺陷
2026 年 10 月 13 日,开发者 jsnell 在 rust-lang/rust 仓库提交了 issue #164250。这一标题较长的报告用一句话即可概括,即结构体的生命周期约束能够通过协变字段赋值被绕开。在 Rust 的 issue 标签体系中,它被标注为 I-unsound(soundness hole),属于类型系统中最严峻的一类问题,含义是「编译器放行了本应拒绝、且会破坏内存安全的程序」。
更令社区震动的是,有人追溯发现这个 bug 至少从 2017 年的 Rust 1.18.0 起就能编译通过,而更早的变体甚至可以上溯至 Rust 1.4。一个潜藏如此之久的底层缺陷,迅速引发了广泛讨论。
漏洞如何被绕过:协变、字段赋值与 Drop 三者叠加
该问题的结构颇为精巧,需要协变(variance)、字段赋值与 Drop 三件事同时具备。复现代码的大致形态如下:
struct Q<T: 'static>(Option<T>);impl<T: 'static> Drop for Q<T> { fn drop(&mut self) { // 这里拿到的应该是 &'static String }}fn main() { let l = String::new(); let mut q = Q(None); q.0 = Some(&l); // 编译器居然放行了}
关键就在 q.0 = Some(&l) 这一行的赋值操作。由于 Option<T> 相对 T 具有协变性,借用检查器在「合一/强制」阶段,把生命周期较短的 &'a String 当作生命周期为 &'static String 的子类型,直接进行了统一处理。
问题在于,这一过程并未重新校验 T: 'static 所设定的上界约束。最终结果是,Drop::drop 通过字段读取到的是一个寿命很短的引用,却被当作 'static 引用使用。
不止能通过编译:悬垂引用被偷渡出去
仅仅能够通过编译还不够触目惊心,原始 PoC 进一步将它转化为可以看得见的实际危害。作者预备了一个名为「保险箱」的结构:
use std::sync::Mutex;static KEPT: Mutex<Vec<Box<dyn std::any::Any + Send>>> = Mutex::new(Vec::new());struct Q<T: 'static + Send>(Option<T>);impl<T: 'static + Send> Drop for Q<T> { fn drop(&mut self) { if let Some(t) = self.0.take() { KEPT.lock().unwrap().push(Box::new(t)); } }}
其 Drop 实现会把原本应随 l 一同销毁的引用装箱后放入全局 static 中保存。等到 l 离开作用域、对应的栈内存被释放之后,再从 KEPT 里把它取出,读到的便是已经释放的内存内容。
打印出的那段 XXXX… 占位串并非原本的字符串,而是 l 释放后被其他变量占用了同一栈槽留下的痕迹。如果开启 AddressSanitizer 运行,程序会直接因 stack-use-after-scope 而崩溃。
Rust 团队为何回应「别慌」
issue 走红之后,Rust 类型组成员 lcnr 少见地亲自出面为事件降温,他的判断包含两点。
第一,这个 bug「即便没在野出现过,本也应是 P-critical」;之所以只定为 P-high,主要原因在于它已存在 11 年仍未被撞上,触发条件并非随手就能命中。
第二,也是最关键的一点:想要真正利用它,开发者必须主动编写一个蓄意用于攻击的 Drop 实现。也就是说,如果只是写正常的业务代码,几乎不可能「无意之中」制造出内存不安全。
因此团队给出的结论十分明确:问题严重,但现实中尚未造成意料之外的内存不安全。
评论区很快贴出了多条关于不变量的分析思路。有开发者指出,更普遍的病灶在于「合一(unification)过程普遍不检查类型的生命周期边界」,这一点比单条字段赋值更值得警惕。也有人提供了不依赖 static 的变体,证明该问题同样可以现身于更常见的代码路径中。外部仓库(非 Rust 官方)甚至出现了一个朝「在两个前端都拒绝聚合存储里的句柄」方向的修复提交。
值得一提的是,由于讨论过热,该 issue 被 Rust 官方多次上锁、解锁、再上锁,从侧面反映出开发者对「Rust 内存安全承诺被撕开口子」一事有多敏感。
给 Rust 开发者的提醒与后续方向
如果你正在编写的代码牵扯到 Drop 与泛型生命周期的搭配,特别是采取把引用存进「保险箱」这类模式,现在不妨多留意几分。
从更长的时间维度看,Rust 团队正在推动下一代 trait solver、region 约束重构以及 Project Zero,目的正是系统性地收敛这类 soundness 隐患。某个隐藏了十一年的裂缝被公之于众,未必就是坏事。它至少说明:「内存安全」是 Rust 借助极其复杂的类型系统所换来的承诺,而该系统自身同样需要持续接受审计。
原文引用了 jsnell 的文章《struct's lifetime requirements can be bypassed via covariant field assignment》,链接为 https://github.com/rust-lang/rust/issues/164250。
小结:Rust issue #164250 显示,协变字段赋值叠加 Drop 可在满足 T: 'static 约束的结构体上骗过借用检查器,原始 PoC 能在启用了 AddressSanitizer 的环境下以 stack-use-after-scope 形式崩溃。但 Rust 方面判断,只有主动编写恶意 Drop 才可利用,正常业务代码难以无意触发,该缺陷仍应被视为严重却未在现实中被利用的类型系统问题。
