11 year old unsoundness bugs found in Rust borrow checker
struct's lifetime requirements can be bypassed via covariant field assignment #164250 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::<&'static String>().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 Activity jsnell added C-bugCategory: This is a bug.Category: This is a bug. on Oct 13, 2026 rustbot added needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triagingThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging on Oct 13, 2026 theemathas added I-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/SoundnessIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness on Oct 13, 2026 rustbot added I-prioritizeIssue needs a team member to assess the impact. Will be replaced by P-{low,medium,high,critical}Issue needs a team member to assess the impact. Will be replaced by P-{low,medium,high,critical} on Oct 13, 2026 theemathas added A-borrow-checkerArea: The borrow checkerArea: The borrow checker T-typesRelevant to the types team, which will review and decide on the PR/issue.Relevant to the types team, which will review and decide on the PR/issue. and removed needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triagingThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging on Oct 13, 2026 theemathas commented on Oct 13, 2026 theemathas on Oct 13, 2026 Last edited by theemathas Contributor More actions 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); } 👀React with 👀3Reacted by Daniel Vogelbacher, Serhiy and BioTomateDE theemathas commented on Oct 13, 2026 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); } theemathas added A-destructorsArea: Destructors (`Drop`, …)Area: Destructors (`Drop`, …) on Oct 13, 2026 rustbot removed I-prioritizeIssue needs a team member to assess the impact. Will be replaced by P-{low,medium,high,critical}Issue needs a team member to assess the impact. Will be replaced by P-{low,medium,high,critical} on Oct 13, 2026 rustbot commented on Oct 13, 2026 rustbot on Oct 13, 2026 · Hidden as outdated show comment More actions rustbot commented on Oct 13, 2026 rustbot added P-criticalCritical priorityCritical priority on Oct 13, 2026 theemathas commented on Oct 13, 2026 theemathas on Oct 13, 2026 Last edited by theemathas Contributor More actions My version of the code has been compiling ever since 1.18.0 (2017) How on earth was this not found before this??? 👀React with 👀7Reacted by Max, Alisa Sireneva, Leon, maxbry-uh, Luke, slaiyer and Screeching Bagel theemathas commented on Oct 13, 2026 My version of the code has been compiling ever since 1.18.0 (2017) How on earth was this not found before this??? theemathas commented on Oct 13, 2026 theemathas on Oct 13, 2026 Contributor More actions Possibly related: #128225 theemathas commented on Oct 13, 2026 Possibly related: #128225 lcnr commented on Oct 13, 2026 lcnr on Oct 13, 2026 Contributor More actions without relying on a static struct MyType<'a, 'b: 'a, 'c>(&'c mut &'a str, &'b str); impl<'a, 'b: 'a, 'c> Drop for MyType<'a, 'b, 'c> { 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}"); } lcnr commented on Oct 13, 2026 without relying on a static struct MyType<'a, 'b: 'a, 'c>(&'c mut &'a str, &'b str); impl<'a, 'b: 'a, 'c> Drop for MyType<'a, 'b, 'c> { 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}"); } lcnr commented on Oct 13, 2026 lcnr on Oct 13, 2026 Contributor More actions unsound since 1.4 🤣 https://rust.godbolt.org/z/j1s1TevhP 😄React with 😄9Reacted by Rémy Rakic, D. Savchenko, Mathijs Follon, FelixKLG, Alex Gershberg, Kamilla 'ova, Tux3, slaiyer and Daniil Kondratev lcnr commented on Oct 13, 2026 unsound since 1.4 🤣 https://rust.godbolt.org/z/j1s1TevhP rustbot removed P-criticalCritical priorityCritical priority on Oct 13, 2026 rustbot commented on Oct 13, 2026 rustbot on Oct 13, 2026 Collaborator More actions Assigning P-high (discussion on Zulip). ❤️React with ❤️1Reacted by Jeremy Lempereur rustbot commented on Oct 13, 2026 Assigning P-high (discussion on Zulip). rustbot added P-highHigh priorityHigh priority on Oct 13, 2026 theemathas commented on Oct 13, 2026 theemathas on Oct 13, 2026 · Hidden as duplicate show comment More actions theemathas commented on Oct 13, 2026 maxdexh commented on Oct 13, 2026 maxdexh on Oct 13, 2026 Last edited by maxdexh Member More actions 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<&'static _> = None; { let l = vec![vec![1]]; let mut q = Q(None, |s| { out = Some(*s); }); q.0 = Some(&l); } println!("{:?}", out); } maxdexh commented on Oct 13, 2026 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<&'static _> = None; { let l = vec![vec![1]]; let mut q = Q(None, |s| { out = Some(*s); }); q.0 = Some(&l); } println!("{:?}", out); } lcnr changed the title [-]struct's `T: 'static` bound bypassed via covariant field assignment[/-] [+]struct's lifetime requirements can be bypassed via covariant field assignment[/+] on Oct 13, 2026 maxdexh added A-lifetimesArea: Lifetimes / regionsArea: Lifetimes / regions A-varianceArea: Variance (https://doc.rust-lang.org/nomicon/subtyping.html)Area: Variance (https://doc.rust-lang.org/nomicon/subtyping.html) on Oct 13, 2026 theemathas commented on Oct 13, 2026 theemathas on Oct 13, 2026 Last edited by theemathas Contributor More actions Another version, cuz why not. I think this version is probably the easiest to understand. use std::time::Duration; use std::thread; struct Q<'a: 'static>(Option<&'a String>); impl<'a: 'static> Drop for Q<'a> { 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)); } theemathas commented on Oct 13, 2026 Another version, cuz why not. I think this version is probably the easiest to understand. use std::time::Duration; use std::thread; struct Q<'a: 'static>(Option<&'a String>); impl<'a: 'static> Drop for Q<'a> { 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)); } maxdexh commented on Oct 13, 2026 maxdexh on Oct 13, 2026 Last edited by maxdexh Member More actions I think unification just generally doesn't check lifetime bounds of the type. Example with regular unification, 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<&'local T, fn(&'static T)>, which is not WF 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); } maxdexh commented on Oct 13, 2026 I think unification just generally doesn't check lifetime bounds of the type. Example with regular unification, 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<&'local T, fn(&'static T)>, which is not WF 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); } Metadata Metadata Assignees Labels Type Projects T-types unsound issues Status open/unblocked Show more project fields Milestone Relationships Development Issue actions Open in GitHub Copilot app