|
| 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(×pec)? { |
| 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(×pec_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