Skip to content

Commit 04ed075

Browse files
committed
Fixes & polishes implementation of FUTEX_(UN)LOCK_PI in miri
1 parent 74f8fc1 commit 04ed075

File tree

1 file changed

+144
-54
lines changed
  • src/tools/miri/src/shims/unix/linux

1 file changed

+144
-54
lines changed

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

+144-54
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
use crate::*;
1+
use std::time::Duration;
2+
23
use crate::shims::unix::env::EvalContextExt;
4+
use crate::*;
35

46
/// Implementation of the SYS_futex syscall.
57
/// `args` is the arguments *after* the syscall number.
@@ -43,6 +45,46 @@ pub fn futex<'tcx>(
4345
let futex_unlock_pi = this.eval_libc_i32("FUTEX_UNLOCK_PI");
4446
let futex_waiters = this.eval_libc_u32("FUTEX_WAITERS");
4547

48+
// Ok(None) for EINVAL set, Ok(Some(None)) for no timeout (infinity), Ok(Some(Some(...))) for a timeout.
49+
// Forgive me, I don't want to create an enum for this return value.
50+
fn read_timeout<'tcx>(
51+
this: &mut MiriInterpCx<'tcx>,
52+
arg3: &OpTy<'tcx>,
53+
use_realtime_clock: bool,
54+
use_absolute_time: bool,
55+
dest: &MPlaceTy<'tcx>,
56+
) -> InterpResult<'tcx, Option<Option<(TimeoutClock, TimeoutAnchor, Duration)>>> {
57+
let timeout = this.deref_pointer_as(arg3, this.libc_ty_layout("timespec"))?;
58+
interp_ok(Some(if this.ptr_is_null(timeout.ptr())? {
59+
None
60+
} else {
61+
let duration = match this.read_timespec(&timeout)? {
62+
Some(duration) => duration,
63+
None => {
64+
this.set_last_error(LibcError("EINVAL"))?;
65+
this.write_scalar(Scalar::from_target_isize(-1, this), dest)?;
66+
return interp_ok(None);
67+
}
68+
};
69+
let timeout_clock = if use_realtime_clock {
70+
this.check_no_isolation(
71+
"`futex` syscall with `op=FUTEX_WAIT` and non-null timeout with `FUTEX_CLOCK_REALTIME`",
72+
)?;
73+
TimeoutClock::RealTime
74+
} else {
75+
TimeoutClock::Monotonic
76+
};
77+
let timeout_anchor = if use_absolute_time {
78+
// FUTEX_WAIT_BITSET uses an absolute timestamp.
79+
TimeoutAnchor::Absolute
80+
} else {
81+
// FUTEX_WAIT uses a relative timestamp.
82+
TimeoutAnchor::Relative
83+
};
84+
Some((timeout_clock, timeout_anchor, duration))
85+
}))
86+
}
87+
4688
// FUTEX_PRIVATE enables an optimization that stops it from working across processes.
4789
// Miri doesn't support that anyway, so we ignore that flag.
4890
match op & !futex_private {
@@ -54,7 +96,7 @@ pub fn futex<'tcx>(
5496
// This is identical to FUTEX_WAIT, except:
5597
// - The timeout is absolute rather than relative.
5698
// - You can specify the bitset to selecting what WAKE operations to respond to.
57-
op if op & !futex_realtime == futex_wait || op & !futex_realtime == futex_wait_bitset || op == futex_lock_pi => {
99+
op if op & !futex_realtime == futex_wait || op & !futex_realtime == futex_wait_bitset => {
58100
let wait_bitset = op & !futex_realtime == futex_wait_bitset;
59101

60102
let bitset = if wait_bitset {
@@ -77,44 +119,25 @@ pub fn futex<'tcx>(
77119
u32::MAX
78120
};
79121

80-
81-
let val = this.read_scalar(&args[2])?.to_u32()?;
122+
// We ensured at least 4 arguments above so these work.
123+
let val = this.read_scalar(&args[2])?.to_i32()?;
124+
let Some(timeout) = read_timeout(
125+
this,
126+
&args[3],
127+
op & futex_realtime == futex_realtime,
128+
wait_bitset,
129+
dest,
130+
)?
131+
else {
132+
return interp_ok(());
133+
};
82134

83135
if bitset == 0 {
84136
this.set_last_error(LibcError("EINVAL"))?;
85137
this.write_scalar(Scalar::from_target_isize(-1, this), dest)?;
86138
return interp_ok(());
87139
}
88140

89-
let timeout = this.deref_pointer_as(&args[3], this.libc_ty_layout("timespec"))?;
90-
let timeout = if this.ptr_is_null(timeout.ptr())? {
91-
None
92-
} else {
93-
let duration = match this.read_timespec(&timeout)? {
94-
Some(duration) => duration,
95-
None => {
96-
this.set_last_error(LibcError("EINVAL"))?;
97-
this.write_scalar(Scalar::from_target_isize(-1, this), dest)?;
98-
return interp_ok(());
99-
}
100-
};
101-
let timeout_clock = if op & futex_realtime == futex_realtime {
102-
this.check_no_isolation(
103-
"`futex` syscall with `op=FUTEX_WAIT` and non-null timeout with `FUTEX_CLOCK_REALTIME`",
104-
)?;
105-
TimeoutClock::RealTime
106-
} else {
107-
TimeoutClock::Monotonic
108-
};
109-
let timeout_anchor = if wait_bitset {
110-
// FUTEX_WAIT_BITSET uses an absolute timestamp.
111-
TimeoutAnchor::Absolute
112-
} else {
113-
// FUTEX_WAIT uses a relative timestamp.
114-
TimeoutAnchor::Relative
115-
};
116-
Some((timeout_clock, timeout_anchor, duration))
117-
};
118141
// There may be a concurrent thread changing the value of addr
119142
// and then invoking the FUTEX_WAKE syscall. It is critical that the
120143
// effects of this and the other thread are correctly observed,
@@ -158,16 +181,12 @@ pub fn futex<'tcx>(
158181
//
159182
// Thankfully, preemptions cannot happen inside a Miri shim, so we do not need to
160183
// do anything special to guarantee fence-load-comparison atomicity.
161-
let futex_val = this.read_scalar_atomic(&addr, AtomicReadOrd::SeqCst)?.to_u32()?;
162-
if if op == futex_lock_pi {
163-
futex_val != 0
164-
} else {
165-
futex_val == val
166-
} {
184+
this.atomic_fence(AtomicFenceOrd::SeqCst)?;
185+
// Read an `i32` through the pointer, regardless of any wrapper types.
186+
// It's not uncommon for `addr` to be passed as another type than `*mut i32`, such as `*const AtomicI32`.
187+
let futex_val = this.read_scalar_atomic(&addr, AtomicReadOrd::Relaxed)?.to_i32()?;
188+
if val == futex_val {
167189
// The value still matches, so we block the thread and make it wait for FUTEX_WAKE.
168-
if op == futex_lock_pi {
169-
this.write_scalar_atomic(Scalar::from_u32(val | futex_waiters), &addr , AtomicWriteOrd::SeqCst)?;
170-
}
171190
this.futex_wait(
172191
addr_usize,
173192
bitset,
@@ -177,10 +196,6 @@ pub fn futex<'tcx>(
177196
dest.clone(),
178197
this.eval_libc("ETIMEDOUT"),
179198
);
180-
} else if op == futex_lock_pi {
181-
let tid = this.linux_gettid()?;
182-
this.write_scalar_atomic(tid, &addr, AtomicWriteOrd::SeqCst)?;
183-
this.write_scalar(Scalar::from_target_isize(0, this), dest)?;
184199
} else {
185200
// The futex value doesn't match the expected value, so we return failure
186201
// right away without sleeping: -1 and errno set to EAGAIN.
@@ -195,8 +210,15 @@ pub fn futex<'tcx>(
195210
// Does not access the futex value at *addr.
196211
// FUTEX_WAKE_BITSET: (int *addr, int op = FUTEX_WAKE, int val, const timespect *_unused, int *_unused, unsigned int bitset)
197212
// Same as FUTEX_WAKE, but allows you to specify a bitset to select which threads to wake up.
198-
op if op == futex_wake || op == futex_wake_bitset || op == futex_unlock_pi => {
199-
let val = if op == futex_unlock_pi { 1 } else { this.read_scalar(&args[2])?.to_i32()? };
213+
op if op == futex_wake || op == futex_wake_bitset => {
214+
if args.len() < 3 {
215+
throw_ub_format!(
216+
"incorrect number of arguments for `futex` syscall with `op=FUTEX_WAKE`: got {}, expected at least 3",
217+
args.len()
218+
);
219+
}
220+
221+
let val = this.read_scalar(&args[2])?.to_i32()?;
200222

201223
let bitset = if op == futex_wake_bitset {
202224
let [_, _, _, timeout, uaddr2, bitset, ..] = args else {
@@ -219,11 +241,7 @@ pub fn futex<'tcx>(
219241
// Together with the SeqCst fence in futex_wait, this makes sure that futex_wait
220242
// will see the latest value on addr which could be changed by our caller
221243
// before doing the syscall.
222-
if op == futex_unlock_pi {
223-
this.write_scalar_atomic(Scalar::from_u32(0), &addr, AtomicWriteOrd::SeqCst)?;
224-
} else {
225-
this.atomic_fence(AtomicFenceOrd::SeqCst)?;
226-
}
244+
this.atomic_fence(AtomicFenceOrd::SeqCst)?;
227245
let mut n = 0;
228246
#[allow(clippy::arithmetic_side_effects)]
229247
for _ in 0..val {
@@ -233,7 +251,79 @@ pub fn futex<'tcx>(
233251
break;
234252
}
235253
}
236-
this.write_scalar(Scalar::from_target_isize(if op != futex_unlock_pi { n } else { 0 }, this), dest)?;
254+
this.write_scalar(Scalar::from_target_isize(n, this), dest)?;
255+
}
256+
op if op == futex_lock_pi => {
257+
if args.len() < 4 {
258+
throw_ub_format!(
259+
"incorrect number of arguments for `futex` syscall with `op=FUTEX_LOCK_PI`: got {}, expected at least 4",
260+
args.len()
261+
);
262+
}
263+
264+
// FUTEX_LOCK_PI uses absolute CLOCK_REALTIME timestamp.
265+
let Some(timeout) = read_timeout(this, &args[3], true, true, dest)? else {
266+
return interp_ok(());
267+
};
268+
269+
// The same as above. This makes modifications visible to us.
270+
this.atomic_fence(AtomicFenceOrd::SeqCst)?;
271+
272+
// For bitand working properly, we read it as a u32.
273+
let futex_val = this.read_scalar_atomic(&addr, AtomicReadOrd::Relaxed)?.to_u32()?;
274+
275+
if futex_val == 0 {
276+
// 0 means unlocked - then lock it.
277+
//
278+
// The tid of the owner is store to *addr.
279+
// N.B. it is not the same as posix thread id.
280+
let tid = this.linux_gettid()?;
281+
this.write_scalar_atomic(tid, &addr, AtomicWriteOrd::Relaxed)?;
282+
} else {
283+
// Other values mean locked.
284+
//
285+
// Mark the futex as contended.
286+
this.write_scalar_atomic(
287+
Scalar::from_u32(futex_val | futex_waiters),
288+
&addr,
289+
AtomicWriteOrd::Relaxed,
290+
)?;
291+
292+
// Put ourselves into the wait queue.
293+
this.futex_wait(
294+
addr_usize,
295+
u32::MAX,
296+
timeout,
297+
Scalar::from_target_isize(0, this), // retval_succ
298+
Scalar::from_target_isize(-1, this), // retval_timeout
299+
dest.clone(),
300+
this.eval_libc("ETIMEDOUT"),
301+
);
302+
}
303+
304+
// This ensures all loads afterwards get updated value of *addr.
305+
this.atomic_fence(AtomicFenceOrd::SeqCst)?;
306+
307+
// FUTEX_LOCK_PI returns 0 on success.
308+
this.write_scalar(Scalar::from_target_isize(0, this), dest)?;
309+
}
310+
op if op == futex_unlock_pi => {
311+
// This ensures all modifications happen before.
312+
this.atomic_fence(AtomicFenceOrd::SeqCst)?;
313+
314+
// Clear locked state.
315+
this.write_scalar_atomic(Scalar::from_u32(0), &addr, AtomicWriteOrd::Relaxed)?;
316+
317+
// This ensures all loads afterwards get updated value of *addr.
318+
// There are no preemptions so no one can wake after we set the futex to unlocked
319+
// and before we use futex_wake to wake one waiter.
320+
this.atomic_fence(AtomicFenceOrd::SeqCst)?;
321+
322+
// Unlocking wakes zero or one waiters.
323+
let _ = this.futex_wake(addr_usize, u32::MAX)?;
324+
325+
// FUTEX_UNLOCK_PI returns 0 on success.
326+
this.write_scalar(Scalar::from_target_isize(0, this), dest)?;
237327
}
238328
op => throw_unsup_format!("Miri does not support `futex` syscall with op={}", op),
239329
}

0 commit comments

Comments
 (0)