一条完全不需要写 unsafe 的标准 Rust 代码,直接把已释放的栈内存读了出来,甚至在 AddressSanitizer 诊断下当场触发了段错误。
2026年10月13日,GitHub 的 rust-lang/rust 仓库出现编号为 #164250 的 Issue。报告指出,开发者仅需通过对一个协变字段直接赋值,就能彻底骗过 Rust 的借用检查器,绕过外层结构体原本强加的生命周期边界。官方团队迅速打上 I-unsound 标签,定级为 P-high 并交由类型系统团队主导排查。这不是常见的边界溢出,而是一个在类型推断最底层潜伏了近11年的逻辑盲区。
纯Safe代码里的幽灵指针
Rust 对外界许下的核心承诺极其坚固:只要代码里没有显式写出 unsafe,编译器就会负责证明所有内存操作的绝对安全,未定义行为(Undefined Behavior)绝无立足之地。
然而在 Issue #164250 给出的验证代码里,整套防线被几行极简的代码穿透。开发者定义了一个带有泛型约束的包装结构体 Q<T: 'static + Send>,并在析构函数 Drop 中将内部数据转移到一个全局静态向量里。随后在局部作用域内,仅通过 q.0 = Some(&local) 对公共字段直接覆写,编译器便接受了这段赋值。
当局部作用域结束,Drop 析构器照常执行,本应随栈帧销毁的短生命周期引用,被顺利存进了静态变量。作用域外分配的新字符串直接复用了原有的栈地址,程序输出了一整串意料之外的覆写数据。
在通常认知中,Rust 借助变型(Variance)和生命周期子类型化严格构筑安全边界:可变借用具有不变性,原本不能将短生命周期的引用强行塞给更长生命周期的位置。但当这套规则遇上字段直接赋值与析构器协同运作时,编译器漏算了外层类型强加在内部的完整边界。
潜伏十一年的工程回声
如果这只是某次大版本重构新引入的倒退,它不过是一次常规的发布事故。但这个漏洞的严重性在于其惊人的存活寿命。
社区最初用精简代码将其回溯至 2017 年的 Rust 1.18.0,随后更进一步的改写证实,早在 2015年10月29日 发布的 Rust 1.4.0 里,这段漏洞代码就已经能顺利编译并触发悬垂指针。这意味着它在整个 Rust 生态的眼皮底下,安全无虞地潜伏了近11年。
虽然不少讨论容易将其与 2015 年 5 月提出来的那个关于隐含边界(implied bounds)的悬挂 Issue #25860 混为一谈,但编译器维护者已经澄清,两者的类型系统调用链并不完全重合。
更加剧紧迫感的是,在随后的讨论中,社区安全研究者进一步构造出了具备实际攻击潜力的验证代码。通过精确布局内存分配、复用被释放结构中的函数指针,或者通过标准输入填充释放后的内存,攻击者已经能实现调用劫持。这证明它绝不仅仅是理论上的逻辑不自洽,而是一枚能够转化为现实利用的可执行武器。
边界与盲区的双面镜像
面对这颗埋藏了长达十一年的暗雷,不同阵营呈现出了截然相反的解读姿态。
悲观者认为,Safe Rust 苦心经营的绝对安全神话彻底破产。它血淋淋地展示了一个残酷逻辑:即便是纯 Safe 代码,在底层类型系统规则自身存在破损时,同样无法阻挡任意代码执行的渗透。
类型系统的严密性,终究受制于编译器实现的工程极限。
务实派则给出了完全相反的评估。十一年的漫长时间,从另一个角度说明了该漏洞的触发条件有多么苛刻:需要同时凑齐特化的生命周期泛型约束、直接对公开字段的覆写赋值,以及刚好能利用这层时机逃逸的析构函数。在任何正常的工程逻辑里,极少有人会写出这种结构畸形的代码。
- 风险.官方团队在推出修复补丁的同时,必须动用全量生态测试工具 Crater 进行全局回归检测,避免修补底层逻辑时破坏合法代码。
这印证了经典的工程悖论。再精巧的形式化验证设计,落在数百万行由人手写就的 C++ 或 Rust 编译器实现中,依然难以逃脱工程盲区。从这个意义上看,将代码里的 unsafe 排查殆尽只是内存安全治理的起点,类型系统健全性(Soundness)与编译器实现本身的纠偏,才是一场永无止境的拉锯战。
