Skip to content

Commit fb30824

Browse files
authored
Merge pull request rust-lang#4209 from LorrensP-2158466/freebsd_futex
Implement FreeBSD syscall _umtx_op for futex support
2 parents d0ea07e + 88a82be commit fb30824

File tree

5 files changed

+521
-1
lines changed

5 files changed

+521
-1
lines changed

src/tools/miri/ci/ci.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ case $HOST_TARGET in
164164
# Partially supported targets (tier 2)
165165
BASIC="empty_main integer heap_alloc libc-mem vec string btreemap" # ensures we have the basics: pre-main code, system allocator
166166
UNIX="hello panic/panic panic/unwind concurrency/simple atomic libc-mem libc-misc libc-random env num_cpus" # the things that are very similar across all Unixes, and hence easily supported there
167-
TEST_TARGET=x86_64-unknown-freebsd run_tests_minimal $BASIC $UNIX time hashmap random threadname pthread fs libc-pipe
167+
TEST_TARGET=x86_64-unknown-freebsd run_tests_minimal $BASIC $UNIX time hashmap random threadname pthread fs libc-pipe concurrency sync
168168
TEST_TARGET=i686-unknown-freebsd run_tests_minimal $BASIC $UNIX time hashmap random threadname pthread fs libc-pipe
169169
TEST_TARGET=aarch64-linux-android run_tests_minimal $BASIC $UNIX time hashmap random sync concurrency thread epoll eventfd
170170
TEST_TARGET=wasm32-wasip2 run_tests_minimal $BASIC wasm

src/tools/miri/src/shims/unix/freebsd/foreign_items.rs

+8
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use rustc_middle::ty::Ty;
22
use rustc_span::Symbol;
33
use rustc_target::callconv::{Conv, FnAbi};
44

5+
use super::sync::EvalContextExt as _;
56
use crate::shims::unix::*;
67
use crate::*;
78

@@ -55,6 +56,13 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
5556
this.write_scalar(res, dest)?;
5657
}
5758

59+
// Synchronization primitives
60+
"_umtx_op" => {
61+
let [obj, op, val, uaddr, uaddr2] =
62+
this.check_shim(abi, Conv::C, link_name, args)?;
63+
this._umtx_op(obj, op, val, uaddr, uaddr2, dest)?;
64+
}
65+
5866
// File related shims
5967
// For those, we both intercept `func` and `call@FBSD_1.0` symbols cases
6068
// since freebsd 12 the former form can be expected.
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
pub mod foreign_items;
2+
pub mod sync;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
//! Contains FreeBSD-specific synchronization functions
2+
3+
use core::time::Duration;
4+
5+
use crate::concurrency::sync::FutexRef;
6+
use crate::*;
7+
8+
pub struct FreeBsdFutex {
9+
futex: FutexRef,
10+
}
11+
12+
/// Extended variant of the `timespec` struct.
13+
pub struct UmtxTime {
14+
timeout: Duration,
15+
abs_time: bool,
16+
timeout_clock: TimeoutClock,
17+
}
18+
19+
impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {}
20+
pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
21+
/// Implementation of the FreeBSD [`_umtx_op`](https://man.freebsd.org/cgi/man.cgi?query=_umtx_op&sektion=2&manpath=FreeBSD+14.2-RELEASE+and+Ports) syscall.
22+
/// This is used for futex operations on FreeBSD.
23+
///
24+
/// `obj`: a pointer to the futex object (can be a lot of things, mostly *AtomicU32)
25+
/// `op`: the futex operation to run
26+
/// `val`: the current value of the object as a `c_long` (for wait/wake)
27+
/// `uaddr`: `op`-specific optional parameter, pointer-sized integer or pointer to an `op`-specific struct
28+
/// `uaddr2`: `op`-specific optional parameter, pointer-sized integer or pointer to an `op`-specific struct
29+
/// `dest`: the place this syscall returns to, 0 for success, -1 for failure
30+
///
31+
/// # Note
32+
/// Curently only the WAIT and WAKE operations are implemented.
33+
fn _umtx_op(
34+
&mut self,
35+
obj: &OpTy<'tcx>,
36+
op: &OpTy<'tcx>,
37+
val: &OpTy<'tcx>,
38+
uaddr: &OpTy<'tcx>,
39+
uaddr2: &OpTy<'tcx>,
40+
dest: &MPlaceTy<'tcx>,
41+
) -> InterpResult<'tcx> {
42+
let this = self.eval_context_mut();
43+
44+
let obj = this.read_pointer(obj)?;
45+
let op = this.read_scalar(op)?.to_i32()?;
46+
let val = this.read_target_usize(val)?;
47+
let uaddr = this.read_target_usize(uaddr)?;
48+
let uaddr2 = this.read_pointer(uaddr2)?;
49+
50+
let wait = this.eval_libc_i32("UMTX_OP_WAIT");
51+
let wait_uint = this.eval_libc_i32("UMTX_OP_WAIT_UINT");
52+
let wait_uint_private = this.eval_libc_i32("UMTX_OP_WAIT_UINT_PRIVATE");
53+
54+
let wake = this.eval_libc_i32("UMTX_OP_WAKE");
55+
let wake_private = this.eval_libc_i32("UMTX_OP_WAKE_PRIVATE");
56+
57+
let timespec_layout = this.libc_ty_layout("timespec");
58+
let umtx_time_layout = this.libc_ty_layout("_umtx_time");
59+
assert!(
60+
timespec_layout.size != umtx_time_layout.size,
61+
"`struct timespec` and `struct _umtx_time` should have different sizes."
62+
);
63+
64+
match op {
65+
// UMTX_OP_WAIT_UINT and UMTX_OP_WAIT_UINT_PRIVATE only differ in whether they work across
66+
// processes or not. For Miri, we can treat them the same.
67+
op if op == wait || op == wait_uint || op == wait_uint_private => {
68+
let obj_layout =
69+
if op == wait { this.machine.layouts.isize } else { this.machine.layouts.u32 };
70+
let obj = this.ptr_to_mplace(obj, obj_layout);
71+
72+
// Read the Linux futex wait implementation in Miri to understand why this fence is needed.
73+
this.atomic_fence(AtomicFenceOrd::SeqCst)?;
74+
let obj_val = this
75+
.read_scalar_atomic(&obj, AtomicReadOrd::Acquire)?
76+
.to_bits(obj_layout.size)?; // isize and u32 can have different sizes
77+
78+
if obj_val == u128::from(val) {
79+
// This cannot fail since we already did an atomic acquire read on that pointer.
80+
// Acquire reads are only allowed on mutable memory.
81+
let futex_ref = this
82+
.get_sync_or_init(obj.ptr(), |_| FreeBsdFutex { futex: Default::default() })
83+
.unwrap()
84+
.futex
85+
.clone();
86+
87+
// From the manual:
88+
// The timeout is specified by passing either the address of `struct timespec`, or its
89+
// extended variant, `struct _umtx_time`, as the `uaddr2` argument of _umtx_op().
90+
// They are distinguished by the `uaddr` value, which must be equal
91+
// to the size of the structure pointed to by `uaddr2`, casted to uintptr_t.
92+
let timeout = if this.ptr_is_null(uaddr2)? {
93+
// no timeout parameter
94+
None
95+
} else {
96+
if uaddr == umtx_time_layout.size.bytes() {
97+
// `uaddr2` points to a `struct _umtx_time`.
98+
let umtx_time_place = this.ptr_to_mplace(uaddr2, umtx_time_layout);
99+
100+
let umtx_time = match this.read_umtx_time(&umtx_time_place)? {
101+
Some(ut) => ut,
102+
None => {
103+
return this
104+
.set_last_error_and_return(LibcError("EINVAL"), dest);
105+
}
106+
};
107+
108+
let anchor = if umtx_time.abs_time {
109+
TimeoutAnchor::Absolute
110+
} else {
111+
TimeoutAnchor::Relative
112+
};
113+
114+
Some((umtx_time.timeout_clock, anchor, umtx_time.timeout))
115+
} else if uaddr == timespec_layout.size.bytes() {
116+
// RealTime clock can't be used in isolation mode.
117+
this.check_no_isolation("`_umtx_op` with `timespec` timeout")?;
118+
119+
// `uaddr2` points to a `struct timespec`.
120+
let timespec = this.ptr_to_mplace(uaddr2, timespec_layout);
121+
let duration = match this.read_timespec(&timespec)? {
122+
Some(duration) => duration,
123+
None => {
124+
return this
125+
.set_last_error_and_return(LibcError("EINVAL"), dest);
126+
}
127+
};
128+
129+
// FreeBSD does not seem to document which clock is used when the timeout
130+
// is passed as a `struct timespec*`. Based on discussions online and the source
131+
// code (umtx_copyin_umtx_time() in kern_umtx.c), it seems to default to CLOCK_REALTIME,
132+
// so that's what we also do.
133+
// Discussion in golang: https://github.com/golang/go/issues/17168#issuecomment-250235271
134+
Some((TimeoutClock::RealTime, TimeoutAnchor::Relative, duration))
135+
} else {
136+
return this.set_last_error_and_return(LibcError("EINVAL"), dest);
137+
}
138+
};
139+
140+
let dest = dest.clone();
141+
this.futex_wait(
142+
futex_ref,
143+
u32::MAX, // we set the bitset to include all bits
144+
timeout,
145+
callback!(
146+
@capture<'tcx> {
147+
dest: MPlaceTy<'tcx>,
148+
}
149+
|ecx, unblock: UnblockKind| match unblock {
150+
UnblockKind::Ready => {
151+
// From the manual:
152+
// If successful, all requests, except UMTX_SHM_CREAT and UMTX_SHM_LOOKUP
153+
// sub-requests of the UMTX_OP_SHM request, will return zero.
154+
ecx.write_int(0, &dest)
155+
}
156+
UnblockKind::TimedOut => {
157+
ecx.set_last_error_and_return(LibcError("ETIMEDOUT"), &dest)
158+
}
159+
}
160+
),
161+
);
162+
interp_ok(())
163+
} else {
164+
// The manual doesn’t specify what should happen if the futex value doesn’t match the expected one.
165+
// On FreeBSD 14.2, testing shows that WAIT operations return 0 even when the value is incorrect.
166+
this.write_int(0, dest)?;
167+
interp_ok(())
168+
}
169+
}
170+
// UMTX_OP_WAKE and UMTX_OP_WAKE_PRIVATE only differ in whether they work across
171+
// processes or not. For Miri, we can treat them the same.
172+
op if op == wake || op == wake_private => {
173+
let Some(futex_ref) =
174+
this.get_sync_or_init(obj, |_| FreeBsdFutex { futex: Default::default() })
175+
else {
176+
// From Linux implemenation:
177+
// No AllocId, or no live allocation at that AllocId.
178+
// Return an error code. (That seems nicer than silently doing something non-intuitive.)
179+
// This means that if an address gets reused by a new allocation,
180+
// we'll use an independent futex queue for this... that seems acceptable.
181+
return this.set_last_error_and_return(LibcError("EFAULT"), dest);
182+
};
183+
let futex_ref = futex_ref.futex.clone();
184+
185+
// Saturating cast for when usize is smaller than u64.
186+
let count = usize::try_from(val).unwrap_or(usize::MAX);
187+
188+
// Read the Linux futex wake implementation in Miri to understand why this fence is needed.
189+
this.atomic_fence(AtomicFenceOrd::SeqCst)?;
190+
191+
// `_umtx_op` doesn't return the amount of woken threads.
192+
let _woken = this.futex_wake(
193+
&futex_ref,
194+
u32::MAX, // we set the bitset to include all bits
195+
count,
196+
)?;
197+
198+
// From the manual:
199+
// If successful, all requests, except UMTX_SHM_CREAT and UMTX_SHM_LOOKUP
200+
// sub-requests of the UMTX_OP_SHM request, will return zero.
201+
this.write_int(0, dest)?;
202+
interp_ok(())
203+
}
204+
op => {
205+
throw_unsup_format!("Miri does not support `_umtx_op` syscall with op={}", op)
206+
}
207+
}
208+
}
209+
210+
/// Parses a `_umtx_time` struct.
211+
/// Returns `None` if the underlying `timespec` struct is invalid.
212+
fn read_umtx_time(&mut self, ut: &MPlaceTy<'tcx>) -> InterpResult<'tcx, Option<UmtxTime>> {
213+
let this = self.eval_context_mut();
214+
// Only flag allowed is UMTX_ABSTIME.
215+
let abs_time = this.eval_libc_u32("UMTX_ABSTIME");
216+
217+
let timespec_place = this.project_field(ut, 0)?;
218+
// Inner `timespec` must still be valid.
219+
let duration = match this.read_timespec(&timespec_place)? {
220+
Some(dur) => dur,
221+
None => return interp_ok(None),
222+
};
223+
224+
let flags_place = this.project_field(ut, 1)?;
225+
let flags = this.read_scalar(&flags_place)?.to_u32()?;
226+
let abs_time_flag = flags == abs_time;
227+
228+
let clock_id_place = this.project_field(ut, 2)?;
229+
let clock_id = this.read_scalar(&clock_id_place)?.to_i32()?;
230+
let timeout_clock = this.translate_umtx_time_clock_id(clock_id)?;
231+
232+
interp_ok(Some(UmtxTime { timeout: duration, abs_time: abs_time_flag, timeout_clock }))
233+
}
234+
235+
/// Translate raw FreeBSD clockid to a Miri TimeoutClock.
236+
/// FIXME: share this code with the pthread and clock_gettime shims.
237+
fn translate_umtx_time_clock_id(&mut self, raw_id: i32) -> InterpResult<'tcx, TimeoutClock> {
238+
let this = self.eval_context_mut();
239+
240+
let timeout = if raw_id == this.eval_libc_i32("CLOCK_REALTIME") {
241+
// RealTime clock can't be used in isolation mode.
242+
this.check_no_isolation("`_umtx_op` with `CLOCK_REALTIME` timeout")?;
243+
TimeoutClock::RealTime
244+
} else if raw_id == this.eval_libc_i32("CLOCK_MONOTONIC") {
245+
TimeoutClock::Monotonic
246+
} else {
247+
throw_unsup_format!("unsupported clock id {raw_id}");
248+
};
249+
interp_ok(timeout)
250+
}
251+
}

0 commit comments

Comments
 (0)