Skip to main content

miri/shims/unix/macos/
sync.rs

1//! Contains macOS-specific synchronization functions.
2//!
3//! For `os_unfair_lock`, see the documentation
4//! <https://developer.apple.com/documentation/os/synchronization?language=objc>
5//! and in case of underspecification its implementation
6//! <https://github.com/apple-oss-distributions/libplatform/blob/a00a4cc36da2110578bcf3b8eeeeb93dcc7f4e11/src/os/lock.c#L645>.
7//!
8//! Note that we don't emulate every edge-case behaviour of the locks. Notably,
9//! we don't abort when locking a lock owned by a thread that has already exited
10//! and we do not detect copying of the lock, but macOS doesn't guarantee anything
11//! in that case either.
12//! FIXME: synchronize behavior on lock copies/moves with macOS guarantees. Apparently there
13//! is hope that macOS might eventually guarantee more than it does today.
14
15use std::cell::Cell;
16use std::time::Duration;
17
18use rustc_abi::{Endian, FieldIdx, Size};
19
20use crate::concurrency::sync::{AccessKind, FutexRef, SyncObj};
21use crate::*;
22
23// The in-memory marker values we use to indicate whether objects have been initialized.
24const SYNC_UNINIT: u8 = 0;
25const SYNC_INIT: u8 = 1;
26
27#[derive(Clone)]
28enum MacOsUnfairLock {
29    Active {
30        mutex_ref: MutexRef,
31    },
32    /// If a lock gets copied while being held, we put it in this state.
33    /// It seems like in the real implementation, the lock actually remembers who held it,
34    /// and still behaves as-if it was held by that thread in the new location. In Miri, we don't
35    /// know who actually owns this lock at the moment.
36    PermanentlyLockedByUnknown,
37}
38
39impl SyncObj for MacOsUnfairLock {
40    fn on_access<'tcx>(&self, access_kind: AccessKind) -> InterpResult<'tcx> {
41        if let MacOsUnfairLock::Active { mutex_ref } = self
42            && !mutex_ref.queue_is_empty()
43        {
44            throw_ub_format!(
45                "{access_kind} of `os_unfair_lock` is forbidden while the queue is non-empty"
46            );
47        }
48        interp_ok(())
49    }
50
51    fn delete_on_write(&self) -> bool {
52        true
53    }
54}
55
56pub enum MacOsFutexTimeout<'a, 'tcx> {
57    None,
58    Relative { clock_op: &'a OpTy<'tcx>, timeout_op: &'a OpTy<'tcx> },
59    Absolute { clock_op: &'a OpTy<'tcx>, timeout_op: &'a OpTy<'tcx> },
60}
61
62/// Metadata for a macOS futex.
63///
64/// Since macOS 11.0, Apple has exposed the previously private futex API consisting
65/// of `os_sync_wait_on_address` (and friends) and `os_sync_wake_by_address_{any, all}`.
66/// These work with different value sizes and flags, which are validated to be consistent.
67/// This structure keeps track of both the futex queue and these values.
68struct MacOsFutex {
69    futex: FutexRef,
70    /// The size in bytes of the atomic primitive underlying this futex.
71    size: Cell<u64>,
72    /// Whether the futex is shared across process boundaries.
73    shared: Cell<bool>,
74}
75
76impl SyncObj for MacOsFutex {}
77
78#[derive(Clone)]
79struct MacOsSemaphore {
80    semaphore: SemaphoreRef,
81    init_value: u64,
82}
83
84// The offset into the semaphore where we are storing the "init" flag.
85const SEM_INIT_OFFSET: Size = Size::ZERO;
86
87// Constants from the dispatch semaphore API.
88const DISPATCH_TIME_NOW: u64 = 0;
89const DISPATCH_TIME_FOREVER: u64 = !0;
90
91impl SyncObj for MacOsSemaphore {
92    fn on_access<'tcx>(&self, access_kind: AccessKind) -> InterpResult<'tcx> {
93        if !self.semaphore.queue_is_empty() {
94            throw_ub_format!(
95                "{access_kind} of `os_unfair_lock` is forbidden while the queue is non-empty"
96            );
97        }
98        interp_ok(())
99    }
100
101    fn delete_on_write(&self) -> bool {
102        true
103    }
104}
105
106impl<'tcx> EvalContextExtPriv<'tcx> for crate::MiriInterpCx<'tcx> {}
107trait EvalContextExtPriv<'tcx>: crate::MiriInterpCxExt<'tcx> {
108    fn os_unfair_lock_get_data<'a>(
109        &'a mut self,
110        lock_ptr: &OpTy<'tcx>,
111    ) -> InterpResult<'tcx, &'a MacOsUnfairLock>
112    where
113        'tcx: 'a,
114    {
115        // `os_unfair_lock_s` wraps a single `u32` field. We use the first byte to store the "init"
116        // flag. Due to macOS always being little endian, that's the least significant byte.
117        let this = self.eval_context_mut();
118        assert!(this.tcx.data_layout.endian == Endian::Little);
119
120        let lock = this.deref_pointer_as(lock_ptr, this.libc_ty_layout("os_unfair_lock_s"))?;
121        this.get_immovable_sync_with_static_init(
122            &lock,
123            Size::ZERO, // offset for init tracking
124            SYNC_UNINIT,
125            SYNC_INIT,
126            |this| {
127                let field = this.project_field(&lock, FieldIdx::from_u32(0))?;
128                let val = this.read_scalar(&field)?.to_u32()?;
129                if val == 0 {
130                    interp_ok(MacOsUnfairLock::Active { mutex_ref: MutexRef::new() })
131                } else if val == 1 {
132                    // This is a lock that got copied while it is initialized. We de-initialize
133                    // locks when they get released, so it got copied while locked. Unfortunately
134                    // that is something `std` needs to support (the guard could have been leaked).
135                    // On the plus side, we know nobody was queued for the lock while it got copied;
136                    // that would have been rejected by our `on_access`.
137                    // The real implementation would apparently remember who held the old lock, and
138                    // consider them to hold the copy as well -- but our copies don't preserve sync
139                    // object metadata so we instead move the lock into a "permanently locked"
140                    // state.
141                    interp_ok(MacOsUnfairLock::PermanentlyLockedByUnknown)
142                } else {
143                    throw_ub_format!("`os_unfair_lock` was not properly initialized at this location, or it got overwritten");
144                }
145            },
146        )
147    }
148
149    fn dispatch_semaphore_get_data<'a>(
150        &'a mut self,
151        sem_ptr: &ImmTy<'tcx>,
152    ) -> InterpResult<'tcx, &'a MacOsSemaphore>
153    where
154        'tcx: 'a,
155    {
156        let this = self.eval_context_mut();
157        let sem = this.deref_pointer_as(sem_ptr, this.machine.layouts.u8)?;
158        this.get_immovable_sync_with_static_init(
159            &sem,
160            SEM_INIT_OFFSET,
161            SYNC_UNINIT,
162            SYNC_INIT,
163            |_ecx| throw_ub_format!("`dispatch_semaphore_t` value has not been initialized with `dispatch_semaphore_create`"),
164        )
165    }
166}
167
168impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {}
169pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
170    /// Implements [`os_sync_wait_on_address`], [`os_sync_wait_on_address_with_deadline`]
171    /// and [`os_sync_wait_on_address_with_timeout`].
172    ///
173    /// [`os_sync_wait_on_address`]: https://developer.apple.com/documentation/os/os_sync_wait_on_address?language=objc
174    /// [`os_sync_wait_on_address_with_deadline`]: https://developer.apple.com/documentation/os/os_sync_wait_on_address_with_deadline?language=objc
175    /// [`os_sync_wait_on_address_with_timeout`]: https://developer.apple.com/documentation/os/os_sync_wait_on_address_with_timeout?language=objc
176    fn os_sync_wait_on_address(
177        &mut self,
178        addr_op: &OpTy<'tcx>,
179        value_op: &OpTy<'tcx>,
180        size_op: &OpTy<'tcx>,
181        flags_op: &OpTy<'tcx>,
182        timeout: MacOsFutexTimeout<'_, 'tcx>,
183        dest: &MPlaceTy<'tcx>,
184    ) -> InterpResult<'tcx> {
185        let this = self.eval_context_mut();
186        let none = this.eval_libc_u32("OS_SYNC_WAIT_ON_ADDRESS_NONE");
187        let shared = this.eval_libc_u32("OS_SYNC_WAIT_ON_ADDRESS_SHARED");
188        let absolute_clock = this.eval_libc_u32("OS_CLOCK_MACH_ABSOLUTE_TIME");
189
190        let ptr = this.read_pointer(addr_op)?;
191        let value = this.read_scalar(value_op)?.to_u64()?;
192        let size = this.read_target_usize(size_op)?;
193        let flags = this.read_scalar(flags_op)?.to_u32()?;
194
195        let clock_timeout = match timeout {
196            MacOsFutexTimeout::None => None,
197            MacOsFutexTimeout::Relative { clock_op, timeout_op } => {
198                let clock = this.read_scalar(clock_op)?.to_u32()?;
199                let timeout = this.read_scalar(timeout_op)?.to_u64()?;
200                Some((clock, TimeoutStyle::Relative, timeout))
201            }
202            MacOsFutexTimeout::Absolute { clock_op, timeout_op } => {
203                let clock = this.read_scalar(clock_op)?.to_u32()?;
204                let timeout = this.read_scalar(timeout_op)?.to_u64()?;
205                Some((clock, TimeoutStyle::Absolute, timeout))
206            }
207        };
208
209        // Perform validation of the arguments.
210        let addr = ptr.addr().bytes();
211        if addr == 0
212            || !matches!(size, 4 | 8)
213            || !addr.is_multiple_of(size)
214            || (flags != none && flags != shared)
215            || clock_timeout
216                .is_some_and(|(clock, _, timeout)| clock != absolute_clock || timeout == 0)
217        {
218            this.set_errno_and_return_neg1(LibcError("EINVAL"), dest)?;
219            return interp_ok(());
220        }
221
222        let is_shared = flags == shared;
223        let deadline = clock_timeout.map(|(_, style, timeout)| {
224            // The only clock that is currently supported is the monotonic clock (checked above).
225            // While the deadline argument of `os_sync_wait_on_address_with_deadline`
226            // is actually not in nanoseconds but in the units of `mach_current_time`,
227            // the two are equivalent in miri.
228            this.machine.timeout(TimeoutClock::Monotonic, style, Duration::from_nanos(timeout))
229        });
230
231        // See the Linux futex implementation for why this fence exists.
232        this.atomic_fence(AtomicFenceOrd::SeqCst)?;
233
234        let layout = this.machine.layouts.uint(Size::from_bytes(size)).unwrap();
235        let futex_val = this
236            .read_scalar_atomic(&this.ptr_to_mplace(ptr, layout), AtomicReadOrd::Acquire)?
237            .to_bits(Size::from_bytes(size))?;
238
239        let futex = this
240            .get_sync_or_init(ptr, |_| {
241                MacOsFutex {
242                    futex: Default::default(),
243                    size: Cell::new(size),
244                    shared: Cell::new(is_shared),
245                }
246            })
247            .unwrap();
248
249        // Detect mismatches between the flags and sizes used on this address
250        // by comparing it with the parameters used by the other waiters in
251        // the current list. If the list is currently empty, update those
252        // parameters.
253        if futex.futex.waiters() == 0 {
254            futex.size.set(size);
255            futex.shared.set(is_shared);
256        } else if futex.size.get() != size || futex.shared.get() != is_shared {
257            this.set_errno_and_return_neg1(LibcError("EINVAL"), dest)?;
258            return interp_ok(());
259        }
260
261        if futex_val == value.into() {
262            // If the values are the same, we have to block.
263            let futex_ref = futex.futex.clone();
264            let dest = dest.clone();
265            this.futex_wait(
266                futex_ref.clone(),
267                u32::MAX, // bitset
268                deadline,
269                callback!(
270                    @capture<'tcx> {
271                        dest: MPlaceTy<'tcx>,
272                        futex_ref: FutexRef,
273                    }
274                    |this, unblock: UnblockKind| {
275                        match unblock {
276                            UnblockKind::Ready => {
277                                let remaining = futex_ref.waiters().try_into().unwrap();
278                                this.write_scalar(Scalar::from_i32(remaining), &dest)
279                            }
280                            UnblockKind::TimedOut => {
281                                this.set_errno_and_return_neg1(LibcError("ETIMEDOUT"), &dest)
282                            }
283                        }
284                    }
285                ),
286            );
287        } else {
288            // else retrieve the current number of waiters.
289            let waiters = futex.futex.waiters().try_into().unwrap();
290            this.write_scalar(Scalar::from_i32(waiters), dest)?;
291        }
292
293        interp_ok(())
294    }
295
296    /// Implements [`os_sync_wake_by_address_all`] and [`os_sync_wake_by_address_any`].
297    ///
298    /// [`os_sync_wake_by_address_all`]: https://developer.apple.com/documentation/os/os_sync_wake_by_address_all?language=objc
299    /// [`os_sync_wake_by_address_any`]: https://developer.apple.com/documentation/os/os_sync_wake_by_address_any?language=objc
300    fn os_sync_wake_by_address(
301        &mut self,
302        addr_op: &OpTy<'tcx>,
303        size_op: &OpTy<'tcx>,
304        flags_op: &OpTy<'tcx>,
305        all: bool,
306        dest: &MPlaceTy<'tcx>,
307    ) -> InterpResult<'tcx> {
308        let this = self.eval_context_mut();
309        let none = this.eval_libc_u32("OS_SYNC_WAKE_BY_ADDRESS_NONE");
310        let shared = this.eval_libc_u32("OS_SYNC_WAKE_BY_ADDRESS_SHARED");
311
312        let ptr = this.read_pointer(addr_op)?;
313        let size = this.read_target_usize(size_op)?;
314        let flags = this.read_scalar(flags_op)?.to_u32()?;
315
316        // Perform validation of the arguments.
317        let addr = ptr.addr().bytes();
318        if addr == 0 || !matches!(size, 4 | 8) || (flags != none && flags != shared) {
319            this.set_errno_and_return_neg1(LibcError("EINVAL"), dest)?;
320            return interp_ok(());
321        }
322
323        let is_shared = flags == shared;
324
325        let Some(futex) = this.get_sync_or_init(ptr, |_| {
326            MacOsFutex {
327                futex: Default::default(),
328                size: Cell::new(size),
329                shared: Cell::new(is_shared),
330            }
331        }) else {
332            // No AllocId, or no live allocation at that AllocId. Return an
333            // error code. (That seems nicer than silently doing something
334            // non-intuitive.) This means that if an address gets reused by a
335            // new allocation, we'll use an independent futex queue for this...
336            // that seems acceptable.
337            this.set_errno_and_return_neg1(LibcError("ENOENT"), dest)?;
338            return interp_ok(());
339        };
340
341        if futex.futex.waiters() == 0 {
342            this.set_errno_and_return_neg1(LibcError("ENOENT"), dest)?;
343            return interp_ok(());
344        // If there are waiters in the queue, they have all used the parameters
345        // stored in `futex` (we check this in `os_sync_wait_on_address` above).
346        // Detect mismatches between "our" parameters and the parameters used by
347        // the waiters and return an error in that case.
348        } else if futex.size.get() != size || futex.shared.get() != is_shared {
349            this.set_errno_and_return_neg1(LibcError("EINVAL"), dest)?;
350            return interp_ok(());
351        }
352
353        let futex_ref = futex.futex.clone();
354
355        // See the Linux futex implementation for why this fence exists.
356        this.atomic_fence(AtomicFenceOrd::SeqCst)?;
357        this.futex_wake(&futex_ref, u32::MAX, if all { usize::MAX } else { 1 })?;
358        this.write_scalar(Scalar::from_i32(0), dest)?;
359        interp_ok(())
360    }
361
362    fn os_unfair_lock_lock(&mut self, lock_op: &OpTy<'tcx>) -> InterpResult<'tcx> {
363        let this = self.eval_context_mut();
364
365        let MacOsUnfairLock::Active { mutex_ref } = this.os_unfair_lock_get_data(lock_op)? else {
366            // Trying to lock a perma-locked lock. On macOS this would block or abort depending
367            // on whether the current thread is considered to be the one holding this lock. We
368            // don't know who is considered to be holding the lock so we don't know what to do.
369            throw_unsup_format!(
370                "attempted to lock an os_unfair_lock that was copied while being locked"
371            );
372        };
373        let mutex_ref = mutex_ref.clone();
374
375        if let Some(owner) = mutex_ref.owner() {
376            if owner == this.active_thread() {
377                // Matching the current macOS implementation: abort on reentrant locking.
378                throw_machine_stop!(TerminationInfo::Abort(
379                    "attempted to lock an os_unfair_lock that is already locked by the current thread".to_owned()
380                ));
381            }
382
383            this.mutex_enqueue_and_block(mutex_ref, None);
384        } else {
385            this.mutex_lock(&mutex_ref)?;
386        }
387
388        interp_ok(())
389    }
390
391    fn os_unfair_lock_trylock(
392        &mut self,
393        lock_op: &OpTy<'tcx>,
394        dest: &MPlaceTy<'tcx>,
395    ) -> InterpResult<'tcx> {
396        let this = self.eval_context_mut();
397
398        let MacOsUnfairLock::Active { mutex_ref } = this.os_unfair_lock_get_data(lock_op)? else {
399            // Trying to lock a perma-locked lock. That behaves the same no matter who the owner is
400            // so we can implement the real behavior here.
401            this.write_scalar(Scalar::from_bool(false), dest)?;
402            return interp_ok(());
403        };
404        let mutex_ref = mutex_ref.clone();
405
406        if mutex_ref.owner().is_some() {
407            // Contrary to the blocking lock function, this does not check for reentrancy.
408            this.write_scalar(Scalar::from_bool(false), dest)?;
409        } else {
410            this.mutex_lock(&mutex_ref)?;
411            this.write_scalar(Scalar::from_bool(true), dest)?;
412        }
413
414        interp_ok(())
415    }
416
417    fn os_unfair_lock_unlock(&mut self, lock_op: &OpTy<'tcx>) -> InterpResult<'tcx> {
418        let this = self.eval_context_mut();
419
420        let MacOsUnfairLock::Active { mutex_ref } = this.os_unfair_lock_get_data(lock_op)? else {
421            // We don't know who the owner is so we cannot proceed.
422            throw_unsup_format!(
423                "attempted to unlock an os_unfair_lock that was copied while being locked"
424            );
425        };
426        let mutex_ref = mutex_ref.clone();
427
428        // Now, unlock.
429        if this.mutex_unlock(&mutex_ref)?.is_none() {
430            // Matching the current macOS implementation: abort.
431            throw_machine_stop!(TerminationInfo::Abort(
432                "attempted to unlock an os_unfair_lock not owned by the current thread".to_owned()
433            ));
434        }
435
436        // If the lock is not locked by anyone now, it went quiet.
437        // Reset to zero so that it can be moved and initialized again for the next phase.
438        if mutex_ref.owner().is_none() {
439            let lock_place = this.deref_pointer_as(lock_op, this.machine.layouts.u32)?;
440            this.write_scalar_atomic(Scalar::from_u32(0), &lock_place, AtomicWriteOrd::Relaxed)?;
441        }
442
443        interp_ok(())
444    }
445
446    fn os_unfair_lock_assert_owner(&mut self, lock_op: &OpTy<'tcx>) -> InterpResult<'tcx> {
447        let this = self.eval_context_mut();
448
449        let MacOsUnfairLock::Active { mutex_ref } = this.os_unfair_lock_get_data(lock_op)? else {
450            // We don't know who the owner is so we cannot proceed.
451            throw_unsup_format!(
452                "attempted to assert the owner of an os_unfair_lock that was copied while being locked"
453            );
454        };
455        let mutex_ref = mutex_ref.clone();
456
457        if mutex_ref.owner().is_none_or(|o| o != this.active_thread()) {
458            throw_machine_stop!(TerminationInfo::Abort(
459                "called os_unfair_lock_assert_owner on an os_unfair_lock not owned by the current thread".to_owned()
460            ));
461        }
462
463        // The lock is definitely not quiet since we are the owner.
464
465        interp_ok(())
466    }
467
468    fn os_unfair_lock_assert_not_owner(&mut self, lock_op: &OpTy<'tcx>) -> InterpResult<'tcx> {
469        let this = self.eval_context_mut();
470
471        let MacOsUnfairLock::Active { mutex_ref } = this.os_unfair_lock_get_data(lock_op)? else {
472            // We don't know who the owner is so we cannot proceed.
473            throw_unsup_format!(
474                "attempted to assert the owner of an os_unfair_lock that was copied while being locked"
475            );
476        };
477        let mutex_ref = mutex_ref.clone();
478
479        if mutex_ref.owner().is_some_and(|o| o == this.active_thread()) {
480            throw_machine_stop!(TerminationInfo::Abort(
481                "called os_unfair_lock_assert_not_owner on an os_unfair_lock owned by the current thread".to_owned()
482            ));
483        }
484
485        // If the lock is not locked by anyone now, it went quiet.
486        // Reset to zero so that it can be moved and initialized again for the next phase.
487        if mutex_ref.owner().is_none() {
488            let lock_place = this.deref_pointer_as(lock_op, this.machine.layouts.u32)?;
489            this.write_scalar_atomic(Scalar::from_u32(0), &lock_place, AtomicWriteOrd::Relaxed)?;
490        }
491
492        interp_ok(())
493    }
494
495    fn dispatch_semaphore_create(
496        &mut self,
497        value: &OpTy<'tcx>,
498        dest: &MPlaceTy<'tcx>,
499    ) -> InterpResult<'tcx> {
500        let this = self.eval_context_mut();
501
502        let value = this.read_target_isize(value)?;
503
504        let Ok(value) = u64::try_from(value) else {
505            throw_machine_stop!(TerminationInfo::Abort(
506                "dispatch_semaphore_create called with a negative value".to_owned()
507            ));
508        };
509
510        // Create an allocation to hold the semaphore "state" (mostly meta state for us).
511        let alloc = this.allocate(this.machine.layouts.u8, MiriMemoryKind::Runtime.into())?;
512        // Initialize semaphore.
513        let data = MacOsSemaphore { semaphore: SemaphoreRef::new(value), init_value: value };
514        this.init_immovable_sync(&alloc, SEM_INIT_OFFSET, SYNC_INIT, data)?;
515
516        this.write_pointer(alloc.ptr(), dest)?;
517        interp_ok(())
518    }
519
520    fn dispatch_semaphore_wait(
521        &mut self,
522        sem_ptr: &OpTy<'tcx>,
523        timeout: &OpTy<'tcx>,
524        dest: &MPlaceTy<'tcx>,
525    ) -> InterpResult<'tcx> {
526        let this = self.eval_context_mut();
527
528        let sem_ptr = this.read_immediate(sem_ptr)?;
529        let timeout = this.read_scalar(timeout)?.to_u64()?;
530
531        let deadline = if timeout == DISPATCH_TIME_FOREVER {
532            None
533        } else {
534            // `timeout` is an absolute nanosecond value on the monotone clock.
535            // (This also handles `DISPATCH_TIME_NOW` as that is 0.)
536            let deadline =
537                this.machine.monotonic_clock.epoch().add_lossy(Duration::from_nanos(timeout));
538            Some(deadline.into())
539        };
540
541        let data = this.dispatch_semaphore_get_data(&sem_ptr)?.clone();
542        let dest = dest.clone();
543        this.semaphore_decrement(
544            data.semaphore,
545            deadline,
546            callback!(
547                @capture<'tcx> {
548                    dest: MPlaceTy<'tcx>,
549                }
550                |this, unblock: UnblockKind| {
551                    match unblock {
552                        UnblockKind::Ready => this.write_null(&dest),
553                        UnblockKind::TimedOut => this.write_int(1, &dest),
554                    }
555                }
556            ),
557        )?;
558
559        interp_ok(())
560    }
561
562    fn dispatch_semaphore_signal(
563        &mut self,
564        sem_ptr: &OpTy<'tcx>,
565        dest: &MPlaceTy<'tcx>,
566    ) -> InterpResult<'tcx> {
567        let this = self.eval_context_mut();
568
569        let sem_ptr = this.read_immediate(sem_ptr)?;
570
571        let data = this.dispatch_semaphore_get_data(&sem_ptr)?.clone();
572        let woke_thread = this.semaphore_increment(&data.semaphore)?;
573
574        // The `dispatch_semaphore_signal` docs do not discuss the return value,
575        // but the Swift-level docs say:
576        // > This function returns non-zero if a thread is woken. Otherwise, zero is returned.
577
578        this.write_int(woke_thread, dest)?;
579        interp_ok(())
580    }
581
582    fn dispatch_release(&mut self, sem_ptr: &OpTy<'tcx>) -> InterpResult<'tcx> {
583        let this = self.eval_context_mut();
584
585        let sem_ptr = this.read_immediate(sem_ptr)?;
586
587        let data = this.dispatch_semaphore_get_data(&sem_ptr)?.clone();
588        // macOS aborts if the final value is below the initial value.
589        if data.semaphore.value() < data.init_value {
590            throw_machine_stop!(TerminationInfo::Abort(
591                "dispatch_semaphore destroyed while in use".to_owned()
592            ));
593        }
594
595        // Freeing the memory will also free the meta-object in `AllocExtra`.
596        let sem_ptr = sem_ptr.to_scalar().to_pointer(this);
597        this.deallocate_ptr(sem_ptr, None, MiriMemoryKind::Runtime.into())?;
598
599        interp_ok(())
600    }
601
602    fn dispatch_time(
603        &mut self,
604        when: &OpTy<'tcx>,
605        delta: &OpTy<'tcx>,
606        dest: &MPlaceTy<'tcx>,
607    ) -> InterpResult<'tcx> {
608        let this = self.eval_context_mut();
609
610        let when = this.read_scalar(when)?.to_u64()?;
611        let delta = this.read_scalar(delta)?.to_i64()?;
612
613        if when != DISPATCH_TIME_NOW {
614            throw_unsup_format!(
615                "dispatch_time is only supported with `when` being `DISPATCH_TIME_NOW`"
616            );
617        }
618        let Ok(delta) = u64::try_from(delta) else {
619            throw_unsup_format!("dispatch_time with negative `delta` is not supported");
620        };
621
622        // `delta` is in nanoseconds. Uses the monotone clock.
623        let deadline = this.machine.monotonic_clock.now().add_lossy(Duration::from_nanos(delta));
624        // Convert back to absolute nanoseconds for return value.
625        let duration = deadline.duration_since(this.machine.monotonic_clock.epoch());
626        let Ok(nanos) = u64::try_from(duration.as_nanos()) else {
627            throw_unsup_format!("dispatch_time with too big `delta` is not supported");
628        };
629        this.write_int(nanos, dest)?;
630        interp_ok(())
631    }
632}