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