Change:
// since we're not dealing with multiple threads. If it's not equal, // synchronization is left to the mutex, making relaxed memory ordering for // the `owner` field fine in all cases. <ins> // // On systems without 64 bit atomics we also store the address of a TLS variable // along the 64-bit TID. We then first check that address against the address // of that variable on the current thread, and only if they compare equal do we // compare the actual TIDs. Because we only ever read the TID on the same thread // that it was written on (or a thread sharing the TLS block with that writer thread), // we don't need to further synchronize the TID accesses, so they can be regular 64-bit // non-atomic accesses. </ins> #[unstable(feature = "reentrant_lock", issue = "121440")] pub struct ReentrantLock<T: ?Sized> { mutex: sys::Mutex, <del> owner: AtomicUsize, </del> <ins> owner: Tid, </ins> lock_count: UnsafeCell<u32>, data: T, } <ins> cfg_if!( if #[cfg(target_has_atomic = "64")] { use crate::sync::atomic::{AtomicU64, Ordering::Relaxed}; struct Tid(AtomicU64); impl Tid { const fn new() -> Self { Self(AtomicU64::new(0)) } #[inline] fn contains(&self, owner: ThreadId) -> bool { owner.as_u64().get() == self.0.load(Relaxed) } #[inline] // This is just unsafe to match the API of the Tid type below. unsafe fn set(&self, tid: Option<ThreadId>) { let value = tid.map_or(0, |tid| tid.as_u64().get()); self.0.store(value, Relaxed); } } } else { /// Returns the address of a TLS variable. This is guaranteed to /// be unique across all currently alive threads. fn tls_addr() -> usize { thread_local! { static X: u8 = const { 0u8 } }; X.with(|p| <*const u8>::addr(p)) } use crate::sync::atomic::{ AtomicUsize, Ordering, }; struct Tid { // When a thread calls `set()`, this value gets updated to // the address of a thread local on that thread. This is // used as a first check in `contains()`; if the `tls_addr` // doesn't match the TLS address of the current thread, then // the ThreadId also can't match. Only if the TLS addresses do // match do we read out the actual TID. // Note also that we can use relaxed atomic operations here, because // we only ever read from the tid if `tls_addr` matches the current // TLS address. In that case, either the the tid has been set by // the current thread, or by a thread that has terminated before // the current thread was created. In either case, no further // synchronization is needed (as per <https://github.com/rust-lang/miri/issues/3450>) tls_addr: AtomicUsize, tid: UnsafeCell<u64>, } unsafe impl Send for Tid {} unsafe impl Sync for Tid {} impl Tid { const fn new() -> Self { Self { tls_addr: AtomicUsize::new(0), tid: UnsafeCell::new(0) } } #[inline] // NOTE: This assumes that `owner` is the ID of the current // thread, and may spuriously return `false` if that's not the case. fn contains(&self, owner: ThreadId) -> bool { // SAFETY: See the comments in the struct definition. self.tls_addr.load(Ordering::Relaxed) == tls_addr() && unsafe { *self.tid.get() } == owner.as_u64().get() } #[inline] // This may only be called by one thread at a time, and can lead to // race conditions otherwise. unsafe fn set(&self, tid: Option<ThreadId>) { // It's important that we set `self.tls_addr` to 0 if the tid is // cleared. Otherwise, there might be race conditions between // `set()` and `get()`. let tls_addr = if tid.is_some() { tls_addr() } else { 0 }; let value = tid.map_or(0, |tid| tid.as_u64().get()); self.tls_addr.store(tls_addr, Ordering::Relaxed); unsafe { *self.tid.get() = value }; } } } ); </ins> #[unstable(feature = "reentrant_lock", issue = "121440")] unsafe impl<T: Send + ?Sized> Send for ReentrantLock<T> {} #[unstable(feature = "reentrant_lock", issue = "121440")]

Discussion:
The code below verifies that the only one thread holds the ReentrantLock by recording the owner thread id inside. It should be true since the lock is never unlocked (note that the lock guard is leaked). In practice, in some executions the assertion fails: Inspired by report from , which also explains why this fails. Ideally the assertion failure wouldn't be among possible executions.
This is a bit of rule-lawyering, but this isn't guaranteed. The documentation of says: And all of these things are fulfilled, even in the case you describe.
That part is false, since according to the documentation lock is unlocked when a lock guard is dropped. No lock guard is ever dropped in the example, so another thread still holds the lock. If you would like to allow this execution, you would have to update the documentation to explain when and why lock was unlocked. Although, I would consider this to be clearly undesirable behavior. Furthermore we already have ThreadId with necessary guarantees could be used to address the issue.
No, the other thread has died. Unfortunately, getting the is much more expensive, as it lives in , which is , and therefore needs a TLS destructor. Also, it will not be available in TLS destructors. We can get around this, of course, but the current approach is quite efficient, so it will be hard to beat.
I agree that if this behavior stays as is, it should at least be very explicitly documented (something like "If a thread previously held the lock and terminated without releasing it, another thread may then be able to acquire the lock without deadlocking"). Still I'd prefer for the semantics to be "If any thread terminates while holding a lock, no other thread may ever reacquire the lock", which would require something like which is unique over the lifetime of the process. I'm not convinced that the approach would be much slower than the current one, it's mostly just cloning and then dropping an Arc, which is 2 atomic ops, and it saves a library call. On the other hand, using would then always require a 64 bit ID, which might need some form of spin lock protecting every access on platforms that don't have 64 bit atomics.
I know the point of is to avoid deadlock, but my thinking is that if someone uses on the guard, they probably had a good reason for it, and might want to keep it locked.

I'm kinda fighting my own bias here though, since I've also been trying to prevent deadlocks in Rust.
As this seems important to so many people, we should definitely add those docs. I'm opposed to fixing this however, unless we find a really efficient solution. This gets called on every single , so it's definitely on the hot path. Also, we might consider using for this in the future, which might be more efficient on Linux than the TLS access. s can get reused, so if we fix this, we wouldn't be able to do that.
Modulo the need to use 64-bit atomics, a approach can be made almost entirely identical in performance to the current one: in when the current thread threadlocal, also have a thread local that only stores the thread ID and is set when the current thread is initialized and claims a thread ID. is replaced with consuming something like the following from : The common case -- the thread was created by Rust and has its data already created -- only gains a pointer read and predicted comparison against zero over the current which uses the thread local address.
Since the tid is only ever written to while holding a mutex, we could just use a seqlock to store it on platforms which only support 32-bit atomics (which I believe all platforms with do), something like this: . While this means at least 4 atomic operations per tid access (including a store) instead of just 1, this seems acceptable to me because it would only affect 32 bit platforms anyways, the majority of platforms could just use a single 64 bit atomic access. Also, reader-writer contention would be extremely low, as the tid only gets written to when a thread acquires the first lock and when the last lock gets released. Would this together with the more efficient fetching by be a reasonable tradeoff to allow the stricter guarantee for process-lifetime unique thread ids?
So the answer here is that the lock gets implicitly unlocked when the thread dies. This is not how works, but doesn't seem outrageous either. Of course if it can be avoided at reasonable cost, that seems better. I was about to remark on the unsoundness of seqlocks, but this variant actually seems fine -- you are using relaxed atomic accesses for the data, so there's no data race, just a race condition. Nice!

Note that it only looks unlocked to a subsequent thread if that thread is assigned the same TLS block as the thread that held the lock. Any other thread would still deadlock, which means we currently can't guarantee either behavior consistently.
Sure, this behavior is non-deterministic. I was just talking about the specific execution we are considering, where the second thread does acquire the lock.