Skip to content

Commit 23ff2ea

Browse files
committed
Auto merge of #3973 - RalfJung:os-unfair-lock, r=RalfJung
ensure that a macOS os_unfair_lock that is moved while being held is not implicitly unlocked Fixes #3859 We mark an os_unfair_lock that is moved while being held as "poisoned", which means it is not considered forever locked. That's not quite what the real implementation does, but allowing arbitrary moves-while-locked would likely expose a ton of implementation details, so hopefully this is good enough.
2 parents 549527f + 9a4cd35 commit 23ff2ea

File tree

10 files changed

+297
-122
lines changed

10 files changed

+297
-122
lines changed

src/concurrency/sync.rs

+97-63
Original file line numberDiff line numberDiff line change
@@ -193,75 +193,109 @@ impl<'tcx> AllocExtra<'tcx> {
193193
/// If `init` is set to this, we consider the primitive initialized.
194194
pub const LAZY_INIT_COOKIE: u32 = 0xcafe_affe;
195195

196-
/// Helper for lazily initialized `alloc_extra.sync` data:
197-
/// this forces an immediate init.
198-
pub fn lazy_sync_init<'tcx, T: 'static + Copy>(
199-
ecx: &mut MiriInterpCx<'tcx>,
200-
primitive: &MPlaceTy<'tcx>,
201-
init_offset: Size,
202-
data: T,
203-
) -> InterpResult<'tcx> {
204-
let (alloc, offset, _) = ecx.ptr_get_alloc_id(primitive.ptr(), 0)?;
205-
let (alloc_extra, _machine) = ecx.get_alloc_extra_mut(alloc)?;
206-
alloc_extra.sync.insert(offset, Box::new(data));
207-
// Mark this as "initialized".
208-
let init_field = primitive.offset(init_offset, ecx.machine.layouts.u32, ecx)?;
209-
ecx.write_scalar_atomic(
210-
Scalar::from_u32(LAZY_INIT_COOKIE),
211-
&init_field,
212-
AtomicWriteOrd::Relaxed,
213-
)?;
214-
interp_ok(())
215-
}
216-
217-
/// Helper for lazily initialized `alloc_extra.sync` data:
218-
/// Checks if the primitive is initialized, and return its associated data if so.
219-
/// Otherwise, calls `new_data` to initialize the primitive.
220-
pub fn lazy_sync_get_data<'tcx, T: 'static + Copy>(
221-
ecx: &mut MiriInterpCx<'tcx>,
222-
primitive: &MPlaceTy<'tcx>,
223-
init_offset: Size,
224-
name: &str,
225-
new_data: impl FnOnce(&mut MiriInterpCx<'tcx>) -> InterpResult<'tcx, T>,
226-
) -> InterpResult<'tcx, T> {
227-
// Check if this is already initialized. Needs to be atomic because we can race with another
228-
// thread initializing. Needs to be an RMW operation to ensure we read the *latest* value.
229-
// So we just try to replace MUTEX_INIT_COOKIE with itself.
230-
let init_cookie = Scalar::from_u32(LAZY_INIT_COOKIE);
231-
let init_field = primitive.offset(init_offset, ecx.machine.layouts.u32, ecx)?;
232-
let (_init, success) = ecx
233-
.atomic_compare_exchange_scalar(
234-
&init_field,
235-
&ImmTy::from_scalar(init_cookie, ecx.machine.layouts.u32),
236-
init_cookie,
237-
AtomicRwOrd::Relaxed,
238-
AtomicReadOrd::Relaxed,
239-
/* can_fail_spuriously */ false,
240-
)?
241-
.to_scalar_pair();
242-
243-
if success.to_bool()? {
244-
// If it is initialized, it must be found in the "sync primitive" table,
245-
// or else it has been moved illegally.
246-
let (alloc, offset, _) = ecx.ptr_get_alloc_id(primitive.ptr(), 0)?;
247-
let alloc_extra = ecx.get_alloc_extra(alloc)?;
248-
let data = alloc_extra
249-
.get_sync::<T>(offset)
250-
.ok_or_else(|| err_ub_format!("`{name}` can't be moved after first use"))?;
251-
interp_ok(*data)
252-
} else {
253-
let data = new_data(ecx)?;
254-
lazy_sync_init(ecx, primitive, init_offset, data)?;
255-
interp_ok(data)
256-
}
257-
}
258-
259196
// Public interface to synchronization primitives. Please note that in most
260197
// cases, the function calls are infallible and it is the client's (shim
261198
// implementation's) responsibility to detect and deal with erroneous
262199
// situations.
263200
impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {}
264201
pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
202+
/// Helper for lazily initialized `alloc_extra.sync` data:
203+
/// this forces an immediate init.
204+
fn lazy_sync_init<T: 'static + Copy>(
205+
&mut self,
206+
primitive: &MPlaceTy<'tcx>,
207+
init_offset: Size,
208+
data: T,
209+
) -> InterpResult<'tcx> {
210+
let this = self.eval_context_mut();
211+
212+
let (alloc, offset, _) = this.ptr_get_alloc_id(primitive.ptr(), 0)?;
213+
let (alloc_extra, _machine) = this.get_alloc_extra_mut(alloc)?;
214+
alloc_extra.sync.insert(offset, Box::new(data));
215+
// Mark this as "initialized".
216+
let init_field = primitive.offset(init_offset, this.machine.layouts.u32, this)?;
217+
this.write_scalar_atomic(
218+
Scalar::from_u32(LAZY_INIT_COOKIE),
219+
&init_field,
220+
AtomicWriteOrd::Relaxed,
221+
)?;
222+
interp_ok(())
223+
}
224+
225+
/// Helper for lazily initialized `alloc_extra.sync` data:
226+
/// Checks if the primitive is initialized:
227+
/// - If yes, fetches the data from `alloc_extra.sync`, or calls `missing_data` if that fails
228+
/// and stores that in `alloc_extra.sync`.
229+
/// - Otherwise, calls `new_data` to initialize the primitive.
230+
fn lazy_sync_get_data<T: 'static + Copy>(
231+
&mut self,
232+
primitive: &MPlaceTy<'tcx>,
233+
init_offset: Size,
234+
missing_data: impl FnOnce() -> InterpResult<'tcx, T>,
235+
new_data: impl FnOnce(&mut MiriInterpCx<'tcx>) -> InterpResult<'tcx, T>,
236+
) -> InterpResult<'tcx, T> {
237+
let this = self.eval_context_mut();
238+
239+
// Check if this is already initialized. Needs to be atomic because we can race with another
240+
// thread initializing. Needs to be an RMW operation to ensure we read the *latest* value.
241+
// So we just try to replace MUTEX_INIT_COOKIE with itself.
242+
let init_cookie = Scalar::from_u32(LAZY_INIT_COOKIE);
243+
let init_field = primitive.offset(init_offset, this.machine.layouts.u32, this)?;
244+
let (_init, success) = this
245+
.atomic_compare_exchange_scalar(
246+
&init_field,
247+
&ImmTy::from_scalar(init_cookie, this.machine.layouts.u32),
248+
init_cookie,
249+
AtomicRwOrd::Relaxed,
250+
AtomicReadOrd::Relaxed,
251+
/* can_fail_spuriously */ false,
252+
)?
253+
.to_scalar_pair();
254+
255+
if success.to_bool()? {
256+
// If it is initialized, it must be found in the "sync primitive" table,
257+
// or else it has been moved illegally.
258+
let (alloc, offset, _) = this.ptr_get_alloc_id(primitive.ptr(), 0)?;
259+
let (alloc_extra, _machine) = this.get_alloc_extra_mut(alloc)?;
260+
if let Some(data) = alloc_extra.get_sync::<T>(offset) {
261+
interp_ok(*data)
262+
} else {
263+
let data = missing_data()?;
264+
alloc_extra.sync.insert(offset, Box::new(data));
265+
interp_ok(data)
266+
}
267+
} else {
268+
let data = new_data(this)?;
269+
this.lazy_sync_init(primitive, init_offset, data)?;
270+
interp_ok(data)
271+
}
272+
}
273+
274+
/// Get the synchronization primitive associated with the given pointer,
275+
/// or initialize a new one.
276+
fn get_sync_or_init<'a, T: 'static>(
277+
&'a mut self,
278+
ptr: Pointer,
279+
new: impl FnOnce(&'a mut MiriMachine<'tcx>) -> InterpResult<'tcx, T>,
280+
) -> InterpResult<'tcx, &'a T>
281+
where
282+
'tcx: 'a,
283+
{
284+
let this = self.eval_context_mut();
285+
// Ensure there is memory behind this pointer, so that this allocation
286+
// is truly the only place where the data could be stored.
287+
this.check_ptr_access(ptr, Size::from_bytes(1), CheckInAllocMsg::InboundsTest)?;
288+
289+
let (alloc, offset, _) = this.ptr_get_alloc_id(ptr, 0)?;
290+
let (alloc_extra, machine) = this.get_alloc_extra_mut(alloc)?;
291+
// Due to borrow checker reasons, we have to do the lookup twice.
292+
if alloc_extra.get_sync::<T>(offset).is_none() {
293+
let new = new(machine)?;
294+
alloc_extra.sync.insert(offset, Box::new(new));
295+
}
296+
interp_ok(alloc_extra.get_sync::<T>(offset).unwrap())
297+
}
298+
265299
#[inline]
266300
/// Get the id of the thread that currently owns this lock.
267301
fn mutex_get_owner(&mut self, id: MutexId) -> ThreadId {

src/concurrency/thread.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ macro_rules! callback {
5959
@unblock = |$this:ident| $unblock:block
6060
) => {
6161
callback!(
62-
@capture<$tcx, $($lft),*> { $($name: $type),+ }
62+
@capture<$tcx, $($lft),*> { $($name: $type),* }
6363
@unblock = |$this| $unblock
6464
@timeout = |_this| {
6565
unreachable!(

src/shims/unix/macos/sync.rs

+83-19
Original file line numberDiff line numberDiff line change
@@ -10,28 +10,42 @@
1010
//! and we do not detect copying of the lock, but macOS doesn't guarantee anything
1111
//! in that case either.
1212
13+
use rustc_target::abi::Size;
14+
1315
use crate::*;
1416

15-
struct MacOsUnfairLock {
16-
id: MutexId,
17+
#[derive(Copy, Clone)]
18+
enum MacOsUnfairLock {
19+
Poisoned,
20+
Active { id: MutexId },
1721
}
1822

1923
impl<'tcx> EvalContextExtPriv<'tcx> for crate::MiriInterpCx<'tcx> {}
2024
trait EvalContextExtPriv<'tcx>: crate::MiriInterpCxExt<'tcx> {
21-
fn os_unfair_lock_getid(&mut self, lock_ptr: &OpTy<'tcx>) -> InterpResult<'tcx, MutexId> {
25+
fn os_unfair_lock_get_data(
26+
&mut self,
27+
lock_ptr: &OpTy<'tcx>,
28+
) -> InterpResult<'tcx, MacOsUnfairLock> {
2229
let this = self.eval_context_mut();
2330
let lock = this.deref_pointer(lock_ptr)?;
24-
// We store the mutex ID in the `sync` metadata. This means that when the lock is moved,
25-
// that's just implicitly creating a new lock at the new location.
26-
let (alloc, offset, _) = this.ptr_get_alloc_id(lock.ptr(), 0)?;
27-
let (alloc_extra, machine) = this.get_alloc_extra_mut(alloc)?;
28-
if let Some(data) = alloc_extra.get_sync::<MacOsUnfairLock>(offset) {
29-
interp_ok(data.id)
30-
} else {
31-
let id = machine.sync.mutex_create();
32-
alloc_extra.sync.insert(offset, Box::new(MacOsUnfairLock { id }));
33-
interp_ok(id)
34-
}
31+
this.lazy_sync_get_data(
32+
&lock,
33+
Size::ZERO, // offset for init tracking
34+
|| {
35+
// If we get here, due to how we reset things to zero in `os_unfair_lock_unlock`,
36+
// this means the lock was moved while locked. This can happen with a `std` lock,
37+
// but then any future attempt to unlock will just deadlock. In practice, terrible
38+
// things can probably happen if you swap two locked locks, since they'd wake up
39+
// from the wrong queue... we just won't catch all UB of this library API then (we
40+
// would need to store some unique identifer in-memory for this, instead of a static
41+
// LAZY_INIT_COOKIE). This can't be hit via `std::sync::Mutex`.
42+
interp_ok(MacOsUnfairLock::Poisoned)
43+
},
44+
|ecx| {
45+
let id = ecx.machine.sync.mutex_create();
46+
interp_ok(MacOsUnfairLock::Active { id })
47+
},
48+
)
3549
}
3650
}
3751

@@ -40,7 +54,21 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
4054
fn os_unfair_lock_lock(&mut self, lock_op: &OpTy<'tcx>) -> InterpResult<'tcx> {
4155
let this = self.eval_context_mut();
4256

43-
let id = this.os_unfair_lock_getid(lock_op)?;
57+
let MacOsUnfairLock::Active { id } = this.os_unfair_lock_get_data(lock_op)? else {
58+
// Trying to get a poisoned lock. Just block forever...
59+
this.block_thread(
60+
BlockReason::Sleep,
61+
None,
62+
callback!(
63+
@capture<'tcx> {}
64+
@unblock = |_this| {
65+
panic!("we shouldn't wake up ever")
66+
}
67+
),
68+
);
69+
return interp_ok(());
70+
};
71+
4472
if this.mutex_is_locked(id) {
4573
if this.mutex_get_owner(id) == this.active_thread() {
4674
// Matching the current macOS implementation: abort on reentrant locking.
@@ -64,7 +92,12 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
6492
) -> InterpResult<'tcx> {
6593
let this = self.eval_context_mut();
6694

67-
let id = this.os_unfair_lock_getid(lock_op)?;
95+
let MacOsUnfairLock::Active { id } = this.os_unfair_lock_get_data(lock_op)? else {
96+
// Trying to get a poisoned lock. That never works.
97+
this.write_scalar(Scalar::from_bool(false), dest)?;
98+
return interp_ok(());
99+
};
100+
68101
if this.mutex_is_locked(id) {
69102
// Contrary to the blocking lock function, this does not check for
70103
// reentrancy.
@@ -80,40 +113,71 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
80113
fn os_unfair_lock_unlock(&mut self, lock_op: &OpTy<'tcx>) -> InterpResult<'tcx> {
81114
let this = self.eval_context_mut();
82115

83-
let id = this.os_unfair_lock_getid(lock_op)?;
116+
let MacOsUnfairLock::Active { id } = this.os_unfair_lock_get_data(lock_op)? else {
117+
// The lock is poisoned, who knows who owns it... we'll pretend: someone else.
118+
throw_machine_stop!(TerminationInfo::Abort(
119+
"attempted to unlock an os_unfair_lock not owned by the current thread".to_owned()
120+
));
121+
};
122+
123+
// Now, unlock.
84124
if this.mutex_unlock(id)?.is_none() {
85125
// Matching the current macOS implementation: abort.
86126
throw_machine_stop!(TerminationInfo::Abort(
87127
"attempted to unlock an os_unfair_lock not owned by the current thread".to_owned()
88128
));
89129
}
90130

131+
// If the lock is not locked by anyone now, it went quer.
132+
// Reset to zero so that it can be moved and initialized again for the next phase.
133+
if !this.mutex_is_locked(id) {
134+
let lock_place = this.deref_pointer_as(lock_op, this.machine.layouts.u32)?;
135+
this.write_scalar_atomic(Scalar::from_u32(0), &lock_place, AtomicWriteOrd::Relaxed)?;
136+
}
137+
91138
interp_ok(())
92139
}
93140

94141
fn os_unfair_lock_assert_owner(&mut self, lock_op: &OpTy<'tcx>) -> InterpResult<'tcx> {
95142
let this = self.eval_context_mut();
96143

97-
let id = this.os_unfair_lock_getid(lock_op)?;
144+
let MacOsUnfairLock::Active { id } = this.os_unfair_lock_get_data(lock_op)? else {
145+
// The lock is poisoned, who knows who owns it... we'll pretend: someone else.
146+
throw_machine_stop!(TerminationInfo::Abort(
147+
"called os_unfair_lock_assert_owner on an os_unfair_lock not owned by the current thread".to_owned()
148+
));
149+
};
98150
if !this.mutex_is_locked(id) || this.mutex_get_owner(id) != this.active_thread() {
99151
throw_machine_stop!(TerminationInfo::Abort(
100152
"called os_unfair_lock_assert_owner on an os_unfair_lock not owned by the current thread".to_owned()
101153
));
102154
}
103155

156+
// The lock is definitely not quiet since we are the owner.
157+
104158
interp_ok(())
105159
}
106160

107161
fn os_unfair_lock_assert_not_owner(&mut self, lock_op: &OpTy<'tcx>) -> InterpResult<'tcx> {
108162
let this = self.eval_context_mut();
109163

110-
let id = this.os_unfair_lock_getid(lock_op)?;
164+
let MacOsUnfairLock::Active { id } = this.os_unfair_lock_get_data(lock_op)? else {
165+
// The lock is poisoned, who knows who owns it... we'll pretend: someone else.
166+
return interp_ok(());
167+
};
111168
if this.mutex_is_locked(id) && this.mutex_get_owner(id) == this.active_thread() {
112169
throw_machine_stop!(TerminationInfo::Abort(
113170
"called os_unfair_lock_assert_not_owner on an os_unfair_lock owned by the current thread".to_owned()
114171
));
115172
}
116173

174+
// If the lock is not locked by anyone now, it went quer.
175+
// Reset to zero so that it can be moved and initialized again for the next phase.
176+
if !this.mutex_is_locked(id) {
177+
let lock_place = this.deref_pointer_as(lock_op, this.machine.layouts.u32)?;
178+
this.write_scalar_atomic(Scalar::from_u32(0), &lock_place, AtomicWriteOrd::Relaxed)?;
179+
}
180+
117181
interp_ok(())
118182
}
119183
}

0 commit comments

Comments
 (0)