-
Notifications
You must be signed in to change notification settings - Fork 44
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
A little soundness hole around drop order of thread_local!(static …)
#75
Comments
For putting this into perspective; going as far as spawning a thread in the destructor of a thread-local is not necessary for unsoundness. The following simpler code too reliably reports UB on current miri (the use std::{cell::Cell, thread, time::Duration};
use thread_local::ThreadLocal;
static FOO: ThreadLocal<Cell<i32>> = ThreadLocal::new();
struct WriteOnDrop(&'static Cell<i32>);
thread_local!(static W: Cell<WriteOnDrop> = unreachable!());
impl Drop for WriteOnDrop {
fn drop(&mut self) {
self.0.set(0);
}
}
fn main() {
let _ = thread::spawn(|| W.set(WriteOnDrop(FOO.get_or_default())));
thread::sleep(Duration::from_millis(100));
FOO.get().unwrap().set(0);
} |
Hmm, I would have expected the lock on
|
Ah I see the problem now. The issue is that the lifetime returned by You're right, I don't really see any alternative other than a |
I would like to put emphasis on this, as get() is necessary for an API I have designed using ThreadLocal, and I use T: Sync+Send. To alleviate the need for a breaking change, I would suggest introducing with(), deprecating get(), and offering alternatives for get() for when T: Send+Sync. I'm not sure on a good naming scheme for the alternatives, however |
I think |
Here's the code to reproduce:
(run on rustexplorer.com [where you'll just see it compiles&runs and doesn't panic; I haven’t made the data race particularly “observable” at run-time, to keep the code more clean])
With the additional
FOO.get_or_default();
line uncommented as indicated, for panic-free execution on miri, this gets reported UB as expected:The issue here is the reasoning behind the
T: Send ==> ThreadLocal<T>: Sync
logic not being 100% watertight. As stated in the docs:but the “can only occur after a thread has exited” part is enforced via a thread-local (the
THREAD_GUARD: ThreadGuard
one if I'm not mistaken). After that gets dropped, destructors of otherthread_local!(static …);
items can still run. BecauseThreadLocal
also doesn’t come with a.with(|x| …)
-style API, borrows of'static
lifetime can be created and thus a borrow of aThreadLocal
’s contents can be made available to such destructors after the value has already been marked as re-usable by other threads via theThreadGuard
dropped beforehand.I can’t really think of any particularly satisfying alternative workarounds in implementation, i.e. without significant API-change: Technically, a
with(|x| …)
-style API should probably “fix” this. Theget()
-style API can also be kept in addition, as long as the contained type isSync
(because unlikestd::thread_local!
, thisThreadLocal
does not suffer from issues of the contents being dropped too early, when accessed during drops of thread locals).The text was updated successfully, but these errors were encountered: