Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Data reachable from thread-local storage of the main thread should not be considered leaked #2881

Closed
lukechu10 opened this issue May 8, 2023 · 12 comments · Fixed by #2931
Closed
Labels
A-leaks Area: affects the memory leak checker E-good-first-issue A good way to start contributing, mentoring is available

Comments

@lukechu10
Copy link

Miri does not detect any leak for the following code since the reference is stored in a static variable and is therefore accessible for the whole duration of the program.

use std::sync::Mutex;

static REF: Mutex<Option<&'static i32>> = Mutex::new(None);

pub fn main() {
    let a = 123;
    let b = Box::new(a);
    let r = Box::leak(b);
    println!("{r}");
    *REF.lock().unwrap() = Some(r);
}

However, replacing the static with a thread_local!, Miri complains about a memory leak:

use std::cell::Cell;

// static REF: Mutex<Option<&'static i32>> = Mutex::new(None);

pub fn main() {
    let a = 123;
    let b = Box::new(a);
    let r = Box::leak(b);
    println!("{r}");

    thread_local! {
        static REF: Cell<Option<&'static i32>> = Cell::new(None);
    }

    REF.with(|cell| {
        cell.set(Some(r));
    })
}
@RalfJung
Copy link
Member

RalfJung commented May 8, 2023

Yes, that is by design. Threads come and go and so having a reference pointed-to by a thread local after the thread disappeared and all its thread-local storage got cleaned up is a leak.

@saethlin
Copy link
Member

saethlin commented May 8, 2023

In Rust, static items require a const initializer and thus are not initialized at runtime, and they also are never dropped. This avoids the static initialization fiasco that C++ suffers from.

And yeah as Ralf points out thread-locals aren't leaked. So I'm not sure, which side of this comparison do you think is confusing?

@lukechu10
Copy link
Author

Ah ok that makes sense. However, Valgrind doesn't detect a leak in the thread_local case. Is there a reason for this disparity between Valgrind and miri?

@RalfJung
Copy link
Member

RalfJung commented May 9, 2023

I have no idea how valgrind decides what to make a leak or not. 🤷

Does it consider this a leak? IMO it clearly should:

use std::cell::Cell;

pub fn main() {
    thread_local! {
        static REF: Cell<Option<&'static i32>> = Cell::new(None);
    }

    std::thread::spawn(|| REF.with(|cell| {
        let a = 123;
        let b = Box::new(a);
        let r = Box::leak(b);
        cell.set(Some(r));
    })).join().unwrap();
    
    // Imagine the program running for a long time while the thread is gone
    // and this memory still sits around, unused -- leaked.
}

@lukechu10
Copy link
Author

lukechu10 commented May 9, 2023

Does it consider this a leak? IMO it clearly should:

I just tried it and yes it is considered a leak by both Valgrind and Miri.

Also in my original example, Valgrind marks the memory as "still reachable".

@RalfJung
Copy link
Member

RalfJung commented May 9, 2023 via email

@RalfJung
Copy link
Member

RalfJung commented May 9, 2023

This should actually be fairly easy to do: before running the leak check (around here), mark the main thread (thread ID 0) data of all TlsEntry (these things are in tls.rs) as 'static roots' by adding them to this vector.

@RalfJung RalfJung changed the title Reference stored in thread_local! considered a leak whereas static is not Data reachable from thread-local storage of the main thread should not be considered leaked May 9, 2023
@RalfJung RalfJung added E-good-first-issue A good way to start contributing, mentoring is available A-leaks Area: affects the memory leak checker labels May 9, 2023
@max-heller
Copy link
Contributor

This should actually be fairly easy to do: before running the leak check (around here), mark the main thread (thread ID 0) data of all TlsEntry (these things are in tls.rs) as 'static roots' by adding them to this vector.

I tried this approach and am running into some issues:

  • Thread locals are not yet initialized before and are destroyed after here, so we can't get their AllocIds right before the leakage check. I worked around this by hooking into TlsData::store_tls() to store AllocIds in static_roots when TLS is stored.

  • After adding the AllocIds to static_roots, the example that should work still fails. I'm guessing this is because information about the contents of the allocation has been deleted by the time the leakage check runs, but I'm not sure how to work around this.

Any ideas? My attempt is here

@saethlin
Copy link
Member

saethlin commented Jun 7, 2023

Thread locals are not yet initialized before and are destroyed after

Maybe you could try sticking something deeper into the scheduler code? What you probably want to do is hook into the transition from running the main thread to TlsDtors.

the example that should work still fails

Based on the logging you added, you're adding something but it's the wrong thing.

I'm guessing this is because information about the contents of the allocation has been deleted by the time the leakage check runs

That possible. I traced the AllocId that your logs say are added as a static root (./miri run demo.rs -Zmiri-track-alloc-id=1000) and it seems to point into some indirection in the standard library LocalKey impl. The standard library's thread-locals are notoriously complicated: rust-lang/rust#110897

So yeah I wouldn't be surprised if there is some extra layer of indirection that gets torn down by TLS destructors. So there are two possible fixes for this, which both may or may not work:

  • Do the traversal that the leak checker does right before TLS destructors run, so that you snag everything reachable through main thread TLS and add all of that to the static roots
  • Move the leak check to before TLS destructors run

I feel like the first one has lower chance of going wrong somehow?

@RalfJung
Copy link
Member

RalfJung commented Jun 7, 2023

Do the traversal that the leak checker does right before TLS destructors run, so that you snag everything reachable through main thread TLS and add all of that to the static roots

That doesn't sound right. Static roots already apply recursively, everything reachable from then is not considered leaked.

There are 2 kinds of thread-local state (thread-local statics and the OS-provided "TLS keys"), and they both get cleaned up on thread exit:

this.machine.tls.delete_all_thread_tls(this.get_active_thread());

this.terminate_active_thread()?;

Marking the main thread data as static roots needs to happen before that cleanup.

@max-heller
Copy link
Contributor

There are 2 kinds of thread-local state (thread-local statics and the OS-provided "TLS keys"), and they both get cleaned up on thread exit:

Thanks, I was missing the former. Added those as static roots before they're destroyed and verified that the Cell-containing thread-local is indeed stored as a static root:

     Running `target/debug/miri tests/pass/issues/issue-miri-2881.rs -Zmiri-track-alloc-id=1397`
note: tracking was triggered
  --> tests/pass/issues/issue-miri-2881.rs:8:5
   |
8  | /     thread_local! {
9  | |         static REF: Cell<Option<&'static i32>> = Cell::new(None);
10 | |     }
   | |_____^ created thread-local static allocation of 24 bytes (alignment 8 bytes) with id 1397
   |
...
note: inside `main`
  --> tests/pass/issues/issue-miri-2881.rs:12:5
   |
12 | /     REF.with(move |cell| {
13 | |         cell.set(Some(r));
14 | |     })
   | |______^
   = note: this note originates in the macro `$crate::thread::local_impl::thread_local_inner` which comes from the expansion of the macro `thread_local` (in Nightly builds, run with -Z macro-backtrace for more info)

note: tracking was triggered
  |
  = note: freed allocation with id 1397
  = note: (no span available)

but still no luck in having the leak check acknowledge the box as reachable:

error: memory leaked: alloc1338 (Rust heap, size: 4, align: 4), allocated here:
...
note: inside `main`
   --> tests/pass/issues/issue-miri-2881.rs:5:13
    |
5   |     let b = Box::new(a);
    |             ^^^^^^^^^^^

@oli-obk
Copy link
Contributor

oli-obk commented Jun 14, 2023

Could you open a PR? That makes it easier to review and track the changes you do.

@bors bors closed this as completed in 6db4c80 Nov 12, 2023
RalfJung pushed a commit to RalfJung/rust that referenced this issue Nov 12, 2023
Treat thread-local statics on main thread as static roots for leakage analysis

Miri currently treats allocations as leaked if they're only referenced in thread-local statics. For threads other than the main thread, this is correct, since the thread can terminate before the program does, but references in the main thread's locals should be treated as living for the duration of the program since the thread lives for the duration of the program.

This PR adds thread-local statics and TLS keys as "static roots" for leakage analysis, but does not yet bless the example program from rust-lang#2881. See rust-lang/miri#2881 (comment)

Closes rust-lang#2881
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-leaks Area: affects the memory leak checker E-good-first-issue A good way to start contributing, mentoring is available
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants