Skip to content

Commit 6d4e664

Browse files
committed
Implements miri shims for FUTEX_LOCK_PI, FUTEX_UNLOCK_PI
1 parent d7b0eb7 commit 6d4e664

File tree

4 files changed

+279
-35
lines changed

4 files changed

+279
-35
lines changed

src/tools/miri/src/concurrency/sync.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,8 @@ struct FutexWaiter {
159159
thread: ThreadId,
160160
/// The bitset used by FUTEX_*_BITSET, or u32::MAX for other operations.
161161
bitset: u32,
162+
/// Extra info stored for this waiter.
163+
extra: Option<u32>,
162164
}
163165

164166
/// The state of all synchronization objects.
@@ -783,6 +785,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
783785
timeout: Option<(TimeoutClock, TimeoutAnchor, Duration)>,
784786
retval_succ: Scalar,
785787
retval_timeout: Scalar,
788+
waiter_extra: Option<u32>,
786789
dest: MPlaceTy<'tcx>,
787790
errno_timeout: Scalar,
788791
) {
@@ -791,7 +794,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
791794
let futex = &mut this.machine.sync.futexes.entry(addr).or_default();
792795
let waiters = &mut futex.waiters;
793796
assert!(waiters.iter().all(|waiter| waiter.thread != thread), "thread is already waiting");
794-
waiters.push_back(FutexWaiter { thread, bitset });
797+
waiters.push_back(FutexWaiter { thread, bitset, extra: waiter_extra });
795798
this.block_thread(
796799
BlockReason::Futex { addr },
797800
timeout,
@@ -848,4 +851,22 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
848851
this.unblock_thread(waiter.thread, BlockReason::Futex { addr })?;
849852
interp_ok(true)
850853
}
854+
855+
/// Returns the extra info of the top waiter.
856+
/// The caller must ensure there are waiters.
857+
fn futex_top_waiter_extra(&mut self, addr: u64) -> Option<u32> {
858+
let this = self.eval_context_mut();
859+
let futex = &mut this.machine.sync.futexes.get(&addr)?;
860+
futex.waiters.front().unwrap().extra
861+
}
862+
863+
/// Returns the number of waiters
864+
fn futex_waiter_count(&mut self, addr: u64) -> usize {
865+
let this = self.eval_context_mut();
866+
if let Some(futex) = &mut this.machine.sync.futexes.get(&addr) {
867+
futex.waiters.len()
868+
} else {
869+
0
870+
}
871+
}
851872
}

src/tools/miri/src/shims/unix/linux/sync.rs

Lines changed: 181 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
use std::time::Duration;
2+
3+
use crate::shims::unix::env::EvalContextExt;
14
use crate::*;
25

36
/// Implementation of the SYS_futex syscall.
@@ -15,19 +18,18 @@ pub fn futex<'tcx>(
1518
// may or may not be left out from the `syscall()` call.
1619
// Therefore we don't use `check_arg_count` here, but only check for the
1720
// number of arguments to fall within a range.
18-
let [addr, op, val, ..] = args else {
21+
let [addr, op, ..] = args else {
1922
throw_ub_format!(
2023
"incorrect number of arguments for `futex` syscall: got {}, expected at least 3",
2124
args.len()
2225
);
2326
};
2427

25-
// The first three arguments (after the syscall number itself) are the same to all futex operations:
26-
// (int *addr, int op, int val).
28+
// The first two arguments (after the syscall number itself) are the same to all futex operations:
29+
// (int *addr, int op).
2730
// We checked above that these definitely exist.
2831
let addr = this.read_pointer(addr)?;
2932
let op = this.read_scalar(op)?.to_i32()?;
30-
let val = this.read_scalar(val)?.to_i32()?;
3133

3234
// This is a vararg function so we have to bring our own type for this pointer.
3335
let addr = this.ptr_to_mplace(addr, this.machine.layouts.i32);
@@ -39,6 +41,50 @@ pub fn futex<'tcx>(
3941
let futex_wake = this.eval_libc_i32("FUTEX_WAKE");
4042
let futex_wake_bitset = this.eval_libc_i32("FUTEX_WAKE_BITSET");
4143
let futex_realtime = this.eval_libc_i32("FUTEX_CLOCK_REALTIME");
44+
let futex_lock_pi = this.eval_libc_i32("FUTEX_LOCK_PI");
45+
let futex_unlock_pi = this.eval_libc_i32("FUTEX_UNLOCK_PI");
46+
let futex_waiters = this.eval_libc_u32("FUTEX_WAITERS");
47+
let futex_tid_mask = this.eval_libc_u32("FUTEX_TID_MASK");
48+
49+
// Ok(None) for EINVAL set, Ok(Some(None)) for no timeout (infinity), Ok(Some(Some(...))) for a timeout.
50+
// Forgive me, I don't want to create an enum for this return value.
51+
fn read_timeout<'tcx>(
52+
this: &mut MiriInterpCx<'tcx>,
53+
arg3: &OpTy<'tcx>,
54+
use_realtime_clock: bool,
55+
use_absolute_time: bool,
56+
dest: &MPlaceTy<'tcx>,
57+
) -> InterpResult<'tcx, Option<Option<(TimeoutClock, TimeoutAnchor, Duration)>>> {
58+
let timeout = this.deref_pointer_as(arg3, this.libc_ty_layout("timespec"))?;
59+
interp_ok(Some(if this.ptr_is_null(timeout.ptr())? {
60+
None
61+
} else {
62+
let duration = match this.read_timespec(&timeout)? {
63+
Some(duration) => duration,
64+
None => {
65+
this.set_last_error(LibcError("EINVAL"))?;
66+
this.write_scalar(Scalar::from_target_isize(-1, this), dest)?;
67+
return interp_ok(None);
68+
}
69+
};
70+
let timeout_clock = if use_realtime_clock {
71+
this.check_no_isolation(
72+
"`futex` syscall with `op=FUTEX_WAIT` and non-null timeout with `FUTEX_CLOCK_REALTIME`",
73+
)?;
74+
TimeoutClock::RealTime
75+
} else {
76+
TimeoutClock::Monotonic
77+
};
78+
let timeout_anchor = if use_absolute_time {
79+
// FUTEX_WAIT_BITSET uses an absolute timestamp.
80+
TimeoutAnchor::Absolute
81+
} else {
82+
// FUTEX_WAIT uses a relative timestamp.
83+
TimeoutAnchor::Relative
84+
};
85+
Some((timeout_clock, timeout_anchor, duration))
86+
}))
87+
}
4288

4389
// FUTEX_PRIVATE enables an optimization that stops it from working across processes.
4490
// Miri doesn't support that anyway, so we ignore that flag.
@@ -74,41 +120,25 @@ pub fn futex<'tcx>(
74120
u32::MAX
75121
};
76122

123+
// We ensured at least 4 arguments above so these work.
124+
let val = this.read_scalar(&args[2])?.to_i32()?;
125+
let Some(timeout) = read_timeout(
126+
this,
127+
&args[3],
128+
op & futex_realtime == futex_realtime,
129+
wait_bitset,
130+
dest,
131+
)?
132+
else {
133+
return interp_ok(());
134+
};
135+
77136
if bitset == 0 {
78137
this.set_last_error(LibcError("EINVAL"))?;
79138
this.write_scalar(Scalar::from_target_isize(-1, this), dest)?;
80139
return interp_ok(());
81140
}
82141

83-
let timeout = this.deref_pointer_as(&args[3], this.libc_ty_layout("timespec"))?;
84-
let timeout = if this.ptr_is_null(timeout.ptr())? {
85-
None
86-
} else {
87-
let duration = match this.read_timespec(&timeout)? {
88-
Some(duration) => duration,
89-
None => {
90-
this.set_last_error(LibcError("EINVAL"))?;
91-
this.write_scalar(Scalar::from_target_isize(-1, this), dest)?;
92-
return interp_ok(());
93-
}
94-
};
95-
let timeout_clock = if op & futex_realtime == futex_realtime {
96-
this.check_no_isolation(
97-
"`futex` syscall with `op=FUTEX_WAIT` and non-null timeout with `FUTEX_CLOCK_REALTIME`",
98-
)?;
99-
TimeoutClock::RealTime
100-
} else {
101-
TimeoutClock::Monotonic
102-
};
103-
let timeout_anchor = if wait_bitset {
104-
// FUTEX_WAIT_BITSET uses an absolute timestamp.
105-
TimeoutAnchor::Absolute
106-
} else {
107-
// FUTEX_WAIT uses a relative timestamp.
108-
TimeoutAnchor::Relative
109-
};
110-
Some((timeout_clock, timeout_anchor, duration))
111-
};
112142
// There may be a concurrent thread changing the value of addr
113143
// and then invoking the FUTEX_WAKE syscall. It is critical that the
114144
// effects of this and the other thread are correctly observed,
@@ -164,6 +194,7 @@ pub fn futex<'tcx>(
164194
timeout,
165195
Scalar::from_target_isize(0, this), // retval_succ
166196
Scalar::from_target_isize(-1, this), // retval_timeout
197+
None,
167198
dest.clone(),
168199
this.eval_libc("ETIMEDOUT"),
169200
);
@@ -182,6 +213,15 @@ pub fn futex<'tcx>(
182213
// FUTEX_WAKE_BITSET: (int *addr, int op = FUTEX_WAKE, int val, const timespect *_unused, int *_unused, unsigned int bitset)
183214
// Same as FUTEX_WAKE, but allows you to specify a bitset to select which threads to wake up.
184215
op if op == futex_wake || op == futex_wake_bitset => {
216+
if args.len() < 3 {
217+
throw_ub_format!(
218+
"incorrect number of arguments for `futex` syscall with `op=FUTEX_WAKE`: got {}, expected at least 3",
219+
args.len()
220+
);
221+
}
222+
223+
let val = this.read_scalar(&args[2])?.to_i32()?;
224+
185225
let bitset = if op == futex_wake_bitset {
186226
let [_, _, _, timeout, uaddr2, bitset, ..] = args else {
187227
throw_ub_format!(
@@ -215,6 +255,114 @@ pub fn futex<'tcx>(
215255
}
216256
this.write_scalar(Scalar::from_target_isize(n, this), dest)?;
217257
}
258+
op if op == futex_lock_pi => {
259+
if args.len() < 4 {
260+
throw_ub_format!(
261+
"incorrect number of arguments for `futex` syscall with `op=FUTEX_LOCK_PI`: got {}, expected at least 4",
262+
args.len()
263+
);
264+
}
265+
266+
// FUTEX_LOCK_PI uses absolute CLOCK_REALTIME timestamp.
267+
let Some(timeout) = read_timeout(this, &args[3], true, true, dest)? else {
268+
return interp_ok(());
269+
};
270+
271+
// The tid of the owner is store to *addr.
272+
// N.B. it is not the same as posix thread id.
273+
let tid = this.linux_gettid()?.to_u32()?;
274+
275+
// The same as above. This makes modifications visible to us.
276+
this.atomic_fence(AtomicFenceOrd::SeqCst)?;
277+
278+
// For bitand working properly, we read it as a u32.
279+
let futex_val = this.read_scalar_atomic(&addr, AtomicReadOrd::Relaxed)?.to_u32()?;
280+
281+
if futex_val == 0 {
282+
// 0 means unlocked - then lock it.
283+
this.write_scalar_atomic(Scalar::from_u32(tid), &addr, AtomicWriteOrd::Relaxed)?;
284+
285+
// This ensures all loads afterwards get updated value of *addr.
286+
this.atomic_fence(AtomicFenceOrd::SeqCst)?;
287+
288+
// FUTEX_LOCK_PI returns 0 on success.
289+
this.write_scalar(Scalar::from_target_isize(0, this), dest)?;
290+
} else if (futex_val & futex_tid_mask) == tid {
291+
// Locked by self.
292+
this.set_last_error(LibcError("EDEADLK"))?;
293+
this.write_scalar(Scalar::from_target_isize(-1, this), dest)?;
294+
} else {
295+
// Other values mean locked.
296+
// Mark the futex as contended.
297+
this.write_scalar_atomic(
298+
Scalar::from_u32(futex_val | futex_waiters),
299+
&addr,
300+
AtomicWriteOrd::Relaxed,
301+
)?;
302+
303+
// This ensures all loads afterwards get updated value of *addr.
304+
this.atomic_fence(AtomicFenceOrd::SeqCst)?;
305+
306+
// Put ourselves into the wait queue.
307+
this.futex_wait(
308+
addr_usize,
309+
u32::MAX,
310+
timeout,
311+
Scalar::from_target_isize(0, this), // retval_succ
312+
Scalar::from_target_isize(-1, this), // retval_timeout
313+
Some(tid),
314+
dest.clone(),
315+
this.eval_libc("ETIMEDOUT"),
316+
);
317+
}
318+
}
319+
op if op == futex_unlock_pi => {
320+
let tid = this.linux_gettid()?.to_u32()?;
321+
322+
// This ensures all modifications happen before.
323+
this.atomic_fence(AtomicFenceOrd::SeqCst)?;
324+
325+
// Check we owns lock.
326+
let futex_val = this.read_scalar_atomic(&addr, AtomicReadOrd::Relaxed)?.to_u32()?;
327+
if (futex_val & futex_tid_mask) != tid {
328+
this.set_last_error(LibcError("EINVAL"))?;
329+
this.write_scalar(Scalar::from_target_isize(-1, this), dest)?;
330+
return interp_ok(());
331+
}
332+
333+
let waiter_count = this.futex_waiter_count(addr_usize);
334+
let new_futex_val = if waiter_count == 0 {
335+
0
336+
} else {
337+
let Some(new_tid) = this.futex_top_waiter_extra(addr_usize) else {
338+
// The waiter is not waiting using FUTEX_LOCK_PI.
339+
this.set_last_error(LibcError("EINVAL"))?;
340+
this.write_scalar(Scalar::from_target_isize(-1, this), dest)?;
341+
return interp_ok(());
342+
};
343+
344+
if waiter_count > 1 {
345+
// Still contended after we unlocks.
346+
new_tid | futex_waiters
347+
} else {
348+
new_tid
349+
}
350+
};
351+
352+
// Update locked state.
353+
this.write_scalar_atomic(Scalar::from_u32(new_futex_val), &addr, AtomicWriteOrd::Relaxed)?;
354+
355+
// This ensures all loads afterwards get updated value of *addr.
356+
// There are no preemptions so no one can wake after we set the futex to unlocked
357+
// and before we use futex_wake to wake one waiter.
358+
this.atomic_fence(AtomicFenceOrd::SeqCst)?;
359+
360+
// Unlocking wakes zero or one waiters.
361+
let _ = this.futex_wake(addr_usize, u32::MAX)?;
362+
363+
// FUTEX_UNLOCK_PI returns 0 on success.
364+
this.write_scalar(Scalar::from_target_isize(0, this), dest)?;
365+
}
218366
op => throw_unsup_format!("Miri does not support `futex` syscall with op={}", op),
219367
}
220368

src/tools/miri/src/shims/windows/sync.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
181181
timeout,
182182
Scalar::from_i32(1), // retval_succ
183183
Scalar::from_i32(0), // retval_timeout
184+
None,
184185
dest.clone(),
185186
this.eval_windows("c", "ERROR_TIMEOUT"),
186187
);

0 commit comments

Comments
 (0)