Skip to content

Commit 818642e

Browse files
committed
throw an error if a synchronization object is read/written while threads are queued
1 parent 3a95120 commit 818642e

14 files changed

+244
-22
lines changed

src/borrow_tracker/mod.rs

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -115,15 +115,6 @@ impl VisitProvenance for GlobalStateInner {
115115
/// We need interior mutable access to the global state.
116116
pub type GlobalState = RefCell<GlobalStateInner>;
117117

118-
impl fmt::Display for AccessKind {
119-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120-
match self {
121-
AccessKind::Read => write!(f, "read access"),
122-
AccessKind::Write => write!(f, "write access"),
123-
}
124-
}
125-
}
126-
127118
/// Policy on whether to recurse into fields to retag
128119
#[derive(Copy, Clone, Debug)]
129120
pub enum RetagFields {

src/concurrency/init_once.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ impl InitOnceRef {
5757
pub fn begin(&self) {
5858
self.0.borrow_mut().begin();
5959
}
60+
61+
pub fn queue_is_empty(&self) -> bool {
62+
self.0.borrow().waiters.is_empty()
63+
}
6064
}
6165

6266
impl VisitProvenance for InitOnceRef {

src/concurrency/sync.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ use crate::*;
1616

1717
/// A trait for the synchronization metadata that can be attached to a memory location.
1818
pub trait SyncObj: Any {
19+
/// Determines whether reads/writes to this object's location are currently permitted.
20+
fn on_access<'tcx>(&self, _access_kind: AccessKind) -> InterpResult<'tcx> {
21+
interp_ok(())
22+
}
23+
1924
/// Determines whether this object's metadata shall be deleted when a write to its
2025
/// location occurs.
2126
fn delete_on_write(&self) -> bool {
@@ -62,6 +67,10 @@ impl MutexRef {
6267
pub fn owner(&self) -> Option<ThreadId> {
6368
self.0.borrow().owner
6469
}
70+
71+
pub fn queue_is_empty(&self) -> bool {
72+
self.0.borrow().queue.is_empty()
73+
}
6574
}
6675

6776
impl VisitProvenance for MutexRef {
@@ -138,6 +147,11 @@ impl RwLockRef {
138147
pub fn is_write_locked(&self) -> bool {
139148
self.0.borrow().is_write_locked()
140149
}
150+
151+
pub fn queue_is_empty(&self) -> bool {
152+
let inner = self.0.borrow();
153+
inner.reader_queue.is_empty() && inner.writer_queue.is_empty()
154+
}
141155
}
142156

143157
impl VisitProvenance for RwLockRef {
@@ -165,8 +179,8 @@ impl CondvarRef {
165179
Self(Default::default())
166180
}
167181

168-
pub fn is_awaited(&self) -> bool {
169-
!self.0.borrow().waiters.is_empty()
182+
pub fn queue_is_empty(&self) -> bool {
183+
self.0.borrow().waiters.is_empty()
170184
}
171185
}
172186

src/helpers.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::num::NonZero;
22
use std::sync::Mutex;
33
use std::time::Duration;
4-
use std::{cmp, iter};
4+
use std::{cmp, fmt, iter};
55

66
use rand::RngCore;
77
use rustc_abi::{Align, ExternAbi, FieldIdx, FieldsShape, Size, Variants};
@@ -29,6 +29,15 @@ pub enum AccessKind {
2929
Write,
3030
}
3131

32+
impl fmt::Display for AccessKind {
33+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34+
match self {
35+
AccessKind::Read => write!(f, "read access"),
36+
AccessKind::Write => write!(f, "write access"),
37+
}
38+
}
39+
}
40+
3241
/// Gets an instance for a path.
3342
///
3443
/// A `None` namespace indicates we are looking for a module.

src/machine.rs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1547,6 +1547,11 @@ impl<'tcx> Machine<'tcx> for MiriMachine<'tcx> {
15471547
if let Some(borrow_tracker) = &alloc_extra.borrow_tracker {
15481548
borrow_tracker.before_memory_read(alloc_id, prov_extra, range, machine)?;
15491549
}
1550+
// Check if there are any sync objects that would like to prevent reading this memory.
1551+
for (_offset, obj) in alloc_extra.sync_objs.range(range.start..range.end()) {
1552+
obj.on_access(AccessKind::Read)?;
1553+
}
1554+
15501555
interp_ok(())
15511556
}
15521557

@@ -1590,11 +1595,13 @@ impl<'tcx> Machine<'tcx> for MiriMachine<'tcx> {
15901595
// Delete sync objects that don't like writes.
15911596
// Most of the time, we can just skip this.
15921597
if !alloc_extra.sync_objs.is_empty() {
1593-
let to_delete = alloc_extra
1594-
.sync_objs
1595-
.range(range.start..range.end())
1596-
.filter_map(|(offset, obj)| obj.delete_on_write().then_some(*offset))
1597-
.collect::<Vec<_>>();
1598+
let mut to_delete = vec![];
1599+
for (offset, obj) in alloc_extra.sync_objs.range(range.start..range.end()) {
1600+
obj.on_access(AccessKind::Write)?;
1601+
if obj.delete_on_write() {
1602+
to_delete.push(*offset);
1603+
}
1604+
}
15981605
for offset in to_delete {
15991606
alloc_extra.sync_objs.remove(&offset);
16001607
}

src/shims/unix/macos/sync.rs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,17 @@ enum MacOsUnfairLock {
2525
}
2626

2727
impl SyncObj for MacOsUnfairLock {
28+
fn on_access<'tcx>(&self, access_kind: AccessKind) -> InterpResult<'tcx> {
29+
if let MacOsUnfairLock::Active { mutex_ref } = self
30+
&& !mutex_ref.queue_is_empty()
31+
{
32+
throw_ub_format!(
33+
"{access_kind} to `os_unfair_lock` is forbidden while the queue is non-empty"
34+
);
35+
}
36+
interp_ok(())
37+
}
38+
2839
fn delete_on_write(&self) -> bool {
2940
true
3041
}
@@ -81,10 +92,10 @@ trait EvalContextExtPriv<'tcx>: crate::MiriInterpCxExt<'tcx> {
8192
// This is a lock that got copied while it is initialized. We de-initialize
8293
// locks when they get released, so it got copied while locked. Unfortunately
8394
// that is something `std` needs to support (the guard could have been leaked).
84-
// So we behave like a futex-based lock whose wait queue got pruned: any attempt
85-
// to acquire the lock will just wait forever.
86-
// In practice there actually could be a wait queue there, if someone moves a
87-
// lock *while threads are queued*; this is UB we will not detect.
95+
// On the plus side, we know nobody was queued for the lock while it got copied;
96+
// that would have been rejected by our `on_access`. So we behave like a
97+
// futex-based lock would in this case: any attempt to acquire the lock will
98+
// just wait forever, since there's nobody to wake us up.
8899
interp_ok(MacOsUnfairLock::PermanentlyLocked)
89100
} else {
90101
throw_ub_format!("`os_unfair_lock` was not properly initialized at this location, or it got overwritten");

src/shims/unix/sync.rs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,15 @@ struct PthreadMutex {
110110
}
111111

112112
impl SyncObj for PthreadMutex {
113+
fn on_access<'tcx>(&self, access_kind: AccessKind) -> InterpResult<'tcx> {
114+
if !self.mutex_ref.queue_is_empty() {
115+
throw_ub_format!(
116+
"{access_kind} to `pthread_mutex_t` is forbidden while the queue is non-empty"
117+
);
118+
}
119+
interp_ok(())
120+
}
121+
113122
fn delete_on_write(&self) -> bool {
114123
true
115124
}
@@ -230,6 +239,15 @@ struct PthreadRwLock {
230239
}
231240

232241
impl SyncObj for PthreadRwLock {
242+
fn on_access<'tcx>(&self, access_kind: AccessKind) -> InterpResult<'tcx> {
243+
if !self.rwlock_ref.queue_is_empty() {
244+
throw_ub_format!(
245+
"{access_kind} to `pthread_rwlock_t` is forbidden while the queue is non-empty"
246+
);
247+
}
248+
interp_ok(())
249+
}
250+
233251
fn delete_on_write(&self) -> bool {
234252
true
235253
}
@@ -361,6 +379,15 @@ struct PthreadCondvar {
361379
}
362380

363381
impl SyncObj for PthreadCondvar {
382+
fn on_access<'tcx>(&self, access_kind: AccessKind) -> InterpResult<'tcx> {
383+
if !self.condvar_ref.queue_is_empty() {
384+
throw_ub_format!(
385+
"{access_kind} to `pthread_cond_t` is forbidden while the queue is non-empty"
386+
);
387+
}
388+
interp_ok(())
389+
}
390+
364391
fn delete_on_write(&self) -> bool {
365392
true
366393
}
@@ -900,7 +927,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
900927
// Reading the field also has the side-effect that we detect double-`destroy`
901928
// since we make the field uninit below.
902929
let condvar = &cond_get_data(this, cond_op)?.condvar_ref;
903-
if condvar.is_awaited() {
930+
if !condvar.queue_is_empty() {
904931
throw_ub_format!("destroying an awaited conditional variable");
905932
}
906933

src/shims/windows/sync.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ struct WindowsInitOnce {
1212
}
1313

1414
impl SyncObj for WindowsInitOnce {
15+
fn on_access<'tcx>(&self, access_kind: AccessKind) -> InterpResult<'tcx> {
16+
if !self.init_once.queue_is_empty() {
17+
throw_ub_format!(
18+
"{access_kind} to `INIT_ONCE` is forbidden while the queue is non-empty"
19+
);
20+
}
21+
interp_ok(())
22+
}
23+
1524
fn delete_on_write(&self) -> bool {
1625
true
1726
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//@only-target: darwin
2+
#![feature(sync_unsafe_cell)]
3+
4+
use std::cell::SyncUnsafeCell;
5+
use std::sync::atomic::*;
6+
use std::thread;
7+
8+
fn main() {
9+
let lock = SyncUnsafeCell::new(libc::OS_UNFAIR_LOCK_INIT);
10+
11+
thread::scope(|s| {
12+
// First thread: grabs the lock.
13+
s.spawn(|| {
14+
unsafe { libc::os_unfair_lock_lock(lock.get()) };
15+
thread::yield_now();
16+
unreachable!();
17+
});
18+
// Second thread: queues for the lock.
19+
s.spawn(|| {
20+
unsafe { libc::os_unfair_lock_lock(lock.get()) };
21+
unreachable!();
22+
});
23+
// Third thread: tries to read the lock while second thread is queued.
24+
s.spawn(|| {
25+
let atomic_ref = unsafe { &*lock.get().cast::<AtomicU32>() };
26+
let _val = atomic_ref.load(Ordering::Relaxed); //~ERROR: read access to `os_unfair_lock` is forbidden while the queue is non-empty
27+
});
28+
});
29+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
error: Undefined Behavior: read access to `os_unfair_lock` is forbidden while the queue is non-empty
2+
--> tests/fail-dep/concurrency/apple_os_unfair_lock_move_with_queue.rs:LL:CC
3+
|
4+
LL | ... let _val = atomic_ref.load(Ordering::Relaxed);
5+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Undefined Behavior occurred here
6+
|
7+
= help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
8+
= help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
9+
10+
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
11+
12+
error: aborting due to 1 previous error
13+

0 commit comments

Comments
 (0)