Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement FreeBSD syscall _umtx_op for futex support #4209

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ci/ci.sh
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ case $HOST_TARGET in
# Partially supported targets (tier 2)
BASIC="empty_main integer heap_alloc libc-mem vec string btreemap" # ensures we have the basics: pre-main code, system allocator
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
TEST_TARGET=x86_64-unknown-freebsd run_tests_minimal $BASIC $UNIX time hashmap random threadname pthread fs libc-pipe
TEST_TARGET=x86_64-unknown-freebsd run_tests_minimal $BASIC $UNIX time hashmap random threadname pthread fs libc-pipe concurrency sync
TEST_TARGET=i686-unknown-freebsd run_tests_minimal $BASIC $UNIX time hashmap random threadname pthread fs libc-pipe
TEST_TARGET=aarch64-linux-android run_tests_minimal $BASIC $UNIX time hashmap random sync threadname pthread epoll eventfd
TEST_TARGET=wasm32-wasip2 run_tests_minimal $BASIC wasm
Expand Down
7 changes: 7 additions & 0 deletions src/shims/unix/freebsd/foreign_items.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use rustc_middle::ty::Ty;
use rustc_span::Symbol;
use rustc_target::callconv::{Conv, FnAbi};

use super::sync::EvalContextExt as _;
use crate::shims::unix::*;
use crate::*;

Expand Down Expand Up @@ -55,6 +56,12 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
this.write_scalar(res, dest)?;
}

"_umtx_op" => {
let [obj, op, val, uaddr, uaddr2] =
this.check_shim(abi, Conv::C, link_name, args)?;
this._umtx_op(obj, op, val, uaddr, uaddr2, dest)?;
}

// File related shims
// For those, we both intercept `func` and `call@FBSD_1.0` symbols cases
// since freebsd 12 the former form can be expected.
Expand Down
1 change: 1 addition & 0 deletions src/shims/unix/freebsd/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pub mod foreign_items;
pub mod sync;
193 changes: 193 additions & 0 deletions src/shims/unix/freebsd/sync.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
//! Contains FreeBSD-specific synchronization functions

use core::time::Duration;

use crate::concurrency::sync::FutexRef;
use crate::*;

pub struct FreeBSDFutex {
futex: FutexRef,
}

pub struct UmtxTime {
timeout: Duration,
flags: u32,
_clock_id: u32, // TODO: I'm not understanding why this is needed atm
}

impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {}
pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
/// 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. :
/// This is used for futex operations.
///
/// `obj`: a pointer to the futex object (can be a lot of things, mostly *AtomicU32)
/// `op`: the futex operation to run
/// `val`: the current value of the object as a `c_long` (for wait/wake)
/// `uaddr`: pointer to optional parameter (mostly timeouts)
/// `uaddr2`: pointer to optional parameter (mostly timeouts)
/// `dest`: the place this syscall returns to, 0 for success, -1 for failure
fn _umtx_op(
&mut self,
obj: &OpTy<'tcx>,
op: &OpTy<'tcx>,
val: &OpTy<'tcx>,
uaddr: &OpTy<'tcx>,
uaddr2: &OpTy<'tcx>,
dest: &MPlaceTy<'tcx>,
) -> InterpResult<'tcx> {
let this = self.eval_context_mut();

let obj = this.read_pointer(obj)?;
let op = this.read_scalar(op)?.to_i32()?;
let val = this.read_target_usize(val)?;
let uaddr2 = this.read_pointer(uaddr2)?;

let wait = this.eval_libc_i32("UMTX_OP_WAIT");
let wait_uint = this.eval_libc_i32("UMTX_OP_WAIT_UINT");
let wait_uint_private = this.eval_libc_i32("UMTX_OP_WAIT_UINT_PRIVATE");

let wake = this.eval_libc_i32("UMTX_OP_WAKE");
let wake_private = this.eval_libc_i32("UMTX_OP_WAKE_PRIVATE");

let absolute_time_flag = this.eval_libc_u32("UMTX_ABSTIME");

match op {
// UMTX_OP_WAIT_UINT has a private variant that enables an optimization that stops it from working across processes.
// Miri doesn't support that anyway, so we ignore that variant and use the same implementation for all wait ops.
op if op == wait || op == wait_uint || op == wait_uint_private => {
// TODO: the normal UMTX_OP_WAIT accepts a `long` instead of u64,
// does this really matter?? (probably)

let obj = this.ptr_to_mplace(obj, this.machine.layouts.i32);

// TODO: the Linux futex syscall sets a fence here, but what I understand from the manual is that
// these semantics need to be done by the user. Should this still be added?
if u64::from(this.read_scalar_atomic(&obj, AtomicReadOrd::Acquire)?.to_u32()?)
== val
{
let futex_ref = this
.get_sync_or_init(obj.ptr(), |_| FreeBSDFutex { futex: Default::default() })
.unwrap()
.futex
.clone();

// TODO: This can be cleaned up no? :)
// From the man page:
// If `uaddr2` is null than `uaddr` can point to an optional timespec parameter
// otherwise `uaddr2` must point to a `_umtx_time` parameter and the value of `uaddr`
// must be equal to the size of that struct.
let timeout = if this.ptr_is_null(uaddr2)? {
if this.ptr_is_null(this.read_pointer(uaddr)?)? {
None
} else {
let timespec =
this.deref_pointer_as(uaddr, this.libc_ty_layout("timespec"))?;
let duration = match this.read_timespec(&timespec)? {
Some(duration) => duration,
None => {
return this
.set_last_error_and_return(LibcError("EINVAL"), dest);
}
};

Some((TimeoutClock::Monotonic, TimeoutAnchor::Relative, duration))
}
} else {
let umtx_time_place =
this.ptr_to_mplace(uaddr2, this.libc_ty_layout("_umtx_time"));
let uaddr_as_size = this.read_target_usize(uaddr)?;

if umtx_time_place.layout().size.bytes() != uaddr_as_size {
return this.set_last_error_and_return(LibcError("EINVAL"), dest);
}

// Inner `timespec` must still be valid.
let umtx_time = match this.read_umtx_time(&umtx_time_place)? {
Some(duration) => duration,
None => {
return this.set_last_error_and_return(LibcError("EINVAL"), dest);
}
};

let anchor = if umtx_time.flags == absolute_time_flag {
TimeoutAnchor::Absolute
} else {
TimeoutAnchor::Relative
};

Some((TimeoutClock::Monotonic, anchor, umtx_time.timeout))
};

let dest = dest.clone();
this.futex_wait(
futex_ref,
u32::MAX, // we set the bitset to include all bits
timeout,
callback!(
@capture<'tcx> {
dest: MPlaceTy<'tcx>,
}
|ecx, unblock: UnblockKind| match unblock {
UnblockKind::Ready => {
ecx.write_int(0, &dest)
}
UnblockKind::TimedOut => {
ecx.set_last_error_and_return(LibcError("ETIMEDOUT"), &dest)
}
}
),
);
interp_ok(())
} else {
// The manual doesn't document what should happen if `val` is invalid, so we error out.
// TODO: do something else?
this.set_last_error_and_return(LibcError("EINVAL"), dest)
}
}
// UMTX_OP_WAKE has a private variant that enables an optimization that stops it from working across processes.
// Miri doesn't support that anyway, so we ignore that variant and use the same implementation for all wait ops.
op if op == wake || op == wake_private => {
let Some(futex_ref) =
this.get_sync_or_init(obj, |_| FreeBSDFutex { futex: Default::default() })
else {
// From Linux implemenation:
// No AllocId, or no live allocation at that AllocId.
// Return an error code. (That seems nicer than silently doing something non-intuitive.)
// This means that if an address gets reused by a new allocation,
// we'll use an independent futex queue for this... that seems acceptable.
return this.set_last_error_and_return(LibcError("EFAULT"), dest);
};
let futex_ref = futex_ref.futex.clone();

// `_umtx_op` doesn't return the amount of woken threads.
let _woken = this.futex_wake(
&futex_ref,
u32::MAX, // we set the bitset to include all bits
val as usize, /* TODO: I can't read a usize from val, I haven't found a way as of now */
)?;
this.write_int(0, dest)?;
interp_ok(())
}
op => {
throw_unsup_format!("Miri does not support `_umtx_op` syscall with op={}", op)
}
}
}

/// Parses a `_umtx_time` struct.
/// Returns `None` if the underlying `timespec` struct is invalid.
fn read_umtx_time(&mut self, ut: &MPlaceTy<'tcx>) -> InterpResult<'tcx, Option<UmtxTime>> {
let this = self.eval_context_mut();
let timespec_place = this.project_field(ut, 0)?;
// Inner `timespec` must still be valid.
let duration = match this.read_timespec(&timespec_place)? {
Some(dur) => dur,
None => return interp_ok(None),
};
let flags_place = this.project_field(ut, 1)?;
let flags = this.read_scalar(&flags_place)?.to_u32()?;
let clock_id_place = this.project_field(ut, 2)?;
let clock_id = this.read_scalar(&clock_id_place)?.to_u32()?;
interp_ok(Some(UmtxTime { timeout: duration, flags, _clock_id: clock_id }))
}
}
170 changes: 170 additions & 0 deletions tests/pass-dep/concurrency/freebsd-futex.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
//@only-target: freebsd
//@compile-flags: -Zmiri-preemption-rate=0

use std::mem::MaybeUninit;
use std::ptr::{self, addr_of};
use std::sync::atomic::AtomicU32;
use std::time::{Duration, Instant};
use std::{io, thread};

fn wake_nobody() {
// TODO: _umtx_op does not return how many threads were woken up
// How do i test this?
}
fn wake_dangling() {
let futex = Box::new(0);
let ptr: *const u32 = &*futex;
drop(futex);

// Expect error since this is now "unmapped" memory.
unsafe {
assert_eq!(
libc::_umtx_op(
ptr as *const AtomicU32 as *mut _,
libc::UMTX_OP_WAKE_PRIVATE,
0,
ptr::null_mut::<libc::c_void>(),
ptr::null_mut::<libc::c_void>(),
),
-1
);
assert_eq!(io::Error::last_os_error().raw_os_error().unwrap(), libc::EFAULT);
}
}

fn wait_wrong_val() {
let futex: u32 = 123;

unsafe {
assert_eq!(
libc::_umtx_op(
ptr::from_ref(&futex).cast_mut().cast(),
libc::UMTX_OP_WAIT_UINT_PRIVATE,
456,
ptr::null_mut::<libc::c_void>(),
ptr::null_mut::<libc::c_void>(),
),
-1
);
// man page doesn't document but we set EINVAL for consistency?
assert_eq!(io::Error::last_os_error().raw_os_error().unwrap(), libc::EINVAL);
}
}

fn wait_timeout() {
let start = Instant::now();

let futex: u32 = 123;

// Wait for 200ms, with nobody waking us up early
unsafe {
assert_eq!(
libc::_umtx_op(
ptr::from_ref(&futex).cast_mut().cast(),
libc::UMTX_OP_WAIT_UINT_PRIVATE,
123,
&mut libc::timespec { tv_sec: 0, tv_nsec: 200_000_000 } as *mut _ as *mut _,
ptr::null_mut::<libc::c_void>(),
),
-1
);
// man page doesn't document but we set EINVAL for consistency
assert_eq!(io::Error::last_os_error().raw_os_error().unwrap(), libc::ETIMEDOUT);
}

assert!((200..1000).contains(&start.elapsed().as_millis()));
}
fn wait_absolute_timeout() {
let start = Instant::now();

// Get the current monotonic timestamp as timespec.
let mut timeout = unsafe {
let mut now: MaybeUninit<libc::timespec> = MaybeUninit::uninit();
assert_eq!(libc::clock_gettime(libc::CLOCK_MONOTONIC, now.as_mut_ptr()), 0);
now.assume_init()
};

// Add 200ms.
timeout.tv_nsec += 200_000_000;
if timeout.tv_nsec > 1_000_000_000 {
timeout.tv_nsec -= 1_000_000_000;
timeout.tv_sec += 1;
}

// Create umtx_timeout struct
let umtx_timeout = libc::_umtx_time {
_timeout: timeout,
_flags: libc::UMTX_ABSTIME,
_clockid: libc::CLOCK_MONOTONIC as u32,
};
let umtx_timeout_ptr = &umtx_timeout as *const _;
let umtx_timeout_size = std::mem::size_of_val(&umtx_timeout);

let futex: u32 = 123;

// Wait for 200ms from now, with nobody waking us up early.
unsafe {
assert_eq!(
libc::_umtx_op(
ptr::from_ref(&futex).cast_mut().cast(),
libc::UMTX_OP_WAIT_UINT_PRIVATE,
123,
ptr::without_provenance_mut(umtx_timeout_size),
umtx_timeout_ptr as *mut _,
),
-1
);
// man page doesn't document but we set EINVAL for consistency
assert_eq!(io::Error::last_os_error().raw_os_error().unwrap(), libc::ETIMEDOUT);
}
assert!((200..1000).contains(&start.elapsed().as_millis()));
}

fn wait_wake() {
let start = Instant::now();

static mut FUTEX: u32 = 0;

let t = thread::spawn(move || {
thread::sleep(Duration::from_millis(200));
unsafe {
assert_eq!(
libc::_umtx_op(
addr_of!(FUTEX) as *mut _,
libc::UMTX_OP_WAKE_PRIVATE,
10, // Wake up 10 threads, but we can't check that we woken up 1.
ptr::null_mut::<libc::c_void>(),
ptr::null_mut::<libc::c_void>(),
),
0
);
}
});

unsafe {
assert_eq!(
libc::_umtx_op(
addr_of!(FUTEX) as *mut _,
libc::UMTX_OP_WAIT_UINT_PRIVATE,
0, // FUTEX is 0
ptr::null_mut::<libc::c_void>(),
ptr::null_mut::<libc::c_void>(),
),
0
);
}

// When running this in stress-gc mode, things can take quite long.
// So the timeout is 3000 ms.
assert!((200..3000).contains(&start.elapsed().as_millis()));
t.join().unwrap();
}

fn main() {
wake_nobody();
wake_dangling();
wait_wrong_val();
wait_timeout();
wait_absolute_timeout();
wait_wake();
}
Loading