Hacker News — AI on Front Page · · 10 min read

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.

239 pts · 107 comments on Hacker News

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

Copy link
Copy link
Open
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.regression-from-stable-to-stablePerformance or correctness regression from one stable version to another.Performance or correctness regression from one stable version to another.

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, BioTomateDE, Timur Tugushev, palapapa, Ori Yadid, SeniorMars, dzmitry-lahoda, Til and Marco Quinten
  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
    on Oct 13, 2026 · Hidden as outdated
    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, Luke, slaiyer, Screeching Bagel, Slanterns, maliciousblahaj, Timur Tugushev and 9 more
  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}");
    }
    Reacted by dzmitry-lahoda and Martijn Gribnau
  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, Tux3, slaiyer, Daniil Kondratev, Timur Tugushev and 20 more
  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
    on Oct 13, 2026 · Hidden as duplicate
    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));
    }
    Reacted by Filip Dobrosavljević
  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 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
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.regression-from-stable-to-stablePerformance or correctness regression from one stable version to another.Performance or correctness regression from one stable version to another.

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    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.

    More from Hacker News — AI on Front Page