Profile
Back to NewsBack
Hacker News 7 min
Reader Mode
11 year old unsoundness bugs found in Rust borrow checker

11 year old unsoundness bugs found in Rust borrow checker

2 hours ago

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

Open

Description

@jsnell
Issue body actions

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

Reactions are currently unavailable

Activity

  1. 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
  2. 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
  3. 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
  4. added
    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
  5. theemathas commented on Oct 13, 2026

    @theemathas
    Contributor
    More actions

    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
  6. 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
  7. rustbot commented on Oct 13, 2026

    @rustbot
    show commentMore actions
  8. theemathas commented on Oct 13, 2026

    @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???

    Reacted by Max, Alisa Sireneva, Leon, maxbry-uh and Luke
  9. theemathas commented on Oct 13, 2026

    @theemathas
    Contributor
    More actions

    Possibly related: #128225

  10. lcnr commented on Oct 13, 2026

    @lcnr
    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}");
    }
  11. lcnr commented on Oct 13, 2026

    @lcnr
    Contributor
    More actions
    Reacted by Rémy Rakic, D. Savchenko, Mathijs Follon, FelixKLG, Alex Gershberg, Kamilla 'ova and Tux3
  12. rustbot commented on Oct 13, 2026

    @rustbot
    Collaborator
    More actions

    Assigning P-high (discussion on Zulip).

    Reacted by Jeremy Lempereur
  13. theemathas commented on Oct 13, 2026

    @theemathas
    show commentMore actions
  14. maxdexh commented on Oct 13, 2026

    @maxdexh
    Member
    More actions

    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);
    }
  15. 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
  16. 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
  17. theemathas commented on Oct 13, 2026

    @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));
    }
  18. maxdexh commented on Oct 13, 2026

    @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: 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);
    }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-borrow-checkerArea: The borrow checkerArea: The borrow checkerA-destructorsArea: Destructors (`Drop`, …)Area: Destructors (`Drop`, …)A-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)C-bugCategory: This is a bug.Category: This is a bug.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/SoundnessP-highHigh priorityHigh priorityT-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.

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    Chat with me