Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit b0c85ba

Browse files
authoredJul 18, 2024
Rollup merge of rust-lang#124881 - Sp00ph:reentrant_lock_tid, r=joboet
Use ThreadId instead of TLS-address in `ReentrantLock` Fixes rust-lang#123458 `ReentrantLock` currently uses the address of a thread local variable as an ID that's unique across all currently running threads. This can lead to uninituitive behavior as in rust-lang#123458 if TLS blocks get reused. This PR changes `ReentrantLock` to instead use the `ThreadId` provided by `std` as the unique ID. `ThreadId` guarantees uniqueness across the lifetime of the whole process, so we don't need to worry about reusing IDs of terminated threads. The main appeal of this PR is thus the possibility of changing the `ReentrantLock` API to guarantee that if a thread leaks a lock guard, no other thread may ever acquire that lock again. This does entail some complications: - previously, the only way to retrieve the current thread ID would've been using `thread::current().id()` which creates a temporary `Arc` and which isn't available in TLS destructors. As part of this PR, the thread ID instead gets cached in its own thread local, as suggested [here](rust-lang#123458 (comment)). - `ThreadId` is always 64-bit whereas the current implementation uses a usize-sized ID. Since this ID needs to be updated atomically, we can't simply use a single atomic variable on 32 bit platforms. Instead, we fall back to using a (sound) seqlock on 32-bit platforms, which works because only one thread at a time can write to the ID. This seqlock is technically susceptible to the ABA problem, but the attack vector to create actual unsoundness has to be very specific: - You would need to be able to lock+unlock the lock exactly 2^31 times (or a multiple thereof) while a thread trying to lock it sleeps - The sleeping thread would have to suspend after reading one half of the thread id but before reading the other half - The teared result from combining the halves of the thread ID would have to exactly line up with the sleeping thread's ID The risk of this occurring seems slim enough to be acceptable to me, but correct me if I'm wrong. This also means that the size of the lock increases by 8 bytes on 32-bit platforms, but this also shouldn't be an issue. Performance wise, I did some crude testing of the only case where this could lead to real slowdowns, which is the case of locking a `ReentrantLock` that's already locked by the current thread. On both aarch64 and x86-64, there is (expectedly) pretty much no performance hit. I didn't have any 32-bit platforms to test the seqlock performance on, so I did the next best thing and just forced the 64-bit platforms to use the seqlock implementation. There, the performance degraded by ~1-2ns/(lock+unlock) on x86-64 and ~6-8ns/(lock+unlock) on aarch64, which is measurable but seems acceptable to me seeing as 32-bit platforms should be a small minority anyways. cc `@joboet` `@RalfJung` `@CAD97`
2 parents cc4ed95 + 7e21850 commit b0c85ba

File tree

2 files changed

+144
-26
lines changed

2 files changed

+144
-26
lines changed
 

‎std/src/sync/reentrant_lock.rs

+115-23
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
#[cfg(all(test, not(target_os = "emscripten")))]
22
mod tests;
33

4+
use cfg_if::cfg_if;
5+
46
use crate::cell::UnsafeCell;
57
use crate::fmt;
68
use crate::ops::Deref;
79
use crate::panic::{RefUnwindSafe, UnwindSafe};
8-
use crate::sync::atomic::{AtomicUsize, Ordering::Relaxed};
910
use crate::sys::sync as sys;
11+
use crate::thread::{current_id, ThreadId};
1012

1113
/// A re-entrant mutual exclusion lock
1214
///
@@ -53,8 +55,8 @@ use crate::sys::sync as sys;
5355
//
5456
// The 'owner' field tracks which thread has locked the mutex.
5557
//
56-
// We use current_thread_unique_ptr() as the thread identifier,
57-
// which is just the address of a thread local variable.
58+
// We use thread::current_id() as the thread identifier, which is just the
59+
// current thread's ThreadId, so it's unique across the process lifetime.
5860
//
5961
// If `owner` is set to the identifier of the current thread,
6062
// we assume the mutex is already locked and instead of locking it again,
@@ -72,14 +74,109 @@ use crate::sys::sync as sys;
7274
// since we're not dealing with multiple threads. If it's not equal,
7375
// synchronization is left to the mutex, making relaxed memory ordering for
7476
// the `owner` field fine in all cases.
77+
//
78+
// On systems without 64 bit atomics we also store the address of a TLS variable
79+
// along the 64-bit TID. We then first check that address against the address
80+
// of that variable on the current thread, and only if they compare equal do we
81+
// compare the actual TIDs. Because we only ever read the TID on the same thread
82+
// that it was written on (or a thread sharing the TLS block with that writer thread),
83+
// we don't need to further synchronize the TID accesses, so they can be regular 64-bit
84+
// non-atomic accesses.
7585
#[unstable(feature = "reentrant_lock", issue = "121440")]
7686
pub struct ReentrantLock<T: ?Sized> {
7787
mutex: sys::Mutex,
78-
owner: AtomicUsize,
88+
owner: Tid,
7989
lock_count: UnsafeCell<u32>,
8090
data: T,
8191
}
8292

93+
cfg_if!(
94+
if #[cfg(target_has_atomic = "64")] {
95+
use crate::sync::atomic::{AtomicU64, Ordering::Relaxed};
96+
97+
struct Tid(AtomicU64);
98+
99+
impl Tid {
100+
const fn new() -> Self {
101+
Self(AtomicU64::new(0))
102+
}
103+
104+
#[inline]
105+
fn contains(&self, owner: ThreadId) -> bool {
106+
owner.as_u64().get() == self.0.load(Relaxed)
107+
}
108+
109+
#[inline]
110+
// This is just unsafe to match the API of the Tid type below.
111+
unsafe fn set(&self, tid: Option<ThreadId>) {
112+
let value = tid.map_or(0, |tid| tid.as_u64().get());
113+
self.0.store(value, Relaxed);
114+
}
115+
}
116+
} else {
117+
/// Returns the address of a TLS variable. This is guaranteed to
118+
/// be unique across all currently alive threads.
119+
fn tls_addr() -> usize {
120+
thread_local! { static X: u8 = const { 0u8 } };
121+
122+
X.with(|p| <*const u8>::addr(p))
123+
}
124+
125+
use crate::sync::atomic::{
126+
AtomicUsize,
127+
Ordering,
128+
};
129+
130+
struct Tid {
131+
// When a thread calls `set()`, this value gets updated to
132+
// the address of a thread local on that thread. This is
133+
// used as a first check in `contains()`; if the `tls_addr`
134+
// doesn't match the TLS address of the current thread, then
135+
// the ThreadId also can't match. Only if the TLS addresses do
136+
// match do we read out the actual TID.
137+
// Note also that we can use relaxed atomic operations here, because
138+
// we only ever read from the tid if `tls_addr` matches the current
139+
// TLS address. In that case, either the the tid has been set by
140+
// the current thread, or by a thread that has terminated before
141+
// the current thread was created. In either case, no further
142+
// synchronization is needed (as per <https://github.com/rust-lang/miri/issues/3450>)
143+
tls_addr: AtomicUsize,
144+
tid: UnsafeCell<u64>,
145+
}
146+
147+
unsafe impl Send for Tid {}
148+
unsafe impl Sync for Tid {}
149+
150+
impl Tid {
151+
const fn new() -> Self {
152+
Self { tls_addr: AtomicUsize::new(0), tid: UnsafeCell::new(0) }
153+
}
154+
155+
#[inline]
156+
// NOTE: This assumes that `owner` is the ID of the current
157+
// thread, and may spuriously return `false` if that's not the case.
158+
fn contains(&self, owner: ThreadId) -> bool {
159+
// SAFETY: See the comments in the struct definition.
160+
self.tls_addr.load(Ordering::Relaxed) == tls_addr()
161+
&& unsafe { *self.tid.get() } == owner.as_u64().get()
162+
}
163+
164+
#[inline]
165+
// This may only be called by one thread at a time, and can lead to
166+
// race conditions otherwise.
167+
unsafe fn set(&self, tid: Option<ThreadId>) {
168+
// It's important that we set `self.tls_addr` to 0 if the tid is
169+
// cleared. Otherwise, there might be race conditions between
170+
// `set()` and `get()`.
171+
let tls_addr = if tid.is_some() { tls_addr() } else { 0 };
172+
let value = tid.map_or(0, |tid| tid.as_u64().get());
173+
self.tls_addr.store(tls_addr, Ordering::Relaxed);
174+
unsafe { *self.tid.get() = value };
175+
}
176+
}
177+
}
178+
);
179+
83180
#[unstable(feature = "reentrant_lock", issue = "121440")]
84181
unsafe impl<T: Send + ?Sized> Send for ReentrantLock<T> {}
85182
#[unstable(feature = "reentrant_lock", issue = "121440")]
@@ -134,7 +231,7 @@ impl<T> ReentrantLock<T> {
134231
pub const fn new(t: T) -> ReentrantLock<T> {
135232
ReentrantLock {
136233
mutex: sys::Mutex::new(),
137-
owner: AtomicUsize::new(0),
234+
owner: Tid::new(),
138235
lock_count: UnsafeCell::new(0),
139236
data: t,
140237
}
@@ -184,14 +281,16 @@ impl<T: ?Sized> ReentrantLock<T> {
184281
/// assert_eq!(lock.lock().get(), 10);
185282
/// ```
186283
pub fn lock(&self) -> ReentrantLockGuard<'_, T> {
187-
let this_thread = current_thread_unique_ptr();
188-
// Safety: We only touch lock_count when we own the lock.
284+
let this_thread = current_id();
285+
// Safety: We only touch lock_count when we own the inner mutex.
286+
// Additionally, we only call `self.owner.set()` while holding
287+
// the inner mutex, so no two threads can call it concurrently.
189288
unsafe {
190-
if self.owner.load(Relaxed) == this_thread {
289+
if self.owner.contains(this_thread) {
191290
self.increment_lock_count().expect("lock count overflow in reentrant mutex");
192291
} else {
193292
self.mutex.lock();
194-
self.owner.store(this_thread, Relaxed);
293+
self.owner.set(Some(this_thread));
195294
debug_assert_eq!(*self.lock_count.get(), 0);
196295
*self.lock_count.get() = 1;
197296
}
@@ -226,14 +325,16 @@ impl<T: ?Sized> ReentrantLock<T> {
226325
///
227326
/// This function does not block.
228327
pub(crate) fn try_lock(&self) -> Option<ReentrantLockGuard<'_, T>> {
229-
let this_thread = current_thread_unique_ptr();
230-
// Safety: We only touch lock_count when we own the lock.
328+
let this_thread = current_id();
329+
// Safety: We only touch lock_count when we own the inner mutex.
330+
// Additionally, we only call `self.owner.set()` while holding
331+
// the inner mutex, so no two threads can call it concurrently.
231332
unsafe {
232-
if self.owner.load(Relaxed) == this_thread {
333+
if self.owner.contains(this_thread) {
233334
self.increment_lock_count()?;
234335
Some(ReentrantLockGuard { lock: self })
235336
} else if self.mutex.try_lock() {
236-
self.owner.store(this_thread, Relaxed);
337+
self.owner.set(Some(this_thread));
237338
debug_assert_eq!(*self.lock_count.get(), 0);
238339
*self.lock_count.get() = 1;
239340
Some(ReentrantLockGuard { lock: self })
@@ -308,18 +409,9 @@ impl<T: ?Sized> Drop for ReentrantLockGuard<'_, T> {
308409
unsafe {
309410
*self.lock.lock_count.get() -= 1;
310411
if *self.lock.lock_count.get() == 0 {
311-
self.lock.owner.store(0, Relaxed);
412+
self.lock.owner.set(None);
312413
self.lock.mutex.unlock();
313414
}
314415
}
315416
}
316417
}
317-
318-
/// Get an address that is unique per running thread.
319-
///
320-
/// This can be used as a non-null usize-sized ID.
321-
pub(crate) fn current_thread_unique_ptr() -> usize {
322-
// Use a non-drop type to make sure it's still available during thread destruction.
323-
thread_local! { static X: u8 = const { 0 } }
324-
X.with(|x| <*const _>::addr(x))
325-
}

‎std/src/thread/mod.rs

+29-3
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@
159159
mod tests;
160160

161161
use crate::any::Any;
162-
use crate::cell::{OnceCell, UnsafeCell};
162+
use crate::cell::{Cell, OnceCell, UnsafeCell};
163163
use crate::env;
164164
use crate::ffi::{CStr, CString};
165165
use crate::fmt;
@@ -699,17 +699,22 @@ where
699699
}
700700

701701
thread_local! {
702+
// Invariant: `CURRENT` and `CURRENT_ID` will always be initialized together.
703+
// If `CURRENT` is initialized, then `CURRENT_ID` will hold the same value
704+
// as `CURRENT.id()`.
702705
static CURRENT: OnceCell<Thread> = const { OnceCell::new() };
706+
static CURRENT_ID: Cell<Option<ThreadId>> = const { Cell::new(None) };
703707
}
704708

705709
/// Sets the thread handle for the current thread.
706710
///
707711
/// Aborts if the handle has been set already to reduce code size.
708712
pub(crate) fn set_current(thread: Thread) {
713+
let tid = thread.id();
709714
// Using `unwrap` here can add ~3kB to the binary size. We have complete
710715
// control over where this is called, so just abort if there is a bug.
711716
CURRENT.with(|current| match current.set(thread) {
712-
Ok(()) => {}
717+
Ok(()) => CURRENT_ID.set(Some(tid)),
713718
Err(_) => rtabort!("thread::set_current should only be called once per thread"),
714719
});
715720
}
@@ -719,7 +724,28 @@ pub(crate) fn set_current(thread: Thread) {
719724
/// In contrast to the public `current` function, this will not panic if called
720725
/// from inside a TLS destructor.
721726
pub(crate) fn try_current() -> Option<Thread> {
722-
CURRENT.try_with(|current| current.get_or_init(|| Thread::new_unnamed()).clone()).ok()
727+
CURRENT
728+
.try_with(|current| {
729+
current
730+
.get_or_init(|| {
731+
let thread = Thread::new_unnamed();
732+
CURRENT_ID.set(Some(thread.id()));
733+
thread
734+
})
735+
.clone()
736+
})
737+
.ok()
738+
}
739+
740+
/// Gets the id of the thread that invokes it.
741+
#[inline]
742+
pub(crate) fn current_id() -> ThreadId {
743+
CURRENT_ID.get().unwrap_or_else(|| {
744+
// If `CURRENT_ID` isn't initialized yet, then `CURRENT` must also not be initialized.
745+
// `current()` will initialize both `CURRENT` and `CURRENT_ID` so subsequent calls to
746+
// `current_id()` will succeed immediately.
747+
current().id()
748+
})
723749
}
724750

725751
/// Gets a handle to the thread that invokes it.

0 commit comments

Comments
 (0)
Failed to load comments.