Rust 借用检查器的健全性缺陷是怎么被发现和修掉的

Rust 编译器的类型检查实现本身存在健全性缺陷,也就是本该被拒绝的程序通过了检查。一项针对 rustc 健全性问题的实证研究收集了 2022 年 1 月到 2025 年 9 月之间报告的问题共 30 个,发现多数缺陷从相关特性引入时就已存在,要过很久才被发现。由隐含约束和 trait 对象触发的那部分会破坏内存安全。已经在用 Rust 写服务的团队不必恐慌,稳定版编译器通常拒绝这些问题程序,但需要理解这类缺口出现在哪里。

健全性缺陷和普通 bug 有什么区别

普通编译器 bug 是拒绝该接受的程序,表现是编译失败。健全性缺陷方向相反,接受该拒绝的程序,后果是编译产物带着未定义行为继续跑。

这个方向决定了严重程度。类型系统存在的意义就是让人可以信任「通过了检查」这个结论。这个结论一旦不成立,后面基于它的推理都要打折,包括一行 unsafe 都没写的代码。

研究给出的分类里,被隐含约束和 trait 对象触发的一类问题会落到内存安全上。关联类型以及生命周期与 trait 相互作用是另一些缺陷的集中地,这些写法在泛型库里很常见。

一处具体缺陷长什么样

一份报告中的例子用了这个模式。一个带 Drop 实现的结构体,泛型参数是函数指针类型,同时通过关联类型字段持有一个引用。函数返回实例之后,调用方释放被引用的 Box,析构函数再去读那个引用。

标准构建会拒绝这段程序。开启某个还在开发中的借用检查实现之后,它被接受了,运行后析构函数读到已释放的内存。问题出在活性分析取方差的路径上,判断某个局部变量是否存活时用的方差来自析构约束,而不是变量真实持有的类型。对带自定义 Drop 的泛型类型,这两者可以不一致。

这个例子展示了缺口的位置。借用规则没写错,是一处实现细节在特定组合下取错了信息。

为什么这类问题藏了这么久

缺陷和触发条件同时成熟才会暴露。特性刚引入时使用面窄,边缘组合自然也少。

研究里的生命周期分析印证了这一点,多数问题从特性引入就存在,跨越多个发布版本,需要很长时间才被发现。发现者往往是研究者、验证工具的使用者,或者碰巧写出特定组合的人。

工具链的能力边界是另一层原因。地址消毒器和 Miri 能抓到导致内存错误的健全性缺陷,因为直接检查运行时内存行为。研究同时指出,其他类别的健全性问题,比如类型层面的不一致,这些工具抓不到,而用于形式化推理的工具还不够成熟。

对你的项目意味着什么

  1. 把编译器警告和 lint 当成真实信号。健全性缺陷常出现在相邻的边界条件里,提前拦下比重构时才发现便宜得多。
  2. 在测试与 CI 里跑 Miri。它对涉及 unsafe、裸指针和复杂生命周期的代码尤其有价值,能在合并前抓到越界和释放后使用。
  3. 谨慎使用 nightly 的前沿借用检查选项。上面那个例子在稳定版被拒绝,只在开发中的实现下通过。
  4. 关注依赖的泛型库对关联类型和 trait 对象的用法。
  5. 保留版本回滚能力。修复随版本发布,锁定版本号并记录升级影响面。

常见问题

这算不算 Rust 不会内存安全的证据。不算。它说明类型检查的实现有缺陷,语言设计承诺的是理想化的规则。

我写的代码里没有 unsafe,需要担心吗。需要关注但不必恐慌。有些缺陷确实能影响不含 unsafe 的代码,不过稳定版通常拒绝这些问题程序。

应该改用别的语言吗。这类问题在其他编译器的优化和类型检查里同样存在,保持工具链更新并覆盖高风险代码更实际。