Change:
I think the is incorrect in the sense that it has undefined behavior, for two reasons: This is . Worse, POSiX read-write locks have UB when the thread holding the write lock attempts to acquire it again -- and yet nothing is stopping exactly that from happening in . If we really want to use POSIX rwlocks, I think we have to implement a reentrancy detector. (And then maybe we also want to use that for mutex, so that we can use the static initializer and the most efficient code path?)
I based my statements on points out that at , behavior for re-acquiring the write lock is defined as "may deadlock" -- but not UB. However, we also use this implementation on non-Linux platforms, e.g. macOS.
macOS does explicitly identify the behavior as undefined.
FWIW, -- actually, even is wrong there. I think we should stop using the platform-specific implementations, and just roll our own based on thread (un)parking.
cc - we talked about this a bit during RustConf.
I agree that we're basically at the point that there's a ridiculous amount of constraints from OS primitives that the only program which doesn't have undefined behavior is correct ones, and we all know how often we write correct programs. All these discussions are spread out over a number of issues though and PRs, and it's really difficult to keep it all in my own head at least. "This is UB" is a very strong motivation for reimplementing the standard library's primitives, but I think the scale of a change such as this would probably want to go through the RFC process. Such an RFC would be a great location to centralize all the various hazards of OS primitives and how unreasonable it is for us to try to appease all possible implementations. That in turns provides excellent motivation for alternative strategies.
You mean like this RFC?
Correction: Even some correct programs are UB. Reentrancy on the read lock of an is a perfectly reasonable thing to do, and yet Windows declares it UB. Whether reentrancy that leads to deadlocks is "correct" is open for interpretation, I guess ;) but anyway, it is safe, and hence "correct enough" for UB to be critical.
I've opened an to continue more long-form discussion on the topic of continuing to fix these issues.
I just noticed that the mandate So some time between the 1997 version and the 2018 version, this seems to have changed. Can we expect Apple (and generally all our POSIX platforms) to follow a recent enough POSIX to rely on this?
Older versions of glibc (pre-2.25) use hardware lock elision on Intel CPUs which makes recursive locking succeed when it should otherwise deadlock.
Looks like already says "may deadlock" here could be interpreted as "or cause UB", but then saying "may deadlock" would be redundant as of course a deadlock is a valid implementation of UB. So arguably, UB was not permitted any more for 15 years now. So that's pre-2016 glibc versions, if I got the release date right. Bummer, this means we still need to keep the extra reentrant-rwlock-checks around in libstd (but we should have comments explaining that this is just to defend against non-compliant implementations).
For the question of whether we can rely on macOS using a recent enough POSIX, I found but could not figure out which POSIX version it says macOS conforms with.
Relevant issue:
Oh, that was for Mutex as well, not rust RwLock? ... do they still do that? I would think if one can assume default mutexes to behave the same as "normal" mutexes... obviously not. Okay, I'll have to figure out a way to make Miri check that.
I believe they still do that, but I haven't checked.
For only though, right? Arguably that is compliant with POSIX, the standard doesn't say that implies anything, it just seems like a reasonable assumption (that the glibc people seemingly disagree with). For though, if they still do lock elision (and don't have any reentrancy guards themselves), that's a clear POSIX violation -- right?
Yes, only for and only if you don't use to explicitly select . So they are still standard-compliant (ish). For they currently don't do elision so they are standard compliant. But old glibc is not standard compliant.

Discussion:
// // A pthread mutex initialized with PTHREAD_MUTEX_INITIALIZER will have // a type of PTHREAD_MUTEX_DEFAULT, which has undefined behavior if you <del> // try to re-lock it from the same thread when you already hold a lock. </del> <ins> // try to re-lock it from the same thread when you already hold a lock // (https://pubs.opengroup.org/onlinepubs/9699919799/functions/pthread_mutex_init.html). // This is the case even if PTHREAD_MUTEX_DEFAULT == PTHREAD_MUTEX_NORMAL // (https://github.com/rust-lang/rust/issues/33770#issuecomment-220847521) -- in that // case, `pthread_mutexattr_settype(PTHREAD_MUTEX_DEFAULT)` will of course be the same // as setting it to `PTHREAD_MUTEX_NORMAL`, but not setting any mode will result in // a Mutex where re-locking is UB. </ins> // // In practice, glibc takes advantage of this undefined behavior to // implement hardware lock elision, which uses hardware transactional // memory to avoid acquiring the lock. While a transaction is in // progress, the lock appears to be unlocked. This isn't a problem for // other threads since the transactional memory will abort if a conflict <del> // is detected, however no abort is generated if re-locking from the </del> <ins> // is detected, however no abort is generated when re-locking from the </ins> // same thread. // // Since locking the same mutex twice will result in two aliasing &mut

pub unsafe fn read(&self) { let r = libc::pthread_rwlock_rdlock(self.inner.get()); <del> // According to the pthread_rwlock_rdlock spec, this function **may** // fail with EDEADLK if a deadlock is detected. On the other hand // pthread mutexes will *never* return EDEADLK if they are initialized // as the "fast" kind (which ours always are). As a result, a deadlock // situation may actually return from the call to pthread_rwlock_rdlock // instead of blocking forever (as mutexes and Windows rwlocks do). Note // that not all unix implementations, however, will return EDEADLK for // their rwlocks. </del> <ins> // According to POSIX, when a thread tries to acquire this read lock // while it already holds the write lock // (or vice versa, or tries to acquire the write lock twice), // "the call shall either deadlock or return [EDEADLK]" // (https://pubs.opengroup.org/onlinepubs/9699919799/functions/pthread_rwlock_wrlock.html, // https://pubs.opengroup.org/onlinepubs/9699919799/functions/pthread_rwlock_rdlock.html). // So, in principle, all we have to do here is check `r == 0` to be sure we properly // got the lock. </ins> // <del> // We roughly maintain the deadlocking behavior by panicking to ensure // that this lock acquisition does not succeed. // // We also check whether this lock is already write locked. This // is only possible if it was write locked by the current thread and // the implementation allows recursive locking. The POSIX standard // doesn't require recursively locking a rwlock to deadlock, but we can't // allow that because it could lead to aliasing issues. </del> <ins> // However, (at least) glibc before version 2.25 does not conform to this spec, // and can return `r == 0` even when this thread already holds the write lock. // We thus check for this situation ourselves and panic when detecting that a thread // got the write lock more than once, or got a read and a write lock. </ins> if r == libc::EAGAIN { panic!("rwlock maximum reader count exceeded"); } else if r == libc::EDEADLK || (r == 0 && *self.write_locked.get()) { <ins> // Above, we make sure to only access `write_locked` when `r == 0` to avoid // data races. </ins> if r == 0 { <ins> // `pthread_rwlock_rdlock` succeeded when it should not have. </ins> self.raw_unlock(); } panic!("rwlock read lock would result in deadlock"); } else { <del> assert_eq!(r, 0); </del> <ins> // According to POSIX, for a properly initialized rwlock this can only // return EAGAIN or EDEADLK or 0. We rely on that. debug_assert_eq!(r, 0); </ins> self.num_readers.fetch_add(1, Ordering::Relaxed); } }

let r = libc::pthread_rwlock_tryrdlock(self.inner.get()); if r == 0 { if *self.write_locked.get() { <ins> // `pthread_rwlock_tryrdlock` succeeded when it should not have. </ins> self.raw_unlock(); false } else {

#[inline] pub unsafe fn write(&self) { let r = libc::pthread_rwlock_wrlock(self.inner.get()); <del> // See comments above for why we check for EDEADLK and write_locked. We // also need to check that num_readers is 0. </del> <ins> // See comments above for why we check for EDEADLK and write_locked. For the same reason, // we also need to check that there are no readers (tracked in `num_readers`). </ins> if r == libc::EDEADLK <del> || *self.write_locked.get() </del> <ins> || (r == 0 && *self.write_locked.get()) </ins> || self.num_readers.load(Ordering::Relaxed) != 0 { <ins> // Above, we make sure to only access `write_locked` when `r == 0` to avoid // data races. </ins> if r == 0 { <ins> // `pthread_rwlock_wrlock` succeeded when it should not have. </ins> self.raw_unlock(); } panic!("rwlock write lock would result in deadlock"); } else { <ins> // According to POSIX, for a properly initialized rwlock this can only // return EDEADLK or 0. We rely on that. </ins> debug_assert_eq!(r, 0); } *self.write_locked.get() = true;

let r = libc::pthread_rwlock_trywrlock(self.inner.get()); if r == 0 { if *self.write_locked.get() || self.num_readers.load(Ordering::Relaxed) != 0 { <ins> // `pthread_rwlock_trywrlock` succeeded when it should not have. </ins> self.raw_unlock(); false } else {