Description
The following code fails under miri:
#![feature(reentrant_lock)]
use std::thread;
use std::sync::ReentrantLock;
fn main() {
let lock = ReentrantLock::new(());
let acquire = || drop(lock.lock());
thread::scope(|s| {
s.spawn(acquire);
s.spawn(acquire);
});
}
Commit: e38a828bb8c1faa95be23d1fa902913d218de848
Command: ./miri run -Zmiri-seed=706 -Zmiri-track-weak-memory-loads -Zmiri-preemption-rate=0 race.rs
Result:
note: tracking was triggered
--> /Users/Jonas/.rustup/toolchains/miri/lib/rustlib/src/rust/library/std/src/sync/reentrant_lock.rs:187:16
|
187 | if self.owner.load(Relaxed) == this_thread {
| ^^^^^^^^^^^^^^^^^^^^^^^^ weak memory emulation: outdated value returned from load
|
= note: BACKTRACE on thread `unnamed-2`:
= note: inside `std::sync::ReentrantLock::<()>::lock` at /Users/Jonas/.rustup/toolchains/miri/lib/rustlib/src/rust/library/std/src/sync/reentrant_lock.rs:187:16: 187:40
note: inside closure
--> race.rs:8:27
|
8 | let acquire = || drop(lock.lock());
| ^^^^^^^^^^^
error: Undefined Behavior: Data race detected between (1) non-atomic write on thread `unnamed-1` and (2) non-atomic read on thread `unnamed-2` at alloc1432+0x10. (2) just happened here
--> /Users/Jonas/.rustup/toolchains/miri/lib/rustlib/src/rust/library/std/src/sync/reentrant_lock.rs:244:34
|
244 | *self.lock_count.get() = (*self.lock_count.get()).checked_add(1)?;
| ^^^^^^^^^^^^^^^^^^^^^^^^ Data race detected between (1) non-atomic write on thread `unnamed-1` and (2) non-atomic read on thread `unnamed-2` at alloc1432+0x10. (2) just happened here
|
help: and (1) occurred earlier here
--> race.rs:8:22
|
8 | let acquire = || drop(lock.lock());
| ^^^^^^^^^^^^^^^^^
= help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
= help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
= note: BACKTRACE (of the first span) on thread `unnamed-2`:
= note: inside `std::sync::ReentrantLock::<()>::increment_lock_count` at /Users/Jonas/.rustup/toolchains/miri/lib/rustlib/src/rust/library/std/src/sync/reentrant_lock.rs:244:34: 244:58
= note: inside `std::sync::ReentrantLock::<()>::lock` at /Users/Jonas/.rustup/toolchains/miri/lib/rustlib/src/rust/library/std/src/sync/reentrant_lock.rs:188:17: 188:44
note: inside closure
--> race.rs:8:27
|
8 | let acquire = || drop(lock.lock());
| ^^^^^^^^^^^
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
error: aborting due to 1 previous error
Error: command exited with non-zero code `cargo +miri --quiet run --manifest-path /Users/Jonas/src/miri/Cargo.toml -- -Zmiri-seed=706 -Zmiri-track-weak-memory-loads -Zmiri-preemption-rate=0 race.rs --edition=2021`: 1
ReentrantLock
uses the address of a TLS variable to check whether the current thread already owns the lock. As -Zmiri-track-weak-memory-loads
shows, the second thread reads an outdated owner
value and as with this specific seed the TLS allocation happens to be on the same address, it thinks it already owns the lock; thus triggering UB inside miri.
However, I do not believe this to be an issue with ReentrantLock
. In any allocator, reallocating the same address requires observing that the address has been deallocated. Thus, there must be a happens-before relationship between deallocation and allocation. miri currently does not form such a relationship, leading to the issue above. With happens-before, the outdated read cannot happen because the second thread will observe that the owner has been reset and released after allocating the TLS variable.