11 year old unsoundness bugs found in Rust borrow checker
Mirrored from Hacker News — AI on Front Page for archival readability. Support the source by reading on the original site.
struct's lifetime requirements can be bypassed via covariant field assignment #164250
Description
In the following code:
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)); } } } 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
- addedneeds-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-triagingon Oct 13, 2026
- addedI-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/Soundnesson Oct 13, 2026
- addedI-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
- addedA-borrow-checkerArea: The borrow checkerArea: The borrow checkerT-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 removedneeds-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-triagingon Oct 13, 2026
The core of the unsoundness:
struct Q<T: 'static>(Option<T>); impl<T: 'static> Drop for Q<T> { 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); }
Reacted by Daniel Vogelbacher, Serhiy, BioTomateDE, Timur Tugushev, palapapa, Ori Yadid, SeniorMars, dzmitry-lahoda, Til and Marco Quinten- addedA-destructorsArea: Destructors (`Drop`, …)Area: Destructors (`Drop`, …)on Oct 13, 2026
- removedI-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
My version of the code has been compiling ever since 1.18.0 (2017)
How on earth was this not found before this???
Reacted by Max, Alisa Sireneva, Leon, maxbry-uh, Luke, slaiyer, Screeching Bagel, Slanterns, maliciousblahaj, Timur Tugushev and 9 morePossibly related: #128225
without relying on a
staticstruct 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}"); }
Reacted by dzmitry-lahoda and Martijn Gribnauunsound since 1.4 🤣 https://rust.godbolt.org/z/j1s1TevhP
Reacted by Rémy Rakic, D. Savchenko, Mathijs Follon, FelixKLG, Alex Gershberg, Kamilla 'ova, Tux3, slaiyer, Daniil Kondratev, Timur Tugushev and 20 moreAssigning P-high (discussion on Zulip).
Reacted by Jeremy LempereurAnother version that segfaults/prints freed memory since 1.4
struct Q<T, F: FnMut(&T)>(Option<T>, F); impl<T, F: FnMut(&T)> Drop for Q<T, F> { 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); }
- 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
- addedA-lifetimesArea: Lifetimes / regionsArea: Lifetimes / regionsA-varianceArea: Variance (https://doc.rust-lang.org/nomicon/subtyping.html)Area: Variance (https://doc.rust-lang.org/nomicon/subtyping.html)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)); }
Reacted by Filip DobrosavljevićI think unification just generally doesn't check lifetime bounds of the type. Example with regular unification, without field assignment:
struct Q<T: Copy, F: FnMut(T)>(T, F); impl<T: Copy, F: FnMut(T)> Drop for Q<T, F> { fn drop(&mut self) { self.1(self.0) } } type T = Vec<Vec<i32>>; 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 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); }
Reacted by 9291Sam, Bee4Queer, Maple, Thefool and T-256- added a commit that references this issue on Oct 13, 2026
- addedregression-from-stable-to-stablePerformance or correctness regression from one stable version to another.Performance or correctness regression from one stable version to another.on Oct 13, 2026
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsopen/unblocked
Discussion (0)
Sign in to join the discussion. Free account, 30 seconds — email code or GitHub.
Sign in →No comments yet. Sign in and be the first to say something.