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
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
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 and BioTomateDE
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}"); }
Assigning P-high (discussion on Zulip).
Reacted by Jeremy Lempereur
Another 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[/+]
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)); }
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 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); }