Skip to main content

miri/shims/unix/freebsd/
sync.rs

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