TUESDAY, OCTOBER 13, 2026|No. 18551
Rust · Programming Language

Long-standing Rust borrow checker bugs raise soundness concerns

Multiple critical soundness bugs, some over a decade old, have been discovered in Rust's borrow checker, prompting renewed focus on the language's memory safety guarantees.

A visual representation of code and compiler symbols, symbolizing programming and its complexities.
A visual representation of code and compiler symbols, symbolizing programming and its complexities. · Photo by Florian Olivo on Unsplash
1 sources
Pipeline ingest
3 reads
Positive / Neutral / Negative
0 countries
Related coverage

struct's lifetime requirements can be bypassed via covariant field assignment

Description

In the following code:

use std::sync::Mutex;
static KEPT: Mutex>> = Mutex::new(Vec::new());
struct Q(Option);
impl Drop for Q {
 fn drop(&mut self) { if let Some(t) = self.0.take() { KEPT.lock().unwrap().push(Box::new(t)); } }
}
fn main() {
 {
 let l = String::from("hello world, this is long enough");
 let mut q = Q(None);
 q.0 = Some(&l);
 }
 let _z = String::from("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");
 let k = KEPT.lock().unwrap();
 println!("{}", k[0].downcast_ref::().unwrap());
}

I expected to see this happen: program is not accepted

Instead, this happened: program compiles and prints XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX. With asan, the program will crash with a AddressSanitizer: stack-use-after-scope

Meta

Issue on current nightly, and stables at least down to 1.85.0

Godbolt with asan: https://rust.godbolt.org/z/83MvfbKde

Godbolt without asan: https://rust.godbolt.org/z/GsGdqqMWe


Note:

Given that this issue has been shared online and is currently getting quite a few visitors, here's some additional context.

Rust's type system is intended to prevent memory unsafety. Upholding this promise is something we take very seriously, which is why we've been working on things like the next-generation trait solver, the region constraint rework, and more generally Project Zero. It is a large priority of the Types Team to fix all type system unsoundnesses and to work towards there being a solid foundation to minimize the risk of unknown ones.

This bug is a P-high issue and would have been P-critical regardless of whether people encountered it in the wild. The reason that it is "only P-high" is that it's 11 years old. A bug like this which has not been found during the last 11 years cannot actually be that easy to hit.

Looking at the bug itself, it is also very likely that this unsoundness has not caused memory unsafety by accident. While it looks easy to trigger, to exploit this you need an intentionally malicious Drop implementation.


The core of the unsoundness:

struct Q(Option);

impl Drop for Q {
 fn drop(&mut self) {
 // Code in here has access to a non-'static &String
 // but assumes that it's a &'static String
 }
}

fn main() {
 let l = String::new();
 let mut q = Q(None);
 q.0 = Some(&l);
}

My version of the code has been compiling ever since 1.18.0 (2017)

How on earth was this not found before this???

Possibly related: #128225

struct MyType(&'c mut &'a str, &'b str);

impl Drop for MyType {
 fn drop(&mut self) {
 *self.0 = self.1;
 }
}

fn main() {
 let mut me_out = "rarw";
 {
 let l = String::from("some cool string");
 let mut q = MyType(&mut me_out, "");
 q.1 = &l;

 }
 println!("me_out: {me_out}");
}

Another version that segfaults/prints freed memory since 1.4

struct Q(Option, F);

impl Drop for Q {
 fn drop(&mut self) {
 self.1(self.0.as_ref().unwrap())
 }
}

fn main() {
 let mut out: Option = None;
 {
 let l = vec![vec![1]];
 let mut q = Q(None, |s| {
 out = Some(*s);
 });
 q.0 = Some(&l);
 }
 println!("{:?}", out);
}

Another version, cuz why not. I think this version is probably the easiest to understand.

use std::time::Duration;
use std::thread;

struct Q(Option);

impl Drop for Q {
 fn drop(&mut self) {
 // Code in here has access to a non-'static &String
 // but assumes that it's a &'static String
 let wrong: &'static String = self.0.unwrap();
 thread::spawn(move || {
 thread::sleep(Duration::from_secs(1));
 println!("{}", wrong);
 });
 }
}

fn main() {
 {
 let l = String::from("abc");
 let mut q = Q(None);
 q.0 = Some(&l); // q.0 is supposed to be a &'static String, but we're assigning it a non-'static &String
 }
 thread::sleep(Duration::from_secs(2));
}

I think unification just generally doesn't check lifetime bounds of the type. Example with unifying coercion, without field assignment:

struct Q(T, F);

impl Drop for Q {
 fn drop(&mut self) {
 self.1(self.0)
 }
}

type T = Vec>;

fn main() {
 let long = Box::leak(Box::new(T::new())) as &'static T;
 {
 let local: T = vec![vec![1]];

 // item type is Q, which is ill-formed
 let mut unified = [
 Q(
 long,
 (|s: &'static T| {
 std::thread::spawn(move || {
 println!("{s:?}");
 });
 }) as fn(&'static T),
 ),
 Q(&local, (|_: &T| {}) as fn(&T)),
 ];

 let [l, r] = &mut unified;
 std::mem::swap(&mut l.0, &mut r.0);
 }

 std::thread::sleep_ms(1000);
}

PAN's pipeline reviewed approximately 1 open sources for this article. No human editor reviewed this article before publication.

Related Reads

Show on timeline →

Earlier on PAN

More in Technology →