struct's lifetime requirements can be bypassed via covariant field assignment
Description
opened 12h agoon Oct 13, 2026
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.
~ @lcnr
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
Activity
theemathascommented11 hours agoonOct 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);
}
theemathascommented11 hours agoonOct 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???
theemathascommented11 hours agoonOct 13, 2026
Possibly related: #128225
lcnrcommented11 hours agoonOct 13, 2026
without relying on a static
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}");
}
maxdexhcommented11 hours agoonOct 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 = None;
{
let l = vec![vec![1]];
let mut q = Q(None, |s| {
out = Some(*s);
});
q.0 = Some(&l);
}
println!("{:?}", out);
}
theemathascommented11 hours agoonOct 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(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));
}
maxdexhcommented10 hours agoonOct 13, 2026
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);
}
apirainocommented7 hours agoonOct 13, 2026
Hello everyone, mod here.
We're preventively locking this issue to prevent spam and other unhelpful comments




