Skip to main content

detcore/syscalls/
threads.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9//! System calls for dealing with threads and concurrency.
10
11use std::collections::HashMap;
12use std::marker::PhantomData;
13use std::sync::Arc;
14use std::sync::Mutex;
15use std::time::Duration;
16
17use procfs::process::Process;
18use rand::Rng;
19use reverie::BackendCapabilities;
20use reverie::Error;
21use reverie::GlobalRPC;
22use reverie::Guest;
23use reverie::Pid;
24use reverie::Stack;
25use reverie::syscalls;
26use reverie::syscalls::Addr;
27use reverie::syscalls::AddrMut;
28use reverie::syscalls::CloneFlags;
29use reverie::syscalls::Errno;
30use reverie::syscalls::MemoryAccess;
31use reverie::syscalls::ReadAddr;
32use reverie::syscalls::Syscall;
33use reverie::syscalls::SyscallInfo;
34use reverie::syscalls::Timespec;
35use reverie::syscalls::WaitPidFlag;
36use tracing::debug;
37use tracing::error;
38use tracing::info;
39use tracing::trace;
40
41use crate::SharedMemoryObjectId;
42use crate::config::BlockingMode;
43use crate::config::Config;
44use crate::config::SchedHeuristic;
45use crate::memory::MemoryMetadata;
46use crate::record_or_replay::RecordOrReplay;
47use crate::resources::ExternalOpId;
48use crate::resources::Permission;
49use crate::resources::ResourceID;
50use crate::resources::Resources;
51use crate::scheduler::FutexSignalWatch;
52use crate::scheduler::HostTimedSignalScope;
53use crate::scheduler::SchedValue;
54use crate::scheduler::pid_namespaces::GuestPidAnswer;
55use crate::syscalls::guest_siginfo::GuestSiginfo;
56use crate::syscalls::helpers::NonblockableSyscall;
57use crate::syscalls::helpers::RestartCall;
58use crate::syscalls::helpers::keep_restart_block;
59use crate::syscalls::helpers::record_retry_event;
60use crate::syscalls::helpers::retry_nonblocking_syscall;
61use crate::syscalls::helpers::retry_nonblocking_syscall_with_timeout;
62use crate::syscalls::pi_futex;
63use crate::syscalls::pi_futex::LockPiStep;
64use crate::syscalls::pi_futex::PiFutexState;
65use crate::syscalls::pi_futex::PiHandOver;
66use crate::syscalls::pi_futex::UnlockPiStep;
67use crate::syscalls::robust_list;
68use crate::tool_global::FutexAction;
69use crate::tool_global::GlobalState;
70use crate::tool_global::ResumeStatus;
71use crate::tool_global::await_exact_child_physical_exit;
72use crate::tool_global::cancel_exec;
73use crate::tool_global::child_tid_clear_address;
74use crate::tool_global::consume_child_wait;
75use crate::tool_global::create_child_thread;
76use crate::tool_global::futex_action;
77use crate::tool_global::hold_turn_for_group_exit;
78use crate::tool_global::host_timed_signals;
79use crate::tool_global::prepare_exec;
80use crate::tool_global::process_group;
81use crate::tool_global::ready_child_wait;
82use crate::tool_global::record_host_timed_signals;
83use crate::tool_global::resource_request;
84use crate::tool_global::set_child_tid_address;
85use crate::tool_global::thread_is_live;
86use crate::tool_global::thread_observe_time;
87use crate::tool_global::wait_for_child_lifecycle;
88use crate::tool_global::yield_once;
89use crate::tool_local::Detcore;
90use crate::tool_local::PendingVfork;
91use crate::tool_local::RobustListExit;
92use crate::tool_local::RobustListOwner;
93use crate::tool_local::RobustListWake;
94use crate::tool_local::ThreadState;
95use crate::types::ChildWaitExitClass;
96use crate::types::ChildWaitSelector;
97use crate::types::ChildWaitSpec;
98use crate::types::DetPid;
99use crate::types::DetTid;
100use crate::types::ExactChildWaitState;
101use crate::types::FutexID;
102use crate::types::LogicalTime;
103use crate::types::SigWrapper;
104
105#[path = "affinity.rs"]
106mod affinity;
107#[path = "emulated_waitid.rs"]
108mod emulated_waitid;
109
110// Preserve the historical Detcore ABI while hiding the host's configured CPU
111// count. This represents one virtual CPU in a fixed 128-bit kernel mask.
112pub(crate) const VIRTUAL_CPUSET_BYTES: usize = 16;
113
114const IOPRIO_WHO_PROCESS: libc::c_int = 1;
115const IOPRIO_WHO_PGRP: libc::c_int = 2;
116const IOPRIO_WHO_USER: libc::c_int = 3;
117const IOPRIO_CLASS_SHIFT: libc::c_int = 13;
118const IOPRIO_CLASS_BE: libc::c_int = 2;
119const IOPRIO_BE_NORM: libc::c_int = 4;
120const IOPRIO_DEFAULT_EFFECTIVE: libc::c_int =
121    (IOPRIO_CLASS_BE << IOPRIO_CLASS_SHIFT) | IOPRIO_BE_NORM;
122
123// sched_attr wire contract, from include/uapi/linux/sched/types.h. These are
124// spelled out rather than derived from `size_of::<libc::sched_attr>()`: the
125// value the kernel reports back on the E2BIG paths and the offset past which
126// trailing bytes must be zero are properties of the KERNEL's struct, which is
127// larger than the libc crate's 48-byte mirror. Deriving either from a Rust type
128// would silently re-point the contract if that type ever grows a field.
129const SCHED_ATTR_SIZE_VER0: u32 = 48; // first published struct
130const SCHED_ATTR_SIZE_VER1: u32 = 56; // adds sched_util_{min,max}
131/// The kernel's own `sizeof(struct sched_attr)`. Two things key off it: bytes at
132/// or beyond this offset must be zero, and it is the value written back into
133/// `uattr->size` when the request is refused with E2BIG.
134const SCHED_ATTR_KERNEL_SIZE: u32 = SCHED_ATTR_SIZE_VER1;
135/// `sched_copy_attr` and `sched_getattr` refuse a buffer larger than one page
136/// (kernel/sched/syscalls.c).
137fn sched_attr_max_size() -> u32 {
138    u32::try_from(crate::page::page_size()).expect("the page size fits in u32")
139}
140
141/// The scheduling policy Detcore presents as every thread's current one.
142///
143/// This is not a guess about the host. It is the value `handle_sched_getattr`
144/// writes back for every thread, so it is the policy a guest observes and the
145/// only policy `SCHED_FLAG_KEEP_POLICY` can be keeping.
146const VIRTUAL_CURRENT_POLICY: u32 = libc::SCHED_OTHER as u32;
147
148/// Outcome of scanning the bytes past the kernel's `struct sched_attr`.
149#[derive(Debug, Eq, PartialEq)]
150enum TailVerdict {
151    AllZero,
152    /// A byte the kernel does not understand was set: `copy_struct_from_user`
153    /// reports this as E2BIG, after storing its own size back.
154    NotZeroed,
155    /// Guest memory could not be read before any non-zero byte was seen.
156    Faulted,
157}
158
159/// Reads of eight bytes or fewer take safeptrace's `PTRACE_PEEKDATA` path,
160/// which reads a whole aligned word and BYPASSES GUEST PAGE PROTECTIONS. Only a
161/// read strictly larger than that reaches `process_vm_readv`, which honours
162/// them. Note this is not symmetric with the write side: `write` special-cases
163/// a length of exactly eight, so splitting a write in half escapes it, while
164/// `read` special-cases eight *or fewer* and splitting only makes it worse.
165const MIN_PROTECTION_RESPECTING_READ: usize = std::mem::size_of::<u64>() + 1;
166
167/// Scan `tail_len` bytes at `base + tail_off` the way `check_zeroed_user` does.
168///
169/// Two things here are load-bearing and neither is visible from the outcome
170/// alone, only from the ORDER the outcome is reached in.
171///
172/// First, Linux scans FORWARD and stops at the first thing it finds. A non-zero
173/// byte followed later by an unreadable page is E2BIG, because the scan never
174/// reaches the page; an unreadable page reached before any non-zero byte is
175/// EFAULT. Reading the whole tail up front and judging afterwards collapses
176/// both into EFAULT and silently changes the errno a guest sees.
177///
178/// Second, every read here is kept strictly larger than eight bytes, for the
179/// reason on [`MIN_PROTECTION_RESPECTING_READ`]. Where the remainder is
180/// smaller, the window is extended BACKWARDS over bytes already scanned rather
181/// than shortened; re-reading a byte is free and only the new bytes are judged.
182fn scan_tail_is_zeroed<M: MemoryAccess>(
183    memory: &M,
184    base: AddrMut<u8>,
185    tail_off: usize,
186    tail_len: usize,
187) -> TailVerdict {
188    const CHUNK: usize = 256;
189    let mut done = 0usize;
190    let mut buf = [0u8; CHUNK];
191    while done < tail_len {
192        let remaining = tail_len - done;
193        let want = remaining.min(CHUNK);
194        // Extend backwards when the remainder alone would be a small read.
195        // The window may reach back past the start of the tail into the
196        // `sched_attr` prefix: the kernel has already read those bytes, so they
197        // are readable, and only the new bytes are judged below. Clamping this
198        // to `done` alone left a 1-byte tail with nowhere to grow into and
199        // issued exactly the small read this constant exists to avoid.
200        let back = MIN_PROTECTION_RESPECTING_READ
201            .saturating_sub(want)
202            .min(done + tail_off);
203        let read_len = want + back;
204        let start = tail_off + done - back;
205        let Some(addr) = base
206            .as_raw()
207            .checked_add(start)
208            .and_then(AddrMut::<u8>::from_raw)
209        else {
210            return TailVerdict::Faulted;
211        };
212        // A failed read means zero NEW bytes were obtained; the `got < read_len`
213        // check below is what turns that into `Faulted`. Written `unwrap_or(0)`
214        // rather than `unwrap_or_default()` so the 0 stays visible.
215        let got = memory.read(addr, &mut buf[..read_len]).unwrap_or(0);
216        // Judge only the bytes that are genuinely new AND genuinely read.
217        let new_from = back.min(got);
218        if buf[new_from..got].iter().any(|byte| *byte != 0) {
219            return TailVerdict::NotZeroed;
220        }
221        if got < read_len {
222            // The scan stopped at an unreadable byte with nothing non-zero
223            // before it, which is `check_zeroed_user`'s -EFAULT.
224            return TailVerdict::Faulted;
225        }
226        done += want;
227    }
228    TailVerdict::AllZero
229}
230
231// Scheduling policies, from include/uapi/linux/sched.h.
232const SCHED_FIFO: u32 = 1;
233const SCHED_RR: u32 = 2;
234const SCHED_DEADLINE: u32 = 6;
235const SCHED_EXT: u32 = 7;
236/// The kernel's `MAX_RT_PRIO`; valid real-time priorities are 1..=99.
237const MAX_RT_PRIO: u32 = 100;
238
239// Byte offsets of the fields this handler inspects. Taken from the UAPI struct
240// rather than from a Rust mirror, for the reason given above.
241const SCHED_ATTR_OFF_POLICY: usize = 4;
242const SCHED_ATTR_OFF_FLAGS: usize = 8;
243const SCHED_ATTR_OFF_PRIORITY: usize = 20;
244const SCHED_ATTR_OFF_RUNTIME: usize = 24;
245const SCHED_ATTR_OFF_DEADLINE: usize = 32;
246const SCHED_ATTR_OFF_PERIOD: usize = 40;
247
248// sched_attr.sched_flags bits, from include/uapi/linux/sched.h.
249const SCHED_FLAG_RESET_ON_FORK: u64 = 0x01;
250const SCHED_FLAG_RECLAIM: u64 = 0x02;
251const SCHED_FLAG_DL_OVERRUN: u64 = 0x04;
252const SCHED_FLAG_KEEP_POLICY: u64 = 0x08;
253const SCHED_FLAG_KEEP_PARAMS: u64 = 0x10;
254const SCHED_FLAG_UTIL_CLAMP_MIN: u64 = 0x20;
255const SCHED_FLAG_UTIL_CLAMP_MAX: u64 = 0x40;
256const SCHED_FLAG_UTIL_CLAMP: u64 = SCHED_FLAG_UTIL_CLAMP_MIN | SCHED_FLAG_UTIL_CLAMP_MAX;
257const SCHED_FLAG_ALL: u64 = SCHED_FLAG_RESET_ON_FORK
258    | SCHED_FLAG_RECLAIM
259    | SCHED_FLAG_DL_OVERRUN
260    | SCHED_FLAG_KEEP_POLICY
261    | SCHED_FLAG_KEEP_PARAMS
262    | SCHED_FLAG_UTIL_CLAMP;
263
264/// Whether Linux accepts `policy` as a scheduling policy for `sched_setattr`.
265/// This is the kernel's `valid_policy()`: idle, fair, rt, deadline or ext. Note
266/// the gap at 4 -- SCHED_ISO is reserved and never valid -- and that 7 is
267/// SCHED_EXT, which `valid_policy()` accepts on a kernel built with
268/// CONFIG_SCHED_CLASS_EXT.
269fn is_valid_sched_policy(policy: u32) -> bool {
270    matches!(
271        policy,
272        // SCHED_OTHER/NORMAL, FIFO, RR, BATCH
273        0 | 1 | 2 | 3
274        // SCHED_IDLE
275        | 5
276        | SCHED_DEADLINE
277        | SCHED_EXT
278    )
279}
280
281/// The kernel's `rt_policy()`: the two fixed-priority real-time policies.
282fn is_rt_policy(policy: u32) -> bool {
283    policy == SCHED_FIFO || policy == SCHED_RR
284}
285
286/// The kernel's `__checkparam_dl()`, restricted to the parts that are pure ABI.
287///
288/// The kernel's final test compares the period against
289/// `sysctl_sched_dl_period_{min,max}`, which are runtime-tunable. That bound is
290/// a property of the host's configuration rather than of the interface, so it
291/// is deliberately not reproduced here -- see the handler's doc comment for the
292/// other cases in that class.
293fn deadline_params_are_valid(runtime: u64, deadline: u64, period: u64) -> bool {
294    // deadline != 0
295    if deadline == 0 {
296        return false;
297    }
298    // The kernel truncates DL_SCALE (10) bits, so runtime must be at least that
299    // big to survive the truncation.
300    if runtime < (1u64 << 10) {
301        return false;
302    }
303    // The MSB is reserved for wrap-around and sign handling.
304    if deadline & (1u64 << 63) != 0 || period & (1u64 << 63) != 0 {
305        return false;
306    }
307    // A zero period means "same as the deadline".
308    let period = if period == 0 { deadline } else { period };
309    // runtime <= deadline <= period
310    runtime <= deadline && deadline <= period
311}
312
313/// Apply `sched_copy_attr`'s size rules to the `size` the guest declared.
314///
315/// `Ok(n)` is the effective size to copy with; `Err(())` means the request is
316/// refused with E2BIG *and* the kernel first writes its own struct size back
317/// into `uattr->size`, so the caller owes that store.
318///
319/// The zero case is not a mistake: the kernel carries an explicit ABI
320/// compatibility quirk, `if (!size) size = SCHED_ATTR_SIZE_VER0;`, so a
321/// zero-sized request is a well-formed VER0 request and succeeds.
322fn sched_attr_effective_size(declared: u32) -> Result<u32, ()> {
323    let size = if declared == 0 {
324        SCHED_ATTR_SIZE_VER0
325    } else {
326        declared
327    };
328    if !(SCHED_ATTR_SIZE_VER0..=sched_attr_max_size()).contains(&size) {
329        return Err(());
330    }
331    Ok(size)
332}
333
334/// The descriptor fields this handler inspects, decoded from the guest's bytes.
335#[derive(Clone, Copy, Debug)]
336struct SchedAttrFields {
337    policy: u32,
338    sched_flags: u64,
339    priority: u32,
340    runtime: u64,
341    deadline: u64,
342    period: u64,
343}
344
345/// The checks that run in `sched_copy_attr` and in the `sched_setattr` wrapper,
346/// i.e. everything the kernel decides *before* it looks the target pid up.
347///
348/// `size` is the effective size from [`sched_attr_effective_size`], so the
349/// util-clamp rule can be stated in the same terms the kernel uses.
350///
351/// Measured: with a pid that does not exist, both of these still report their
352/// own errno rather than ESRCH, which is what places them on this side of the
353/// lookup.
354fn validate_sched_attr_before_lookup(size: u32, attr: &SchedAttrFields) -> Result<(), Errno> {
355    // `sched_copy_attr`: util-clamp lives past the VER0 tail, so asking for it
356    // with a VER0 buffer is incoherent.
357    if attr.sched_flags & SCHED_FLAG_UTIL_CLAMP != 0 && size < SCHED_ATTR_SIZE_VER1 {
358        return Err(Errno::EINVAL);
359    }
360    // `sched_setattr`: the policy is compared as a *signed* int, and this test
361    // sits above the KEEP_POLICY substitution below, so the sign bit is refused
362    // even when the policy field is otherwise ignored.
363    if (attr.policy as i32) < 0 {
364        return Err(Errno::EINVAL);
365    }
366    Ok(())
367}
368
369/// The checks inside `__sched_setscheduler`, i.e. everything the kernel decides
370/// *after* it has resolved the target pid.
371///
372/// Measured: with a pid that does not exist, every rule here reports ESRCH
373/// instead, which is what places them on this side of the lookup.
374fn validate_sched_attr_after_lookup(attr: &SchedAttrFields) -> Result<(), Errno> {
375    // `sched_setattr` rewrites the policy to SETPARAM_POLICY (-1) when
376    // KEEP_POLICY is set, and `__sched_setscheduler` then takes its
377    // `policy < 0` branch. That branch does not skip the policy-dependent
378    // rules -- it REUSES THE TASK'S CURRENT POLICY and applies them against
379    // that. `valid_policy()` is the only thing it skips, which is why an
380    // undefined value in the ignored field still passes.
381    //
382    // Detcore's current policy is not unknown: `handle_sched_getattr` reports
383    // SCHED_OTHER, nice 0, priority 0 for every thread, unconditionally, and
384    // that is the whole of the guest-visible scheduling state this sandbox
385    // exposes. So KEEP_POLICY substitutes SCHED_OTHER here. Treating it as
386    // "no policy" and skipping the rules accepted, for instance,
387    // KEEP_POLICY with sched_priority=1, which Linux refuses because a
388    // non-real-time current policy requires priority 0.
389    //
390    // These two sites must stay in step: if `handle_sched_getattr` ever
391    // reports a different virtual policy, this substitution follows it.
392    let policy = if attr.sched_flags & SCHED_FLAG_KEEP_POLICY != 0 {
393        Some(VIRTUAL_CURRENT_POLICY)
394    } else {
395        if !is_valid_sched_policy(attr.policy) {
396            return Err(Errno::EINVAL);
397        }
398        Some(attr.policy)
399    };
400    // No undefined sched_flags bits.
401    if attr.sched_flags & !SCHED_FLAG_ALL != 0 {
402        return Err(Errno::EINVAL);
403    }
404    // Valid priorities are 1..=MAX_RT_PRIO-1 for the real-time policies and
405    // exactly 0 for every other policy, so the priority and the policy have to
406    // agree; the kernel states that as `rt_policy(policy) != (prio != 0)`.
407    if attr.priority > MAX_RT_PRIO - 1 {
408        return Err(Errno::EINVAL);
409    }
410    if let Some(policy) = policy {
411        if is_rt_policy(policy) != (attr.priority != 0) {
412            return Err(Errno::EINVAL);
413        }
414        if policy == SCHED_DEADLINE
415            && !deadline_params_are_valid(attr.runtime, attr.deadline, attr.period)
416        {
417            return Err(Errno::EINVAL);
418        }
419    }
420    Ok(())
421}
422
423// AUTONOMOUS-BOT-IMPLEMENTED
424// TODO-HUMAN-REVIEW(PR-881)
425fn virtual_ioprio(which: libc::c_int) -> Result<i64, Errno> {
426    match which {
427        IOPRIO_WHO_PROCESS => Ok(0),
428        IOPRIO_WHO_PGRP | IOPRIO_WHO_USER => Ok(i64::from(IOPRIO_DEFAULT_EFFECTIVE)),
429        _ => Err(Errno::EINVAL),
430    }
431}
432
433#[repr(C)]
434#[derive(Clone, Copy)]
435struct WaitidSigchldFields {
436    pid: libc::pid_t,
437    uid: libc::uid_t,
438    status: libc::c_int,
439    utime: libc::c_long,
440    stime: libc::c_long,
441}
442
443#[repr(C)]
444union WaitidSiginfoFields {
445    _alignment: *mut libc::c_void,
446    sigchld: WaitidSigchldFields,
447}
448
449#[repr(C)]
450struct WaitidSiginfoHead {
451    _base: [libc::c_int; 3],
452    fields: WaitidSiginfoFields,
453}
454
455fn wait_status_is_termination(status: libc::c_int) -> bool {
456    libc::WIFEXITED(status) || libc::WIFSIGNALED(status)
457}
458
459fn waitid_code_is_termination(code: libc::c_int) -> bool {
460    matches!(code, libc::CLD_EXITED | libc::CLD_KILLED | libc::CLD_DUMPED)
461}
462
463fn canonicalize_waitid_siginfo(info: &mut libc::siginfo_t) {
464    debug_assert!(
465        std::mem::size_of::<WaitidSiginfoHead>() <= std::mem::size_of::<libc::siginfo_t>()
466    );
467    // SAFETY: Linux siginfo_t starts with three c_int fields followed by a
468    // pointer-aligned union. Its SIGCHLD member is pid, uid, status, utime,
469    // and stime in that order. The local repr(C) mirror changes only the two
470    // host CPU-accounting fields and preserves the kernel-populated event.
471    let sigchld = unsafe {
472        &mut (*(info as *mut libc::siginfo_t).cast::<WaitidSiginfoHead>())
473            .fields
474            .sigchld
475    };
476    sigchld.utime = 0;
477    sigchld.stime = 0;
478}
479
480/// `child` is the task `info_value.si_pid` names (`Detcore::task_of_guest_pid`).
481fn finish_waitid_result<T, G>(
482    guest: &mut G,
483    call: syscalls::Waitid,
484    value: i64,
485    mut info_value: libc::siginfo_t,
486    child: DetPid,
487) -> Result<i64, Error>
488where
489    T: RecordOrReplay,
490    G: Guest<Detcore<T>>,
491{
492    // SAFETY: waitid writes either zeroed output or the SIGCHLD siginfo_t
493    // variant, for which libc exposes si_pid.
494    let child_pid = unsafe { info_value.si_pid() };
495    if child_pid != 0 {
496        canonicalize_waitid_siginfo(&mut info_value);
497        guest.memory().write_value(
498            call.info().expect("waitid infop checked before execution"),
499            &info_value,
500        )?;
501        if call.options() & libc::WNOWAIT == 0 && waitid_code_is_termination(info_value.si_code) {
502            guest.thread_state_mut().reap_child_process_cpu_time(child);
503        }
504        if let Some(rusage) = call.rusage() {
505            // Host CPU and scheduling counters are not deterministic.
506            let usage: libc::rusage = unsafe { std::mem::zeroed() };
507            guest.memory().write_value(rusage, &usage)?;
508        }
509    }
510    Ok(value)
511}
512
513#[derive(Debug, Eq, PartialEq)]
514enum ExactWaitPollDecision {
515    ChildReady,
516    AwaitPhysicalExit,
517    ReapAfterLogicalExit,
518    Interrupted,
519    Retry,
520}
521
522fn exact_wait_poll_decision(
523    child_ready: bool,
524    signaled: bool,
525    lifecycle: Option<ExactChildWaitState>,
526) -> ExactWaitPollDecision {
527    if child_ready {
528        ExactWaitPollDecision::ChildReady
529    } else if lifecycle == Some(ExactChildWaitState::PhysicalExitPending) {
530        ExactWaitPollDecision::AwaitPhysicalExit
531    } else if matches!(
532        lifecycle,
533        Some(ExactChildWaitState::LogicallyExited | ExactChildWaitState::PhysicallyExited)
534    ) {
535        ExactWaitPollDecision::ReapAfterLogicalExit
536    } else if signaled {
537        ExactWaitPollDecision::Interrupted
538    } else {
539        ExactWaitPollDecision::Retry
540    }
541}
542
543fn stale_any_wait_must_interrupt(signaled: bool, next_ready: Option<DetPid>) -> bool {
544    signaled && next_ready.is_none()
545}
546
547fn terminal_child_wait_spec(
548    selector: ChildWaitSelector,
549    caller: DetTid,
550    options: libc::c_int,
551) -> ChildWaitSpec {
552    let exit_class = if options & libc::__WALL != 0 {
553        ChildWaitExitClass::Any
554    } else if options & libc::__WCLONE != 0 {
555        ChildWaitExitClass::Clone
556    } else {
557        ChildWaitExitClass::Sigchld
558    };
559    ChildWaitSpec {
560        selector,
561        owner: (options & libc::__WNOTHREAD != 0).then_some(caller),
562        exit_class,
563    }
564}
565
566fn validate_wait4_arguments(pid: libc::pid_t, options: WaitPidFlag) -> Result<(), Errno> {
567    let allowed_options = WaitPidFlag::WNOHANG
568        | WaitPidFlag::WUNTRACED
569        | WaitPidFlag::WCONTINUED
570        | WaitPidFlag::__WNOTHREAD
571        | WaitPidFlag::__WCLONE
572        | WaitPidFlag::__WALL;
573
574    // Linux kernel/exit.c:kernel_wait4 rejects unknown option bits before it
575    // interprets the pid selector.
576    if options.bits() & !allowed_options.bits() != 0 {
577        return Err(Errno::EINVAL);
578    }
579    // The same function returns ESRCH for INT_MIN before looking for children,
580    // because negating that selector is not representable as a pid_t.
581    if pid == libc::pid_t::MIN {
582        return Err(Errno::ESRCH);
583    }
584    Ok(())
585}
586
587// A serial emulated child wait (today KVM) represents terminal events only, including the terminal subset
588// of WUNTRACED. `__WNOTHREAD` is enforced by the logical owner filter in
589// `terminal_child_wait_spec`, and exact-child completion strips it before
590// the backend call. Keep unsupported and unknown low-int bits on the
591// backend's prevalidation path; they must not select or consume a logical
592// child.
593pub(super) fn wait4_uses_terminal_selector(
594    options: WaitPidFlag,
595    serial_emulated_wait: bool,
596) -> bool {
597    if serial_emulated_wait {
598        options.bits() & !(libc::WNOHANG | libc::WUNTRACED | libc::__WNOTHREAD) == 0
599    } else {
600        !options.intersects(
601            WaitPidFlag::WUNTRACED
602                | WaitPidFlag::WCONTINUED
603                | WaitPidFlag::__WCLONE
604                | WaitPidFlag::__WALL,
605        )
606    }
607}
608
609/// The one completion order a backend that emulates child waits
610/// (`BackendCapabilities::emulates_child_waits`) needs for both wait4 and
611/// waitid, split around the backend's exact-child call.
612///
613/// Backend waitability precedes the child's final owner-hook CPU prefix, so
614/// the caller first waits, under its retained grant and birth-owned identity,
615/// for that publication. No new scheduler turn or user-memory identity read
616/// occurs.
617pub(super) async fn await_selected_child_cpu_publication<G, T>(
618    guest: &mut G,
619    child: DetPid,
620    call: &str,
621) -> Result<(), Error>
622where
623    G: Guest<Detcore<T>>,
624    T: RecordOrReplay,
625{
626    if guest
627        .thread_state()
628        .wait_for_child_cpu_publication(child)
629        .await
630    {
631        Ok(())
632    } else {
633        Err(Error::Tool(anyhow::anyhow!(
634            "emulated {call} selected child has no owned final CPU publication"
635        )))
636    }
637}
638
639/// After the backend's exact-child call consumed `child` (it returned the
640/// child, or EFAULT after selecting it), retire it: reap its final CPU time
641/// and consume Detcore's logical wait. Shared by wait4 and waitid.
642pub(super) async fn retire_selected_child<G, T>(
643    guest: &mut G,
644    child: DetPid,
645    call: &str,
646) -> Result<(), Error>
647where
648    G: Guest<Detcore<T>>,
649    T: RecordOrReplay,
650{
651    guest.thread_state_mut().reap_child_process_cpu_time(child);
652    if consume_child_wait(guest, child).await {
653        Ok(())
654    } else {
655        Err(Error::Tool(anyhow::anyhow!(
656            "emulated {call} selected child was not logically consumed"
657        )))
658    }
659}
660
661pub(super) async fn complete_selected_emulated_wait4<G, T>(
662    guest: &mut G,
663    call: syscalls::Wait4,
664    child: DetPid,
665) -> Result<i64, Error>
666where
667    G: Guest<Detcore<T>>,
668    T: RecordOrReplay,
669{
670    await_selected_child_cpu_publication(guest, child, "wait4").await?;
671    // Selection already applied `__WNOTHREAD` through the logical owner
672    // filter; the backend's raw wait4 accepts only WNOHANG and WUNTRACED.
673    // Keep exactly those, as the waitid exact-child completion does.
674    let exact = call
675        .with_pid(child.as_raw())
676        .with_options(WaitPidFlag::from_bits_retain(
677            call.options().bits() & (libc::WNOHANG | libc::WUNTRACED),
678        ));
679    let result = guest.inject(exact).await;
680    match result {
681        Ok(pid) if pid == i64::from(child.as_raw()) => {}
682        // The supported KVM wait4 selects/consumes before either status or
683        // rusage copyout. EFAULT therefore consumed this exact child too.
684        Err(Errno::EFAULT) => {}
685        unexpected => {
686            return Err(Error::Tool(anyhow::anyhow!(
687                "emulated wait4 selected-child invariant failed: {unexpected:?} for {child}"
688            )));
689        }
690    }
691    retire_selected_child(guest, child, "wait4").await?;
692    result.map_err(Error::from)
693}
694
695fn child_wait_can_retry_after_stale(spec: ChildWaitSpec) -> bool {
696    !matches!(spec.selector, ChildWaitSelector::Exact(_))
697}
698
699pub(super) type KernelSigset = u64;
700pub(super) const KERNEL_SIGSET_SIZE: usize = std::mem::size_of::<KernelSigset>();
701
702fn signal_is_blocked(mask: &KernelSigset, signal: SigWrapper) -> bool {
703    let raw_signal = signal.raw();
704    (1..=KernelSigset::BITS as i32).contains(&raw_signal)
705        && mask & (1_u64 << (raw_signal as u32 - 1)) != 0
706}
707
708#[repr(C)]
709#[derive(Clone, Copy)]
710pub(super) struct KernelSigaction {
711    pub(super) handler: u64,
712    pub(super) flags: u64,
713    pub(super) restorer: u64,
714    pub(super) mask: KernelSigset,
715}
716
717#[derive(Clone, Copy, Debug, Eq, PartialEq)]
718pub(super) enum WaitSignalDisposition {
719    Interrupt,
720    Restart,
721}
722
723fn signal_default_disposition_does_not_interrupt_child_wait(signal: SigWrapper) -> bool {
724    matches!(
725        signal.raw(),
726        libc::SIGCHLD | libc::SIGCONT | libc::SIGURG | libc::SIGWINCH
727    )
728}
729
730/// Whether `signal`'s default action ends the process, by terminating it or
731/// dumping core (signal(7)): every signal except those whose default is to be
732/// ignored, to stop or to continue. SIGKILL, which no handler or callback
733/// sees, is left out as well. On DBT, real-time signals do not reach
734/// Detcore's signal path today (<https://github.com/rrnewton/hermit/issues/4192>).
735fn signal_default_action_ends_the_process(signal: SigWrapper) -> bool {
736    !matches!(
737        signal.raw(),
738        libc::SIGCHLD
739            | libc::SIGCONT
740            | libc::SIGURG
741            | libc::SIGWINCH
742            | libc::SIGSTOP
743            | libc::SIGTSTP
744            | libc::SIGTTIN
745            | libc::SIGTTOU
746            | libc::SIGKILL
747    )
748}
749
750/// Whether a run may retire a process's other threads in the turn that
751/// delivers a signal ending the process (`Scheduler::commit_fatal_delivery`):
752/// its threads are serialized, the kernel's signal state is the guest's own,
753/// from which the delivering thread reads that the kernel carries the default
754/// action out, and no gdbserver is attached, whose client decides the signal
755/// when it resumes the thread.
756pub(crate) fn retires_others_at_fatal_delivery(cfg: &Config) -> bool {
757    cfg.sequentialize_threads && cfg.backend.kernel_signal_state_is_guests && !cfg.gdbserver
758}
759
760/// Whether delivering `signal` to the calling thread now ends its process,
761/// by an action left at a default that terminates or dumps core: `None` when
762/// it does not, else whether the kernel's signal state also shows that
763/// resuming the thread with `signal` carries the action out
764/// ([`KernelSignalState::delivery_carries_out_a_fatal_default`]), asked only
765/// when `may_retire_others`. Detcore's copy of the dispositions answers the
766/// first question without a read for most deliveries (a handled or ignored
767/// signal); the kernel's state is read at most once.
768pub(crate) fn fatal_default_delivery<G, T>(
769    guest: &G,
770    signal: i32,
771    may_retire_others: bool,
772) -> Option<bool>
773where
774    G: Guest<Detcore<T>>,
775    T: RecordOrReplay,
776{
777    match fatal_delivery_observation(guest, signal, may_retire_others) {
778        FatalDeliveryObservation::Fatal { may_retire_others } => Some(may_retire_others),
779        FatalDeliveryObservation::Nonfatal | FatalDeliveryObservation::Unknown => None,
780    }
781}
782
783#[derive(Clone, Copy, Debug, PartialEq, Eq)]
784pub(crate) enum FatalDeliveryObservation {
785    Nonfatal,
786    Fatal { may_retire_others: bool },
787    Unknown,
788}
789
790/// Keep unavailable native/copy evidence distinct from a known nonfatal
791/// disposition. The old scheduler decision retains its exact Option mapping.
792pub(crate) fn fatal_delivery_observation<G, T>(
793    guest: &G,
794    signal: i32,
795    may_retire_others: bool,
796) -> FatalDeliveryObservation
797where
798    G: Guest<Detcore<T>>,
799    T: RecordOrReplay,
800{
801    let copied = super::signal_shadow::copied_dispositions_for(guest, signal)
802        .map(|copied| default_action_ends_process(copied.ignored, copied.caught, signal));
803    if copied == Some(false) {
804        return FatalDeliveryObservation::Nonfatal;
805    }
806    let kernel = (copied.is_none() || may_retire_others)
807        .then(|| read_thread_signal_state(guest.pid(), guest.tid()).ok())
808        .flatten();
809    match copied.or_else(|| kernel.map(|state| state.default_action_ends_process(signal))) {
810        Some(true) => FatalDeliveryObservation::Fatal {
811            may_retire_others: may_retire_others
812                && kernel.is_some_and(|state| state.delivery_carries_out_a_fatal_default(signal)),
813        },
814        Some(false) => FatalDeliveryObservation::Nonfatal,
815        None => FatalDeliveryObservation::Unknown,
816    }
817}
818
819fn signal_has_uncatchable_default_disposition(signal: SigWrapper) -> bool {
820    matches!(signal.raw(), libc::SIGKILL | libc::SIGSTOP)
821}
822
823pub(super) async fn wait_signal_disposition<G, T>(
824    guest: &mut G,
825    status: ResumeStatus,
826    guest_signal_mask: &KernelSigset,
827    action_addr: AddrMut<'_, KernelSigaction>,
828    inspect_action: bool,
829) -> Result<Option<WaitSignalDisposition>, Error>
830where
831    G: Guest<Detcore<T>>,
832    T: RecordOrReplay,
833{
834    let ResumeStatus::Signaled(signals) = status else {
835        return Ok(None);
836    };
837    let Some(mut signals) = signals else {
838        return Ok(Some(WaitSignalDisposition::Interrupt));
839    };
840    signals.sort_by_key(|signal| signal.raw());
841    for signal in signals {
842        if signal_is_blocked(guest_signal_mask, signal) {
843            continue;
844        }
845        if signal_has_uncatchable_default_disposition(signal) {
846            return Ok(Some(WaitSignalDisposition::Interrupt));
847        }
848        if !inspect_action {
849            return Ok(Some(WaitSignalDisposition::Interrupt));
850        }
851        let call = syscalls::RtSigaction::new()
852            .with_signum(signal.raw())
853            .with_action(None)
854            .with_old_action(Some(action_addr.cast()))
855            .with_sigsetsize(std::mem::size_of::<u64>());
856        guest.inject_with_retry(call).await?;
857        let action: KernelSigaction = guest.memory().read_value(action_addr)?;
858        if action.handler == libc::SIG_IGN as u64
859            || action.handler == libc::SIG_DFL as u64
860                && signal_default_disposition_does_not_interrupt_child_wait(signal)
861        {
862            continue;
863        }
864        return Ok(Some(
865            if action.handler != libc::SIG_DFL as u64 && action.flags & libc::SA_RESTART as u64 != 0
866            {
867                WaitSignalDisposition::Restart
868            } else {
869                WaitSignalDisposition::Interrupt
870            },
871        ));
872    }
873    Ok(None)
874}
875
876/// The signals a precise futex wait holds in its own check after a wake:
877/// `SIGCHLD` and every signal a host-timed source can post (`host_timed`),
878/// except a signal the scheduler committed to it at the wake (`committed`)
879/// that no host-timed source can post too
880/// (https://github.com/rrnewton/hermit/issues/4005).
881pub(crate) fn futex_wait_held_after_wake(
882    host_timed: KernelSigset,
883    committed: KernelSigset,
884) -> KernelSigset {
885    (kernel_sigset_bit(libc::SIGCHLD) | host_timed) & !(committed & !host_timed)
886}
887
888/// The signals that end a precise futex wait at its own check: those pending
889/// and interrupting it (`interrupting`) that it does not hold (`held`), or,
890/// when there are none, a guest's thread-directed `SIGCHLD` in its private
891/// queue that the schedule recorded (`PrivatePending`). Such a copy may have
892/// been sent before this wait parked, and the drain that ends a parked wait for
893/// it never saw this wait, so only this check can. `/proc` cannot tell it from
894/// a copy a host-timed source posted, so its bit only prompts the question
895/// (https://github.com/rrnewton/hermit/issues/4132).
896///
897/// A pending copy Linux discarded when it was sent does not end the wait,
898/// whatever handler exists now: the scheduler's record of such copies
899/// (`DiscardedSignalControl::Bits`) is subtracted, per queue, from every
900/// signal (`without_discarded`,
901/// <https://github.com/rrnewton/hermit/issues/4293>).
902pub(crate) async fn futex_wait_pending_signals<G, T>(
903    guest: &mut G,
904    state: &KernelSignalState,
905    interrupting: KernelSigset,
906    held: KernelSigset,
907    host_timed: KernelSigset,
908    committed: KernelSigset,
909) -> KernelSigset
910where
911    G: Guest<Detcore<T>>,
912    T: RecordOrReplay,
913{
914    let pending = without_discarded(guest, state, interrupting & !held, committed).await;
915    let sigchld = kernel_sigset_bit(libc::SIGCHLD) & !host_timed;
916    if pending == 0
917        && interrupting & state.thread_pending & held & sigchld != 0
918        && matches!(
919            crate::tool_global::child_exit_sigchld(
920                guest,
921                crate::scheduler::child_exit_sigchld::ChildExitSigchldControl::PrivatePending,
922            )
923            .await,
924            crate::scheduler::child_exit_sigchld::ChildExitSigchldAnswer::Placed(true)
925        )
926    {
927        return sigchld;
928    }
929    pending
930}
931
932/// `pending`, a set of this thread's pending signals, without each one whose
933/// only pending copies Linux discarded when they were sent, as the scheduler
934/// recorded them (`scheduler::discarded_at_send`). Asks the scheduler only
935/// when such a signal could be there.
936///
937/// No signal is exempt, not even one the wake's answer names: that answer
938/// carries the drain's batch too, whose copy a sender may have flushed
939/// (`SIG_IGN`) and sent again, discarded, before this check; Linux then has
940/// no copy to deliver (the Claude re-check of the R2b-1 design, R4-1,
941/// https://github.com/rrnewton/hermit/issues/4273). A copy the scheduler
942/// itself sent kept clears its bit, so applying the record to it changes
943/// nothing. `committed` is kept for the callers' hold and is not used here.
944pub(crate) async fn without_discarded<G, T>(
945    guest: &mut G,
946    state: &KernelSignalState,
947    pending: KernelSigset,
948    committed: KernelSigset,
949) -> KernelSigset
950where
951    G: Guest<Detcore<T>>,
952    T: RecordOrReplay,
953{
954    use crate::scheduler::discarded_at_send::DiscardedSignalAnswer;
955    use crate::scheduler::discarded_at_send::DiscardedSignalControl;
956    use crate::scheduler::discarded_at_send::STANDARD_SIGNALS;
957    if pending & STANDARD_SIGNALS == 0
958        || !crate::scheduler::discarded_at_send::records(guest.config())
959    {
960        return pending;
961    }
962    let DiscardedSignalAnswer::Bits { thread, process } =
963        crate::tool_global::discarded_signal(guest, DiscardedSignalControl::Bits).await
964    else {
965        return pending;
966    };
967    with_kept_copies(state, thread, process, pending, committed)
968}
969
970/// `pending` without each signal whose only pending copies the record marks
971/// as discarded at send (`thread` for the thread's queue, `process` for the
972/// shared queue). The pure part of `without_discarded`. No signal is exempt,
973/// not even one in `committed`, the set the wake's answer names, which can
974/// hold a drain batch's copy that was flushed and sent again, discarded
975/// (R4-1 of the R2b-1 design re-checks,
976/// <https://github.com/rrnewton/hermit/issues/4273>).
977pub(crate) fn with_kept_copies(
978    state: &KernelSignalState,
979    thread: u64,
980    process: u64,
981    pending: KernelSigset,
982    committed: KernelSigset,
983) -> KernelSigset {
984    let _ = committed;
985    let kept = (state.thread_pending & !thread) | (state.shared_pending & !process);
986    pending & kept
987}
988
989/// The one-bit kernel sigset for `raw_signal` (1-based), or 0 when out of range.
990pub(crate) fn kernel_sigset_bit(raw_signal: i32) -> KernelSigset {
991    if (1..=KernelSigset::BITS as i32).contains(&raw_signal) {
992        1_u64 << (raw_signal as u32 - 1)
993    } else {
994        0
995    }
996}
997
998/// The exit signal that a child created by this clone-family call sends to
999/// the calling process when it exits, at a moment set by host timing, as a
1000/// kernel sigset; 0 when there is none to hold. Linux sends a child's exit
1001/// signal to the process that created it. A thread (`CLONE_THREAD`) sends
1002/// none. SIGCHLD is already held in every gated wait. A `CLONE_PARENT` child
1003/// sends the caller's own exit signal to the caller's parent instead, and that
1004/// signal is SIGCHLD or was recorded for that parent when it created the
1005/// caller's process (a reparent to another process resets it to SIGCHLD).
1006pub(crate) fn clone_host_timed_signals(
1007    flags: CloneFlags,
1008    exit_signal: libc::c_int,
1009) -> KernelSigset {
1010    if flags.intersects(CloneFlags::CLONE_THREAD | CloneFlags::CLONE_PARENT)
1011        || exit_signal == libc::SIGCHLD
1012    {
1013        0
1014    } else {
1015        kernel_sigset_bit(exit_signal)
1016    }
1017}
1018
1019/// One thread's signal state as the kernel reports it in
1020/// `/proc/<pid>/task/<tid>/status`.
1021///
1022/// The kernel is the authority for a guest thread's mask, dispositions, and
1023/// pending signals on backends that run the guest as real host threads. Reading
1024/// the state has no effect on the guest, unlike a probe syscall around which a
1025/// signal can be dequeued and held by the backend
1026/// (https://github.com/rrnewton/hermit/issues/3146).
1027#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
1028pub(crate) struct KernelSignalState {
1029    /// Signals pending for the thread or its whole thread group (`SigPnd | ShdPnd`).
1030    pub(crate) pending: KernelSigset,
1031    /// Signals pending on the thread's private queue (`SigPnd`).
1032    pub(crate) thread_pending: KernelSigset,
1033    /// Signals pending on the thread group's shared queue (`ShdPnd`).
1034    pub(crate) shared_pending: KernelSigset,
1035    /// The thread's current signal mask (`SigBlk`).
1036    pub(crate) blocked: KernelSigset,
1037    /// Signals whose disposition is `SIG_IGN` (`SigIgn`).
1038    pub(crate) ignored: KernelSigset,
1039    /// Signals with a handler installed (`SigCgt`).
1040    pub(crate) caught: KernelSigset,
1041}
1042
1043/// [`KernelSignalState::default_action_ends_process`] given only the
1044/// thread's ignored and caught sets, from wherever they come (the kernel's
1045/// `/proc` view or Detcore's copy, `signal_shadow::copied_dispositions`).
1046pub(crate) fn default_action_ends_process(
1047    ignored: KernelSigset,
1048    caught: KernelSigset,
1049    signal: i32,
1050) -> bool {
1051    let bit = kernel_sigset_bit(signal);
1052    let stops_or_continues = [
1053        libc::SIGSTOP,
1054        libc::SIGTSTP,
1055        libc::SIGTTIN,
1056        libc::SIGTTOU,
1057        libc::SIGCONT,
1058    ]
1059    .into_iter()
1060    .fold(0, |set, signal| set | kernel_sigset_bit(signal));
1061    signal == libc::SIGKILL
1062        || ((ignored | caught | default_ignored_signals() | stops_or_continues) & bit) == 0
1063}
1064
1065/// The signals a process with dispositions `ignored` and `caught` discards
1066/// when they are generated and unblocked: those set to `SIG_IGN`, and the
1067/// default-ignored ones with no handler (`sig_task_ignored`).
1068pub(crate) fn ignored_at_generation(ignored: KernelSigset, caught: KernelSigset) -> KernelSigset {
1069    ignored | (default_ignored_signals() & !caught)
1070}
1071
1072impl KernelSignalState {
1073    /// Parse the five signal lines of a `/proc/<pid>/task/<tid>/status` file.
1074    pub(crate) fn parse(status: &str) -> Option<Self> {
1075        let mut thread_pending = None;
1076        let mut shared_pending = None;
1077        let mut state = KernelSignalState::default();
1078        let mut blocked = None;
1079        let mut ignored = None;
1080        let mut caught = None;
1081        for line in status.lines() {
1082            let Some((key, value)) = line.split_once(':') else {
1083                continue;
1084            };
1085            let slot = match key {
1086                "SigPnd" => &mut thread_pending,
1087                "ShdPnd" => &mut shared_pending,
1088                "SigBlk" => &mut blocked,
1089                "SigIgn" => &mut ignored,
1090                "SigCgt" => &mut caught,
1091                _ => continue,
1092            };
1093            *slot = Some(KernelSigset::from_str_radix(value.trim(), 16).ok()?);
1094        }
1095        state.thread_pending = thread_pending?;
1096        state.shared_pending = shared_pending?;
1097        state.pending = state.thread_pending | state.shared_pending;
1098        state.blocked = blocked?;
1099        state.ignored = ignored?;
1100        state.caught = caught?;
1101        Some(state)
1102    }
1103
1104    /// Signals whose delivery, while unblocked in `mask`, ends a blocking wait: a
1105    /// caught signal runs its handler, and a `SIG_DFL` signal terminates, dumps
1106    /// core, or stops the process. Ignored signals and the default-ignored
1107    /// `SIGCHLD`, `SIGCONT`, `SIGURG`, and `SIGWINCH` do not, and neither does the
1108    /// backend's own preemption signal.
1109    pub(crate) fn interrupting(&self, mask: KernelSigset) -> KernelSigset {
1110        let default_action = !(self.ignored | self.caught);
1111        (self.caught | (default_action & !default_ignored_signals()))
1112            & !mask
1113            & !kernel_sigset_bit(reverie::PERF_EVENT_SIGNAL as i32)
1114    }
1115
1116    /// Signals Linux discards when they are generated while unblocked: those
1117    /// set to `SIG_IGN`, and the default-ignored `SIGCHLD`, `SIGCONT`, `SIGURG`
1118    /// and `SIGWINCH` while they have no handler (`sig_task_ignored`). A blocked
1119    /// signal is never discarded, since its disposition may change before it is
1120    /// unblocked. Under ptrace the kernel queues even these for the tracer, so
1121    /// the pending set alone does not show the discard.
1122    pub(crate) fn discarded_at_generation(&self) -> KernelSigset {
1123        self.ignored_at_generation() & !self.blocked
1124    }
1125
1126    /// The signals [`discarded_at_generation`](Self::discarded_at_generation)
1127    /// names, before the mask: those set to `SIG_IGN`, and the default-ignored
1128    /// ones with no handler. A caller that knows a better mask than `SigBlk`
1129    /// (`scheduler::discarded_at_send`) applies its own.
1130    pub(crate) fn ignored_at_generation(&self) -> KernelSigset {
1131        ignored_at_generation(self.ignored, self.caught)
1132    }
1133
1134    /// Whether delivering `signal` now ends the process: its disposition is
1135    /// `SIG_DFL` and its default action terminates or dumps core, not ignores,
1136    /// stops or continues. `SIGKILL` always does.
1137    pub(crate) fn default_action_ends_process(&self, signal: i32) -> bool {
1138        default_action_ends_process(self.ignored, self.caught, signal)
1139    }
1140
1141    /// Whether resuming the thread from a delivery stop with `signal` makes
1142    /// the kernel carry out its default action and end the process: the
1143    /// thread does not block `signal`, which `ptrace_signal` would requeue,
1144    /// and [`Self::default_action_ends_process`].
1145    pub(crate) fn delivery_carries_out_a_fatal_default(&self, signal: i32) -> bool {
1146        self.blocked & kernel_sigset_bit(signal) == 0 && self.default_action_ends_process(signal)
1147    }
1148
1149    /// The job-control stop signals `SIGTSTP`, `SIGTTIN`, and `SIGTTOU` whose
1150    /// disposition is still `SIG_DFL`.
1151    ///
1152    /// Linux does not stop a process for one of these when its process group is
1153    /// orphaned: `get_signal` discards the signal (`is_current_pgrp_orphaned`),
1154    /// and a wait it reached restarts as if no signal had arrived. Whether a
1155    /// group is orphaned depends on the parents and sessions of every process in
1156    /// it, which Detcore does not track.
1157    pub(crate) fn default_job_control_stops(&self) -> KernelSigset {
1158        [libc::SIGTSTP, libc::SIGTTIN, libc::SIGTTOU]
1159            .into_iter()
1160            .fold(0, |set, signal| set | kernel_sigset_bit(signal))
1161            & !(self.ignored | self.caught)
1162    }
1163
1164    /// The signals that end a blocking wait under the guest's `mask`: those
1165    /// [`interrupting`](Self::interrupting) names, without the default job-control
1166    /// stops ([`default_job_control_stops`](Self::default_job_control_stops)) when
1167    /// `defers_default_stops` is set.
1168    ///
1169    /// A wait sets it when Linux's restart of the call keeps its deadline
1170    /// (`NonblockableSyscall::restart_keeps_deadline`): a `poll` with a positive
1171    /// timeout, and a timed futex wait. Detcore keeps that deadline for the
1172    /// kernel's restart only when the wait ends with `ERESTART_RESTARTBLOCK`, which
1173    /// a stop signal the backend holds does not (`held_signal_restart_errno`), so
1174    /// ending such a wait for a default stop that Linux discards in an orphaned
1175    /// process group could restart it with a fresh timeout, and it would return
1176    /// late. Left alone, the wait keeps its deadline, as on hermit main, and a stop
1177    /// that Linux does not discard takes effect when the call returns rather than
1178    /// when the signal arrives (https://github.com/rrnewton/hermit/issues/3358).
1179    /// `SIGSTOP`, a caught stop signal, and a fatal one still end the wait.
1180    pub(crate) fn interrupting_wait(
1181        &self,
1182        mask: KernelSigset,
1183        defers_default_stops: bool,
1184    ) -> KernelSigset {
1185        let interrupting = self.interrupting(mask);
1186        if defers_default_stops {
1187            interrupting & !self.default_job_control_stops()
1188        } else {
1189            interrupting
1190        }
1191    }
1192
1193    /// Pending signals that would end a blocking wait under the guest's `mask`
1194    /// (see [`interrupting_wait`](Self::interrupting_wait)).
1195    pub(crate) fn pending_interrupting(
1196        &self,
1197        mask: KernelSigset,
1198        defers_default_stops: bool,
1199    ) -> KernelSigset {
1200        self.pending & self.interrupting_wait(mask, defers_default_stops)
1201    }
1202
1203    /// The signal the kernel dequeues next for this thread while `mask` is
1204    /// blocked, and the queue it comes from, or `None` if no unblocked signal is
1205    /// pending.
1206    ///
1207    /// As in `dequeue_signal` and `next_signal` (`kernel/signal.c`), the thread's
1208    /// private queue goes before the thread group's shared queue, and within a
1209    /// queue a synchronous signal (`SIGSEGV`, `SIGBUS`, `SIGILL`, `SIGTRAP`,
1210    /// `SIGFPE`, `SIGSYS`) goes before the lowest-numbered other one.
1211    pub(crate) fn next_dequeued(&self, mask: KernelSigset) -> Option<(i32, SignalQueue)> {
1212        let synchronous = SYNCHRONOUS_SIGNALS
1213            .into_iter()
1214            .fold(0, |set, signal| set | kernel_sigset_bit(signal));
1215        let first = |queued: KernelSigset| {
1216            let deliverable = queued & !mask;
1217            let preferred = if deliverable & synchronous != 0 {
1218                deliverable & synchronous
1219            } else {
1220                deliverable
1221            };
1222            (preferred != 0).then(|| preferred.trailing_zeros() as i32 + 1)
1223        };
1224        first(self.thread_pending)
1225            .map(|signal| (signal, SignalQueue::Thread))
1226            .or_else(|| first(self.shared_pending).map(|signal| (signal, SignalQueue::Shared)))
1227    }
1228
1229    /// The signals pending on `queue`.
1230    pub(crate) fn queued(&self, queue: SignalQueue) -> KernelSigset {
1231        match queue {
1232            SignalQueue::Thread => self.thread_pending,
1233            SignalQueue::Shared => self.shared_pending,
1234        }
1235    }
1236}
1237
1238/// The signals whose default action ignores them.
1239pub(crate) fn default_ignored_signals() -> KernelSigset {
1240    [libc::SIGCHLD, libc::SIGCONT, libc::SIGURG, libc::SIGWINCH]
1241        .into_iter()
1242        .fold(0, |set, signal| set | kernel_sigset_bit(signal))
1243}
1244
1245/// The signals an instruction raises, which the kernel dequeues before any other
1246/// signal on the same queue (`SYNCHRONOUS_MASK` in `kernel/signal.c`).
1247const SYNCHRONOUS_SIGNALS: [i32; 6] = [
1248    libc::SIGSEGV,
1249    libc::SIGBUS,
1250    libc::SIGILL,
1251    libc::SIGTRAP,
1252    libc::SIGFPE,
1253    libc::SIGSYS,
1254];
1255
1256/// One of a thread's two queues of pending signals.
1257#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1258pub(crate) enum SignalQueue {
1259    /// The thread's private queue (`SigPnd`).
1260    Thread,
1261    /// The thread group's shared queue (`ShdPnd`).
1262    Shared,
1263}
1264
1265/// Set once per Hermit process, by [`refuse_host_signal_state_reads`], when
1266/// the backend's guest thread IDs do not name host tasks (KVM): a host
1267/// `/proc/<pid>/task/<tid>/status` would then describe an unrelated host task,
1268/// if any, chosen by host thread-ID allocation, so its signal state must never
1269/// decide a guest-visible outcome. Every read that exists today is gated off on
1270/// such a backend by a capability (`Config::models_signal_targets`,
1271/// `supports_blocked_wait_signal_interruption` or one of the four capabilities
1272/// split out of it, `reports_child_exit_publication`, a
1273/// nested PID namespace, or the guard in
1274/// `restartable_internal_io_interrupting_signals`); this latch is the backstop
1275/// for a future path that misses its gate.
1276#[cfg(not(test))]
1277static HOST_SIGNAL_STATE_READS_REFUSED: std::sync::atomic::AtomicBool =
1278    std::sync::atomic::AtomicBool::new(false);
1279
1280#[cfg(test)]
1281thread_local! {
1282    /// The latch for unit tests, per test thread, so that tests reading their
1283    /// own real `/proc` state run beside one that sets it.
1284    static HOST_SIGNAL_STATE_READS_REFUSED_FOR_TEST: std::cell::Cell<bool> =
1285        const { std::cell::Cell::new(false) };
1286}
1287
1288/// Whether a backend with `backend`'s capabilities forbids host `/proc` signal
1289/// state reads for its guest threads: exactly when its guest thread IDs do not
1290/// name host tasks (`procfs_inode::guest_tids_name_host_tasks`).
1291pub(crate) fn refuses_host_signal_state_reads(backend: &BackendCapabilities) -> bool {
1292    !crate::procfs_inode::guest_tids_name_host_tasks(backend, None)
1293}
1294
1295/// Latch the refusal for this process when `backend` forbids host `/proc`
1296/// signal state reads ([`refuses_host_signal_state_reads`]). Called once, when
1297/// Detcore's global state is created for a run.
1298pub(crate) fn refuse_host_signal_state_reads(backend: &BackendCapabilities) {
1299    if !refuses_host_signal_state_reads(backend) {
1300        return;
1301    }
1302    #[cfg(not(test))]
1303    HOST_SIGNAL_STATE_READS_REFUSED.store(true, std::sync::atomic::Ordering::Relaxed);
1304    #[cfg(test)]
1305    HOST_SIGNAL_STATE_READS_REFUSED_FOR_TEST.with(|refused| refused.set(true));
1306}
1307
1308/// Whether host `/proc` signal state reads are refused in this process
1309/// ([`refuse_host_signal_state_reads`]).
1310pub(crate) fn host_signal_state_reads_refused() -> bool {
1311    #[cfg(not(test))]
1312    {
1313        HOST_SIGNAL_STATE_READS_REFUSED.load(std::sync::atomic::Ordering::Relaxed)
1314    }
1315    #[cfg(test)]
1316    {
1317        HOST_SIGNAL_STATE_READS_REFUSED_FOR_TEST.with(|refused| refused.get())
1318    }
1319}
1320
1321/// Read `tid`'s signal state from the kernel.
1322///
1323/// On a backend whose guest thread IDs do not name host tasks
1324/// ([`host_signal_state_reads_refused`]) the read is refused with `ENOSYS`
1325/// before it reaches `/proc`: a caller that needs the state ends the run by
1326/// name (`BlockedWaitSignalError::StateUnreadable`), and one that treats an
1327/// unreadable state as unknown keeps its documented fallback. Either way no
1328/// unrelated host task's state reaches a decision.
1329pub(crate) fn read_kernel_signal_state(pid: Pid, tid: Pid) -> Result<KernelSignalState, Errno> {
1330    #[cfg(test)]
1331    if let Some(result) = signal_state_read_seam::scripted(pid, tid) {
1332        return result;
1333    }
1334    if host_signal_state_reads_refused() {
1335        static REPORTED: std::sync::Once = std::sync::Once::new();
1336        REPORTED.call_once(|| {
1337            error!(
1338                "refused a host /proc signal state read of guest thread {} (process {}): this \
1339                 backend's guest thread IDs do not name host tasks",
1340                tid.as_raw(),
1341                pid.as_raw()
1342            );
1343        });
1344        return Err(Errno::ENOSYS);
1345    }
1346    let status = status_files::read(pid, tid)?;
1347    KernelSignalState::parse(&status).ok_or(Errno::EIO)
1348}
1349
1350/// Drop thread `tid`'s open status files (`status_files`). Called when the
1351/// thread exits; a later read of a reused TID opens the file afresh.
1352pub(crate) fn forget_thread_status(tid: Pid) {
1353    status_files::forget(tid);
1354}
1355
1356/// Keep status files open from now on, in this process (`status_files`).
1357/// Only a process that never shares a guest's descriptor table may call this:
1358/// Hermit's own process, where Detcore traces the guest from outside. Detcore
1359/// linked into a guest (the in-guest runtimes) never calls it, so it reads the
1360/// path each time and no descriptor of its appears in the guest's table.
1361pub(crate) fn keep_status_files_open() {
1362    status_files::enable();
1363}
1364
1365/// `/proc/<pid>/task/<tid>/status`, kept open per traced thread, so that a
1366/// signal-state read is one pread(2) at offset 0 rather than an open(2),
1367/// read(2) and close(2). A gated wait reads its thread's state at every turn,
1368/// and every open(2) runs the host's file-open security hooks: measured on
1369/// 2026-10-24, Hermit spent 20.7% of its samples opening this file and 18.5% in
1370/// security_file_open alone, and on hosts where a BPF file-open hook takes its
1371/// slow path (a mount walk and a contended map update) the same opens made the
1372/// inherited-terminal poll tests of hermit-cli/tests/external_signal_interrupt.rs
1373/// spend six times their ordinary system CPU.
1374///
1375/// An open procfs task file stays bound to the task it was opened for: after
1376/// that thread exits it reads ESRCH, never a later task given the same TID. So
1377/// a failed read drops the entry and opens the path once more, which reads
1378/// exactly what reading the path did before this cache. At most `CAPACITY`
1379/// files are kept, so a guest with many threads cannot exhaust Hermit's own
1380/// descriptors; past that, reads open the path as before.
1381///
1382/// Files are kept only once `enable` has run in the process, which Hermit's
1383/// own binary does at startup ([`keep_status_files_open`]). Where Detcore runs
1384/// inside the guest (liteinst, in-guest-trap, DBT, SaBRe), a kept file would
1385/// be a descriptor in the guest's own table, visible to it and inherited by its
1386/// children; there nothing enables keeping, every read is the plain
1387/// `read_to_string` of the path, and the map is never created (a `BTreeMap`,
1388/// so no hasher state either).
1389mod status_files {
1390    use std::collections::BTreeMap;
1391    use std::fs::File;
1392    use std::os::unix::fs::FileExt;
1393    use std::sync::Arc;
1394    use std::sync::LazyLock;
1395    use std::sync::Mutex;
1396
1397    use reverie::Pid;
1398    use reverie::syscalls::Errno;
1399
1400    /// The most status files kept open at once.
1401    pub(super) const CAPACITY: usize = 256;
1402
1403    /// The kept files, by (pid, tid).
1404    type Kept = BTreeMap<(i32, i32), Arc<File>>;
1405
1406    static OPEN: LazyLock<Mutex<Kept>> = LazyLock::new(|| Mutex::new(BTreeMap::new()));
1407
1408    /// Whether this process keeps files (`enable`).
1409    #[cfg(not(test))]
1410    static KEEPS: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
1411
1412    #[cfg(test)]
1413    thread_local! {
1414        /// The switch for unit tests, per test thread, so that a test that
1415        /// keeps no files runs beside one that keeps them.
1416        static KEEPS_FOR_TEST: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
1417    }
1418
1419    /// Keep files from now on (`keep_status_files_open`).
1420    pub(super) fn enable() {
1421        #[cfg(not(test))]
1422        KEEPS.store(true, std::sync::atomic::Ordering::Relaxed);
1423        #[cfg(test)]
1424        KEEPS_FOR_TEST.with(|keeps| keeps.set(true));
1425    }
1426
1427    fn enabled() -> bool {
1428        #[cfg(not(test))]
1429        {
1430            KEEPS.load(std::sync::atomic::Ordering::Relaxed)
1431        }
1432        #[cfg(test)]
1433        {
1434            KEEPS_FOR_TEST.with(|keeps| keeps.get())
1435        }
1436    }
1437
1438    fn open() -> std::sync::MutexGuard<'static, Kept> {
1439        OPEN.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
1440    }
1441
1442    /// The whole file, read from offset 0. procfs regenerates a status file
1443    /// for every read that starts at offset 0.
1444    fn read_whole(file: &File) -> std::io::Result<String> {
1445        let mut bytes = Vec::with_capacity(4096);
1446        loop {
1447            let filled = bytes.len();
1448            bytes.resize(filled + 4096, 0);
1449            let read = file.read_at(&mut bytes[filled..], filled as u64)?;
1450            bytes.truncate(filled + read);
1451            if read == 0 {
1452                break;
1453            }
1454        }
1455        String::from_utf8(bytes)
1456            .map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error))
1457    }
1458
1459    /// Thread `tid`'s status file text. `ESRCH` when it cannot be read, as
1460    /// when the path could not be read before this cache.
1461    pub(super) fn read(pid: Pid, tid: Pid) -> Result<String, Errno> {
1462        if !enabled() {
1463            let path = format!("/proc/{}/task/{}/status", pid.as_raw(), tid.as_raw());
1464            return std::fs::read_to_string(path).map_err(|_| Errno::ESRCH);
1465        }
1466        let key = (pid.as_raw(), tid.as_raw());
1467        let kept = open().get(&key).cloned();
1468        if let Some(file) = kept {
1469            if let Ok(text) = read_whole(&file)
1470                && !text.is_empty()
1471            {
1472                return Ok(text);
1473            }
1474            open().remove(&key);
1475        }
1476        let path = format!("/proc/{}/task/{}/status", key.0, key.1);
1477        let file = File::open(path).map_err(|_| Errno::ESRCH)?;
1478        let text = read_whole(&file).map_err(|_| Errno::ESRCH)?;
1479        let mut files = open();
1480        if files.len() < CAPACITY {
1481            files.insert(key, Arc::new(file));
1482        }
1483        Ok(text)
1484    }
1485
1486    /// Drop every kept file of thread `tid`.
1487    pub(super) fn forget(tid: Pid) {
1488        if !enabled() {
1489            return;
1490        }
1491        open().retain(|&(_, kept_tid), _| kept_tid != tid.as_raw());
1492    }
1493
1494    /// How many files are kept open now.
1495    #[cfg(test)]
1496    pub(super) fn kept() -> usize {
1497        open().len()
1498    }
1499
1500    /// Whether a file of thread `tid` of process `pid` is kept open.
1501    #[cfg(test)]
1502    pub(super) fn keeps(pid: Pid, tid: Pid) -> bool {
1503        open().contains_key(&(pid.as_raw(), tid.as_raw()))
1504    }
1505
1506    /// The kept file of thread `tid` of process `pid`, if one is kept, for a
1507    /// test to read without the fallback to the path.
1508    #[cfg(test)]
1509    pub(super) fn kept_file(pid: Pid, tid: Pid) -> Option<Arc<File>> {
1510        open().get(&(pid.as_raw(), tid.as_raw())).cloned()
1511    }
1512
1513    /// `read_whole`, for tests.
1514    #[cfg(test)]
1515    pub(super) fn read_kept(file: &File) -> std::io::Result<String> {
1516        read_whole(file)
1517    }
1518}
1519
1520/// Why a blocked wait ended the run instead of returning to the guest
1521/// (https://github.com/rrnewton/hermit/issues/3146). A wait call cannot return
1522/// the underlying error natively, and resuming the guest after the second
1523/// failure would leave every blockable signal blocked in it.
1524#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1525pub(crate) enum BlockedWaitSignalError {
1526    /// The thread still exists, but its signal state could not be read from
1527    /// `/proc` (`errno` is `ESRCH` for an unreadable file, `EIO` for one that
1528    /// does not parse).
1529    StateUnreadable { pid: Pid, tid: Pid, errno: Errno },
1530    /// The guest's own signal mask could not be put back after the wait blocked
1531    /// every signal. `last_error` is the last injected call's error.
1532    MaskNotRestored {
1533        pid: Pid,
1534        tid: Pid,
1535        attempts: usize,
1536        last_error: Option<Errno>,
1537    },
1538}
1539
1540impl std::fmt::Display for BlockedWaitSignalError {
1541    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1542        match self {
1543            Self::StateUnreadable { pid, tid, errno } => write!(
1544                f,
1545                "cannot read the signal state of guest thread {tid} (process {pid}) from \
1546                 /proc while it waits: {errno}"
1547            ),
1548            Self::MaskNotRestored {
1549                pid,
1550                tid,
1551                attempts,
1552                last_error,
1553            } => {
1554                write!(
1555                    f,
1556                    "cannot restore the signal mask of guest thread {tid} (process {pid}) \
1557                     after a blocked wait ({attempts} attempts"
1558                )?;
1559                if let Some(errno) = last_error {
1560                    write!(f, ", last error {errno}")?;
1561                }
1562                f.write_str("); it would resume with every signal blocked")
1563            }
1564        }
1565    }
1566}
1567
1568impl std::error::Error for BlockedWaitSignalError {}
1569
1570/// Whether thread `tid` of process `pid` no longer exists.
1571pub(crate) fn guest_thread_is_gone(pid: Pid, tid: Pid) -> bool {
1572    // SAFETY: signal 0 only checks that the thread exists and may be signalled.
1573    let result = unsafe { libc::syscall(libc::SYS_tgkill, pid.as_raw(), tid.as_raw(), 0) };
1574    result == -1 && std::io::Error::last_os_error().raw_os_error() == Some(libc::ESRCH)
1575}
1576
1577/// Read `tid`'s signal state when `pid` may not be its thread group.
1578///
1579/// Reverie can report the creating process's pid for a process that a raw
1580/// `clone` without `CLONE_THREAD` created, so `/proc/<pid>/task/<tid>` does not
1581/// exist although the thread does. `/proc/<tid>/task/<tid>` names any live
1582/// thread, its own thread group's leader or not, so it is read before the
1583/// thread is taken to be gone (https://github.com/rrnewton/hermit/issues/3982).
1584pub(crate) fn read_thread_signal_state(pid: Pid, tid: Pid) -> Result<KernelSignalState, Errno> {
1585    read_kernel_signal_state(pid, tid).or_else(|error| {
1586        if pid == tid {
1587            Err(error)
1588        } else {
1589            read_kernel_signal_state(tid, tid)
1590        }
1591    })
1592}
1593
1594/// Whether thread `tid` no longer exists, when `pid` may not be its thread
1595/// group ([`read_thread_signal_state`]). `tkill` names the thread without a
1596/// thread group, so a live thread under a wrong `pid` is not taken to be gone.
1597pub(crate) fn thread_is_gone(pid: Pid, tid: Pid) -> bool {
1598    if !guest_thread_is_gone(pid, tid) {
1599        return false;
1600    }
1601    // SAFETY: signal 0 only checks that the thread exists and may be signalled.
1602    let result = unsafe { libc::syscall(libc::SYS_tkill, tid.as_raw(), 0) };
1603    result == -1 && std::io::Error::last_os_error().raw_os_error() == Some(libc::ESRCH)
1604}
1605
1606/// [`read_wait_signal_state`] for a thread whose reported `pid` may not be its
1607/// thread group ([`read_thread_signal_state`]).
1608pub(crate) fn read_thread_wait_signal_state(
1609    pid: Pid,
1610    tid: Pid,
1611) -> Result<KernelSignalState, Error> {
1612    match read_thread_signal_state(pid, tid) {
1613        Ok(state) => Ok(state),
1614        Err(_) if thread_is_gone(pid, tid) => Err(Errno::ERESTARTNOINTR.into()),
1615        Err(errno) => Err(Error::Tool(anyhow::Error::new(
1616            BlockedWaitSignalError::StateUnreadable { pid, tid, errno },
1617        ))),
1618    }
1619}
1620
1621/// Read the signal state of `tid`, which is stopped inside a blocked wait.
1622///
1623/// The read's own error never reaches the guest, because no wait call returns
1624/// `ESRCH` or `EIO` for it. If the thread no longer exists (it was killed while
1625/// it waited), the call ends with `ERESTARTNOINTR`, which no thread observes.
1626/// Otherwise the run ends with [`BlockedWaitSignalError::StateUnreadable`].
1627pub(crate) fn read_wait_signal_state(pid: Pid, tid: Pid) -> Result<KernelSignalState, Error> {
1628    match read_kernel_signal_state(pid, tid) {
1629        Ok(state) => Ok(state),
1630        Err(_) if guest_thread_is_gone(pid, tid) => Err(Errno::ERESTARTNOINTR.into()),
1631        Err(errno) => Err(Error::Tool(anyhow::Error::new(
1632            BlockedWaitSignalError::StateUnreadable { pid, tid, errno },
1633        ))),
1634    }
1635}
1636
1637/// A test seam in front of the `/proc` read in [`read_kernel_signal_state`].
1638#[cfg(test)]
1639pub(crate) mod signal_state_read_seam {
1640    use std::cell::RefCell;
1641
1642    use reverie::Pid;
1643    use reverie::syscalls::Errno;
1644
1645    use super::KernelSignalState;
1646
1647    type Script = Box<dyn FnMut(Pid, Pid) -> Option<Result<KernelSignalState, Errno>>>;
1648
1649    thread_local! {
1650        static SCRIPT: RefCell<Option<Script>> = const { RefCell::new(None) };
1651    }
1652
1653    /// Removes the installed script when dropped.
1654    pub(crate) struct Installed(());
1655
1656    impl Drop for Installed {
1657        fn drop(&mut self) {
1658            SCRIPT.with(|script| script.borrow_mut().take());
1659        }
1660    }
1661
1662    /// Answer this thread's reads with `script`; `None` falls through to `/proc`.
1663    pub(crate) fn install(
1664        script: impl FnMut(Pid, Pid) -> Option<Result<KernelSignalState, Errno>> + 'static,
1665    ) -> Installed {
1666        SCRIPT.with(|slot| *slot.borrow_mut() = Some(Box::new(script)));
1667        Installed(())
1668    }
1669
1670    pub(super) fn scripted(pid: Pid, tid: Pid) -> Option<Result<KernelSignalState, Errno>> {
1671        SCRIPT.with(|slot| {
1672            slot.borrow_mut()
1673                .as_mut()
1674                .and_then(|script| script(pid, tid))
1675        })
1676    }
1677}
1678
1679pub(super) fn blocked_signal_mask() -> KernelSigset {
1680    // Preserve libc's definition of the blockable set (notably its reserved
1681    // NPTL signals) while converting the result to the kernel's one-word ABI.
1682    let mut libc_mask: libc::sigset_t = unsafe { std::mem::zeroed() };
1683    unsafe {
1684        libc::sigfillset(&mut libc_mask);
1685        libc::sigdelset(&mut libc_mask, reverie::PERF_EVENT_SIGNAL as i32);
1686    }
1687    (1..=KernelSigset::BITS as i32).fold(0, |mask, raw_signal| {
1688        if unsafe { libc::sigismember(&libc_mask, raw_signal) } == 1 {
1689            mask | (1_u64 << (raw_signal as u32 - 1))
1690        } else {
1691            mask
1692        }
1693    })
1694}
1695
1696/// Whether a managed child wait reads the guest's mask without replacing it
1697/// (`block_signals_for_disposition`), and so restores nothing after:
1698/// - on a backend whose application signals the runtime already holds while
1699///   the callback runs (DBT, `requires_thread_directed_process_signals`);
1700/// - under the in-guest delivery gate (A5 step 3c.1e B,
1701///   <https://github.com/rrnewton/hermit/issues/4485>): the in-guest runtime
1702///   runs every callback under its own mask, which blocks every blockable
1703///   guest signal, so the guest's are held already; the query is answered from
1704///   the guest's saved mask (`Guest::resume_signal_mask`'s record), and a
1705///   block would make the mask the request publishes, and the guest resumes
1706///   under, "every signal blocked" until the restore.
1707fn reads_mask_in_place(config: &Config) -> bool {
1708    config.backend.requires_thread_directed_process_signals || config.in_guest_signal_delivery
1709}
1710
1711pub(super) async fn block_signals_for_disposition<G, T>(
1712    guest: &mut G,
1713    blocked_mask_addr: Addr<'_, KernelSigset>,
1714    old_mask_addr: AddrMut<'_, KernelSigset>,
1715) -> Result<KernelSigset, Error>
1716where
1717    G: Guest<Detcore<T>>,
1718    T: RecordOrReplay,
1719{
1720    let block_signals = syscalls::RtSigprocmask::new()
1721        .with_how(libc::SIG_SETMASK)
1722        .with_set((!reads_mask_in_place(guest.config())).then_some(blocked_mask_addr.cast()))
1723        .with_oldset(Some(old_mask_addr.cast()))
1724        .with_sigsetsize(KERNEL_SIGSET_SIZE);
1725    guest.inject_with_retry(block_signals).await?;
1726    let guest_mask: KernelSigset = guest.memory().read_value(old_mask_addr)?;
1727    if guest.config().backend.reports_child_exit_publication
1728        || crate::scheduler::discarded_at_send::records(guest.config())
1729    {
1730        // While the private mask is installed, the kernel reports Detcore's
1731        // mask for this thread; the scheduler's child-exit classification and
1732        // its record of copies Linux discarded at send must read the guest's
1733        // own (the Claude re-check of the R2b-1 design, R4-3).
1734        crate::tool_global::set_saved_guest_sigmask(guest, Some(guest_mask)).await;
1735    }
1736    Ok(guest_mask)
1737}
1738
1739pub(super) async fn restore_signals_after_disposition<G, T>(
1740    guest: &mut G,
1741    old_mask_addr: AddrMut<'_, KernelSigset>,
1742) -> Result<(), Error>
1743where
1744    G: Guest<Detcore<T>>,
1745    T: RecordOrReplay,
1746{
1747    if !reads_mask_in_place(guest.config()) {
1748        let old_mask: Addr<'_, KernelSigset> = old_mask_addr.into();
1749        let restore_signals = syscalls::RtSigprocmask::new()
1750            .with_how(libc::SIG_SETMASK)
1751            .with_set(Some(old_mask.cast()))
1752            .with_oldset(None)
1753            .with_sigsetsize(KERNEL_SIGSET_SIZE);
1754        guest.inject_with_retry(restore_signals).await?;
1755    }
1756    if guest.config().backend.reports_child_exit_publication
1757        || crate::scheduler::discarded_at_send::records(guest.config())
1758    {
1759        crate::tool_global::set_saved_guest_sigmask(guest, None).await;
1760    }
1761    Ok(())
1762}
1763
1764async fn interrupted_child_wait_result<G, T, S>(
1765    guest: &mut G,
1766    call: S,
1767    disposition: WaitSignalDisposition,
1768) -> Result<i64, Error>
1769where
1770    G: Guest<Detcore<T>>,
1771    T: RecordOrReplay,
1772    S: SyscallInfo,
1773{
1774    if !guest
1775        .config()
1776        .backend
1777        .requires_thread_directed_process_signals
1778    {
1779        return Err(Errno::ERESTARTSYS.into());
1780    }
1781    if disposition == WaitSignalDisposition::Interrupt {
1782        return Err(Errno::EINTR.into());
1783    }
1784
1785    guest.tail_inject(call).await
1786}
1787
1788fn snapshot_process_group(pid: Pid) -> Result<libc::pid_t, Errno> {
1789    let pgrp = Process::new(pid.as_raw())
1790        .and_then(|process| process.stat())
1791        .map(|stat| stat.pgrp)
1792        .map_err(|_| Errno::ESRCH)?;
1793    if pgrp == 0 {
1794        Err(Errno::EOPNOTSUPP)
1795    } else {
1796        Ok(pgrp)
1797    }
1798}
1799
1800fn guest_fd_status_flags(pid: Pid, fd: libc::c_int) -> Result<libc::c_int, Errno> {
1801    let path = format!("/proc/{}/fdinfo/{}", pid.as_raw(), fd);
1802    let contents = std::fs::read_to_string(path).map_err(|_| Errno::EBADF)?;
1803    let flags = contents
1804        .lines()
1805        .find_map(|line| line.strip_prefix("flags:"))
1806        .map(str::trim)
1807        .ok_or(Errno::EINVAL)?;
1808    libc::c_int::from_str_radix(flags, 8).map_err(|_| Errno::EINVAL)
1809}
1810
1811#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1812enum FutexTimeout {
1813    Relative(u64),
1814    Absolute(LogicalTime),
1815}
1816
1817/// Linux's `timespec64_valid`, which `futex_init_timeout` applies: a negative
1818/// second count or a nanosecond count outside `0..1e9` is EINVAL. Any other
1819/// value is valid, however large.
1820fn futex_timespec_is_valid(timeout: Timespec) -> bool {
1821    timeout.tv_sec >= 0 && (0..1_000_000_000).contains(&timeout.tv_nsec)
1822}
1823
1824/// The largest futex timeout in nanoseconds: `timespec64_to_ktime` clamps a
1825/// valid timespec beyond it to `KTIME_MAX` rather than refusing it.
1826const FUTEX_KTIME_MAX_NANOS: u64 = i64::MAX as u64;
1827
1828fn parse_futex_timeout(futex_op: i32, timeout: Timespec) -> Result<FutexTimeout, Errno> {
1829    if !futex_timespec_is_valid(timeout) {
1830        return Err(Errno::EINVAL);
1831    }
1832    let seconds = timeout.tv_sec as u64;
1833    let nanoseconds = timeout.tv_nsec as u64;
1834    // `timespec64_to_ktime`: KTIME_MAX from KTIME_SEC_MAX seconds on.
1835    let timeout_nanos = if seconds >= FUTEX_KTIME_MAX_NANOS / 1_000_000_000 {
1836        FUTEX_KTIME_MAX_NANOS
1837    } else {
1838        seconds * 1_000_000_000 + nanoseconds
1839    };
1840    // Mask off FUTEX_PRIVATE_FLAG / FUTEX_CLOCK_REALTIME before matching the
1841    // command: FUTEX_WAIT_BITSET measures its timeout as an *absolute* deadline,
1842    // whereas plain FUTEX_WAIT uses a *relative* one. A private-flagged
1843    // FUTEX_WAIT_BITSET (e.g. 0x89) must still be recognized as the BITSET
1844    // command; comparing the raw op would misclassify it as relative and add
1845    // the absolute deadline to the current time (leaking the epoch).
1846    //
1847    // FUTEX_LOCK_PI and FUTEX_LOCK_PI2 take absolute deadlines too
1848    // (`futex_init_timeout`, kernel/futex/syscalls.c:147-157 at v6.16).
1849    if matches!(
1850        futex_op & libc::FUTEX_CMD_MASK,
1851        libc::FUTEX_WAIT_BITSET | libc::FUTEX_LOCK_PI | libc::FUTEX_LOCK_PI2
1852    ) {
1853        Ok(FutexTimeout::Absolute(LogicalTime::from_nanos(
1854            timeout_nanos,
1855        )))
1856    } else {
1857        Ok(FutexTimeout::Relative(timeout_nanos))
1858    }
1859}
1860
1861/// The clock an absolute futex deadline is measured on: CLOCK_REALTIME with
1862/// FUTEX_CLOCK_REALTIME, and always for FUTEX_LOCK_PI, which `do_futex` gives
1863/// `FLAGS_CLOCKRT` (kernel/futex/syscalls.c:114-116 at v6.16); otherwise
1864/// CLOCK_MONOTONIC.
1865fn futex_deadline_clock(futex_op: i32) -> syscalls::ClockId {
1866    if futex_op & libc::FUTEX_CLOCK_REALTIME != 0
1867        || futex_op & libc::FUTEX_CMD_MASK == libc::FUTEX_LOCK_PI
1868    {
1869        syscalls::ClockId::CLOCK_REALTIME
1870    } else {
1871        syscalls::ClockId::CLOCK_MONOTONIC
1872    }
1873}
1874
1875fn rebase_absolute_timeout(
1876    deadline: LogicalTime,
1877    clock_now: LogicalTime,
1878    logical_now: LogicalTime,
1879) -> LogicalTime {
1880    logical_now + Duration::from_nanos(deadline.as_nanos().saturating_sub(clock_now.as_nanos()))
1881}
1882
1883fn absolute_timeout_uses_host_clock(
1884    deadline: LogicalTime,
1885    host_clock_now: LogicalTime,
1886    logical_now: LogicalTime,
1887) -> bool {
1888    deadline.as_nanos().abs_diff(host_clock_now.as_nanos())
1889        < deadline.as_nanos().abs_diff(logical_now.as_nanos())
1890}
1891
1892/// Where a futex wait takes its deadline from.
1893#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1894pub(crate) enum FutexDeadline {
1895    /// The call's own timeout.
1896    FromCall,
1897    /// The deadline an interrupted timed futex wait kept for the kernel's restart
1898    /// (`RestartBlock`).
1899    Restart(Option<LogicalTime>),
1900}
1901
1902/// How a sequentialized `sched_yield` yields to the scheduler
1903/// ([`sched_yield_route`]).
1904#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1905enum SchedYieldRoute {
1906    /// A recorded chaos priority change point with a fresh random priority.
1907    ChaosReprioritize,
1908    /// A one-turn `SchedYield`, with the timeslice statistics reset.
1909    ReplayStrongYield,
1910    /// A `SchedYield` that the scheduler turns into a yield epoch.
1911    ChaosYieldEpoch,
1912    /// A `SleepUntil(0)` yield, granted at once.
1913    WeakYield,
1914    /// The end of the caller's timeslice, as at a timer expiry.
1915    EndTimeslice,
1916}
1917
1918/// Choose how a sequentialized `sched_yield` yields; it reads only the
1919/// configuration, so a record and its replay choose alike.
1920///
1921/// - In chaos mode, thread-interleaving diversity (and thus fairness) comes
1922///   from re-randomizing thread priorities at preemption-timer expirations.
1923///   When timer preemption is disabled (`--max-timeslice disabled`),
1924///   priorities are fixed at thread creation and never change, so a thread
1925///   that spins on sched_yield while holding the numerically-lowest priority
1926///   would starve every thread it waits on (GH #81). There sched_yield is an
1927///   explicit chaos reprioritization point, recorded for chaos replay.
1928/// - With the timer on, chaos still redraws a priority only when a slice
1929///   expires, which a sched_yield loop may never reach
1930///   (https://github.com/rrnewton/hermit/issues/4068). There sched_yield is a
1931///   `SchedYield`, which the scheduler turns into a yield epoch
1932///   (`Scheduler::requeue_after`): the caller keeps its band, and once it has
1933///   passed another runnable thread over more than
1934///   `PLAIN_YIELDS_BEFORE_EPOCH` times it is not selected again until a thread
1935///   that did not yield has had a turn. It changes no
1936///   priority, draws nothing from the chaos PRNG and consumes no recorded
1937///   preemption point, so a preemption replay under `--chaos` makes the same
1938///   decision. A schedule replay keeps the weak yield it always had, and so
1939///   does a backend that does not deliver the preemption timer
1940///   (`BackendCapabilities::delivers_preemption_timer_events`): there a thread handed the
1941///   turn that never makes a syscall would never give it back.
1942/// - `--sched-heuristic=random` also keeps the weak yield. It draws a uniform
1943///   pick among the queued threads at every turn, so a yielder cannot hold
1944///   the CPU from the thread it waits on, and an epoch would only force the
1945///   yielders into near-equal shares: on the fp-reduction chaos cell
1946///   (https://github.com/rrnewton/hermit/issues/4150) it cut the outcomes
1947///   over 32 seeds to fewer than the 7 of 8 classes the cell requires.
1948///   `StickyRandom` keeps its pick across turns, so it keeps the epoch.
1949fn sched_yield_route(cfg: &Config) -> SchedYieldRoute {
1950    if cfg.chaos && cfg.max_timeslice.is_none() {
1951        SchedYieldRoute::ChaosReprioritize
1952    } else if !cfg.chaos && cfg.replay_preemptions_from.is_some() {
1953        SchedYieldRoute::ReplayStrongYield
1954    } else if cfg.chaos
1955        && cfg.replay_schedule_from.is_none()
1956        && cfg.backend.delivers_preemption_timer_events
1957        && cfg.sched_heuristic != SchedHeuristic::Random
1958    {
1959        SchedYieldRoute::ChaosYieldEpoch
1960    } else if cfg.chaos || cfg.replay_schedule_from.is_some() {
1961        SchedYieldRoute::WeakYield
1962    } else {
1963        SchedYieldRoute::EndTimeslice
1964    }
1965}
1966
1967impl<T: RecordOrReplay> Detcore<T> {
1968    async fn futex_timeout_deadline<G: Guest<Self>>(
1969        &self,
1970        guest: &mut G,
1971        futex_flags: i32,
1972        timeout: Option<Addr<'_, Timespec>>,
1973    ) -> Result<Option<LogicalTime>, Error> {
1974        let Some(timeout) = timeout else {
1975            return Ok(None);
1976        };
1977        let timeout = parse_futex_timeout(futex_flags, guest.memory().read_value(timeout)?)?;
1978        match timeout {
1979            FutexTimeout::Relative(nanos) => {
1980                let now = thread_observe_time(guest).await;
1981                Ok(Some(now + Duration::from_nanos(nanos)))
1982            }
1983            FutexTimeout::Absolute(deadline)
1984                if self.cfg.virtualize_time
1985                    && !self.cfg.backend.guest_clock_reads_bypass_backend =>
1986            {
1987                Ok(Some(deadline))
1988            }
1989            FutexTimeout::Absolute(deadline) => {
1990                let clockid = futex_deadline_clock(futex_flags);
1991
1992                let mut stack = guest.stack().await;
1993                let clock_output = syscalls::TimespecMutPtr(stack.reserve());
1994                let _stack_guard = stack.commit()?;
1995                let clock_call = syscalls::ClockGettime::new()
1996                    .with_clockid(clockid)
1997                    .with_tp(Some(clock_output));
1998                if self.cfg.virtualize_time && self.cfg.backend.guest_clock_reads_bypass_backend {
1999                    // Read the same live host clock as a direct guest vDSO call. Replaying a
2000                    // recorded value here would compare this run's host-domain deadline with the
2001                    // previous run's clock and turn a short timeout into an arbitrary long one.
2002                    guest.inject(Syscall::from(clock_call)).await?;
2003                } else {
2004                    self.record_or_replay(guest, clock_call).await?;
2005                }
2006                let clock_now = match parse_futex_timeout(
2007                    libc::FUTEX_WAIT_BITSET,
2008                    guest.memory().read_value(clock_output.0)?,
2009                )? {
2010                    FutexTimeout::Absolute(time) => time,
2011                    FutexTimeout::Relative(_) => unreachable!(),
2012                };
2013                let logical_now = thread_observe_time(guest).await;
2014                if self.cfg.virtualize_time
2015                    && !absolute_timeout_uses_host_clock(deadline, clock_now, logical_now)
2016                {
2017                    return Ok(Some(deadline));
2018                }
2019                Ok(Some(rebase_absolute_timeout(
2020                    deadline,
2021                    clock_now,
2022                    logical_now,
2023                )))
2024            }
2025        }
2026    }
2027
2028    /// The deadline of a futex wait, from the call's timeout or from the record an
2029    /// interrupted wait kept for its restart (`FutexDeadline`).
2030    async fn futex_deadline<G: Guest<Self>>(
2031        &self,
2032        guest: &mut G,
2033        call: syscalls::Futex,
2034        deadline: FutexDeadline,
2035    ) -> Result<Option<LogicalTime>, Error> {
2036        match deadline {
2037            FutexDeadline::FromCall => {
2038                self.futex_timeout_deadline(guest, call.futex_op(), call.timeout())
2039                    .await
2040            }
2041            FutexDeadline::Restart(deadline) => Ok(deadline),
2042        }
2043    }
2044
2045    /// Promotes the open file descriptions whose model must stay one copy
2046    /// (random devices and timerfds, `ThreadState::promote_shared_open_files`)
2047    /// before this handler's native fork, process-copying clone or exec, on a
2048    /// backend whose tool runs inside the guest and whose runtime routes those
2049    /// operations through this handler (in-guest LiteInst). DBT runs them
2050    /// natively and its runtime promotes there; ptrace and KVM keep the one
2051    /// copy outside the guest. It runs inside the caller's turn, before the
2052    /// operation can expose a child or a new image
2053    /// (<https://github.com/rrnewton/hermit/issues/4381>).
2054    fn promote_before_native_lifecycle<G: Guest<Self>>(&self, guest: &G) {
2055        let backend = &guest.config().backend;
2056        if self.cfg.sequentialize_threads
2057            && backend.promotes_open_files_before_native_lifecycle
2058            && !crate::syscalls::timerfd::detcore_runs_outside_the_guest(backend)
2059        {
2060            guest.thread_state().promote_shared_open_files().expect(
2061                "could not publish shared open file descriptions before a native fork or exec",
2062            );
2063        }
2064    }
2065
2066    /// Before a clone that makes a process, where Detcore runs inside each
2067    /// guest process: the record sequences of per-process files move to
2068    /// Detcore's global state (`ThreadState::share_procfs_sequences`), so the
2069    /// copies of the tool's state the fork makes share them, as Linux's
2070    /// processes share the one `struct file` and `seq_file`. In-guest
2071    /// LiteInst does so as it promotes (`promote_before_native_lifecycle`),
2072    /// and DBT's runtime before its native fork; this covers SaBRe, whose
2073    /// runtime promotes nothing. Only with sequentialized threads, whose turns
2074    /// order every process's operations on the sequence.
2075    fn share_procfs_sequences_before_copy<G: Guest<Self>>(&self, guest: &G) {
2076        if self.cfg.sequentialize_threads
2077            && !crate::syscalls::timerfd::detcore_runs_outside_the_guest(&guest.config().backend)
2078        {
2079            guest.thread_state().share_procfs_sequences();
2080        }
2081    }
2082
2083    /// Before a clone that makes a user namespace: Linux makes the caller's
2084    /// effective user ID the new namespace's owner (`create_user_ns`,
2085    /// kernel/user_namespace.c:82-136 in 6.17), and Detcore's record of user
2086    /// namespaces (`scheduler::user_namespaces`) takes that owner to be the
2087    /// real user ID every guest task has. It is, unless the caller runs, or
2088    /// was forked from, an image of a set-user-ID file: Detcore makes the
2089    /// credential-setting calls no-ops, so an exec is the one way a guest
2090    /// task's effective ID comes to differ from its real one, which never
2091    /// changes. The caller's IDs as the kernel beneath holds them (injected
2092    /// `getuid` and `geteuid`, read before the clone, as `create_user_ns`
2093    /// reads them) decide: true for a caller whose two differ, or that reads
2094    /// either as the overflow ID 65534 (two such IDs may still differ), or
2095    /// whose IDs cannot be read. Only once the clone has made the namespace is
2096    /// that noted, for that namespace alone
2097    /// (`UserNamespaces::note_owner_may_differ`), by the turn that registers
2098    /// the new process: a clone that fails (EINVAL for `CLONE_NEWUSER` with
2099    /// `CLONE_FS`; EPERM for a creator whose IDs its namespace does not map)
2100    /// makes no namespace and notes nothing.
2101    async fn user_namespace_owner_may_differ<G: Guest<Self>>(&self, guest: &mut G) -> bool {
2102        /// `overflowuid`'s default, which an ID the caller's user namespace
2103        /// does not map reads as.
2104        const OVERFLOW_ID: i64 = 65534;
2105        let uid = guest.inject(syscalls::Getuid::new()).await;
2106        let euid = guest.inject(syscalls::Geteuid::new()).await;
2107        !matches!((uid, euid), (Ok(uid), Ok(euid)) if uid == euid && uid != OVERFLOW_ID)
2108    }
2109
2110    /// Clone, clone3, fork, vfork system calls
2111    pub async fn handle_clone_family<G: Guest<Self>>(
2112        &self,
2113        guest: &mut G,
2114        clone_family: syscalls::family::CloneFamily,
2115    ) -> Result<i64, Error> {
2116        if let syscalls::family::CloneFamily::Clone3(clone) = &clone_family
2117            && let Some(refusal) = self.clone3_set_tid_refusal(guest, clone).await
2118        {
2119            return refusal;
2120        }
2121        // Linux's EINVAL for the flags, on every backend, so that a backend
2122        // is asked only for a clone Linux would perform and declines only
2123        // one it cannot (`UnsupportedOperation::Clone`). A clone3's stack is
2124        // checked against the user address limit the backend reports, the
2125        // one its kernel's `access_ok` enforces (`clone3_args_einval`).
2126        let user_limit = matches!(clone_family, syscalls::family::CloneFamily::Clone3(_))
2127            .then(|| guest.user_address_limit().ok().map(|limit| limit.max_end))
2128            .flatten();
2129        if linux_refuses_clone_flags(&guest.memory(), &clone_family, user_limit) {
2130            return Err(Errno::EINVAL.into());
2131        }
2132        // clone3's `struct clone_args` is read field by field
2133        // (`clone3_field`): Reverie's whole-struct read fails for a valid
2134        // 64-byte one at the end of a mapping, which lost its flags, exit
2135        // signal and child tid address.
2136        let (flags, exit_signal, child_tid) = match &clone_family {
2137            syscalls::family::CloneFamily::Clone3(clone) => {
2138                let memory = guest.memory();
2139                (
2140                    clone3_field(&memory, clone, 0)
2141                        .map_or_else(CloneFlags::empty, CloneFlags::from_bits_truncate),
2142                    clone3_field(&memory, clone, 32).map_or(0, |signal| signal as libc::c_int),
2143                    clone3_field(&memory, clone, 16).map_or(0, |address| address as usize),
2144                )
2145            }
2146            syscalls::family::CloneFamily::Clone(clone) => (
2147                clone_family.flags(&guest.memory()),
2148                (clone.flags().bits() & 0xff) as libc::c_int,
2149                clone_family.child_tid(&guest.memory()),
2150            ),
2151            #[cfg(not(target_arch = "aarch64"))]
2152            _ => (
2153                clone_family.flags(&guest.memory()),
2154                libc::SIGCHLD,
2155                clone_family.child_tid(&guest.memory()),
2156            ),
2157        };
2158        let ctid = child_tid_clear_address(flags, child_tid);
2159        // Without sequentialized threads a sibling can run while this call
2160        // does, and rewrite the clone's arguments after they are read here or
2161        // the pidfd word before it is read back, so no pidfd is tracked then.
2162        let pidfd_address = guest
2163            .config()
2164            .sequentialize_threads
2165            .then(|| clone_pidfd_address(&clone_family, &guest.memory(), flags))
2166            .flatten();
2167        // Only a CLONE_VFORK parent waits for its child. Every backend runs a
2168        // forked process child concurrently with its parent and dispatches a
2169        // cloned thread's syscalls through this tool.
2170        let is_vfork = flags.contains(CloneFlags::CLONE_VFORK);
2171
2172        // D41 with IR-5 (A5 step 3c.1c): the first statement of the vfork
2173        // branch, before `clone_flags` and `pending_vfork` are set, the
2174        // descriptors promoted or the signal-state copy cloned, so a vfork not
2175        // run leaves none of them for the death path's exit lifecycle to read.
2176        if is_vfork && let Some(refused) = super::helpers::kernel_run_entry_refusal(guest)? {
2177            return Err(refused);
2178        }
2179
2180        // A clone that makes a user namespace makes it the caller's effective
2181        // user ID's (`user_namespace_owner_may_differ`): read now, noted once
2182        // the clone has made the namespace. Only where Detcore renders procfs
2183        // files, whose access checks read the record of user namespaces: not
2184        // under record and replay.
2185        let user_namespace_owner_may_differ = flags.contains(CloneFlags::CLONE_NEWUSER)
2186            && !flags.contains(CloneFlags::CLONE_THREAD)
2187            && !self.cfg.recordreplay_modes
2188            && self.user_namespace_owner_may_differ(guest).await;
2189
2190        // The child's ID, when Detcore chooses the IDs: taken here, in this
2191        // turn, before the vfork parent below gives the turn up.
2192        let clone_plan = self
2193            .plan_clone_id(guest, &Syscall::from(clone_family))
2194            .await?;
2195
2196        let ts = guest.thread_state_mut();
2197        assert_eq!(ts.clone_flags, None);
2198        assert!(ts.pending_vfork.is_none());
2199        ts.clone_flags = Some(flags);
2200
2201        let parent_dettid = ts.dettid;
2202        let child_priority_entropy = if is_vfork
2203            && self.cfg.chaos
2204            && self.cfg.replay_preemptions_from.is_none()
2205            && self.cfg.replay_schedule_from.is_none()
2206        {
2207            let mut parent_chaos_prng = ts.chaos_prng.clone();
2208            Some(parent_chaos_prng.next_u64())
2209        } else {
2210            None
2211        };
2212        let blocking_child_op_id = ExternalOpId::new(parent_dettid, ts.stats.syscall_count);
2213        if is_vfork {
2214            ts.pending_vfork = Some(PendingVfork {
2215                parent_dettid,
2216                op: blocking_child_op_id,
2217                parent_detpid: ts.detpid.expect("detpid unset"),
2218                child_tid_addr: ctid,
2219                flags,
2220                exit_signal,
2221                child_priority_entropy,
2222                user_namespace_owner_may_differ,
2223            });
2224        }
2225
2226        // A child's exit signal other than SIGCHLD reaches this process when
2227        // the child exits, at a moment set by host timing. Record it in this
2228        // turn, before the child exists, for every thread of the caller's
2229        // process, and never forget it (`Scheduler::record_host_timed_signals`).
2230        let exit_signals = clone_host_timed_signals(flags, exit_signal);
2231        if exit_signals != 0 && guest.config().backend.host_timed_signals_are_physical {
2232            record_host_timed_signals(guest, HostTimedSignalScope::Caller, exit_signals).await;
2233        }
2234
2235        if !flags.contains(CloneFlags::CLONE_THREAD) {
2236            self.promote_before_native_lifecycle(guest);
2237            self.share_procfs_sequences_before_copy(guest);
2238        }
2239
2240        // Actions shared with a new process are withdrawn from the
2241        // scheduler before the child exists.
2242        super::signal_shadow::before_clone(guest, flags.bits()).await;
2243
2244        trace!("[detcore, dtid {}] parent invoking clone.", parent_dettid);
2245
2246        // A CLONE_VFORK parent cannot resume until the child execs or exits.
2247        // Relinquish the parent's scheduler turn before entering that
2248        // blocking operation.
2249        if is_vfork && self.cfg.sequentialize_threads {
2250            let mut resources = Resources::new(parent_dettid);
2251            resources.insert(
2252                ResourceID::BlockingVfork(blocking_child_op_id),
2253                Permission::RW,
2254            );
2255            resources.fyi("clone_vfork");
2256            resource_request(guest, resources).await;
2257        }
2258
2259        let call = Syscall::from(clone_family);
2260        let sysno = call.number();
2261        // A plan that leaves the kernel to number the child (or the guest
2262        // to choose) issues the call as main does, with `CLONE_UNTRACED`
2263        // cleared for a tracing backend; a chosen-ID plan issues it through
2264        // `inject_clone_planned`, whose staged copy clears the flag itself.
2265        let (maybe_res, untraced_refusal) = match clone_plan {
2266            super::chosen_ids::ClonePlan::Kernel | super::chosen_ids::ClonePlan::GuestChosen => {
2267                match inject_traced_clone(guest, clone_family, flags).await {
2268                    CloneInjection::Ran(result) => (result, None),
2269                    // No call ran. ENOMEM is what the guest gets with
2270                    // --allow-unsupported-syscalls: a clone Linux could not complete.
2271                    CloneInjection::Refused(reason) => (Err(Errno::ENOMEM), Some(reason)),
2272                }
2273            }
2274            _ => (
2275                self.inject_clone_planned(guest, call, clone_plan, is_vfork)
2276                    .await,
2277                None,
2278            ),
2279        };
2280        // Ask before anything else is injected: the next injection replaces
2281        // the backend's record.
2282        let refusal = if maybe_res.is_err() && untraced_refusal.is_none() {
2283            guest.take_unsupported_refusal()
2284        } else {
2285            None
2286        };
2287
2288        if is_vfork && self.cfg.sequentialize_threads {
2289            let mut resources = Resources::new(parent_dettid);
2290            if maybe_res.is_err() {
2291                // TODO-HUMAN-REVIEW(PR-1152): Review failed deferred-vfork cancellation.
2292                // The scheduler cannot infer failure from the absence of a registered child: a
2293                // successful child may also register after this continuation. Report the known
2294                // injected-syscall outcome explicitly so the scheduler can cancel the barrier and
2295                // re-admit this parent before we propagate the original error below.
2296                resources.insert(
2297                    ResourceID::VforkFailed(blocking_child_op_id),
2298                    Permission::RW,
2299                );
2300                resources.fyi("clone_vfork_failed");
2301            } else {
2302                resources.insert(
2303                    ResourceID::BlockedExternalContinue(blocking_child_op_id),
2304                    Permission::RW,
2305                );
2306                resources.fyi("clone_vfork");
2307            }
2308            resource_request(guest, resources).await;
2309        }
2310
2311        let ts = guest.thread_state_mut();
2312        ts.clone_flags = None; // Unset, now that it has been read by the child.
2313        ts.pending_vfork = None;
2314
2315        // A backend that cannot create the child says so with a typed refusal
2316        // (an in-guest backend runs one thread of the guest; KVM cannot give a
2317        // child some of the sharing Linux allows). Answering the guest with
2318        // the refusal's errno would be an error Linux never gives a valid
2319        // clone, so the run is refused instead, as for an exec the backend
2320        // cannot perform, and named on stderr when the run ends
2321        // (`report_refusal_reason`).
2322        let source = if refusal.is_some() {
2323            "refused by the backend"
2324        } else {
2325            "refused"
2326        };
2327        let diagnostic = refusal
2328            .map(|refusal| refusal.diagnostic.to_string())
2329            .or(untraced_refusal);
2330        if let (Some(diagnostic), Err(errno)) = (diagnostic, &maybe_res) {
2331            let named = CloneFlags::from_bits_retain(flags.bits() & !0xff);
2332            crate::syscalls::helpers::report_refusal_reason(
2333                self.cfg.panic_on_unsupported_syscalls,
2334                &format!(
2335                    "[dtid {parent_dettid}] {sysno} {source}: {diagnostic} (flags {named:?}, \
2336                     exit signal {exit_signal}). No child was created."
2337                ),
2338            );
2339            if !self.cfg.panic_on_unsupported_syscalls {
2340                crate::tool_global::report_unsupported_syscall(guest, sysno).await;
2341            }
2342            return self
2343                .refuse_unserviceable_operation_because(guest, sysno, *errno, &diagnostic)
2344                .await;
2345        }
2346
2347        let res = maybe_res?;
2348
2349        if !flags.contains(CloneFlags::CLONE_THREAD) {
2350            // Only a successful process clone can let another process mutate
2351            // inherited open file descriptions. Failed clone-family calls leave
2352            // the previously observed flock state authoritative.
2353            guest.thread_state().forget_flock_modes();
2354        }
2355
2356        // Match ordinary clone: the parent consumes the priority entropy after
2357        // the child has inherited the parent state.
2358        if is_vfork
2359            && self.cfg.chaos
2360            && self.cfg.replay_preemptions_from.is_none()
2361            && self.cfg.replay_schedule_from.is_none()
2362        {
2363            let _ = guest
2364                .thread_state_mut()
2365                .chaos_prng_next_u64("child_priority");
2366        }
2367
2368        let child_tid = Pid::from_raw(res as i32);
2369        let child_dettid = DetTid::from_raw(child_tid.into()); // TODO(T78538674), virtualized tid/pid
2370        trace!(
2371            "[detcore] dtid {} cloned, continuing parent + register new thread.",
2372            child_dettid
2373        );
2374
2375        if !is_vfork {
2376            create_child_thread(guest, child_dettid, ctid, Some(flags), exit_signal, None).await;
2377            // The clone made the namespace: note its owner, in the turn that
2378            // registered the child (a vfork child notes its own at its
2379            // registration, `Detcore::handle_thread_start`).
2380            if user_namespace_owner_may_differ {
2381                crate::tool_global::note_user_namespace_owner_may_differ(guest, child_dettid).await;
2382            }
2383        }
2384
2385        if let Some(address) = pidfd_address {
2386            self.register_clone_pidfd(guest, address, child_dettid)
2387                .await;
2388        }
2389
2390        {
2391            // The child will have updated their pedigree, we update ours before continuing.
2392            let parent_pedigree = &mut guest.thread_state_mut().pedigree;
2393            let child_pedigree = parent_pedigree.fork_mut();
2394            debug!(
2395                "[dtid {}] after creating child thread (tid {}, pedigree {}) parents pedigree becomes {}",
2396                parent_dettid, child_dettid, child_pedigree, parent_pedigree,
2397            );
2398        }
2399
2400        // A nested creator sees its child's pid in its own PID namespace, as
2401        // the kernel returned it; Reverie reports the container's
2402        // (https://github.com/rrnewton/hermit/issues/4090).
2403        if guest.thread_state().nested_pid_namespace {
2404            let refusal = match crate::tool_global::local_pid(guest, child_dettid).await {
2405                Ok(Some(local)) => return Ok(local as i64),
2406                Ok(None) => {
2407                    format!("child {child_dettid} has no pid recorded in its creator's namespace")
2408                }
2409                Err(reason) => reason,
2410            };
2411            tracing::error!(
2412                "[detcore, dtid {}] the pid a clone in a nested PID namespace returns cannot be translated: {} (https://github.com/rrnewton/hermit/issues/4090)",
2413                parent_dettid,
2414                refusal,
2415            );
2416            return self
2417                .refuse_unserviceable_operation(
2418                    guest,
2419                    reverie::syscalls::Sysno::clone,
2420                    Errno::ENOSYS,
2421                )
2422                .await;
2423        }
2424        Ok(child_dettid.as_raw() as i64)
2425    }
2426
2427    // TODO-HUMAN-REVIEW(PR-2985): Review scheduler tracking of set_tid_address.
2428    /// `set_tid_address` system call.
2429    ///
2430    /// Linux owns the guest-visible registration and return value. Detcore
2431    /// mirrors the accepted address into the scheduler so its modeled
2432    /// CHILD_CLEARTID wake targets the same word as the backend.
2433    pub async fn handle_set_tid_address<G: Guest<Self>>(
2434        &self,
2435        guest: &mut G,
2436        call: syscalls::SetTidAddress,
2437    ) -> Result<i64, Error> {
2438        let address = call.tidptr().map_or(0, |pointer| pointer.as_raw());
2439        crate::reserved_input::expose_local_overlap(
2440            guest,
2441            address as u64,
2442            4,
2443            "clear-tid-reserved-reference",
2444        )?;
2445        let result = self
2446            .record_or_replay(guest, Syscall::SetTidAddress(call))
2447            .await?;
2448        if guest.config().sequentialize_threads {
2449            set_child_tid_address(guest, address).await;
2450        }
2451        trace!(
2452            "[detcore, dtid {}] child TID clear address registered: {address:#x}",
2453            guest.thread_state().dettid,
2454        );
2455        Ok(result)
2456    }
2457
2458    /// `set_robust_list` system call.
2459    ///
2460    /// Still a pass-through: Linux owns the registration and supplies the
2461    /// result, and Detcore only records the head address so it can replay
2462    /// `exit_robust_list()` when the thread dies (see
2463    /// `Self::run_robust_list_owner_death`). Recording happens only after the
2464    /// kernel accepts the call, so a rejected length or address never becomes
2465    /// Detcore state.
2466    ///
2467    /// The call goes through `record_or_replay`, like every other pass-through.
2468    /// Using `Guest::inject` directly would keep the classification but drop the
2469    /// behavior it implies: the syscall would vanish from a `hermit record`
2470    /// trace, and a log recorded by a build that did record it would no longer
2471    /// replay.
2472    // AUTONOMOUS-BOT-IMPLEMENTED
2473    pub async fn handle_set_robust_list<G: Guest<Self>>(
2474        &self,
2475        guest: &mut G,
2476        call: syscalls::SetRobustList,
2477    ) -> Result<i64, Error> {
2478        let head = call.head().map(AddrMut::as_raw);
2479        let len = call.len();
2480        if let Some(head) = head {
2481            crate::reserved_input::expose_local_overlap(
2482                guest,
2483                head as u64,
2484                len as u64,
2485                "robust-list-reserved-reference",
2486            )?;
2487        }
2488        let res = self
2489            .record_or_replay(guest, Syscall::SetRobustList(call))
2490            .await?;
2491        // TODO-HUMAN-REVIEW(PR-2223): Review robust-list
2492        // head tracking used to drive owner-death wakeups.
2493        let recorded = match head {
2494            // `set_robust_list(NULL, ...)` unregisters the list.
2495            None => None,
2496            // Fail closed, and not dead code: `len` is the guest's own argument,
2497            // so a 32-bit guest (or a bug) can present the 12-byte
2498            // `compat_robust_list_head`, whose fields sit at different offsets.
2499            // The 64-bit walk would misread it, so refuse to record it at all
2500            // rather than walk a layout we cannot parse.
2501            Some(_) if len != robust_list::ROBUST_LIST_HEAD_LEN => None,
2502            Some(addr) => Some(addr),
2503        };
2504        guest.thread_state_mut().record_robust_list_head(recorded);
2505        trace!(
2506            "[detcore, dtid {}] robust-list head registered: {:?}",
2507            guest.thread_state().dettid,
2508            recorded,
2509        );
2510        Ok(res)
2511    }
2512
2513    /// Replay Linux's `exit_robust_list()` for the calling thread.
2514    ///
2515    /// Linux runs this from `mm_release()` while a task dies. Detcore performs
2516    /// the same walk and issues the wake against its own modeled waiter pool,
2517    /// which is where precise-mode futex waiters actually live. Ptrace leaves
2518    /// the atomic word transition to Linux on every backend that enters this
2519    /// path ([`OwnerDeathPath`]), and the wake reaches a waiter only after
2520    /// Linux has made it. The algorithm is in
2521    /// [`crate::syscalls::robust_list`]; this function only supplies guest
2522    /// memory and the scheduler wake.
2523    ///
2524    /// Only the precise futex model needs this. Polling and external modes park
2525    /// waiters in the host kernel, which performs its own robust-list cleanup;
2526    /// and without thread sequentialization Detcore does not model futexes at
2527    /// all.
2528    // AUTONOMOUS-BOT-IMPLEMENTED
2529    // TODO-HUMAN-REVIEW(PR-2223): Review owner-death wakeup
2530    // emulation, which changes how a dying thread's peers are scheduled.
2531    async fn run_robust_list_owner_death<G: Guest<Self>>(&self, guest: &mut G) {
2532        let Some(path) = self.robust_owner_death_path() else {
2533            return;
2534        };
2535        let Some(head) = guest.thread_state().robust_list_head else {
2536            return;
2537        };
2538        let dettid = guest.thread_state().dettid;
2539        // Which TID goes in the comparison? Not one Detcore hands out: `gettid`
2540        // is a reviewed pass-through, and glibc puts the value the kernel
2541        // supplied through `CLONE_PARENT_SETTID` into the lock word, not a
2542        // `gettid` result. The comparison is valid for a narrower reason —
2543        // `DetTid` currently *is* the raw namespaced `Tid`. `init_thread_state`
2544        // builds every one with `DetPid::from_raw(tid.into())` (detcore/src/lib.rs,
2545        // `lib.rs:1257` at this commit), four lines under
2546        // `// TODO(T78538674): virtualize tid, extend tid<=>dettid mapping here.`
2547        // Completing that TODO breaks this comparison, which would then have to
2548        // map back to the guest-visible TID.
2549        // `hermit-cli/tests/robust_futex_owner_death.rs` is the tripwire: it
2550        // fails the moment the two diverge.
2551        let owner_tid = dettid.as_raw() as u32;
2552
2553        let mut marked = HashMap::new();
2554        let mut effects = GuestRobustEffects::<'_, G, T> {
2555            guest,
2556            dettid,
2557            staged_wakes: None,
2558            marked: &mut marked,
2559            tool: PhantomData,
2560        };
2561        let outcome = robust_list::exit_robust_list(&mut effects, head, owner_tid).await;
2562        if path == OwnerDeathPath::Barrier {
2563            guest.thread_state().mark_robust_list_walked_by_tool();
2564        }
2565        if outcome.head_unreadable {
2566            trace!(
2567                "[detcore, dtid {}] unreadable robust-list head at {:#x}; no owner-death wakeups",
2568                dettid, head,
2569            );
2570        } else if outcome.aborted || outcome.next_faulted || outcome.truncated {
2571            trace!(
2572                "[detcore, dtid {}] robust-list walk stopped early after {} entr(ies): {:?}",
2573                dettid, outcome.entries_visited, outcome,
2574            );
2575        }
2576    }
2577
2578    /// Read every registered robust list in the current thread group while
2579    /// its address space still exists, but hold its modeled wakes until the
2580    /// corresponding exit callback. On ptrace that callback confirms Linux has
2581    /// completed the atomic owner-word update.
2582    ///
2583    /// On [`OwnerDeathPath::Barrier`] nothing is staged. An exit_group walks
2584    /// the words and wakes their waiters at once instead
2585    /// (`Self::handle_exit_group`), and no waiter runs before the kernel has
2586    /// ended the group. A delivered signal
2587    /// may still be caught or ignored: Detcore must not mark a live owner's
2588    /// word, and that backend's native exit callback cannot tell which signal
2589    /// ended the thread group.
2590    pub(crate) async fn stage_thread_group_robust_list_wakes<G: Guest<Self>>(
2591        &self,
2592        guest: &mut G,
2593        reason: RobustListExit,
2594    ) {
2595        match self.robust_owner_death_path() {
2596            None | Some(OwnerDeathPath::Barrier) => {}
2597            Some(OwnerDeathPath::Kernel) => {
2598                let staged = self
2599                    .read_thread_group_robust_list_wakes(guest, OwnerDeath::Exit)
2600                    .await;
2601                guest.thread_state().stage_robust_list_wakes(reason, staged);
2602            }
2603        }
2604    }
2605
2606    /// Replay Linux's `exit_robust_list()` for each `(owner, head)` of the
2607    /// current thread group, in owner order, as that owner's list, waking at
2608    /// once ([`OwnerDeathPath::Barrier`]). The threads the grant killed have not made
2609    /// their native exits (`ThreadState::wait_for_exit_group_robust_walk`), so
2610    /// every word reads as the guest left it.
2611    async fn run_thread_group_robust_list_owner_death<G: Guest<Self>>(
2612        &self,
2613        guest: &mut G,
2614        heads: Vec<RobustListOwner>,
2615    ) {
2616        // One group exit: a word Linux marks under one owner reads as marked
2617        // under every later one.
2618        let mut marked = HashMap::new();
2619        for RobustListOwner {
2620            dettid: owner,
2621            head,
2622        } in heads
2623        {
2624            let mut effects = GuestRobustEffects::<'_, G, T> {
2625                guest,
2626                dettid: owner,
2627                staged_wakes: None,
2628                marked: &mut marked,
2629                tool: PhantomData,
2630            };
2631            let outcome =
2632                robust_list::exit_robust_list(&mut effects, head, owner.as_raw() as u32).await;
2633            if outcome.head_unreadable || outcome.aborted {
2634                trace!(
2635                    "[detcore, dtid {}] robust-list owner death stopped early: {:?}",
2636                    owner, outcome,
2637                );
2638            }
2639        }
2640    }
2641
2642    /// How this backend orders owner-death wakes after Linux's walk, or `None`
2643    /// where Detcore does not model robust-futex owner death.
2644    ///
2645    /// Only the precise futex model needs owner death modeled. Polling and
2646    /// external modes park waiters in the host kernel, which performs its own
2647    /// robust-list cleanup; and without thread sequentialization Detcore does
2648    /// not model futexes at all.
2649    fn robust_owner_death_path(&self) -> Option<OwnerDeathPath> {
2650        owner_death_path(&self.cfg)
2651    }
2652
2653    /// Before the send of SIGKILL reservation `token`, record the robust list
2654    /// of every thread the send kills (`Scheduler::sigkill_dying_threads`),
2655    /// for its exit callback to walk ([`Self::sigkill_robust_list_wakes`]).
2656    ///
2657    /// Linux walks a dying thread's robust list in its own exit, before its
2658    /// address space goes away (`exit_mm_release`, `futex_exit_release`,
2659    /// `exit_robust_list`), however the thread dies. A SIGKILL leaves no stop
2660    /// at which Detcore could read the list, and the exit callback comes after
2661    /// the address space is gone, so the sender reads it here, in its own turn.
2662    /// Nothing changes the list between this read and Linux's walk: the
2663    /// victims run no guest code again, and no other guest thread takes a turn
2664    /// before the send. Only the backends whose exit callback follows Linux's
2665    /// walk need it ([`OwnerDeathPath::Kernel`]), and those run Detcore in the
2666    /// tracer, which may read every guest's memory.
2667    pub(crate) async fn record_sigkill_robust_lists<G: Guest<Self>>(
2668        &self,
2669        guest: &mut G,
2670        token: u64,
2671    ) {
2672        if self.robust_owner_death_path() != Some(OwnerDeathPath::Kernel) {
2673            return;
2674        }
2675        let mut lists = Vec::new();
2676        for (tid, mm) in crate::tool_global::sigkill_dying_threads(guest, token).await {
2677            if let Some(list) = robust_list::record_robust_list(tid.as_raw()).await {
2678                lists.push((tid, mm, list));
2679            }
2680        }
2681        if !lists.is_empty() {
2682            crate::tool_global::record_sigkill_robust_lists(guest, token, lists).await;
2683        }
2684    }
2685
2686    /// In the exit callback of a thread a SIGKILL ended, replay Linux's
2687    /// `exit_robust_list` over the list the SIGKILL's sender recorded for it
2688    /// ([`Self::record_sigkill_robust_lists`]) and deliver its wakes: one
2689    /// waiter per futex Linux marked `FUTEX_OWNER_DIED` with `FUTEX_WAITERS`
2690    /// set, and one per pending word that held no owner. Linux has made every
2691    /// owner-word transition by now, as for the other exits on this path. A
2692    /// victim's wakes are held until every victim of the send is accounted and
2693    /// then run in victim order (`Scheduler::retain_sigkill_victim_wakes`); the
2694    /// sender of a send that ended its own process still holds its `Exit`
2695    /// grant, so its wakes run at once, before any other thread takes a turn.
2696    pub(crate) async fn sigkill_robust_list_wakes<R: GlobalRPC<GlobalState>>(
2697        &self,
2698        global_state: &R,
2699        thread_state: &ThreadState<T::ThreadState>,
2700        exit_signal: Option<i32>,
2701    ) {
2702        if exit_signal != Some(libc::SIGKILL)
2703            || self.robust_owner_death_path() != Some(OwnerDeathPath::Kernel)
2704            // A list staged for this exit is delivered by the staged path.
2705            || thread_state.has_matching_robust_list_exit(exit_signal)
2706        {
2707            return;
2708        }
2709        let time = thread_state.thread_logical_time.clone();
2710        let mm = thread_state.mm_id;
2711        let Some(list) =
2712            crate::tool_global::take_sigkill_robust_list(time.clone(), global_state, mm).await
2713        else {
2714            return;
2715        };
2716        let dettid = thread_state.dettid;
2717        // The owner word holds the TID the kernel gave the thread, which is the
2718        // raw `DetTid` (see `Self::run_robust_list_owner_death`).
2719        let (futexes, outcome) =
2720            robust_list::owner_death_wakes_in_snapshot(&list, dettid.as_raw() as u32, |address| {
2721                thread_state.futex_id(address, false)
2722            })
2723            .await;
2724        if outcome.head_unreadable || outcome.aborted || outcome.next_faulted || outcome.truncated {
2725            trace!(
2726                "[detcore, dtid {}] SIGKILLed thread's robust-list walk stopped early: {:?}",
2727                dettid, outcome,
2728            );
2729        }
2730        let wakes: Vec<(DetTid, RobustListWake)> = futexes
2731            .into_iter()
2732            .map(|futex| (dettid, RobustListWake { futex }))
2733            .collect();
2734        let identities: Vec<FutexID> = wakes.iter().map(|(_, wake)| wake.futex).collect();
2735        let counts =
2736            crate::tool_global::robust_list_wakes_after_exit(time, global_state, mm, wakes).await;
2737        // A held batch is answered with no counts and logged when it runs.
2738        for (futex, count) in identities.into_iter().zip(counts) {
2739            info!(
2740                "[detcore, dtid {}] robust-list owner death woke {} waiter(s) on futex {:?} after physical exit",
2741                dettid, count, futex,
2742            );
2743        }
2744    }
2745
2746    /// The owner-death wakes a successful execve makes Linux deliver, in
2747    /// delivery order, for a backend that runs the native execve outside
2748    /// Detcore's handler and so cannot stage them in this thread's state (DBT,
2749    /// whose new image starts a fresh runtime). Read while the old image is
2750    /// mapped, before the native exec; none where Detcore does not model
2751    /// owner death (<https://github.com/rrnewton/hermit/issues/4268>).
2752    pub async fn exec_robust_list_wakes<G: Guest<Self>>(
2753        &self,
2754        guest: &mut G,
2755    ) -> Vec<(DetTid, FutexID)> {
2756        if self.robust_owner_death_path().is_none() {
2757            return Vec::new();
2758        }
2759        let wakes = self
2760            .read_thread_group_robust_list_wakes(guest, OwnerDeath::Exec)
2761            .await;
2762        crate::tool_local::flatten_exec_robust_list_wakes(wakes)
2763            .into_iter()
2764            .map(|(owner, wake)| (owner, wake.futex))
2765            .collect()
2766    }
2767
2768    /// The owner-death wakes Linux's `exit_robust_list` would make for every
2769    /// registered robust list in the current thread group, read without
2770    /// changing guest memory: Linux makes the owner-word transitions. Callers
2771    /// decide whether Detcore models owner death at all
2772    /// ([`Self::robust_owner_death_path`]).
2773    ///
2774    /// Each list is walked as its owner's, with that owner's TID, except the
2775    /// caller's own list in an exec by a thread that is not the group leader.
2776    /// `de_thread` gives that caller the leader's TID (`exchange_tids`) before
2777    /// `exec_mm_release` walks its list, so `handle_futex_death` compares the
2778    /// owner words with the thread group ID and leaves a word the caller owns
2779    /// under its old TID alone (fs/exec.c, kernel/futex/core.c).
2780    async fn read_thread_group_robust_list_wakes<G: Guest<Self>>(
2781        &self,
2782        guest: &mut G,
2783        death: OwnerDeath,
2784    ) -> Vec<(DetTid, Vec<RobustListWake>)> {
2785        let caller = guest.thread_state().dettid;
2786        let tgid = guest.thread_state().detpid.expect("detpid unset");
2787        // A thread whose own exit Detcore already walked (its head stays
2788        // registered while an exit_group is filed) is left out, as an
2789        // exit_group's walk leaves it out, so no list is walked twice (Claude
2790        // review of https://github.com/rrnewton/hermit/pull/4352, F4).
2791        let mut heads: Vec<(DetTid, usize)> = guest
2792            .thread_state()
2793            .robust_list_owners_for_exit_group()
2794            .into_iter()
2795            .map(|owner| (owner.dettid, owner.head))
2796            .collect();
2797        if death == OwnerDeath::Exec {
2798            // de_thread waits until every other thread has exited, each walking
2799            // its own list, before exec_mm_release walks the caller's; with
2800            // marks carried across lists, the caller's must come last (Claude
2801            // review of https://github.com/rrnewton/hermit/pull/4277 at
2802            // 65138a17, N5).
2803            heads.sort_by_key(|(owner, _)| *owner == caller);
2804        }
2805        let mut staged = Vec::with_capacity(heads.len());
2806        let mut marked = HashMap::new();
2807        for (owner, head) in heads {
2808            let walked_as = if death == OwnerDeath::Exec && owner == caller {
2809                tgid.as_raw()
2810            } else {
2811                owner.as_raw()
2812            };
2813            let mut wakes = Vec::new();
2814            let mut effects = GuestRobustEffects::<'_, G, T> {
2815                guest,
2816                dettid: owner,
2817                staged_wakes: Some(&mut wakes),
2818                marked: &mut marked,
2819                tool: PhantomData,
2820            };
2821            let outcome = robust_list::exit_robust_list(&mut effects, head, walked_as as u32).await;
2822            if outcome.head_unreadable || outcome.aborted {
2823                trace!(
2824                    "[detcore, dtid {}] could not stage complete robust-list owner-death effects: {:?}",
2825                    owner, outcome,
2826                );
2827            }
2828            staged.push((owner, wakes));
2829        }
2830        staged
2831    }
2832
2833    /// Exit system call
2834    pub async fn handle_exit<G: Guest<Self>>(
2835        &self,
2836        guest: &mut G,
2837        call: syscalls::Exit,
2838    ) -> Result<i64, Error> {
2839        // This thread may be its process's last (`crate::guest_log`).
2840        crate::guest_log::flush_unsent();
2841        // A raw `exit` of a process's last thread ends the process as an
2842        // `exit_group` does, and the scheduler then sends the parent's SIGCHLD
2843        // with Linux's siginfo, as for `handle_exit_group`. Only the exit grant
2844        // knows whether this thread is the last, so every raw exit reports its
2845        // status, kept for this thread alone until that grant settles it.
2846        if self.cfg.backend.reports_kernel_child_exit_signals {
2847            let uid = real_uid(guest.tid().as_raw()).unwrap_or_else(|| unsafe { libc::getuid() });
2848            crate::tool_global::child_exit_sigchld(
2849                guest,
2850                crate::scheduler::child_exit_sigchld::ChildExitSigchldControl::RawExitStatus {
2851                    code: libc::CLD_EXITED,
2852                    status: call.status() & 0xff,
2853                    uid,
2854                },
2855            )
2856            .await;
2857        }
2858        let request = guest.thread_state().mk_request(
2859            ResourceID::Exit {
2860                group: false,
2861                process: guest.thread_state().detpid.expect("detpid unset"),
2862                mm: guest.thread_state().mm_id,
2863            },
2864            Permission::RW,
2865        );
2866        resource_request(guest, request).await;
2867        self.run_robust_list_owner_death(guest).await;
2868        // A leader whose group lives on is a zombie from here until its last
2869        // sibling leaves, and its backend reports its exit only then: it
2870        // leaves the scheduler now, after its last request
2871        // (https://github.com/rrnewton/hermit/issues/4100).
2872        let state = guest.thread_state();
2873        if self.cfg.sequentialize_threads
2874            && state
2875                .detpid
2876                .is_some_and(|pid| pid.as_raw() == state.dettid.as_raw())
2877            && crate::tool_global::retire_exited_leader(guest).await
2878            && self.cfg.replay_data.is_some()
2879        {
2880            // The recorder reads the process's descriptor table through its
2881            // leader, which is a zombie from here, so it would record the
2882            // group's later output as ordinary writes that replay drops
2883            // (https://github.com/rrnewton/hermit/issues/4244).
2884            let message = format!(
2885                "hermit: thread-group leader {} exited while its threads run on, which record \
2886                 and replay cannot yet capture, so the run is refused \
2887                 (https://github.com/rrnewton/hermit/issues/4244)",
2888                guest.thread_state().dettid
2889            );
2890            tracing::error!("{message}");
2891            let _ = std::io::Write::write_fmt(
2892                &mut crate::util::RetryingStderr,
2893                format_args!("{message}\n"),
2894            );
2895            crate::tool_global::unrecoverable_shutdown(
2896                guest,
2897                detcore_model::HERMIT_POLICY_REFUSAL_EXIT,
2898            )
2899            .await;
2900        }
2901        // A table's last holder takes it with its exit: after the grant, just
2902        // before the native exit, as an exit_group does
2903        // (`Self::release_table_at_exit`).
2904        self.release_table_at_exit(guest, false).await;
2905        // It's ok here that we skip running the posthook:
2906        guest.tail_inject(call).await
2907    }
2908
2909    /// Exit_group system call
2910    pub async fn handle_exit_group<G: Guest<Self>>(
2911        &self,
2912        guest: &mut G,
2913        call: syscalls::ExitGroup,
2914    ) -> Result<i64, Error> {
2915        crate::reserved_input::before_exit_group(guest)?;
2916        // One of the last points where this process's unsent warnings can
2917        // still be reported (`crate::guest_log`).
2918        crate::guest_log::flush_unsent();
2919        // Where the kernel also notifies the parent, the scheduler sends the
2920        // parent's SIGCHLD itself and gives it the siginfo Linux would
2921        // (`scheduler::child_exit_sigchld`): CLD_EXITED and the exit code.
2922        if self.cfg.backend.reports_kernel_child_exit_signals {
2923            let uid = real_uid(guest.tid().as_raw()).unwrap_or_else(|| unsafe { libc::getuid() });
2924            crate::tool_global::child_exit_sigchld(
2925                guest,
2926                crate::scheduler::child_exit_sigchld::ChildExitSigchldControl::ExitStatus {
2927                    code: libc::CLD_EXITED,
2928                    status: call.status() & 0xff,
2929                    uid,
2930                },
2931            )
2932            .await;
2933        }
2934        // On [`OwnerDeathPath::Barrier`] the walk follows the grant and wakes
2935        // at once, as an exit does (`Self::run_robust_list_owner_death`):
2936        // nothing can call the exit_group off, the grant logically kills the
2937        // group's other threads, so none of them runs guest code again, and
2938        // the scheduler commits no turn until the kernel has ended every
2939        // thread of the group, so no waiter runs before Linux has marked its
2940        // word. Holding the wakes for the exit callbacks would lose them on
2941        // DBT, which answers a logically killed thread's callback
2942        // `ThreadExited`. Read the registrations at the grant, as Linux's
2943        // walks use the ones current when each thread dies. Filing first keeps
2944        // an exiting thread's head registered until then and holds the killed
2945        // threads' native exits until the walk is done
2946        // (`ThreadState::file_exit_group_for_robust_lists`,
2947        // `ThreadState::wait_for_exit_group_robust_walk`): a killed thread's
2948        // native exit would otherwise let Linux mark and clear its words before
2949        // the walk reads them. The lists stay mapped until the native
2950        // exit_group.
2951        let barrier = self.robust_owner_death_path() == Some(OwnerDeathPath::Barrier);
2952        if barrier {
2953            guest.thread_state().file_exit_group_for_robust_lists();
2954        }
2955        let request = guest.thread_state().mk_request(
2956            ResourceID::Exit {
2957                group: true,
2958                process: guest.thread_state().detpid.expect("detpid unset"),
2959                mm: guest.thread_state().mm_id,
2960            },
2961            Permission::RW,
2962        );
2963        resource_request(guest, request).await;
2964        if barrier {
2965            guest.thread_state().begin_exit_group_robust_walk();
2966            let owners = guest.thread_state().robust_list_owners_for_exit_group();
2967            self.run_thread_group_robust_list_owner_death(guest, owners)
2968                .await;
2969            guest.thread_state().finish_exit_group_robust_walk();
2970        } else {
2971            self.stage_thread_group_robust_list_wakes(guest, RobustListExit::ExitGroup)
2972                .await
2973        }
2974        // Every thread of the process exits with it: a table only they held
2975        // goes with this exit, once the grant has logically ended the other
2976        // threads, so none of them runs again against a table Detcore has let
2977        // go, and before the native exit_group, so no turn runs between
2978        // (`Self::release_table_at_exit`).
2979        self.release_table_at_exit(guest, true).await;
2980        // It's ok here that we skip running the posthook:
2981        guest.tail_inject(call).await
2982    }
2983
2984    /// On DBT, end the thread group in the schedule before a signal that
2985    /// ends it by default is delivered natively
2986    /// (<https://github.com/rrnewton/hermit/issues/4278>). DynamoRIO carries
2987    /// out such a default action itself, and before the process exits it
2988    /// synchronizes with every other thread of it. A thread waiting for its
2989    /// turn is blocked in the client's call to the coordinator, inside a
2990    /// DynamoRIO event callback, where DynamoRIO cannot suspend it, so the
2991    /// dying thread waited for it forever. The `Exit` grant of an exit_group
2992    /// logically kills the group's other threads and cancels their pending
2993    /// requests, so each returns from its callback and can be synchronized;
2994    /// the robust lists are walked as an exit_group's are. Returns whether it
2995    /// ended the group.
2996    ///
2997    /// The decision is the action read in this thread's `InboundSignal` turn.
2998    /// Linux commits it under the signal lock: for an unblocked signal,
2999    /// `complete_signal` sets `SIGNAL_GROUP_EXIT` and queues SIGKILL to every
3000    /// other thread of the group, so no sibling runs another user instruction
3001    /// after the decision. Filing the `Exit` request ends this thread's turn,
3002    /// so the thread first asks that the request be granted at the very next
3003    /// turn (`Scheduler::hold_turn_for_group_exit`). No sibling takes a turn
3004    /// between the decision and the grant, which logically kills the siblings,
3005    /// so none runs guest code before the native delivery (Claude re-check of
3006    /// https://github.com/rrnewton/hermit/pull/4387).
3007    ///
3008    /// Should a sibling still run first, its action change must not undo the
3009    /// decision. DynamoRIO reads the action again when it delivers, so after
3010    /// the grant the action is read again and, if a sibling changed it, set
3011    /// back to `SIG_DFL` in this dying process, and the delivery carries out
3012    /// the committed decision (Codex review of
3013    /// https://github.com/rrnewton/hermit/pull/4387, P1).
3014    ///
3015    /// The gate is this backend's: DynamoRIO carries out a default action in
3016    /// the process and synchronizes every thread before it exits. Today only
3017    /// DBT sends process signals to a named thread
3018    /// (`requires_thread_directed_process_signals`), so that flag selects it.
3019    pub(crate) async fn end_thread_group_before_fatal_delivery<G: Guest<Self>>(
3020        &self,
3021        guest: &mut G,
3022        signal: SigWrapper,
3023    ) -> bool {
3024        if !(self.cfg.sequentialize_threads
3025            && self.cfg.backend.requires_thread_directed_process_signals)
3026            || !signal_default_action_ends_the_process(signal)
3027        {
3028            return false;
3029        }
3030        match self.signal_is_left_at_default(guest, signal).await {
3031            Ok(true) => {}
3032            Ok(false) => return false,
3033            Err(error) => {
3034                tracing::warn!(
3035                    "[detcore, dtid {}] cannot read the disposition of {}: {}",
3036                    guest.thread_state().dettid,
3037                    signal,
3038                    error
3039                );
3040                return false;
3041            }
3042        }
3043        info!(
3044            "[detcore, dtid {}] {} ends its thread group by default",
3045            guest.thread_state().dettid,
3046            signal
3047        );
3048        let barrier = self.robust_owner_death_path() == Some(OwnerDeathPath::Barrier);
3049        if barrier {
3050            guest.thread_state().file_exit_group_for_robust_lists();
3051        }
3052        let dettid = guest.thread_state().dettid;
3053        if !hold_turn_for_group_exit(guest, dettid).await {
3054            tracing::warn!(
3055                "[detcore, dtid {}] the scheduler did not hold the turn for the group exit of {}",
3056                dettid,
3057                signal
3058            );
3059        }
3060        let request = guest.thread_state().mk_request(
3061            ResourceID::Exit {
3062                group: true,
3063                process: guest.thread_state().detpid.expect("detpid unset"),
3064                mm: guest.thread_state().mm_id,
3065            },
3066            Permission::RW,
3067        );
3068        resource_request(guest, request).await;
3069        match self.signal_is_left_at_default(guest, signal).await {
3070            Ok(true) => {}
3071            changed => {
3072                info!(
3073                    "[detcore, dtid {}] the action for {} changed after the delivery decision \
3074                     ({:?}); restoring SIG_DFL in the dying process",
3075                    guest.thread_state().dettid,
3076                    signal,
3077                    changed.map(|default| !default)
3078                );
3079                if let Err(error) = self.restore_default_action(guest, signal).await {
3080                    tracing::warn!(
3081                        "[detcore, dtid {}] cannot restore SIG_DFL for {}: {}",
3082                        guest.thread_state().dettid,
3083                        signal,
3084                        error
3085                    );
3086                }
3087            }
3088        }
3089        if barrier {
3090            guest.thread_state().begin_exit_group_robust_walk();
3091            let owners = guest.thread_state().robust_list_owners_for_exit_group();
3092            self.run_thread_group_robust_list_owner_death(guest, owners)
3093                .await;
3094            guest.thread_state().finish_exit_group_robust_walk();
3095        }
3096        true
3097    }
3098
3099    /// Set the guest's action for `signal` to `SIG_DFL`.
3100    async fn restore_default_action<G: Guest<Self>>(
3101        &self,
3102        guest: &mut G,
3103        signal: SigWrapper,
3104    ) -> Result<(), Error> {
3105        let default = KernelSigaction {
3106            handler: libc::SIG_DFL as u64,
3107            flags: 0,
3108            restorer: 0,
3109            mask: 0,
3110        };
3111        let mut stack = guest.stack().await;
3112        let action_addr = stack.push(default);
3113        let _guard = stack.commit()?;
3114        let call = syscalls::RtSigaction::new()
3115            .with_signum(signal.raw())
3116            .with_action(Some(action_addr.cast()))
3117            .with_old_action(None)
3118            .with_sigsetsize(std::mem::size_of::<u64>());
3119        guest.inject_with_retry(call).await?;
3120        // Detcore's own change, which the signal-state copy does not record:
3121        // the copy, and the actions it shares, are unknown now.
3122        super::signal_shadow::give_up(
3123            guest,
3124            "Detcore reset an action to the default before a fatal delivery",
3125        );
3126        Ok(())
3127    }
3128
3129    /// Whether the guest's action for `signal` is the default (`SIG_DFL`),
3130    /// as the backend reports it to the guest.
3131    async fn signal_is_left_at_default<G: Guest<Self>>(
3132        &self,
3133        guest: &mut G,
3134        signal: SigWrapper,
3135    ) -> Result<bool, Error> {
3136        let mut stack = guest.stack().await;
3137        let action_addr = stack.reserve::<KernelSigaction>();
3138        let _guard = stack.commit()?;
3139        let call = syscalls::RtSigaction::new()
3140            .with_signum(signal.raw())
3141            .with_action(None)
3142            .with_old_action(Some(action_addr.cast()))
3143            .with_sigsetsize(std::mem::size_of::<u64>());
3144        guest.inject_with_retry(call).await?;
3145        let action: KernelSigaction = guest.memory().read_value(action_addr)?;
3146        Ok(action.handler == libc::SIG_DFL as u64)
3147    }
3148
3149    /// Futex system call, which can block.
3150    pub async fn handle_futex<G: Guest<Self>>(
3151        &self,
3152        guest: &mut G,
3153        call: syscalls::Futex,
3154    ) -> Result<i64, Error> {
3155        self.handle_futex_from(guest, call, FutexDeadline::FromCall)
3156            .await
3157    }
3158
3159    /// `handle_futex`, taking a wait's deadline from `deadline`. A `restart_syscall`
3160    /// resumes an interrupted timed futex wait here with the deadline it kept,
3161    /// and the value is compared again, as in Linux's `futex_wait_restart`
3162    /// (`Detcore::handle_restart_syscall`).
3163    pub(crate) async fn handle_futex_from<G: Guest<Self>>(
3164        &self,
3165        guest: &mut G,
3166        call: syscalls::Futex,
3167        deadline: FutexDeadline,
3168    ) -> Result<i64, Error> {
3169        let dettid = guest.thread_state().dettid;
3170        // Detcore emulates the futex commands below, so it also answers, as Linux's
3171        // `do_futex` (kernel/futex/syscalls.c) does, for the ones it does not serve:
3172        // before the futex address is even looked at.
3173        if self.cfg.sequentialize_threads
3174            && !matches!(self.cfg.debug_futex_mode, BlockingMode::External)
3175        {
3176            match classify_futex_command(call.futex_op()) {
3177                FutexCommand::Served => {}
3178                FutexCommand::NoSys => return Err(Error::Errno(Errno::ENOSYS)),
3179                FutexCommand::RequeuePi(name) => {
3180                    return self
3181                        .refuse_futex_command(
3182                            guest,
3183                            name,
3184                            "Detcore does not emulate requeue-PI futexes (PI condition variables)",
3185                        )
3186                        .await;
3187                }
3188            }
3189        }
3190        // FUTEX_REQUEUE, FUTEX_CMP_REQUEUE and FUTEX_WAKE_OP key their words
3191        // without reading `uaddr` (only CMP_REQUEUE compares it), and NULL is a
3192        // valid private key for them, so they take their own path before the
3193        // NULL passthrough and the read below.
3194        if self.cfg.sequentialize_threads
3195            && !matches!(self.cfg.debug_futex_mode, BlockingMode::External)
3196        {
3197            let command = call.futex_op() & libc::FUTEX_CMD_MASK;
3198            // The priority-inheritance lock commands read and write their word
3199            // themselves, NULL included (EFAULT), and park on the precise
3200            // model's waiter pool, which polling mode does not keep.
3201            if let Some(name) = pi_lock_command_name(command) {
3202                if matches!(self.cfg.debug_futex_mode, BlockingMode::Polling) {
3203                    return self
3204                        .refuse_futex_command(guest, name, POLLING_REFUSAL)
3205                        .await;
3206                }
3207                // AUTONOMOUS-BOT-IMPLEMENTED
3208                return if command == libc::FUTEX_UNLOCK_PI {
3209                    self.handle_futex_unlock_pi(guest, call).await
3210                } else {
3211                    self.handle_futex_lock_pi(guest, call, command).await
3212                };
3213            }
3214            // FUTEX_WAKE and FUTEX_WAKE_BITSET only key their word too, and a
3215            // requeue can leave a modeled waiter on any admitted private key,
3216            // NULL or unmapped included, which a wake must still reach.
3217            if matches!(command, libc::FUTEX_WAKE | libc::FUTEX_WAKE_BITSET)
3218                && matches!(self.cfg.debug_futex_mode, BlockingMode::Precise)
3219            {
3220                return self.handle_futex_wake(guest, call, command).await;
3221            }
3222            let name = match command {
3223                libc::FUTEX_REQUEUE => Some("FUTEX_REQUEUE"),
3224                libc::FUTEX_CMP_REQUEUE => Some("FUTEX_CMP_REQUEUE"),
3225                libc::FUTEX_WAKE_OP => Some("FUTEX_WAKE_OP"),
3226                _ => None,
3227            };
3228            if let Some(name) = name {
3229                // Polling waiters are not on a waiter list that a requeue or a
3230                // WAKE_OP's second wake could act on.
3231                if matches!(self.cfg.debug_futex_mode, BlockingMode::Polling) {
3232                    return self
3233                        .refuse_futex_command(guest, name, POLLING_REFUSAL)
3234                        .await;
3235                }
3236                return if command == libc::FUTEX_WAKE_OP {
3237                    // AUTONOMOUS-BOT-IMPLEMENTED
3238                    self.handle_futex_wake_op(guest, call).await
3239                } else {
3240                    // AUTONOMOUS-BOT-IMPLEMENTED
3241                    self.handle_futex_requeue(guest, call, command).await
3242                };
3243            }
3244        }
3245        let ptr = match call.uaddr() {
3246            None => {
3247                // null pointer error:
3248                return Ok(guest.inject(call).await?);
3249            }
3250            Some(x) => x,
3251        };
3252        if self.cfg.sequentialize_threads
3253            && !matches!(self.cfg.debug_futex_mode, BlockingMode::External)
3254            && !AddrMut::as_raw(ptr).is_multiple_of(std::mem::size_of::<u32>())
3255        {
3256            // `get_futex_key` refuses a misaligned futex word before any access.
3257            return Err(Error::Errno(Errno::EINVAL));
3258        }
3259        if !self.cfg.sequentialize_threads {
3260            return crate::syscalls::helpers::inject_keeping_kernel_restart(guest, call).await;
3261        }
3262        let polling = match self.cfg.debug_futex_mode {
3263            BlockingMode::External => {
3264                return self.record_or_replay_blocking(guest, call.into()).await;
3265            }
3266            BlockingMode::Precise => false,
3267            BlockingMode::Polling => true,
3268        };
3269        // Only the waits compare the word, and they read it as Linux's
3270        // `futex_wait_setup` does, with user-mode permissions: EFAULT for an
3271        // unmapped, PROT_NONE or otherwise unreadable word
3272        // (https://github.com/rrnewton/hermit/issues/4030). A wake left to the
3273        // polling mode reads nothing, as Linux's `futex_wake` reads nothing.
3274        //
3275        // Linux checks a wait's other arguments first, without touching the
3276        // word: `sys_futex` (kernel/futex/syscalls.c) copies and validates the
3277        // timeout (EFAULT, EINVAL), then `__futex_wait` refuses a zero bitset
3278        // (EINVAL). A restarted wait keeps its validated deadline.
3279        let init_val = match call.futex_op() & libc::FUTEX_CMD_MASK {
3280            command @ (libc::FUTEX_WAIT | libc::FUTEX_WAIT_BITSET) => {
3281                if matches!(deadline, FutexDeadline::FromCall)
3282                    && let Some(timeout) = call.timeout()
3283                {
3284                    let timeout = read_user_timespec(guest, timeout.as_raw())?;
3285                    if !futex_timespec_is_valid(timeout) {
3286                        return Err(Error::Errno(Errno::EINVAL));
3287                    }
3288                }
3289                if command == libc::FUTEX_WAIT_BITSET && call.val3() == 0 {
3290                    return Err(Error::Errno(Errno::EINVAL));
3291                }
3292                read_futex_word(guest, AddrMut::as_raw(ptr))?
3293            }
3294            _ => 0,
3295        };
3296        trace!(
3297            "[detcore, dtid {}] futex op with memory address containing value {}",
3298            &dettid, init_val
3299        );
3300        if polling {
3301            self.handle_futex_polling(guest, call, init_val, deadline)
3302                .await
3303        } else {
3304            self.handle_futex_blocking(guest, call, init_val, deadline)
3305                .await
3306        }
3307    }
3308
3309    /// Blocking (precise) Futex implementation.
3310    /// Here we use a two-phase request to the scheduler: before and after the futex wait/wake
3311    /// side effects. We EMULATE futex calls and NEVER run them inside the kernel.
3312    pub(crate) async fn handle_futex_blocking<G: Guest<Self>>(
3313        &self,
3314        guest: &mut G,
3315        call: syscalls::Futex,
3316        init_val: i32,
3317        deadline: FutexDeadline,
3318    ) -> Result<i64, Error> {
3319        let ptr = call.uaddr().unwrap();
3320        let futexid = guest.thread_state().futex_id(
3321            AddrMut::as_raw(ptr),
3322            call.futex_op() & libc::FUTEX_PRIVATE_FLAG != 0,
3323        );
3324        let futex_op = call.futex_op() & libc::FUTEX_CMD_MASK;
3325        let bitset = match futex_op {
3326            libc::FUTEX_WAKE_BITSET | libc::FUTEX_WAIT_BITSET => call.val3() as u32,
3327            _ => u32::MAX,
3328        };
3329        if bitset == 0 {
3330            return Err(Error::Errno(Errno::EINVAL));
3331        }
3332        let dettid = guest.thread_state().dettid;
3333        match futex_op {
3334            libc::FUTEX_WAIT | libc::FUTEX_WAIT_BITSET => {
3335                if init_val != call.val() {
3336                    info!(
3337                        "[detcore, dtid {}] Futex wait running immediately because it will fizzle ({} != {}).",
3338                        &dettid,
3339                        init_val,
3340                        call.val()
3341                    );
3342                    Err(Error::Errno(Errno::EAGAIN))
3343                } else {
3344                    let maybe_timeout_lt = self.futex_deadline(guest, call, deadline).await?;
3345                    // D50 reader 9 (A5 step 3c.1c): the capability, or the
3346                    // in-guest delivery gate, which decides by the design's
3347                    // deliverable set under the thread's published mask.
3348                    let gated = guest.config().in_guest_signal_delivery;
3349                    let signal_interruption = guest
3350                        .config()
3351                        .backend
3352                        .supports_blocked_wait_signal_interruption
3353                        || gated;
3354                    // Whether this wait was woken for a signal and is checking again.
3355                    let mut rewait = false;
3356                    // The signals the scheduler committed to this waiter at that wake
3357                    // (`SchedValue::Signaled`), which this turn's check counts although
3358                    // the wait holds them (https://github.com/rrnewton/hermit/issues/4005).
3359                    let mut committed_wake: KernelSigset = 0;
3360                    // Whether a wait request reached the scheduler. A wait that
3361                    // ends before its first one, on a pending signal or a failed
3362                    // read, sends no `WaitFinished`, as before.
3363                    let mut requested = false;
3364                    let res = loop {
3365                        // On a backend whose kernel reports the guest's signal state,
3366                        // a blocked, ignored, or default-ignored signal leaves the wait
3367                        // parked until its wakeup or its original deadline. As in
3368                        // Linux, one already pending when the value matches ends the
3369                        // wait at once with the futex's restart errno. A `SIGCHLD`
3370                        // ends it only when the scheduler committed it to this
3371                        // waiter at its wake, sent at a commit point (a child's
3372                        // child-exit timer, or a POSIX timer: `committed_wake`,
3373                        // https://github.com/rrnewton/hermit/issues/4005), or when it
3374                        // is a guest's thread-directed copy in this thread's private
3375                        // queue that the schedule recorded (`futex_wait_pending_signals`,
3376                        // https://github.com/rrnewton/hermit/issues/4132); any other
3377                        // stays pending until the call returns, because the kernel
3378                        // also posts one for a child event at a moment set by host
3379                        // timing (https://github.com/rrnewton/hermit/issues/3146). Nor does
3380                        // a signal that a host-timed source armed by a guest can
3381                        // post to this process, such as a parent-death signal
3382                        // (`host_timed_signals`, read here in this thread's turn).
3383                        //
3384                        // The same check decides a wait the scheduler woke for a
3385                        // signal. The scheduler commits such a wake without this
3386                        // thread running, and queues the thread to run next so that it
3387                        // is the thread that dequeues the signal. Checking again here,
3388                        // in the thread's own turn, keeps the restart errno for a
3389                        // signal the kernel will deliver to this thread as the call
3390                        // returns. If another thread took the signal first (a chaos
3391                        // schedule can run one ahead) or it became ignored, the wait
3392                        // parks again with its original absolute deadline, as
3393                        // `__futex_wait` retries a wait woken with no signal pending:
3394                        // an expired deadline ends it at the scheduler's next timed pop
3395                        // with ETIMEDOUT. Before parking again it reads the word, as
3396                        // `futex_wait_setup` does on that retry (see the re-check below).
3397                        // Returning the restart errno with nothing to deliver would
3398                        // instead have leaked it to the guest, or restarted the call
3399                        // with a fresh timeout.
3400                        //
3401                        // The scheduler is given only the mask, which only this thread
3402                        // can change and so cannot change while it is parked. A sibling
3403                        // can change the dispositions at any time, so the scheduler
3404                        // reads them when it commits a wake, not here
3405                        // (https://github.com/rrnewton/hermit/issues/3146).
3406                        let signal_watch = match self
3407                            .futex_wait_signal_watch(
3408                                guest,
3409                                call,
3410                                gated,
3411                                signal_interruption,
3412                                &mut committed_wake,
3413                            )
3414                            .await
3415                        {
3416                            Ok(watch) => watch,
3417                            Err(error) => break Err(error),
3418                        };
3419                        if rewait {
3420                            // No signal is pending for this thread, so Linux's
3421                            // `__futex_wait` retries `futex_wait_setup`, which reads the
3422                            // word again with user-mode permissions: EFAULT if it cannot,
3423                            // EAGAIN if it changed, and otherwise the wait parks again
3424                            // (https://github.com/rrnewton/hermit/issues/4033). While the
3425                            // wait was off the waiter list a `FUTEX_WAKE` could not reach
3426                            // it, and a waker that saw no waiter is consistent with
3427                            // exactly this retry window.
3428                            let observed = match read_futex_word(guest, AddrMut::as_raw(ptr)) {
3429                                Ok(observed) => observed,
3430                                Err(error) => break Err(error),
3431                            };
3432                            if observed != call.val() {
3433                                trace!(
3434                                    "[detcore, dtid {}] futex value changed while its wait was woken for a signal ({} != {}); EAGAIN",
3435                                    &dettid,
3436                                    observed,
3437                                    call.val()
3438                                );
3439                                break Err(Error::Errno(Errno::EAGAIN));
3440                            }
3441                            debug!(
3442                                "[detcore, dtid {}] futex wait woken for a signal that is not pending for it; waiting again until {:?}",
3443                                &dettid, maybe_timeout_lt
3444                            );
3445                        }
3446                        requested = true;
3447                        let ans = futex_action(
3448                            guest,
3449                            FutexAction::WaitRequest(maybe_timeout_lt, signal_watch),
3450                            &futexid,
3451                            init_val,
3452                            bitset,
3453                        )
3454                        .await;
3455                        if signal_interruption && let Some(SchedValue::Signaled(committed)) = ans {
3456                            committed_wake = committed;
3457                            // The scheduler ended the wait for a signal that
3458                            // interrupted it under the dispositions the kernel held
3459                            // when the wake was committed. The check at the top of the
3460                            // loop decides, in this thread's turn, whether it is still
3461                            // pending here.
3462                            rewait = true;
3463                            continue;
3464                        }
3465                        break if ans != Some(SchedValue::TimeOut) {
3466                            let expected = call.val();
3467                            // AUTONOMOUS-BOT-IMPLEMENTED
3468                            // TODO-HUMAN-REVIEW(#845): Review exited-thread futex diagnostics.
3469                            match guest.memory().read_value(ptr) {
3470                                Ok(observed) => {
3471                                    trace!(
3472                                        "[detcore, dtid {}] after (emulated) futex wait, memory value is {}, expected {}",
3473                                        &dettid, observed, expected,
3474                                    );
3475                                    if expected == observed {
3476                                        debug!(
3477                                            "WARNING: fishy that the futex value did not change before wakeup. Weird application-level protocol.\n"
3478                                        );
3479                                    }
3480                                }
3481                                Err(error) => trace!(
3482                                    "[detcore, dtid {}] skipped post-wait futex memory diagnostic: {}",
3483                                    &dettid, error,
3484                                ),
3485                            }
3486                            Ok(0)
3487                        } else {
3488                            trace!("[detcore, dtid {}] futex wait timed out", &dettid);
3489                            Err(Error::Errno(Errno::ETIMEDOUT))
3490                        };
3491                    };
3492                    if requested {
3493                        futex_action(guest, FutexAction::WaitFinished, &futexid, init_val, bitset)
3494                            .await;
3495                    }
3496                    keep_restart_block(guest, &res, RestartCall::Futex(call), maybe_timeout_lt)
3497                        .await;
3498                    res
3499                }
3500            }
3501            // `classify_futex_command` already answered every other command.
3502            _ => Err(Error::Errno(Errno::ENOSYS)),
3503        }
3504    }
3505
3506    /// The check a precise futex wait makes before it parks, and again in its
3507    /// own turn after the scheduler woke it for a signal (see
3508    /// `Self::handle_futex_blocking`): `Err` with the call's restart errno
3509    /// when a signal pending for this thread ends the wait, otherwise what the
3510    /// scheduler watches while the thread is parked. `committed_wake` holds the
3511    /// signals the scheduler committed to this waiter at its last wake, which
3512    /// this check counts and clears.
3513    async fn futex_wait_signal_watch<G: Guest<Self>>(
3514        &self,
3515        guest: &mut G,
3516        call: syscalls::Futex,
3517        gated: bool,
3518        signal_interruption: bool,
3519        committed_wake: &mut KernelSigset,
3520    ) -> Result<Option<FutexSignalWatch>, Error> {
3521        let dettid = guest.thread_state().dettid;
3522        if gated {
3523            // Under the gate: a deliverable signal pending at
3524            // the park, or still pending after a wake, ends the
3525            // wait with the futex's restart errno, its end
3526            // recorded for the restart checkpoint (D40).
3527            let state = read_wait_signal_state(guest.pid(), guest.tid())?;
3528            let mask = super::helpers::gated_own_mask(guest, state.blocked);
3529            if let Some(end) = crate::wait_signal_end::wait_end(
3530                &state,
3531                mask,
3532                guest.config().in_guest_reserved_signals,
3533            ) {
3534                let errno = call.kernel_restart_errno();
3535                trace!(
3536                    "[detcore, dtid {}] futex wait ended under the in-guest delivery gate by {:?}: {:?}",
3537                    &dettid, end, errno
3538                );
3539                guest.thread_state_mut().wait_signal_end = Some(end);
3540                return Err(Error::Errno(errno));
3541            }
3542            // The futex wait files no `Resources` request, so
3543            // it publishes the guest's mask itself, before its
3544            // wait request, as every request does under the gate
3545            // (step 3c.1b, carried into 3c.1c): the scheduler's
3546            // readers then see this wait's mask, not an earlier
3547            // request's. None clears the entry, as at a request.
3548            let published =
3549                crate::tool_global::publish_request_guest_mask(guest, &Resources::new(dettid));
3550            crate::tool_global::set_saved_guest_sigmask(guest, published).await;
3551            Ok(Some(FutexSignalWatch {
3552                unblocked: !mask & !kernel_sigset_bit(reverie::PERF_EVENT_SIGNAL as i32),
3553                defers_default_stops: call.restart_keeps_deadline(),
3554                pid: guest.pid().as_raw(),
3555                tid: guest.tid().as_raw(),
3556            }))
3557        } else if signal_interruption {
3558            // A failed read is never the call's errno
3559            // (`read_wait_signal_state`).
3560            let state = read_wait_signal_state(guest.pid(), guest.tid())?;
3561            // A timed futex wait lets a default job-control stop
3562            // wait for its deadline (`KernelSignalState::interrupting_wait`).
3563            let defers_default_stops = call.restart_keeps_deadline();
3564            let host_timed = host_timed_signals(guest).await;
3565            let committed = std::mem::take(committed_wake);
3566            let held = futex_wait_held_after_wake(host_timed, committed);
3567            let pending = futex_wait_pending_signals(
3568                guest,
3569                &state,
3570                state.pending_interrupting(state.blocked, defers_default_stops),
3571                held,
3572                host_timed,
3573                committed,
3574            )
3575            .await;
3576            if pending != 0 {
3577                let errno = call.kernel_restart_errno();
3578                trace!(
3579                    "[detcore, dtid {}] futex wait interrupted by pending signals {:#x}: {:?}",
3580                    &dettid, pending, errno
3581                );
3582                return Err(Error::Errno(errno));
3583            }
3584            Ok(Some(FutexSignalWatch {
3585                unblocked: !state.blocked & !kernel_sigset_bit(reverie::PERF_EVENT_SIGNAL as i32),
3586                defers_default_stops,
3587                pid: guest.pid().as_raw(),
3588                tid: guest.tid().as_raw(),
3589            }))
3590        } else {
3591            Ok(None)
3592        }
3593    }
3594
3595    /// What the scheduler holds for the PI futex `futexid`, whose word holds
3596    /// `word` (`Scheduler::pi_futex_state`), or `None` for a logically killed
3597    /// caller, which the scheduler answers with a plain value and which never
3598    /// returns to the guest.
3599    async fn pi_futex_state<G: Guest<Self>>(
3600        &self,
3601        guest: &mut G,
3602        futexid: &FutexID,
3603        word: u32,
3604    ) -> Option<PiFutexState> {
3605        match futex_action(
3606            guest,
3607            FutexAction::PiState(pi_futex::word_owner(word)),
3608            futexid,
3609            word as i32,
3610            u32::MAX,
3611        )
3612        .await
3613        {
3614            Some(SchedValue::PiFutex(state)) => Some(state),
3615            _ => None,
3616        }
3617    }
3618
3619    /// FUTEX_LOCK_PI, FUTEX_LOCK_PI2 and FUTEX_TRYLOCK_PI, as Linux's
3620    /// `futex_lock_pi` (kernel/futex/pi.c:918-1125 at v6.16), in its order:
3621    /// - a LOCK_PI or LOCK_PI2 timeout is copied and checked first (`EFAULT`,
3622    ///   `EINVAL`), by `sys_futex` (kernel/futex/syscalls.c:168-175). It is an
3623    ///   absolute deadline, on CLOCK_REALTIME for LOCK_PI and on CLOCK_MONOTONIC
3624    ///   for LOCK_PI2 unless it sets FUTEX_CLOCK_REALTIME (`futex_init_timeout`,
3625    ///   syscalls.c:147-157). TRYLOCK_PI takes none (`futex_cmd_has_timeout`);
3626    /// - the word is keyed for writing (`get_futex_key`: `EINVAL` misaligned,
3627    ///   `EFAULT`) and read (`EFAULT`);
3628    /// - `pi_futex::lock_pi_step` decides, as `futex_lock_pi_atomic` does, from
3629    ///   the word and the scheduler's state for it; a word it changes is written
3630    ///   with the guest's permissions (`EFAULT`);
3631    /// - a blocked caller parks as a PI waiter behind the owner. It ends handed
3632    ///   the lock (0), at its deadline (`ETIMEDOUT`), or for a pending signal
3633    ///   with `ERESTARTNOINTR`, which the kernel always restarts (pi.c:1124);
3634    ///   the restarted call starts over and reads its absolute deadline again,
3635    ///   as Linux's does.
3636    ///
3637    /// A waiter handed the lock by its owner's death writes its own TID into the
3638    /// word (`pi_futex::owner_death_word`). A waiter handed the lock and then
3639    /// signalled before its turn keeps the lock, as Linux's
3640    /// `rt_mutex_cleanup_proxy_lock` keeps it (pi.c:1067-1068).
3641    ///
3642    /// The word holds a TID as the caller's PID namespace numbers it. Detcore's
3643    /// thread IDs are the container namespace's, so a caller in a nested PID
3644    /// namespace is refused by name.
3645    ///
3646    /// The read and write of the word are atomic with respect to the guest:
3647    /// Detcore runs one guest thread at a time and this one holds the turn, so
3648    /// none of the kernel's compare-and-exchange retries arises. A word in a
3649    /// file-backed shared mapping, which a process outside Hermit could change,
3650    /// is refused by name, as FUTEX_WAKE_OP's is.
3651    // TODO-HUMAN-REVIEW(#4616): Review the emulated priority-inheritance lock:
3652    // its word transitions, first-in-first-out waiter order and owner-death
3653    // hand-over.
3654    async fn handle_futex_lock_pi<G: Guest<Self>>(
3655        &self,
3656        guest: &mut G,
3657        call: syscalls::Futex,
3658        command: i32,
3659    ) -> Result<i64, Error> {
3660        let trylock = command == libc::FUTEX_TRYLOCK_PI;
3661        let name = pi_lock_command_name(command).expect("a PI lock command");
3662        if guest.thread_state().nested_pid_namespace {
3663            return self
3664                .refuse_futex_command(guest, name, NESTED_PID_NAMESPACE_REFUSAL)
3665                .await;
3666        }
3667        if !trylock && let Some(timeout) = call.timeout() {
3668            let timeout = read_user_timespec(guest, timeout.as_raw())?;
3669            if !futex_timespec_is_valid(timeout) {
3670                return Err(Error::Errno(Errno::EINVAL));
3671            }
3672        }
3673        let private = call.futex_op() & libc::FUTEX_PRIVATE_FLAG != 0;
3674        let uaddr = futex_uaddr(&call);
3675        let futexid = futex_key(guest, uaddr, private, KeyAccess::Write).await?;
3676        if futex_word_is_file_backed(guest, uaddr) {
3677            return self
3678                .refuse_futex_command(guest, name, FILE_BACKED_WORD_REFUSAL)
3679                .await;
3680        }
3681        let dettid = guest.thread_state().dettid;
3682        let caller = pi_futex::guest_tid(dettid);
3683        let gated = guest.config().in_guest_signal_delivery;
3684        let signal_interruption = guest
3685            .config()
3686            .backend
3687            .supports_blocked_wait_signal_interruption
3688            || gated;
3689        // The deadline, computed once, at the first park.
3690        let mut deadline: Option<Option<LogicalTime>> = None;
3691        let mut committed_wake: KernelSigset = 0;
3692        // The signal check made after a wake for a signal, before the word is
3693        // read again.
3694        let mut rewait_watch: Option<Option<FutexSignalWatch>> = None;
3695        let mut requested = false;
3696        let res = loop {
3697            let word = match read_futex_word(guest, uaddr) {
3698                Ok(word) => word as u32,
3699                Err(error) => break Err(error),
3700            };
3701            let Some(state) = self.pi_futex_state(guest, &futexid, word).await else {
3702                break Err(Error::Errno(Errno::ERESTARTNOINTR));
3703            };
3704            let step = pi_futex::lock_pi_step(word, caller, state, trylock);
3705            trace!(
3706                "[detcore, dtid {}] {:?} on the word at {:#x}, holding {:#x}, with {:?}: {:?}",
3707                dettid,
3708                pi_lock_command_name(command),
3709                uaddr,
3710                word,
3711                state,
3712                step
3713            );
3714            let (owner, write) = match step {
3715                LockPiStep::Fail { errno, write } => {
3716                    break write
3717                        .map_or(Ok(()), |value| write_futex_word(guest, uaddr, value))
3718                        .and(Err(Error::Errno(errno)));
3719                }
3720                LockPiStep::Acquire(value) => {
3721                    break write_futex_word(guest, uaddr, value).map(|()| 0);
3722                }
3723                LockPiStep::Block { owner, write } => (owner, write),
3724            };
3725            if let Some(value) = write
3726                && let Err(error) = write_futex_word(guest, uaddr, value)
3727            {
3728                break Err(error);
3729            }
3730            let signal_watch = match rewait_watch.take() {
3731                Some(watch) => watch,
3732                None => match self
3733                    .futex_wait_signal_watch(
3734                        guest,
3735                        call,
3736                        gated,
3737                        signal_interruption,
3738                        &mut committed_wake,
3739                    )
3740                    .await
3741                {
3742                    Ok(watch) => watch,
3743                    Err(error) => break Err(error),
3744                },
3745            };
3746            let timeout = match deadline {
3747                Some(timeout) => timeout,
3748                None => {
3749                    let timeout = if trylock {
3750                        None
3751                    } else {
3752                        match self
3753                            .futex_timeout_deadline(guest, call.futex_op(), call.timeout())
3754                            .await
3755                        {
3756                            Ok(timeout) => timeout,
3757                            Err(error) => break Err(error),
3758                        }
3759                    };
3760                    *deadline.insert(timeout)
3761                }
3762            };
3763            requested = true;
3764            let answer = futex_action(
3765                guest,
3766                FutexAction::PiLockWait(timeout, signal_watch, owner),
3767                &futexid,
3768                word as i32,
3769                u32::MAX,
3770            )
3771            .await;
3772            match futex_action(guest, FutexAction::PiTake, &futexid, 0, u32::MAX).await {
3773                Some(SchedValue::PiTaken(Some(PiHandOver::Unlock))) => break Ok(0),
3774                Some(SchedValue::PiTaken(Some(PiHandOver::OwnerDeath))) => {
3775                    break write_futex_word(guest, uaddr, pi_futex::owner_death_word(caller))
3776                        .map(|()| 0);
3777                }
3778                _ => {}
3779            }
3780            match answer {
3781                Some(SchedValue::TimeOut) => break Err(Error::Errno(Errno::ETIMEDOUT)),
3782                Some(SchedValue::Signaled(committed)) if signal_interruption => {
3783                    // Linux's waiter stays queued unless the signal is still
3784                    // pending in its own turn: check that first, then decide
3785                    // again from the word.
3786                    committed_wake = committed;
3787                    match self
3788                        .futex_wait_signal_watch(
3789                            guest,
3790                            call,
3791                            gated,
3792                            signal_interruption,
3793                            &mut committed_wake,
3794                        )
3795                        .await
3796                    {
3797                        Ok(watch) => rewait_watch = Some(watch),
3798                        Err(error) => break Err(error),
3799                    }
3800                }
3801                // A wake for a signal where the backend does not report the
3802                // kernel's signal state, or a logically killed caller, which
3803                // never returns to the guest: the kernel restarts the call.
3804                _ => break Err(Error::Errno(Errno::ERESTARTNOINTR)),
3805            }
3806        };
3807        if requested {
3808            futex_action(guest, FutexAction::WaitFinished, &futexid, 0, u32::MAX).await;
3809        }
3810        res
3811    }
3812
3813    /// FUTEX_UNLOCK_PI, as Linux's `futex_unlock_pi` (kernel/futex/pi.c:1132-1293
3814    /// at v6.16), in its order: the word is read first (`EFAULT`), only the
3815    /// thread it names may unlock it (`EPERM`), then it is keyed for writing
3816    /// (`EINVAL` misaligned, `EFAULT`), and `pi_futex::unlock_pi_step` decides:
3817    /// 0 for a word with no waiter, a hand-over to the longest PI waiter, whose
3818    /// TID is written with `FUTEX_WAITERS` before it is woken (`wake_futex_pi`),
3819    /// or `EINVAL` where the word and the queued waiters disagree. The word is
3820    /// written with the guest's permissions (`EFAULT`), atomically with respect
3821    /// to the guest for the reason `Self::handle_futex_lock_pi` gives.
3822    // TODO-HUMAN-REVIEW(#4616): Review the emulated priority-inheritance unlock
3823    // and its hand-over to the longest waiter.
3824    async fn handle_futex_unlock_pi<G: Guest<Self>>(
3825        &self,
3826        guest: &mut G,
3827        call: syscalls::Futex,
3828    ) -> Result<i64, Error> {
3829        if guest.thread_state().nested_pid_namespace {
3830            return self
3831                .refuse_futex_command(guest, "FUTEX_UNLOCK_PI", NESTED_PID_NAMESPACE_REFUSAL)
3832                .await;
3833        }
3834        let uaddr = futex_uaddr(&call);
3835        let word = read_futex_word(guest, uaddr)? as u32;
3836        let dettid = guest.thread_state().dettid;
3837        if pi_futex::word_owner(word) != pi_futex::guest_tid(dettid) {
3838            return Err(Error::Errno(Errno::EPERM));
3839        }
3840        let private = call.futex_op() & libc::FUTEX_PRIVATE_FLAG != 0;
3841        let futexid = futex_key(guest, uaddr, private, KeyAccess::Write).await?;
3842        if futex_word_is_file_backed(guest, uaddr) {
3843            return self
3844                .refuse_futex_command(guest, "FUTEX_UNLOCK_PI", FILE_BACKED_WORD_REFUSAL)
3845                .await;
3846        }
3847        let Some(state) = self.pi_futex_state(guest, &futexid, word).await else {
3848            return Err(Error::Errno(Errno::ERESTARTNOINTR));
3849        };
3850        let step = pi_futex::unlock_pi_step(dettid, state);
3851        trace!(
3852            "[detcore, dtid {}] FUTEX_UNLOCK_PI on the word at {:#x}, holding {:#x}, with {:?}: {:?}",
3853            dettid, uaddr, word, state, step
3854        );
3855        match step {
3856            UnlockPiStep::Fail(errno) => Err(Error::Errno(errno)),
3857            UnlockPiStep::Release => write_futex_word(guest, uaddr, 0).map(|()| 0),
3858            UnlockPiStep::HandOff { write, .. } => {
3859                write_futex_word(guest, uaddr, write)?;
3860                // The state was read in this turn, so its next owner is still
3861                // queued; a logically killed caller is answered EINTR instead.
3862                let woken = match futex_action(guest, FutexAction::PiHandOff, &futexid, 0, u32::MAX)
3863                    .await
3864                {
3865                    Some(SchedValue::Value(1)) => 1,
3866                    Some(SchedValue::Value(e)) if e == nix::errno::Errno::EINTR as u64 => 0,
3867                    other => panic!("impossible answer to a PI futex hand-over: {:?}", other),
3868                };
3869                if self.cfg.yields_after_futex_wake(woken) {
3870                    guest.thread_state_mut().yield_after_futex_wake = true;
3871                }
3872                Ok(0)
3873            }
3874        }
3875    }
3876
3877    /// FUTEX_WAKE and FUTEX_WAKE_BITSET, as Linux's `futex_wake`
3878    /// (kernel/futex/waitwake.c): `-EINVAL` for a zero bitset, then the word is
3879    /// keyed for reading (`futex_key`) and never read, and up to `val` modeled
3880    /// waiters whose bitset matches are woken (a count of 0 or below wakes one).
3881    async fn handle_futex_wake<G: Guest<Self>>(
3882        &self,
3883        guest: &mut G,
3884        call: syscalls::Futex,
3885        command: i32,
3886    ) -> Result<i64, Error> {
3887        let bitset = if command == libc::FUTEX_WAKE_BITSET {
3888            call.val3() as u32
3889        } else {
3890            u32::MAX
3891        };
3892        if bitset == 0 {
3893            return Err(Error::Errno(Errno::EINVAL));
3894        }
3895        let private = call.futex_op() & libc::FUTEX_PRIVATE_FLAG != 0;
3896        let futexid = futex_key(guest, futex_uaddr(&call), private, KeyAccess::Read).await?;
3897        let num = match futex_action(
3898            guest,
3899            FutexAction::WakeRequest(call.val()),
3900            &futexid,
3901            0,
3902            bitset,
3903        )
3904        .await
3905        .expect("futex wake must return value")
3906        {
3907            SchedValue::Value(num) => num,
3908            // `futex_wake` stops with EINVAL at a PI waiter, after waking those
3909            // before it (kernel/futex/waitwake.c:180-199).
3910            SchedValue::PiWaiterReached { woken } => {
3911                if self.cfg.yields_after_futex_wake(woken) {
3912                    guest.thread_state_mut().yield_after_futex_wake = true;
3913                }
3914                return Err(Error::Errno(Errno::EINVAL));
3915            }
3916            other => panic!("impossible answer to a futex wake: {:?}", other),
3917        };
3918        let _ = futex_action(guest, FutexAction::WakeFinished(0), &futexid, 0, bitset).await;
3919        if self.cfg.yields_after_futex_wake(num) {
3920            guest.thread_state_mut().yield_after_futex_wake = true;
3921        }
3922        Ok(num as i64)
3923    }
3924
3925    /// FUTEX_REQUEUE and FUTEX_CMP_REQUEUE on plain futexes, as Linux's
3926    /// `futex_requeue` (kernel/futex/requeue.c) with `requeue_pi == 0`, in its
3927    /// order:
3928    /// - `-EINVAL` if `nr_wake` (`val`) or `nr_requeue` (`val2`, passed in the
3929    ///   timeout slot) is negative as an `int`, before any address is looked at;
3930    /// - both words keyed (`futex_key`): `-EINVAL` if misaligned, `-EFAULT` if
3931    ///   outside the user address range or, for a shared key, unmapped;
3932    /// - for FUTEX_CMP_REQUEUE only, the `uaddr` word read with the guest's own
3933    ///   permissions (`-EFAULT`), then `-EAGAIN` if it differs from `val3`.
3934    ///   Plain FUTEX_REQUEUE never reads either word;
3935    /// - wake the first `nr_wake` waiters, move up to `nr_requeue` more onto
3936    ///   `uaddr2` (`Scheduler::requeue_futex_waiters`), and return how many were
3937    ///   woken plus moved.
3938    // TODO-HUMAN-REVIEW(https://github.com/rrnewton/hermit/pull/4020): Review the
3939    // emulated futex requeue and its waiter ordering.
3940    async fn handle_futex_requeue<G: Guest<Self>>(
3941        &self,
3942        guest: &mut G,
3943        call: syscalls::Futex,
3944        command: i32,
3945    ) -> Result<i64, Error> {
3946        let nr_wake = call.val();
3947        let nr_requeue = futex_val2(&call) as i32;
3948        if nr_wake < 0 || nr_requeue < 0 {
3949            return Err(Error::Errno(Errno::EINVAL));
3950        }
3951        let private = call.futex_op() & libc::FUTEX_PRIVATE_FLAG != 0;
3952        let uaddr = futex_uaddr(&call);
3953        let futexid = futex_key(guest, uaddr, private, KeyAccess::Read).await?;
3954        let futexid2 = futex_key(guest, futex_uaddr2(&call), private, KeyAccess::Read).await?;
3955        let mut current = 0;
3956        if command == libc::FUTEX_CMP_REQUEUE {
3957            current = read_futex_word(guest, uaddr)?;
3958            if current != call.val3() {
3959                return Err(Error::Errno(Errno::EAGAIN));
3960            }
3961        }
3962        let woken_and_moved = match futex_action(
3963            guest,
3964            FutexAction::RequeueRequest(futexid2, nr_wake as u32, nr_requeue as u32),
3965            &futexid,
3966            current,
3967            u32::MAX,
3968        )
3969        .await
3970        {
3971            Some(SchedValue::Requeued { woken, moved }) => {
3972                if self.cfg.yields_after_futex_wake(woken) {
3973                    guest.thread_state_mut().yield_after_futex_wake = true;
3974                }
3975                woken + moved
3976            }
3977            // A logically killed or replaced caller is answered with a plain value, as
3978            // its wakes are; it never returns to the guest.
3979            Some(SchedValue::Value(num)) => num,
3980            // `futex_requeue` stops with EINVAL at a PI waiter, keeping what it
3981            // woke and moved before it (kernel/futex/requeue.c:585-603).
3982            Some(SchedValue::PiWaiterReached { woken }) => {
3983                if self.cfg.yields_after_futex_wake(woken) {
3984                    guest.thread_state_mut().yield_after_futex_wake = true;
3985                }
3986                return Err(Error::Errno(Errno::EINVAL));
3987            }
3988            other => panic!("impossible answer to a futex requeue: {:?}", other),
3989        };
3990        let _ = futex_action(
3991            guest,
3992            FutexAction::WakeFinished(0),
3993            &futexid,
3994            current,
3995            u32::MAX,
3996        )
3997        .await;
3998        Ok(woken_and_moved as i64)
3999    }
4000
4001    /// FUTEX_WAKE_OP, as Linux's `futex_wake_op` (kernel/futex/waitwake.c), in
4002    /// its order: key both words (`futex_key`; the `uaddr` word is never read);
4003    /// apply the operation encoded in `val3` to the word at `uaddr2`
4004    /// (`futex_atomic_op_inuser`); wake up to `nr_wake` (`val`) waiters on
4005    /// `uaddr` and, if the encoded comparison of the old value holds, up to
4006    /// `nr_wake2` (`val2`, in the timeout slot) waiters on `uaddr2`. Returns the
4007    /// total woken.
4008    /// - An unknown operation is `-ENOSYS` before the word is touched; an
4009    ///   unknown comparison is `-ENOSYS` after it was written, and nobody is
4010    ///   woken. So a private NULL `uaddr2` (a valid key) with an unknown
4011    ///   operation is `-ENOSYS`.
4012    /// - The word is read and written as exactly four bytes with the guest's own
4013    ///   permissions, so an unreadable or unwritable word is `-EFAULT` and a word
4014    ///   at the end of a page next to a hole is fine.
4015    ///
4016    /// Both wakes count a waiter before comparing, so a count of 0 or below wakes
4017    /// one, and neither looks at the waiters' bitsets, as `futex_wake_op` does not.
4018    /// The read-modify-write is atomic with respect to the guest because Detcore
4019    /// runs one guest thread at a time and this one holds the turn, and only
4020    /// guest threads can reach a private or anonymous shared word. A word in a
4021    /// file-backed shared mapping can also be written by a process outside
4022    /// Hermit, which Linux's locked instruction would not lose, so that case is
4023    /// refused by name before any side effect.
4024    // TODO-HUMAN-REVIEW(https://github.com/rrnewton/hermit/pull/4020): Review the
4025    // emulated FUTEX_WAKE_OP and its read-modify-write of the guest word.
4026    async fn handle_futex_wake_op<G: Guest<Self>>(
4027        &self,
4028        guest: &mut G,
4029        call: syscalls::Futex,
4030    ) -> Result<i64, Error> {
4031        let nr_wake = call.val();
4032        let nr_wake2 = futex_val2(&call) as i32;
4033        let private = call.futex_op() & libc::FUTEX_PRIVATE_FLAG != 0;
4034        let uaddr2 = futex_uaddr2(&call);
4035        let futexid = futex_key(guest, futex_uaddr(&call), private, KeyAccess::Read).await?;
4036        let futexid2 = futex_key(guest, uaddr2, private, KeyAccess::Write).await?;
4037        let op = FutexWakeOp::decode(call.val3() as u32);
4038        if !op.has_known_operation() {
4039            return Err(Error::Errno(Errno::ENOSYS));
4040        }
4041        if futex_word_is_file_backed(guest, uaddr2) {
4042            return self
4043                .refuse_futex_command(guest, "FUTEX_WAKE_OP", FILE_BACKED_WORD_REFUSAL)
4044                .await;
4045        }
4046        let old = read_futex_word(guest, uaddr2)?;
4047        let new = op.apply(old).expect("operation was checked to be known");
4048        write_futex_word(guest, uaddr2, new as u32)?;
4049        let Some(wake2) = op.compare(old) else {
4050            return Err(Error::Errno(Errno::ENOSYS));
4051        };
4052        let mut woken = 0;
4053        for (futex, count, wake) in [(futexid, nr_wake, true), (futexid2, nr_wake2, wake2)] {
4054            if !wake {
4055                continue;
4056            }
4057            woken += match futex_action(guest, FutexAction::WakeRequest(count), &futex, 0, u32::MAX)
4058                .await
4059            {
4060                Some(SchedValue::Value(num)) => num,
4061                // `futex_wake_op` stops with EINVAL at a PI waiter, after the
4062                // operation (kernel/futex/waitwake.c:301-303, 315-317).
4063                Some(SchedValue::PiWaiterReached { woken: num }) => {
4064                    if self.cfg.yields_after_futex_wake(woken + num) {
4065                        guest.thread_state_mut().yield_after_futex_wake = true;
4066                    }
4067                    return Err(Error::Errno(Errno::EINVAL));
4068                }
4069                other => panic!("impossible answer to a futex wake: {:?}", other),
4070            };
4071            let _ = futex_action(guest, FutexAction::WakeFinished(0), &futex, 0, u32::MAX).await;
4072        }
4073        if self.cfg.yields_after_futex_wake(woken) {
4074            guest.thread_state_mut().yield_after_futex_wake = true;
4075        }
4076        Ok(woken as i64)
4077    }
4078
4079    /// Refuse a futex command Detcore cannot serve deterministically, naming it,
4080    /// through the unsupported-syscall policy (`refuse_unserviceable_operation`).
4081    /// A run that is not fail-closed gets `-ENOSYS`, which is what Linux returns
4082    /// for every priority-inheritance command when built without CONFIG_FUTEX_PI
4083    /// (kernel/futex/pi.c, kernel/futex/requeue.c).
4084    async fn refuse_futex_command<G: Guest<Self>>(
4085        &self,
4086        guest: &mut G,
4087        name: &'static str,
4088        reason: &'static str,
4089    ) -> Result<i64, Error> {
4090        tracing::error!(
4091            "[detcore, dtid {}] futex command {} is not supported: {}",
4092            guest.thread_state().dettid,
4093            name,
4094            reason,
4095        );
4096        self.refuse_unserviceable_operation(guest, reverie::syscalls::Sysno::futex, Errno::ENOSYS)
4097            .await
4098    }
4099
4100    /// Futex system call, alternative implemenattion where we treat futexes as InternalIOPolling
4101    /// operations.
4102    pub(crate) async fn handle_futex_polling<G: Guest<Self>>(
4103        &self,
4104        guest: &mut G,
4105        call: syscalls::Futex,
4106        init_val: i32,
4107        deadline: FutexDeadline,
4108    ) -> Result<i64, Error> {
4109        fn make_futex_wake_request(dettid: DetTid) -> Resources {
4110            let mut rsrc = Resources::new(dettid);
4111            rsrc.fyi("futex_wake");
4112            rsrc
4113        }
4114
4115        fn make_futex_wait_request(dettid: DetTid) -> Resources {
4116            let mut rsrc = Resources::new(dettid);
4117            rsrc.insert(ResourceID::InternalIOPolling, Permission::W);
4118            rsrc.fyi("futex_wait");
4119            rsrc
4120        }
4121
4122        let dettid = guest.thread_state().dettid;
4123        let futex_op = call.futex_op() & libc::FUTEX_CMD_MASK;
4124        match futex_op {
4125            libc::FUTEX_WAKE | libc::FUTEX_WAKE_BITSET => {
4126                let rsrc = make_futex_wake_request(dettid);
4127                resource_request(guest, rsrc.clone()).await; // Linearize this operation as a separate COMMIT.
4128                let res = guest.inject(call).await;
4129                // FIXME: With the non-blocking version of futex_wait, `res` will always be 0.  It
4130                // is quite difficult to tell how many polling waiters we unblocked with a given
4131                // wake, without going back to modeling futexes like `handle_futex_blocking` does.
4132                Ok(res?)
4133            }
4134            libc::FUTEX_WAIT | libc::FUTEX_WAIT_BITSET => {
4135                if init_val != call.val() {
4136                    info!(
4137                        "[detcore, dtid {}] Futex wait running immediately because it will fizzle ({} != {}).",
4138                        dettid,
4139                        init_val,
4140                        call.val()
4141                    );
4142                    let res = guest.inject(call).await;
4143                    Ok(res?)
4144                } else {
4145                    let rsrc = make_futex_wait_request(dettid);
4146                    let deadline = self.futex_deadline(guest, call, deadline).await?;
4147                    let res =
4148                        retry_nonblocking_syscall_with_timeout(guest, call, rsrc, deadline).await;
4149                    keep_restart_block(guest, &res, RestartCall::Futex(call), deadline).await;
4150                    let res = res?;
4151                    trace!(
4152                        "[detcore, dtid {}] after futex wait, memory value is {}",
4153                        &dettid,
4154                        guest.memory().read_value(call.uaddr().unwrap()).unwrap()
4155                    );
4156                    Ok(res)
4157                }
4158            }
4159            // `classify_futex_command` already answered every other command.
4160            _ => Err(Error::Errno(Errno::ENOSYS)),
4161        }
4162    }
4163
4164    /// The image `call` runs if it succeeds
4165    /// ([`crate::exec_record::ExecRecord::resolve`]), looked up as the kernel
4166    /// will look it up: through the calling thread's root and working
4167    /// directory, or the directory descriptor, or with `AT_EMPTY_PATH` the
4168    /// descriptor itself. `None` where Detcore cannot see the thread's
4169    /// filesystem ([`Self::guest_task_directory`]) or the kernel would
4170    /// refuse the exec; the new image's `/proc/<pid>/exe` link is then the
4171    /// kernel's.
4172    fn exec_image<G: Guest<Self>>(
4173        &self,
4174        guest: &G,
4175        call: &syscalls::Execveat,
4176    ) -> Option<crate::exec_record::ExecRecord> {
4177        let path: std::path::PathBuf = call.path()?.read(&guest.memory()).ok()?;
4178        self.exec_image_at(guest, call.dirfd(), &path)
4179    }
4180
4181    /// The image this one's exec ran, from the pathname the exec was given
4182    /// (`AT_EXECFN`), resolved as [`Self::exec_image`] resolves an `execve`'s
4183    /// from the working directory, which an exec keeps. For an exec Detcore
4184    /// did not prepare: a backend's copied vfork child runs its `execve`
4185    /// without Detcore. `None` where the auxiliary vector has no such entry
4186    /// or its string cannot be read.
4187    pub(crate) fn exec_image_from_execfn<G: Guest<Self>>(
4188        &self,
4189        guest: &G,
4190    ) -> Option<crate::exec_record::ExecRecord> {
4191        use reverie::syscalls::FromToRaw;
4192        let address = guest.auxv().at_execfn()?.as_raw();
4193        let path: std::path::PathBuf = Option::<syscalls::PathPtr>::from_raw(address)?
4194            .read(&guest.memory())
4195            .ok()?;
4196        self.exec_image_at(guest, libc::AT_FDCWD, &path)
4197    }
4198
4199    /// The name this image's exec gave it as Linux names it from the
4200    /// filename the exec was given (`kbasename(bprm->filename)`,
4201    /// `procfs::task_stat::TaskComm::of_exec_filename`), read from the
4202    /// image's `AT_EXECFN`, which points at a copy of that filename on its
4203    /// initial stack (`create_elf_tables`). `None` where the auxiliary vector
4204    /// has no such entry or its string cannot be read.
4205    pub(crate) fn execfn_comm<G: Guest<Self>>(
4206        &self,
4207        guest: &G,
4208    ) -> Option<crate::procfs::task_stat::TaskComm> {
4209        use std::os::unix::ffi::OsStrExt;
4210
4211        use reverie::syscalls::FromToRaw;
4212        let address = guest.auxv().at_execfn()?.as_raw();
4213        let path: std::path::PathBuf = Option::<syscalls::PathPtr>::from_raw(address)?
4214            .read(&guest.memory())
4215            .ok()?;
4216        Some(crate::procfs::task_stat::TaskComm::of_exec_filename(
4217            path.as_os_str().as_bytes(),
4218        ))
4219    }
4220
4221    /// `Self::exec_image` for a `path` the caller already read from the
4222    /// guest: public for a backend that runs the native `execve` outside
4223    /// Detcore's handler, prepares the exec itself (`crate::prepare_exec`)
4224    /// and reads guest memory its own way.
4225    pub fn exec_image_at<G: Guest<Self>>(
4226        &self,
4227        guest: &G,
4228        dirfd: i32,
4229        path: &std::path::Path,
4230    ) -> Option<crate::exec_record::ExecRecord> {
4231        let task = self.guest_task_directory(guest)?;
4232        let root = std::fs::read_link(task.join("root")).ok()?;
4233        let inside = |link: std::path::PathBuf| {
4234            Some(std::path::Path::new("/").join(link.strip_prefix(&root).ok()?))
4235        };
4236        let cwd = inside(std::fs::read_link(task.join("cwd")).ok()?)?;
4237        let spelled = if path.is_absolute() || dirfd == libc::AT_FDCWD {
4238            cwd.join(path)
4239        } else {
4240            let descriptor = task.join("fd").join(dirfd.to_string());
4241            let directory = inside(std::fs::read_link(descriptor).ok()?)?;
4242            // An empty path names the descriptor itself; without
4243            // `AT_EMPTY_PATH` the exec fails, and the record with it.
4244            if path.as_os_str().is_empty() {
4245                directory
4246            } else {
4247                directory.join(path)
4248            }
4249        };
4250        // An exec of the caller's own `exe` link runs the image it executes,
4251        // which the link names, also where the kernel's link names the
4252        // guest's launcher (SaBRe's loader).
4253        match self.resolve_exe_subject(guest, &spelled, true) {
4254            Some(crate::syscalls::namespace::ExeLinkSubject::Own) => {
4255                return guest.thread_state().exe.as_deref().cloned();
4256            }
4257            // Another process's image, which only the coordinator records.
4258            Some(crate::syscalls::namespace::ExeLinkSubject::Other(_)) => return None,
4259            None => {}
4260        }
4261        crate::exec_record::ExecRecord::resolve(&root, &cwd, &spelled)
4262    }
4263
4264    /// [`Self::exec_image`] where the tool cannot read the guest's procfs
4265    /// directory but sees the guest's files in its own filesystem view
4266    /// (`Detcore::exec_record_root` is `/`: KVM, whose executor opens every
4267    /// absolute guest path in the tool's own view), resolved as that
4268    /// executor resolves the exec (reverie-linuxemu's
4269    /// `resolve_guest_exec_image`): an absolute path as it is, a relative one
4270    /// with a slash from the working directory the calling thread's own
4271    /// `getcwd` gives, and an exec of the caller's own `exe` link spelled
4272    /// under `/proc` the image it executes. Only an ELF image is recorded:
4273    /// `None` for a script (the executor reads its interpreter its own way),
4274    /// for a name without a slash (which the executor searches on `PATH`,
4275    /// where Linux does not), for a path relative to a directory descriptor
4276    /// or empty (`AT_EMPTY_PATH`), for any other path under `/proc` (the
4277    /// tool's `/proc` is not the guest's), in a nested PID namespace, and for
4278    /// the tool's own image, which a path through the tool's `/proc` could
4279    /// have led to. Without a record the new image's `exe` link is the
4280    /// executor's, as before (`Detcore::readlink_own_task_link`); with one,
4281    /// Detcore answers only while it names the image exactly, and the
4282    /// executor holds the image open, so its inode cannot be reused meanwhile.
4283    async fn exec_image_without_procfs<G: Guest<Self>>(
4284        &self,
4285        guest: &mut G,
4286        call: &syscalls::Execveat,
4287    ) -> Option<crate::exec_record::ExecRecord> {
4288        use std::os::unix::ffi::OsStrExt;
4289        use std::os::unix::fs::MetadataExt;
4290        if crate::procfs_inode::tool_can_name_guest_descriptors(&self.cfg.backend)
4291            || guest.thread_state().nested_pid_namespace
4292        {
4293            return None;
4294        }
4295        let root = self.exec_record_root(guest)?;
4296        let path: std::path::PathBuf = call.path()?.read(&guest.memory()).ok()?;
4297        if path.as_os_str().is_empty()
4298            || (!path.is_absolute() && call.dirfd() != libc::AT_FDCWD)
4299            || (!path.is_absolute() && path.components().count() < 2)
4300        {
4301            return None;
4302        }
4303        let cwd = if path.is_absolute() {
4304            None
4305        } else {
4306            let directory = self.injected_working_directory(guest, None, 0).await.ok()?;
4307            Some(std::path::PathBuf::from(
4308                <std::ffi::OsString as std::os::unix::ffi::OsStringExt>::from_vec(directory),
4309            ))
4310            .filter(|directory| directory.is_absolute())
4311        };
4312        let spelled = match &cwd {
4313            Some(cwd) => cwd.join(&path),
4314            None => path.clone(),
4315        };
4316        let own_pid = self.own_detpid(guest).as_raw();
4317        if spelled.starts_with("/proc") {
4318            return match crate::syscalls::namespace::exe_link_subject(
4319                spelled.as_os_str().as_bytes(),
4320                own_pid,
4321            ) {
4322                Some(crate::syscalls::namespace::ExeLinkSubject::Own) => {
4323                    guest.thread_state().exe.as_deref().cloned()
4324                }
4325                _ => None,
4326            };
4327        }
4328        let record = crate::exec_record::ExecRecord::resolve_from(&root, cwd.as_deref(), &path)?;
4329        // The file the path names itself, which the record is only where it
4330        // is no script (a script's record is its interpreter's).
4331        let named = std::fs::metadata(root.join(spelled.strip_prefix("/").ok()?))
4332            .ok()
4333            .map(|metadata| (metadata.dev(), metadata.ino()));
4334        let tool_image = std::fs::metadata("/proc/self/exe")
4335            .ok()
4336            .map(|metadata| (metadata.dev(), metadata.ino()));
4337        (Some(record.identity()) == named && Some(record.identity()) != tool_image)
4338            .then_some(record)
4339    }
4340
4341    /// Execveat system call.  Doesn't return if successful.
4342    pub async fn handle_execveat<G: Guest<Self>>(
4343        &self,
4344        guest: &mut G,
4345        call: syscalls::Execveat,
4346    ) -> Result<i64, Error> {
4347        self.promote_before_native_lifecycle(guest);
4348        let (old_metadata, old_memory_metadata, table_is_shared, dettid, detpid, old_mm_id) = {
4349            let thread_state = guest.thread_state();
4350            (
4351                Arc::clone(&thread_state.file_metadata),
4352                Arc::clone(&thread_state.memory_metadata),
4353                Arc::strong_count(&thread_state.file_metadata) > 1,
4354                thread_state.dettid,
4355                thread_state.detpid.expect("detpid unset"),
4356                thread_state.mm_id,
4357            )
4358        };
4359        let (mut new_metadata, closed_open_files, exec_fd_blocking) = {
4360            let metadata = old_metadata.lock().unwrap();
4361            // The exec'ing thread leads its process from here, alone.
4362            let mut new_metadata = metadata.for_exec(dettid);
4363            new_metadata.hold_alone(detpid);
4364            (
4365                new_metadata.clone(),
4366                metadata.open_files_closed_on_exec(table_is_shared),
4367                new_metadata.exec_blocking_overrides(),
4368            )
4369        };
4370        // The close-on-exec descriptors this exec drops from the old table go
4371        // at the new image's first callback, if the exec succeeds
4372        // (`Detcore::release_exec_dropped_descriptors`).
4373        self.keep_table_for_exec(guest, &old_metadata, &mut new_metadata, detpid);
4374        let preserve_exec_fd_status = guest.config().backend.rediscovers_descriptors_after_exec;
4375        let exe = match self.exec_image(guest, &call) {
4376            Some(exe) => Some(exe),
4377            None => self.exec_image_without_procfs(guest, &call).await,
4378        };
4379
4380        prepare_exec(
4381            guest,
4382            old_mm_id,
4383            if preserve_exec_fd_status {
4384                exec_fd_blocking
4385            } else {
4386                Default::default()
4387            },
4388            // This handler stages the exec's owner-death wakes itself, below.
4389            Vec::new(),
4390            exe,
4391        )
4392        .await;
4393
4394        let mut released_ports = Vec::new();
4395        for open_file_id in closed_open_files {
4396            if let Some(port) = self.release_port_for_open_file(guest, open_file_id).await {
4397                released_ports.push((open_file_id, port));
4398            }
4399        }
4400
4401        // A successful execve walks every robust list in the thread group
4402        // before it replaces the address space (see
4403        // `ThreadState::stage_robust_list_wakes_for_exec`), then clears
4404        // `task->robust_list`; the new image re-registers its own. Read the
4405        // lists while the old image is still mapped.
4406        // Linux marks the owner words itself at a successful exec, and a failed
4407        // exec must leave them alone, so Detcore only reads them here. Only
4408        // where Linux's exec runs under this handler ([`OwnerDeathPath::Kernel`]):
4409        // on DBT Detcore never sees the execve, and a word the native client
4410        // does not rewrite to a TID Linux knows is never marked
4411        // (https://github.com/rrnewton/hermit/issues/4268).
4412        let exec_robust_wakes = match self.robust_owner_death_path() {
4413            Some(OwnerDeathPath::Kernel) => Some(
4414                self.read_thread_group_robust_list_wakes(guest, OwnerDeath::Exec)
4415                    .await,
4416            ),
4417            Some(OwnerDeathPath::Barrier) | None => None,
4418        };
4419        let old_robust_list_head;
4420        // The exec'ing thread leads its process after a successful exec, so
4421        // the table exec gives it is the process's (`process_files`), also
4422        // for a thread that did not lead it before.
4423        let old_process_files = guest.thread_state().process_files;
4424
4425        {
4426            let thread_state = guest.thread_state_mut();
4427            thread_state.file_metadata = Arc::new(Mutex::new(new_metadata));
4428            thread_state.adopt_process_files();
4429            thread_state.memory_metadata = Arc::new(Mutex::new(MemoryMetadata::new()));
4430            thread_state.mm_id = old_mm_id.for_exec(detpid);
4431            old_robust_list_head = thread_state.take_robust_list_for_exec();
4432            if let Some(wakes) = exec_robust_wakes {
4433                thread_state.stage_robust_list_wakes_for_exec(wakes);
4434            }
4435        }
4436
4437        // execve(2) doesn't return upon success.
4438        let errno = self.record_or_replay(guest, call).await.unwrap_err();
4439        // Ask before anything else is injected: the next injection replaces
4440        // the backend's record.
4441        let refusal = guest.take_unsupported_refusal();
4442
4443        {
4444            let thread_state = guest.thread_state_mut();
4445            thread_state.file_metadata = old_metadata;
4446            thread_state.process_files = old_process_files;
4447            thread_state.memory_metadata = old_memory_metadata;
4448            thread_state.mm_id = old_mm_id;
4449            thread_state.restore_robust_list_after_failed_exec(old_robust_list_head);
4450            // The old image runs on, and so do its robust-mutex owners.
4451            thread_state.take_robust_list_wakes_for_exec();
4452        }
4453
4454        cancel_exec(guest).await;
4455        for (open_file_id, port) in released_ports {
4456            self.restore_port_for_open_file(guest, open_file_id, port)
4457                .await;
4458        }
4459
4460        // A backend that cannot perform this exec says so with a typed
4461        // refusal: for example one that cannot promote a worker thread to
4462        // group leader, which refuses after validating the replacement image,
4463        // and also when a host syscall its exec preflight makes (such as the
4464        // KVM backend's faccessat2 permission check) fails with ENOSYS.
4465        // Diagnose it only after normal failed-exec rollback. Any other
4466        // failure, including an ENOSYS the backend did not report as a
4467        // refusal, is the guest's ordinary exec error.
4468        if let Some(refusal) = refusal {
4469            tracing::error!(
4470                "[detcore, dtid {dettid}] {}; the replacement image did not run",
4471                refusal.diagnostic
4472            );
4473            if !self.cfg.panic_on_unsupported_syscalls {
4474                crate::tool_global::report_unsupported_syscall(guest, call.number()).await;
4475            }
4476            return self
4477                .refuse_unserviceable_operation_because(
4478                    guest,
4479                    call.number(),
4480                    errno,
4481                    refusal.diagnostic,
4482                )
4483                .await;
4484        }
4485
4486        Err(errno.into())
4487    }
4488
4489    // AUTONOMOUS-BOT-IMPLEMENTED
4490    // TODO-HUMAN-REVIEW(#258): Confirm one-turn exclusion semantics across scheduler modes.
4491    /// End the current logical timeslice for a sequentialized sched_yield.
4492    pub async fn handle_sched_yield<G: Guest<Self>>(
4493        &self,
4494        guest: &mut G,
4495        call: syscalls::SchedYield,
4496    ) -> Result<i64, Error> {
4497        if self.cfg.sequentialize_threads {
4498            match sched_yield_route(&self.cfg) {
4499                SchedYieldRoute::ChaosReprioritize => {
4500                    let change_time = guest.thread_state().thread_logical_time.as_nanos();
4501                    let request = Self::random_priority_changepoint_request(guest, change_time);
4502                    resource_request(guest, request).await;
4503                }
4504                SchedYieldRoute::ReplayStrongYield => {
4505                    if self.cfg.max_timeslice.is_some() {
4506                        guest
4507                            .thread_state_mut()
4508                            .reset_timeslice_for_explicit_yield();
4509                    }
4510                    let request = Self::sched_yield_request(guest);
4511                    resource_request(guest, request).await;
4512                }
4513                SchedYieldRoute::ChaosYieldEpoch => {
4514                    let request = Self::sched_yield_request(guest);
4515                    resource_request(guest, request).await;
4516                }
4517                SchedYieldRoute::WeakYield
4518                    if !self.cfg.chaos && guest.thread_state().slice_boundary.is_some() =>
4519                {
4520                    // Typed replay must observe this actual syscall's boundary,
4521                    // even if its expected marker is missing. The existing
4522                    // replay close still posts the weak-yield resource.
4523                    self.end_timeslice_for_sched_yield(guest).await;
4524                }
4525                SchedYieldRoute::WeakYield => {
4526                    let request = Self::yield_request(guest);
4527                    resource_request(guest, request).await;
4528                }
4529                SchedYieldRoute::EndTimeslice => self.end_timeslice_for_sched_yield(guest).await,
4530            }
4531            trace!("sched_yield yielded to the scheduler; NOT performing actual syscall");
4532            Ok(0)
4533        } else {
4534            Ok(self.record_or_replay(guest, call).await?)
4535        }
4536    }
4537
4538    /// `child`'s pid as the calling thread names it: its DetTid at depth 0,
4539    /// its pid in the caller's namespace below that
4540    /// (https://github.com/rrnewton/hermit/issues/4090). Detcore picks the
4541    /// child of a wait itself and injects the exact wait, which the kernel
4542    /// reads in the caller's namespace.
4543    async fn guest_visible_pid<G: Guest<Self>>(
4544        &self,
4545        guest: &mut G,
4546        sysno: reverie::syscalls::Sysno,
4547        child: DetPid,
4548    ) -> Result<i32, Error> {
4549        if !guest.thread_state().nested_pid_namespace {
4550            return Ok(child.as_raw());
4551        }
4552        let refusal = match crate::tool_global::local_pid(guest, child).await {
4553            Ok(Some(local)) => return Ok(local),
4554            Ok(None) => format!("child {child} is not visible in the caller's namespace"),
4555            Err(reason) => reason,
4556        };
4557        Err(self.refuse_nested_pid(guest, sysno, &refusal).await)
4558    }
4559
4560    /// The task a pid the kernel answered the calling thread with names: the
4561    /// pid itself at depth 0, translated from the caller's namespace below
4562    /// that, before it enters Detcore's bookkeeping
4563    /// (https://github.com/rrnewton/hermit/issues/4090).
4564    async fn task_of_guest_pid<G: Guest<Self>>(
4565        &self,
4566        guest: &mut G,
4567        sysno: reverie::syscalls::Sysno,
4568        pid: i32,
4569    ) -> Result<DetPid, Error> {
4570        if !guest.thread_state().nested_pid_namespace || pid <= 0 {
4571            return Ok(DetPid::from_raw(pid));
4572        }
4573        let refusal = match crate::tool_global::guest_pid(guest, pid).await {
4574            GuestPidAnswer::Task { task, .. } => return Ok(task),
4575            GuestPidAnswer::NoSuchPid => {
4576                format!("the kernel answered pid {pid}, which no recorded task has")
4577            }
4578            GuestPidAnswer::Untranslatable(reason) => reason,
4579        };
4580        Err(self.refuse_nested_pid(guest, sysno, &refusal).await)
4581    }
4582
4583    /// Track the pidfd a `CLONE_PIDFD` clone wrote at `address` as
4584    /// `pidfd_open`'s is tracked: a close-on-exec pidfd naming the new
4585    /// process, `child`, so that `pidfd_send_signal`, `waitid(P_PIDFD)` and
4586    /// the descriptor calls know it. Before, Detcore did not know the
4587    /// descriptor, and `pidfd_send_signal` on it failed with EBADF
4588    /// (https://github.com/rrnewton/hermit/issues/4430). The kernel installs it
4589    /// in the caller's descriptor table after copying that table for the
4590    /// child, so the child does not have it; the child's Detcore table was
4591    /// copied while the clone ran, so the same holds there.
4592    ///
4593    /// Only a child that shares neither the caller's memory nor its
4594    /// descriptor table, and is no vfork child, gets here
4595    /// (`clone_pidfd_address`), and only with sequentialized threads: no other
4596    /// task can then run between the kernel's clone and this, in the caller's
4597    /// own turn, so neither the clone's arguments nor the word nor the
4598    /// descriptor can change in between. The clone has
4599    /// succeeded whatever happens here, so nothing here fails it: a pidfd
4600    /// that cannot be tracked stays open and unknown to Detcore (EBADF through
4601    /// it, as before), and the reason is logged.
4602    async fn register_clone_pidfd<G: Guest<Self>>(
4603        &self,
4604        guest: &mut G,
4605        address: usize,
4606        child: DetTid,
4607    ) {
4608        let dettid = guest.thread_state().dettid;
4609        let skip = |reason: String| {
4610            tracing::error!(
4611                "[detcore, dtid {}] the pidfd of CLONE_PIDFD child {} is not tracked: {}; pidfd \
4612                 calls through it answer EBADF (https://github.com/rrnewton/hermit/issues/4430)",
4613                dettid,
4614                child,
4615                reason,
4616            );
4617        };
4618        let fd: libc::c_int = match Addr::<libc::c_int>::from_raw(address)
4619            .map(|address| guest.memory().read_value(address))
4620        {
4621            Some(Ok(fd)) => fd,
4622            // The kernel wrote it, or the clone would have failed with EFAULT;
4623            // a task sharing the memory may have unmapped it since.
4624            _ => return skip(format!("its word at {address:#x} cannot be read back")),
4625        };
4626        // The kernel has just allocated it, so a number Detcore already
4627        // tracks means another thread of the caller rewrote the word.
4628        if guest.thread_state().with_detfd(fd, |_| ()).is_ok() {
4629            return skip(format!(
4630                "its word names descriptor {fd}, which Detcore already tracks"
4631            ));
4632        }
4633        if let Err(errno) = self
4634            .track_held_fd(
4635                guest,
4636                fd,
4637                nix::fcntl::OFlag::O_CLOEXEC,
4638                crate::fd::FdType::Pidfd,
4639            )
4640            .await
4641        {
4642            return skip(format!("tracking descriptor {fd} failed with {errno}"));
4643        }
4644        if let Err(error) = guest.thread_state().with_detfd(fd, |detfd| {
4645            detfd.set_pidfd_target(DetPid::from_raw(child.as_raw()))
4646        }) {
4647            skip(format!("descriptor {fd} has no Detcore entry: {error}"));
4648        }
4649    }
4650
4651    /// Refuse, by name and through the unsupported-operation policy, a
4652    /// `clone3` whose `set_tid` asks for an id that a task of this run had
4653    /// (`thread_id_refused`): a reaped child's pid or an exited thread's tid,
4654    /// which Linux would give out again, or a live task's id, whose release
4655    /// can race the call (https://github.com/rrnewton/hermit/issues/4370).
4656    /// Detcore keeps per-task state by that id (the task's clock, its
4657    /// recorded schedule and preemptions, its priority), and a new task given
4658    /// it took over the old one's: the run crashed with "thread time should
4659    /// never go down", or recorded two tasks as one. `None` lets the call go
4660    /// ahead, and the kernel's own answer stands, whenever the call cannot
4661    /// reuse such an id or the kernel would fail it first: no `set_tid`; a
4662    /// `clone_args` size, exit signal or `set_tid` array the kernel rejects
4663    /// (E2BIG, EINVAL, EFAULT); a caller without the capability to choose a
4664    /// pid (EPERM); an id never used; or the pid of a zombie child of the
4665    /// caller's process (EEXIST).
4666    ///
4667    /// The entry that names Detcore's id is `set_tid[0]`, or `set_tid[1]`
4668    /// with `CLONE_NEWPID`, whose `set_tid[0]` is the pid inside the new
4669    /// namespace; any later entries name pids in the namespaces holding
4670    /// Hermit's container. Where those would make Linux fail the call, with
4671    /// EINVAL for more levels than exist or EPERM for a level the caller has
4672    /// no capability over, the refusal is given instead: the conservative
4673    /// side, since the id is not handed out either way. So is a nonzero tail
4674    /// past the fields this code knows, which a kernel that does not know
4675    /// them answers with E2BIG. From a nested PID namespace, whose pids
4676    /// Detcore does not translate for this, any `set_tid` the checks above
4677    /// let through is refused (issue 4155).
4678    async fn clone3_set_tid_refusal<G: Guest<Self>>(
4679        &self,
4680        guest: &mut G,
4681        clone: &syscalls::Clone3,
4682    ) -> Option<Result<i64, Error>> {
4683        // Refuse only a call the kernel would accept as far as `set_tid` goes:
4684        // any other shape goes to the kernel, whose own error stands. The
4685        // exceptions are deliberate, on the conservative side: a struct or
4686        // array Detcore cannot read although the kernel may is refused before
4687        // the kernel's later checks are known (EFAULT under
4688        // --allow-unsupported-syscalls), and so is CLONE_NEWTIME on a kernel
4689        // before 6.3, which refuses it with EINVAL. `struct clone_args`
4690        // carries `set_tid` at offset 64 and `set_tid_size` at offset 72.
4691        // The kernel zero-extends a shorter struct, so one of 73 bytes or
4692        // more can name a set_tid; a shorter one names none or is EINVAL.
4693        const SET_TID_SIZE_OFFSET: usize = 72;
4694        // The kernel's limit on PID namespace nesting (MAX_PID_NS_LEVEL).
4695        const MAX_SET_TID: u64 = 32;
4696        let size = clone.size();
4697        let page = usize::try_from(unsafe { libc::sysconf(libc::_SC_PAGESIZE) }).ok()?;
4698        if size <= SET_TID_SIZE_OFFSET || size > page {
4699            return None;
4700        }
4701        // The kernel copies all `size` bytes first: a fault there is its
4702        // EFAULT. A nonzero tail beyond the fields this code knows is not
4703        // read as a reason to let the call through, since a newer kernel may
4704        // accept it.
4705        // Read as copy_from_user reads it, with the guest's own access
4706        // (`chosen_ids::read_clone_args`). A struct every kernel refuses
4707        // (proved to fault included) goes to the kernel. One Detcore cannot
4708        // read although the kernel may (a page mapped writable, not readable,
4709        // or a mapped page 0) cannot be checked, so it is refused below.
4710        let witness = super::chosen_ids::FaultWitness::for_backend(
4711            &guest.config().backend,
4712            guest.tid().as_raw(),
4713        );
4714        let memory = guest.memory();
4715        let mut raw = match super::chosen_ids::read_clone_args(&memory, witness, clone) {
4716            super::chosen_ids::Clone3Read::Bytes(bytes) => bytes,
4717            super::chosen_ids::Clone3Read::Refused => return None,
4718            super::chosen_ids::Clone3Read::Unreadable { .. } => {
4719                return Some(
4720                    self.refuse_unchecked_set_tid(
4721                        guest,
4722                        "clone3's struct clone_args cannot be read with the guest's own access",
4723                    )
4724                    .await,
4725                );
4726            }
4727        };
4728        // Zero-extended, as the kernel extends a shorter struct.
4729        if raw.len() < super::chosen_ids::CLONE_ARGS_SIZE_VER2 {
4730            raw.resize(super::chosen_ids::CLONE_ARGS_SIZE_VER2, 0);
4731        }
4732        let field = |offset: usize| u64::from_ne_bytes(raw[offset..offset + 8].try_into().unwrap());
4733        let flags = field(0);
4734        let exit_signal = field(32);
4735        let set_tid = field(64);
4736        let set_tid_size = field(72);
4737        if !clone3_signal_fields_valid(flags, exit_signal) {
4738            return None;
4739        }
4740        if set_tid_size == 0 || set_tid_size > MAX_SET_TID {
4741            return None;
4742        }
4743        // The kernel copies the whole array next (EFAULT) and takes no entry
4744        // below 1 (EINVAL).
4745        let mut tids = vec![0 as libc::pid_t; set_tid_size as usize];
4746        if !super::chosen_ids::within_user_memory(
4747            set_tid as usize,
4748            tids.len() * std::mem::size_of::<libc::pid_t>(),
4749        ) {
4750            return None;
4751        }
4752        if set_tid == 0 {
4753            // Linux: EINVAL, before reading anything.
4754            return None;
4755        }
4756        // A fault the kernel must take too is its EFAULT. Otherwise an array
4757        // Detcore cannot read (a page mapped writable, not readable) may
4758        // still be read by the kernel: it is refused below, once the checks
4759        // that need no entry have passed, since its ids cannot be checked.
4760        let read =
4761            super::chosen_ids::read_pid_array(&memory, set_tid as usize, &mut tids).is_some();
4762        if !read
4763            && super::chosen_ids::range_faults(
4764                witness,
4765                set_tid as usize,
4766                tids.len() * std::mem::size_of::<libc::pid_t>(),
4767            )
4768        {
4769            return None;
4770        }
4771        // With CLONE_NEWPID the first entry is the pid in the new namespace,
4772        // which must be 1 since that namespace has no init yet (EINVAL).
4773        let newpid = flags & libc::CLONE_NEWPID as u64 != 0;
4774        if read && (tids.iter().any(|tid| *tid < 1) || (newpid && tids[0] != 1)) {
4775            return None;
4776        }
4777        // Choosing a pid takes CAP_SYS_ADMIN or CAP_CHECKPOINT_RESTORE over
4778        // the namespace, and a caller with neither in its effective set gets
4779        // EPERM. A set that cannot be read counts as privileged, so the
4780        // refusal below still guards Detcore's state.
4781        const CAP_SYS_ADMIN: u32 = 21;
4782        const CAP_CHECKPOINT_RESTORE: u32 = 40;
4783        let thread = guest.thread_state();
4784        let status = format!(
4785            "/proc/{}/task/{}/status",
4786            thread.detpid.expect("detpid unset"),
4787            thread.dettid
4788        );
4789        // Only where host tasks describe guest threads: elsewhere (KVM) that
4790        // file is an unrelated host task's, so the set counts as unread.
4791        let host_readable = matches!(witness, super::chosen_ids::FaultWitness::HostMaps(_));
4792        let capabilities = host_readable
4793            .then(|| std::fs::read_to_string(status).ok())
4794            .flatten()
4795            .and_then(|status| {
4796                status
4797                    .lines()
4798                    .find_map(|line| line.strip_prefix("CapEff:"))
4799                    .and_then(|mask| u64::from_str_radix(mask.trim(), 16).ok())
4800            });
4801        let pid_capabilities = (1u64 << CAP_SYS_ADMIN) | (1u64 << CAP_CHECKPOINT_RESTORE);
4802        if capabilities.is_some_and(|mask| mask & pid_capabilities == 0) {
4803            return None;
4804        }
4805        // From a nested PID namespace the array names pids at several levels,
4806        // which Detcore does not translate for this: refuse any such call.
4807        if guest.thread_state().nested_pid_namespace {
4808            return Some(Err(self
4809                .refuse_nested_pid(
4810                    guest,
4811                    reverie::syscalls::Sysno::clone3,
4812                    "clone3 set_tid names pids in a namespace Detcore does not translate",
4813                )
4814                .await));
4815        }
4816        // The array runs from the new task's namespace outwards, so the
4817        // caller's (the container's) pid is entry 0, or entry 1 with
4818        // CLONE_NEWPID. Entries after it name pids in the namespaces holding
4819        // Hermit's container, which Detcore does not track.
4820        if set_tid_size <= u64::from(newpid) {
4821            return None;
4822        }
4823        if !read {
4824            return Some(
4825                self.refuse_unchecked_set_tid(
4826                    guest,
4827                    "clone3 set_tid's array cannot be read with the guest's own access",
4828                )
4829                .await,
4830            );
4831        }
4832        let tid = tids[usize::from(newpid)];
4833        if !crate::tool_global::thread_id_refused(guest, DetTid::from_raw(tid)).await {
4834            return None;
4835        }
4836        let message = format!(
4837            "hermit: clone3 set_tid asks for id {tid}, which an earlier task of this run had; \
4838             Detcore keeps per-task state by that id and cannot give it to a new task \
4839             (https://github.com/rrnewton/hermit/issues/4370)"
4840        );
4841        tracing::error!(
4842            "[detcore, dtid {}] {}",
4843            guest.thread_state().dettid,
4844            message
4845        );
4846        if guest.config().panic_on_unsupported_syscalls {
4847            use std::io::Write as _;
4848            let _ = writeln!(crate::util::RetryingStderr, "{message}");
4849        }
4850        Some(
4851            self.refuse_unserviceable_operation(
4852                guest,
4853                reverie::syscalls::Sysno::clone3,
4854                Errno::EEXIST,
4855            )
4856            .await,
4857        )
4858    }
4859
4860    /// Refuse a `clone3` whose `set_tid` Detcore cannot check against the
4861    /// run's earlier tasks, because `what` cannot be read with the guest's own
4862    /// access while the kernel may read it: the conservative side, since a
4863    /// reused id would merge two tasks' state in Detcore. With
4864    /// `--allow-unsupported-syscalls` the call fails with `EFAULT`.
4865    async fn refuse_unchecked_set_tid<G: Guest<Self>>(
4866        &self,
4867        guest: &mut G,
4868        what: &str,
4869    ) -> Result<i64, Error> {
4870        let message = format!(
4871            "hermit: {what}, so Detcore cannot check its set_tid against the run's earlier tasks \
4872             (https://github.com/rrnewton/hermit/issues/4370)"
4873        );
4874        tracing::error!(
4875            "[detcore, dtid {}] {}",
4876            guest.thread_state().dettid,
4877            message
4878        );
4879        if guest.config().panic_on_unsupported_syscalls {
4880            use std::io::Write as _;
4881            let _ = writeln!(crate::util::RetryingStderr, "{message}");
4882        }
4883        self.refuse_unserviceable_operation(guest, reverie::syscalls::Sysno::clone3, Errno::EFAULT)
4884            .await
4885    }
4886
4887    /// Refuse by name a call from a nested PID namespace whose pids Detcore
4888    /// cannot translate (https://github.com/rrnewton/hermit/issues/4155).
4889    async fn refuse_nested_pid<G: Guest<Self>>(
4890        &self,
4891        guest: &mut G,
4892        sysno: reverie::syscalls::Sysno,
4893        reason: &str,
4894    ) -> Error {
4895        tracing::error!(
4896            "[detcore, dtid {}] {} from a nested PID namespace is not supported: {} (https://github.com/rrnewton/hermit/issues/4155)",
4897            guest.thread_state().dettid,
4898            sysno,
4899            reason,
4900        );
4901        match self
4902            .refuse_unserviceable_operation(guest, sysno, Errno::ENOSYS)
4903            .await
4904        {
4905            Err(error) => error,
4906            Ok(_) => Errno::ENOSYS.into(),
4907        }
4908    }
4909
4910    /// wait4 system call
4911    /// This is handled by the scheduler and not passed to the record/replay layer.
4912    // TODO-HUMAN-REVIEW(PR-587): Confirm wait4 rusage canonicalization boundaries.
4913    pub async fn handle_wait4<G: Guest<Self>>(
4914        &self,
4915        guest: &mut G,
4916        call: syscalls::Wait4,
4917    ) -> Result<i64, Error> {
4918        let serial_emulated_wait =
4919            guest.config().backend.emulates_child_waits && guest.config().sequentialize_threads;
4920        let mut emulated_wait_consumed = false;
4921        let dettid = guest.thread_state().dettid;
4922        let mut rsrc = Resources::new(dettid);
4923        rsrc.insert(ResourceID::InternalIOPolling, Permission::W);
4924        rsrc.fyi("wait4");
4925
4926        validate_wait4_arguments(call.pid(), call.options())?;
4927
4928        let parent = guest.thread_state().detpid.expect("detpid unset");
4929        // A nested caller names its children by its own namespace's pids
4930        // (https://github.com/rrnewton/hermit/issues/4090). Its process-group
4931        // ids are not translated yet, nor are emulated waits.
4932        let nested = guest.thread_state().nested_pid_namespace;
4933        if nested && (call.pid() == 0 || call.pid() < -1 || serial_emulated_wait) {
4934            return Err(self
4935                .refuse_nested_pid(
4936                    guest,
4937                    reverie::syscalls::Sysno::wait4,
4938                    "a process-group wait, or a wait on a backend that emulates child waits",
4939                )
4940                .await);
4941        }
4942        let named_child = if call.pid() > 0 && nested {
4943            match crate::tool_global::guest_pid(guest, call.pid()).await {
4944                GuestPidAnswer::Task { task, .. } => task,
4945                // No task of the caller's namespace has that pid, so it is
4946                // not a child: Linux answers ECHILD.
4947                GuestPidAnswer::NoSuchPid => return Err(Errno::ECHILD.into()),
4948                GuestPidAnswer::Untranslatable(reason) => {
4949                    return Err(self
4950                        .refuse_nested_pid(guest, reverie::syscalls::Sysno::wait4, &reason)
4951                        .await);
4952                }
4953            }
4954        } else {
4955            DetPid::from_raw(call.pid())
4956        };
4957        // Stop/continue events need a backend waitability callback. Non-SIGCHLD
4958        // process clones also remain legacy until backends distinguish
4959        // PTRACE_EVENT_CLONE from CLONE_THREAD. The common matcher already
4960        // carries those filters so activation does not require another model.
4961        let selector = if !wait4_uses_terminal_selector(call.options(), serial_emulated_wait) {
4962            None
4963        } else {
4964            match call.pid() {
4965                pid if pid > 0 => Some(ChildWaitSelector::Exact(named_child)),
4966                -1 => Some(ChildWaitSelector::Any),
4967                0 => process_group(guest, parent)
4968                    .await
4969                    .map(ChildWaitSelector::ProcessGroup),
4970                pid if pid < -1 => Some(ChildWaitSelector::ProcessGroup(DetPid::from_raw(-pid))),
4971                _ => unreachable!(),
4972            }
4973        };
4974        let spec = selector
4975            .map(|selector| terminal_child_wait_spec(selector, dettid, call.options().bits()));
4976        let complete_lineage = guest.config().backend.tracks_process_children;
4977        let managed_spec = if let Some(spec) = spec {
4978            let (_, has_child) = ready_child_wait(guest, spec).await;
4979            (has_child || complete_lineage).then_some(spec)
4980        } else {
4981            None
4982        };
4983
4984        let value = if call.options().contains(WaitPidFlag::WNOHANG) {
4985            resource_request(guest, rsrc.clone()).await;
4986            info!(
4987                "[dtid {}] Executing non-blocking wait4 in one shot.",
4988                dettid
4989            );
4990            if let Some(spec) = spec {
4991                'select_child: loop {
4992                    let (ready, has_child) = ready_child_wait(guest, spec).await;
4993                    let Some(child) = ready else {
4994                        break if has_child {
4995                            0
4996                        } else if complete_lineage {
4997                            return Err(Errno::ECHILD.into());
4998                        } else {
4999                            guest.inject_with_retry(call).await?
5000                        };
5001                    };
5002                    let _ = await_exact_child_physical_exit(guest, child).await;
5003                    let exact_call = call.with_pid(
5004                        self.guest_visible_pid(guest, reverie::syscalls::Sysno::wait4, child)
5005                            .await?,
5006                    );
5007                    loop {
5008                        let result = if serial_emulated_wait {
5009                            complete_selected_emulated_wait4(guest, exact_call, child).await
5010                        } else {
5011                            guest
5012                                .inject_with_retry(exact_call)
5013                                .await
5014                                .map_err(Error::from)
5015                        };
5016                        emulated_wait_consumed =
5017                            serial_emulated_wait && matches!(result, Ok(value) if value > 0);
5018                        match result {
5019                            Ok(value) if value != 0 => break 'select_child value,
5020                            Ok(_) => yield_once().await,
5021                            Err(Error::Errno(Errno::ECHILD)) => {
5022                                let _ = consume_child_wait(guest, child).await;
5023                                if child_wait_can_retry_after_stale(spec) {
5024                                    continue 'select_child;
5025                                }
5026                                return Err(Errno::ECHILD.into());
5027                            }
5028                            Err(error) => return Err(error),
5029                        }
5030                    }
5031                }
5032            } else {
5033                guest.inject_with_retry(call).await?
5034            }
5035        } else if let Some(spec) = managed_spec {
5036            {
5037                // The ptrace backend must block ordinary signals until child
5038                // readiness is resolved. DBT already delays application signal
5039                // delivery while this callback is active, and replacing its
5040                // application mask here also hides those signals from
5041                // rt_sigpending. Read that mask without changing it instead.
5042                let blocked_mask = blocked_signal_mask();
5043                let mut stack = guest.stack().await;
5044                let blocked_mask_addr = stack.push(blocked_mask);
5045                let old_mask_addr = stack.reserve::<KernelSigset>();
5046                let action_addr = stack.reserve::<KernelSigaction>();
5047                let _mask_guard = stack.commit()?;
5048                let guest_signal_mask =
5049                    block_signals_for_disposition(guest, blocked_mask_addr, old_mask_addr).await?;
5050                let inspect_signal_action = guest
5051                    .config()
5052                    .backend
5053                    .requires_thread_directed_process_signals;
5054
5055                let poll_call = call.with_options(call.options() | WaitPidFlag::WNOHANG);
5056                let mut pending_signal = None;
5057                let result: Result<i64, Error> = loop {
5058                    let status = wait_for_child_lifecycle(guest, spec).await;
5059                    if pending_signal.is_none() {
5060                        pending_signal = wait_signal_disposition(
5061                            guest,
5062                            status,
5063                            &guest_signal_mask,
5064                            action_addr,
5065                            inspect_signal_action,
5066                        )
5067                        .await?;
5068                    }
5069                    let (ready, has_child) = ready_child_wait(guest, spec).await;
5070                    if let Some(child) = ready {
5071                        let _ = await_exact_child_physical_exit(guest, child).await;
5072                        let result = if serial_emulated_wait {
5073                            complete_selected_emulated_wait4(guest, call, child).await
5074                        } else {
5075                            match self
5076                                .guest_visible_pid(guest, reverie::syscalls::Sysno::wait4, child)
5077                                .await
5078                            {
5079                                Ok(pid) => guest
5080                                    .inject_with_retry(call.with_pid(pid))
5081                                    .await
5082                                    .map_err(Error::from),
5083                                Err(error) => Err(error),
5084                            }
5085                        };
5086                        emulated_wait_consumed =
5087                            serial_emulated_wait && matches!(result, Ok(value) if value > 0);
5088                        match result {
5089                            Ok(value) => break Ok(value),
5090                            Err(Error::Errno(Errno::ECHILD)) => {
5091                                let _ = consume_child_wait(guest, child).await;
5092                                if child_wait_can_retry_after_stale(spec) {
5093                                    let (next_ready, _) = ready_child_wait(guest, spec).await;
5094                                    if stale_any_wait_must_interrupt(
5095                                        pending_signal.is_some(),
5096                                        next_ready,
5097                                    ) {
5098                                        break interrupted_child_wait_result(
5099                                            guest,
5100                                            call,
5101                                            pending_signal.expect("signal checked above"),
5102                                        )
5103                                        .await;
5104                                    }
5105                                    continue;
5106                                }
5107                                break Err(Errno::ECHILD.into());
5108                            }
5109                            Err(error) => break Err(error),
5110                        }
5111                    }
5112                    if !has_child {
5113                        break Err(Errno::ECHILD.into());
5114                    }
5115                    if serial_emulated_wait {
5116                        // A broad backend poll can become consuming while the
5117                        // owner callback is still publishing logical/CPU state.
5118                        // Only a selected exact identity may cross that boundary.
5119                        if let Some(disposition) = pending_signal {
5120                            break interrupted_child_wait_result(guest, call, disposition).await;
5121                        }
5122                        continue;
5123                    }
5124                    match guest.inject(poll_call).await {
5125                        Ok(value) => {
5126                            if value > 0 {
5127                                break Ok(value);
5128                            }
5129                            if let Some(disposition) = pending_signal {
5130                                break interrupted_child_wait_result(guest, call, disposition)
5131                                    .await;
5132                            }
5133                        }
5134                        Err(errno) => break Err(errno.into()),
5135                    }
5136                };
5137
5138                restore_signals_after_disposition(guest, old_mask_addr).await?;
5139                result?
5140            }
5141        } else {
5142            // wait4 is a scheduler poll, not a record/replay data read (see doc above),
5143            // so it is not routed through the record/replay subtool.
5144            retry_nonblocking_syscall(guest, call, rsrc, None).await?
5145        };
5146        // The kernel answered in the caller's namespace; the guest gets that
5147        // pid, and Detcore's bookkeeping the task it names.
5148        let reaped = if value > 0 {
5149            self.task_of_guest_pid(guest, reverie::syscalls::Sysno::wait4, value as i32)
5150                .await?
5151        } else {
5152            DetPid::from_raw(value as i32)
5153        };
5154        let consumed_termination = if emulated_wait_consumed || value <= 0 {
5155            false
5156        } else if let Some(status) = call.wstatus() {
5157            wait_status_is_termination(guest.memory().read_value(status)?)
5158        } else if !call
5159            .options()
5160            .intersects(WaitPidFlag::WUNTRACED | WaitPidFlag::WCONTINUED)
5161        {
5162            // Without them, wait4 reports only a child that terminated. (The
5163            // exited child's CPU time is recorded where its own Tool runs,
5164            // which on the in-guest backends is not the parent's, so it
5165            // cannot tell; https://github.com/rrnewton/hermit/issues/4266.)
5166            true
5167        } else {
5168            // A stop or continue report leaves the child running; once it
5169            // has exited, the report was its termination.
5170            guest
5171                .thread_state()
5172                .has_exited_child_process_cpu_time(reaped)
5173                || matches!(
5174                    crate::tool_global::exact_child_wait_state(guest, reaped).await,
5175                    ExactChildWaitState::LogicallyExited
5176                        | ExactChildWaitState::PhysicalExitPending
5177                        | ExactChildWaitState::PhysicallyExited
5178                )
5179        };
5180        if consumed_termination {
5181            guest.thread_state_mut().reap_child_process_cpu_time(reaped);
5182            let _ = consume_child_wait(guest, reaped).await;
5183        }
5184        if value > 0
5185            && let Some(rusage) = call.rusage()
5186        {
5187            // Host CPU and scheduling counters are not deterministic.
5188            let usage: libc::rusage = unsafe { std::mem::zeroed() };
5189            guest.memory().write_value(rusage, &usage)?;
5190        }
5191        Ok(value)
5192    }
5193
5194    // AUTONOMOUS-BOT-IMPLEMENTED
5195    // TODO-HUMAN-REVIEW(#274): Review waitid polling and compatibility boundaries.
5196    // TODO-HUMAN-REVIEW(#2246): Review waitid progress and child-ready signal precedence.
5197    /// waitid system call
5198    /// This is handled by the scheduler and not passed to the record/replay layer.
5199    pub async fn handle_waitid<G: Guest<Self>>(
5200        &self,
5201        guest: &mut G,
5202        mut call: syscalls::Waitid,
5203    ) -> Result<i64, Error> {
5204        // A backend that emulates child waits (today KVM) can delegate ordered
5205        // user copyout to itself on the serial path. Enter before the legacy
5206        // NULL rejection and whole-siginfo writes; ptrace, DBT and
5207        // nonsequential execution retain their existing path.
5208        if guest.config().backend.emulates_child_waits && guest.config().sequentialize_threads {
5209            // Its selector reads the guest's pid as a DetPid
5210            // (`emulated_waitid::handle`), so a nested caller is refused by name
5211            // (https://github.com/rrnewton/hermit/issues/4155).
5212            if guest.thread_state().nested_pid_namespace {
5213                return Err(self
5214                    .refuse_nested_pid(
5215                        guest,
5216                        reverie::syscalls::Sysno::waitid,
5217                        "a wait on a backend that emulates child waits",
5218                    )
5219                    .await);
5220            }
5221            return emulated_waitid::handle(guest, call).await;
5222        }
5223        // A nested caller names its children by its own namespace's pids
5224        // (https://github.com/rrnewton/hermit/issues/4090); its process-group
5225        // ids are not translated yet.
5226        let nested = guest.thread_state().nested_pid_namespace;
5227        if nested && call.which() == libc::P_PGID as i32 {
5228            return Err(self
5229                .refuse_nested_pid(
5230                    guest,
5231                    reverie::syscalls::Sysno::waitid,
5232                    "a process-group wait",
5233                )
5234                .await);
5235        }
5236        let named_child = if nested && call.which() == libc::P_PID as i32 && call.pid() > 0 {
5237            match crate::tool_global::guest_pid(guest, call.pid()).await {
5238                GuestPidAnswer::Task { task, .. } => task,
5239                GuestPidAnswer::NoSuchPid => return Err(Errno::ECHILD.into()),
5240                GuestPidAnswer::Untranslatable(reason) => {
5241                    return Err(self
5242                        .refuse_nested_pid(guest, reverie::syscalls::Sysno::waitid, &reason)
5243                        .await);
5244                }
5245            }
5246        } else {
5247            DetPid::from_raw(call.pid())
5248        };
5249        let dettid = guest.thread_state().dettid;
5250        let mut rsrc = Resources::new(dettid);
5251        rsrc.insert(ResourceID::InternalIOPolling, Permission::W);
5252        rsrc.fyi("waitid");
5253
5254        let event_options = libc::WEXITED | libc::WSTOPPED | libc::WCONTINUED;
5255        let allowed_options = event_options
5256            | libc::WNOHANG
5257            | libc::WNOWAIT
5258            | libc::__WNOTHREAD
5259            | libc::__WALL
5260            | libc::__WCLONE;
5261        if call.options() & event_options == 0 || call.options() & !allowed_options != 0 {
5262            return Err(Errno::EINVAL.into());
5263        }
5264
5265        // POSIX requires non-null infop. Linux accepts null, but that form can
5266        // expose host rusage and requires backend-neutral scratch memory for
5267        // deterministic polling. Reject it uniformly instead of diverging or
5268        // panicking on DBT's unsupported scratch stack.
5269        if call.info().is_none() {
5270            return Err(Errno::EFAULT.into());
5271        }
5272
5273        // Keep clone-class waits on the legacy path until ptrace and KVM both
5274        // register non-SIGCHLD clone children as processes rather than threads.
5275        let terminal_events_only = call.options() & libc::WEXITED != 0
5276            && call.options() & (libc::WSTOPPED | libc::WCONTINUED | libc::__WCLONE | libc::__WALL)
5277                == 0;
5278
5279        // The parked nonterminal path still uses kernel polling. Preserve its
5280        // P_PGID(0) entry snapshot until backend lifecycle events replace it.
5281        if !terminal_events_only && call.which() == libc::P_PGID as i32 && call.pid() == 0 {
5282            call = call.with_pid(snapshot_process_group(guest.pid())?);
5283        }
5284
5285        // A blocking waitid on an O_NONBLOCK pidfd must return EAGAIN rather
5286        // than being converted to WNOHANG. Acquire the scheduler resource first,
5287        // then snapshot fdinfo and issue the one-shot wait without another yield.
5288        let pidfd_nonblocking =
5289            if call.which() == libc::P_PIDFD as i32 && call.options() & libc::WNOHANG == 0 {
5290                resource_request(guest, rsrc.clone()).await;
5291                guest_fd_status_flags(guest.pid(), call.pid())? & libc::O_NONBLOCK != 0
5292            } else {
5293                false
5294            };
5295        if call.which() == libc::P_PIDFD as i32
5296            && call.options() & libc::WNOHANG == 0
5297            && !pidfd_nonblocking
5298        {
5299            // Polling a numeric pidfd cannot preserve Linux's held file
5300            // reference if another thread closes and reuses the descriptor.
5301            // Reject the blocking form until Detcore can retain that identity.
5302            return Err(Errno::EOPNOTSUPP.into());
5303        }
5304        let info = call
5305            .info()
5306            .expect("waitid infop checked above")
5307            .cast::<GuestSiginfo>();
5308
5309        // Unlike wait4, waitid returns zero both when it reports a child event and
5310        // when WNOHANG finds nothing. Polling must inspect si_pid to distinguish
5311        // those cases.
5312        // Known limitation: without backend-neutral scratch memory, an invalid
5313        // non-null infop faults on the first physical poll rather than after a
5314        // child becomes waitable.
5315        // The lifecycle scheduler currently models terminal child events for
5316        // exact and any-child selectors. Group membership and stop/continue
5317        // state remain on the legacy kernel-polling path.
5318        let terminal_spec = if terminal_events_only {
5319            let selector = match call.which() {
5320                which if which == libc::P_PID as i32 => Some(ChildWaitSelector::Exact(named_child)),
5321                which if which == libc::P_ALL as i32 => Some(ChildWaitSelector::Any),
5322                which if which == libc::P_PGID as i32 => {
5323                    let group = if call.pid() == 0 {
5324                        process_group(guest, guest.thread_state().detpid.expect("detpid unset"))
5325                            .await
5326                    } else {
5327                        Some(DetPid::from_raw(call.pid()))
5328                    };
5329                    group.map(ChildWaitSelector::ProcessGroup)
5330                }
5331                _ => None,
5332            };
5333            selector.map(|selector| terminal_child_wait_spec(selector, dettid, call.options()))
5334        } else {
5335            None
5336        };
5337        let complete_lineage = guest.config().backend.tracks_process_children;
5338        let managed_terminal_spec = if let Some(spec) = terminal_spec {
5339            let (_, has_child) = ready_child_wait(guest, spec).await;
5340            (has_child || complete_lineage).then_some(spec)
5341        } else {
5342            None
5343        };
5344
5345        if call.options() & libc::WNOHANG != 0 || pidfd_nonblocking {
5346            if !pidfd_nonblocking {
5347                resource_request(guest, rsrc).await;
5348            }
5349            info!(
5350                "[dtid {}] Executing non-blocking waitid in one shot.",
5351                dettid
5352            );
5353            'select_child: loop {
5354                let selected = if let Some(spec) = terminal_spec {
5355                    let (ready, has_child) = ready_child_wait(guest, spec).await;
5356                    if ready.is_none() && has_child {
5357                        guest.memory().write_value(info, &GuestSiginfo::zeroed())?;
5358                        return Ok(0);
5359                    }
5360                    if ready.is_none() && complete_lineage {
5361                        return Err(Errno::ECHILD.into());
5362                    }
5363                    ready
5364                } else {
5365                    None
5366                };
5367                if let Some(child) = selected {
5368                    let _ = await_exact_child_physical_exit(guest, child).await;
5369                }
5370                let effective_call = match selected {
5371                    Some(child) => call.with_which(libc::P_PID as i32).with_pid(
5372                        self.guest_visible_pid(guest, reverie::syscalls::Sysno::waitid, child)
5373                            .await?,
5374                    ),
5375                    None => call,
5376                };
5377                loop {
5378                    guest.memory().write_value(info, &GuestSiginfo::zeroed())?;
5379                    let value = match guest.inject_with_retry(effective_call).await {
5380                        Ok(value) => value,
5381                        Err(Errno::ECHILD) if selected.is_some() => {
5382                            let child = selected.expect("selected child checked above");
5383                            let _ = consume_child_wait(guest, child).await;
5384                            if terminal_spec.is_some_and(child_wait_can_retry_after_stale) {
5385                                continue 'select_child;
5386                            }
5387                            return Err(Errno::ECHILD.into());
5388                        }
5389                        Err(errno) => return Err(errno.into()),
5390                    };
5391                    let info_value: GuestSiginfo = guest.memory().read_value(info)?;
5392                    let child_pid = info_value.pid();
5393                    if child_pid == 0 && selected.is_some() {
5394                        yield_once().await;
5395                        continue;
5396                    }
5397                    let consumed = child_pid != 0
5398                        && call.options() & libc::WNOWAIT == 0
5399                        && waitid_code_is_termination(info_value.code());
5400                    let child = self
5401                        .task_of_guest_pid(guest, reverie::syscalls::Sysno::waitid, child_pid)
5402                        .await?;
5403                    let result = finish_waitid_result(guest, call, value, info_value.0, child)?;
5404                    if consumed {
5405                        let _ = consume_child_wait(guest, child).await;
5406                    }
5407                    return Ok(result);
5408                }
5409            }
5410        }
5411
5412        {
5413            // A signal can arrive after the scheduler wakes this logical wait but
5414            // before the zero-timeout kernel probe that resolves Linux's
5415            // child-ready-versus-interrupt precedence. The ptrace backend blocks
5416            // ordinary signals across that probe, then restores the guest's exact
5417            // mask before returning. DBT reads the mask without replacing it because
5418            // DynamoRIO already delays application delivery while this callback runs.
5419            // The tracer's private preemption signal must remain unblocked.
5420            let blocked_mask = blocked_signal_mask();
5421            let mut stack = guest.stack().await;
5422            let blocked_mask_addr = stack.push(blocked_mask);
5423            let old_mask_addr = stack.reserve::<KernelSigset>();
5424            let action_addr = stack.reserve::<KernelSigaction>();
5425            let _mask_guard = stack.commit()?;
5426            let guest_signal_mask =
5427                block_signals_for_disposition(guest, blocked_mask_addr, old_mask_addr).await?;
5428            let inspect_signal_action = guest
5429                .config()
5430                .backend
5431                .requires_thread_directed_process_signals;
5432
5433            let poll_call = call.with_options(call.options() | libc::WNOHANG);
5434            let mut pending_signal = None;
5435            let result: Result<i64, Error> = loop {
5436                // Match the polling protocol used by wait4: the first request with
5437                // poll_attempt zero establishes an ordinary runnable turn, while later
5438                // nonzero attempts receive the scheduler's poller backoff. Omitting the
5439                // first request starts directly as a poller and can keep the run queue
5440                // nonempty forever, preventing logical time from reaching a pending
5441                // signal's deadline.
5442                //
5443                // Do not return on Signaled yet. Linux lets an already-waitable child
5444                // status win over an interrupt, so the zero-timeout kernel probe below
5445                // remains authoritative when readiness and a signal coincide.
5446                let managed_spec = managed_terminal_spec;
5447                // Both ways of parking inside waitid -- the scheduler-managed child
5448                // wait and the legacy kernel-polling loop -- can now be resumed with
5449                // the signals that woke the thread, so both consult the guest mask.
5450                let status = if let Some(spec) = managed_spec {
5451                    wait_for_child_lifecycle(guest, spec).await
5452                } else {
5453                    resource_request(guest, rsrc.clone()).await
5454                };
5455                if pending_signal.is_none() {
5456                    pending_signal = wait_signal_disposition(
5457                        guest,
5458                        status,
5459                        &guest_signal_mask,
5460                        action_addr,
5461                        inspect_signal_action,
5462                    )
5463                    .await?;
5464                }
5465                let (ready, has_child) = if let Some(spec) = managed_spec {
5466                    ready_child_wait(guest, spec).await
5467                } else {
5468                    (None, true)
5469                };
5470                if let Some(child) = ready {
5471                    let _ = await_exact_child_physical_exit(guest, child).await;
5472                    if let Err(error) = guest.memory().write_value(info, &GuestSiginfo::zeroed()) {
5473                        break Err(error.into());
5474                    }
5475                    let pid = match self
5476                        .guest_visible_pid(guest, reverie::syscalls::Sysno::waitid, child)
5477                        .await
5478                    {
5479                        Ok(pid) => pid,
5480                        Err(error) => break Err(error),
5481                    };
5482                    let exact_call = call.with_which(libc::P_PID as i32).with_pid(pid);
5483                    match guest.inject_with_retry(exact_call).await {
5484                        Ok(value) => {
5485                            let info_value = match guest.memory().read_value(info) {
5486                                Ok(value) => value,
5487                                Err(error) => break Err(error.into()),
5488                            };
5489                            let reported = match self
5490                                .task_of_guest_pid(
5491                                    guest,
5492                                    reverie::syscalls::Sysno::waitid,
5493                                    info_value.pid(),
5494                                )
5495                                .await
5496                            {
5497                                Ok(task) => task,
5498                                Err(error) => break Err(error),
5499                            };
5500                            break finish_waitid_result(guest, call, value, info_value.0, reported);
5501                        }
5502                        Err(Errno::ECHILD) => {
5503                            let _ = consume_child_wait(guest, child).await;
5504                            if managed_spec.is_some_and(child_wait_can_retry_after_stale) {
5505                                let (next_ready, _) =
5506                                    ready_child_wait(guest, managed_spec.expect("managed spec"))
5507                                        .await;
5508                                if stale_any_wait_must_interrupt(
5509                                    pending_signal.is_some(),
5510                                    next_ready,
5511                                ) {
5512                                    break interrupted_child_wait_result(
5513                                        guest,
5514                                        call,
5515                                        pending_signal.expect("signal checked above"),
5516                                    )
5517                                    .await;
5518                                }
5519                                continue;
5520                            }
5521                            break Err(Errno::ECHILD.into());
5522                        }
5523                        Err(errno) => break Err(errno.into()),
5524                    }
5525                }
5526                if managed_spec.is_some() && !has_child {
5527                    break Err(Errno::ECHILD.into());
5528                }
5529
5530                if let Err(error) = guest.memory().write_value(info, &GuestSiginfo::zeroed()) {
5531                    break Err(error.into());
5532                }
5533                let result = guest.inject(poll_call).await;
5534                match result {
5535                    Ok(value) => {
5536                        let info_value = match guest.memory().read_value(info) {
5537                            Ok(value) => value,
5538                            Err(error) => break Err(error.into()),
5539                        };
5540                        // waitid writes the SIGCHLD variant of siginfo_t. A zeroed
5541                        // structure is used only for the no-event WNOHANG result.
5542                        let child_pid = info_value.pid();
5543                        match exact_wait_poll_decision(
5544                            child_pid != 0,
5545                            pending_signal.is_some(),
5546                            None,
5547                        ) {
5548                            ExactWaitPollDecision::ChildReady => {
5549                                let reported = match self
5550                                    .task_of_guest_pid(
5551                                        guest,
5552                                        reverie::syscalls::Sysno::waitid,
5553                                        child_pid,
5554                                    )
5555                                    .await
5556                                {
5557                                    Ok(task) => task,
5558                                    Err(error) => break Err(error),
5559                                };
5560                                break finish_waitid_result(
5561                                    guest,
5562                                    call,
5563                                    value,
5564                                    info_value.0,
5565                                    reported,
5566                                );
5567                            }
5568                            ExactWaitPollDecision::Interrupted => {
5569                                break interrupted_child_wait_result(
5570                                    guest,
5571                                    call,
5572                                    pending_signal.expect("signal checked above"),
5573                                )
5574                                .await;
5575                            }
5576                            ExactWaitPollDecision::Retry => {}
5577                            ExactWaitPollDecision::AwaitPhysicalExit
5578                            | ExactWaitPollDecision::ReapAfterLogicalExit => unreachable!(),
5579                        }
5580                        if managed_spec.is_some() {
5581                            if !has_child {
5582                                break Ok(value);
5583                            }
5584                            continue;
5585                        }
5586                        rsrc.poll_attempt += 1;
5587                        trace!(
5588                            "Retry #{} for waitid because no child state is ready",
5589                            rsrc.poll_attempt
5590                        );
5591                        record_retry_event(guest, poll_call).await;
5592                    }
5593                    Err(Errno::ERESTARTSYS) if pending_signal.is_some() => {
5594                        break Err(Errno::EINTR.into());
5595                    }
5596                    Err(errno) => break Err(errno.into()),
5597                }
5598            };
5599
5600            restore_signals_after_disposition(guest, old_mask_addr).await?;
5601            if result.is_ok() && call.options() & libc::WNOWAIT == 0 {
5602                let info_value: GuestSiginfo = guest.memory().read_value(info)?;
5603                let child_pid = info_value.pid();
5604                if child_pid != 0 && waitid_code_is_termination(info_value.code()) {
5605                    let child = self
5606                        .task_of_guest_pid(guest, reverie::syscalls::Sysno::waitid, child_pid)
5607                        .await?;
5608                    let _ = consume_child_wait(guest, child).await;
5609                }
5610            }
5611            result
5612        }
5613    }
5614
5615    // AUTONOMOUS-BOT-IMPLEMENTED
5616    // TODO-HUMAN-REVIEW(#546)
5617    /// Accept valid affinity masks without changing the host scheduler.
5618    /// Under `--virtual-cpus` the mask is checked and kept as Linux does
5619    /// (`virtual_sched_setaffinity`); Hermit's historical single CPU accepts
5620    /// any readable mask and stays CPU 0.
5621    pub async fn handle_sched_setaffinity<G: Guest<Self>>(
5622        &self,
5623        guest: &mut G,
5624        call: syscalls::SchedSetaffinity,
5625    ) -> Result<i64, Error> {
5626        if let Some(cpus) = self.cfg.virtual_cpus {
5627            return self
5628                .virtual_sched_setaffinity(guest, call, cpus.get())
5629                .await;
5630        }
5631        let size_bytes = call.len() as usize;
5632        if size_bytes == 0 {
5633            return Err(Errno::EINVAL.into());
5634        }
5635
5636        let mask = call.mask().ok_or(Errno::EFAULT)?;
5637        let mask: Addr<u8> = mask.cast();
5638        let mut requested = [0u8; VIRTUAL_CPUSET_BYTES];
5639        let bytes_to_read = size_bytes.min(VIRTUAL_CPUSET_BYTES);
5640        guest
5641            .memory()
5642            .read_exact(mask, &mut requested[..bytes_to_read])?;
5643        info!(
5644            "Suppressing sched_setaffinity mask {:?}; affinity remains virtual CPU 0",
5645            requested
5646        );
5647        Ok(0)
5648    }
5649
5650    // AUTONOMOUS-BOT-IMPLEMENTED
5651    // TODO-HUMAN-REVIEW(#546)
5652    /// Report that we are on cpu 0, irrespective of what physical CPU we are on.
5653    ///
5654    /// Linux's `sched_getaffinity` accepts any length whose bits cover
5655    /// `nr_cpu_ids` (one, for Hermit's single virtual CPU) and that is a
5656    /// multiple of `sizeof(long)`, copies `min(len, cpumask_size())` bytes and
5657    /// returns that count. Detcore's mask is `VIRTUAL_CPUSET_BYTES` long
5658    /// (`kernel_max` 127), so an 8-byte set, which `CPU_ALLOC` gives for the
5659    /// one CPU that `sysconf(_SC_NPROCESSORS_CONF)` reports, receives its
5660    /// first 8 bytes (<https://github.com/rrnewton/hermit/issues/4160>).
5661    /// Under `--virtual-cpus` it is the named task's mask over the machine's
5662    /// CPUs (`virtual_sched_getaffinity`).
5663    pub async fn handle_sched_getaffinity<G: Guest<Self>>(
5664        &self,
5665        guest: &mut G,
5666        call: syscalls::SchedGetaffinity,
5667    ) -> Result<i64, Error> {
5668        if let Some(cpus) = self.cfg.virtual_cpus {
5669            return self
5670                .virtual_sched_getaffinity(guest, call, cpus.get())
5671                .await;
5672        }
5673        let size_bytes: usize = call.len() as usize;
5674        if size_bytes == 0 || !size_bytes.is_multiple_of(std::mem::size_of::<libc::c_ulong>()) {
5675            return Err(Errno::EINVAL.into());
5676        }
5677        let copied = size_bytes.min(VIRTUAL_CPUSET_BYTES);
5678
5679        // N.B. we can't use an opaque, type-safe representation such as
5680        // nix::sched::CpuSet currently.  The problem is that the
5681        // SchedGetAffinity syscall treats this field as a u64.
5682        let mut cpu_set = [0u8; VIRTUAL_CPUSET_BYTES];
5683        cpu_set[0] = 1;
5684
5685        info!(
5686            "Suppressing sched_getaffinity and returning {}-byte virtualized result, {:?}",
5687            copied,
5688            &cpu_set[..copied]
5689        );
5690        if let Some(mask) = call.mask() {
5691            let mask: AddrMut<u8> = mask.cast();
5692            guest.memory().write_exact(mask, &cpu_set[..copied])?;
5693            // From the man page:
5694            // > On success, the raw sched_getaffinity() system call returns the size (in bytes) of
5695            // > the cpumask_t data type that is used internally by the kernel to represent the CPU
5696            // > set bit mask.
5697            // Linux returns the bytes it copied, the smaller of that size and the caller's.
5698            Ok(copied as i64)
5699        } else {
5700            Err(Error::Errno(Errno::EFAULT))
5701        }
5702    }
5703
5704    /// sched_getparam under Hermit. Detcore replaces the Linux scheduler with its
5705    /// own deterministic one, so a thread's Linux scheduling parameters are
5706    /// inert. Report a fixed SCHED_OTHER priority of 0. The value is emulated
5707    /// (never injected), so it is identical across --verify runs and
5708    /// record/replay.
5709    // AUTONOMOUS-BOT-IMPLEMENTED
5710    // TODO-HUMAN-REVIEW(#720)
5711    pub async fn handle_sched_getparam<G: Guest<Self>>(
5712        &self,
5713        guest: &mut G,
5714        call: syscalls::SchedGetparam,
5715    ) -> Result<i64, Error> {
5716        if let Some(param) = call.param() {
5717            write_sched_getparam(&mut guest.memory(), param)?;
5718        }
5719        Ok(0)
5720    }
5721
5722    /// sched_rr_get_interval under Hermit. The round-robin quantum is a property
5723    /// of the Linux scheduler, which Detcore does not use, so report a fixed zero
5724    /// interval. Being a constant, it is deterministic across --verify and
5725    /// record/replay.
5726    // AUTONOMOUS-BOT-IMPLEMENTED
5727    // TODO-HUMAN-REVIEW(#720)
5728    pub async fn handle_sched_rr_get_interval<G: Guest<Self>>(
5729        &self,
5730        guest: &mut G,
5731        call: syscalls::SchedRrGetInterval,
5732    ) -> Result<i64, Error> {
5733        if let Some(tp) = call.tp() {
5734            let t = Timespec {
5735                tv_sec: 0,
5736                tv_nsec: 0,
5737            };
5738            guest.memory().write_value(tp, &t)?;
5739        }
5740        Ok(0)
5741    }
5742
5743    /// sched_getattr under Hermit. Detcore replaces the Linux scheduler with its
5744    /// own deterministic one, so a thread's Linux scheduling attributes are inert.
5745    /// Report a fixed SCHED_OTHER policy with zeroed nice/priority/flags. The
5746    /// value is emulated (never injected), so it is identical across --verify runs
5747    /// and record/replay. Re-enables `chrt` under --strict.
5748    // AUTONOMOUS-BOT-IMPLEMENTED
5749    // TODO-HUMAN-REVIEW(#791)
5750    pub async fn handle_sched_getattr<G: Guest<Self>>(
5751        &self,
5752        guest: &mut G,
5753        call: syscalls::SchedGetattr,
5754    ) -> Result<i64, Error> {
5755        // `flags` is reserved and must be zero; the kernel rejects anything else.
5756        if call.flags() != 0 {
5757            return Err(Errno::EINVAL.into());
5758        }
5759        // The caller must provide room for at least the base sched_attr (VER0).
5760        let attr_size = std::mem::size_of::<libc::sched_attr>();
5761        if (call.size() as usize) < attr_size {
5762            return Err(Errno::EINVAL.into());
5763        }
5764        let attr = call.attr().ok_or(Errno::EINVAL)?;
5765        // SAFETY: sched_attr is a plain-old-data struct; an all-zero bit pattern is
5766        // a valid SCHED_OTHER descriptor (nice/priority/flags/runtime/... all 0).
5767        let mut sa: libc::sched_attr = unsafe { std::mem::zeroed() };
5768        sa.size = attr_size as u32;
5769        sa.sched_policy = libc::SCHED_OTHER as u32;
5770        // SAFETY: reinterpret the POD struct as its raw bytes to copy it into the
5771        // guest's buffer.
5772        let bytes = unsafe {
5773            std::slice::from_raw_parts(&sa as *const libc::sched_attr as *const u8, attr_size)
5774        };
5775        let dst: AddrMut<u8> = attr.cast();
5776        guest.memory().write_exact(dst, bytes)?;
5777        info!(
5778            "Emulating sched_getattr(pid={}): fixed SCHED_OTHER, nice 0, priority 0",
5779            call.pid()
5780        );
5781        Ok(0)
5782    }
5783
5784    // AUTONOMOUS-BOT-IMPLEMENTED
5785    // TODO-HUMAN-REVIEW(PR-841): Review virtual sched_setattr no-op policy.
5786    /// Linux scheduler attributes cannot affect Detcore's replacement
5787    /// scheduler, so a *well-formed* request is accepted as a deterministic
5788    /// no-op, matching the existing sched_setscheduler and sched_setparam
5789    /// policy.
5790    ///
5791    /// Suppressing the effect is not the same as accepting arguments Linux
5792    /// refuses, nor as refusing arguments Linux accepts. Both directions are
5793    /// guest-visible: a probe that expects EINVAL and sees success takes the
5794    /// wrong branch, and so does one that expects success and sees E2BIG.
5795    ///
5796    /// The order below is the kernel's, and it is guest-visible when two
5797    /// arguments are wrong at once, because the kernel returns the *first*
5798    /// applicable error. `sched_setattr()` screens `uattr`, `pid` and `flags`
5799    /// together; `sched_copy_attr()` then handles the size, the trailing bytes
5800    /// and the util-clamp size rule; the signed-policy test follows; the target
5801    /// pid is resolved *there*, in the middle; and only then does
5802    /// `__sched_setscheduler()` judge the policy, the flags, the priority and
5803    /// the deadline parameters. Putting any of that last group before the pid
5804    /// lookup makes a request against a nonexistent pid report EINVAL where
5805    /// Linux reports ESRCH.
5806    ///
5807    /// # Determinism
5808    ///
5809    /// Every check is a pure function of the guest's own arguments. The one
5810    /// piece of state consulted is the pid lookup, which asks the scheduler's
5811    /// own task table via `thread_is_live` -- Detcore state, replayed
5812    /// identically -- rather than the host's process table, which would leak
5813    /// unrelated host processes into a guest-visible answer.
5814    ///
5815    /// Specifically NOT `tool_global::resolve_kill_targets`, which
5816    /// models `kill(2)` and so recognises only thread-group leaders; asking it
5817    /// this question reports ESRCH for a live non-leader thread.
5818    ///
5819    /// # Deliberately not emulated
5820    ///
5821    /// Three behaviours are excluded, under one rule: **an answer that depends
5822    /// on the host's kernel configuration or the caller's privileges is not
5823    /// reproduced, because a deterministic sandbox must not vary with the
5824    /// machine underneath it.** Each was measured natively and would differ on
5825    /// a differently-built or differently-privileged host:
5826    ///
5827    /// * EPERM for a real-time priority, a SCHED_DEADLINE admission, or a
5828    ///   negative nice, which depends on `CAP_SYS_NICE` and `RLIMIT_RTPRIO`.
5829    /// * EOPNOTSUPP for util-clamp on a VER1 buffer, which depends on
5830    ///   `CONFIG_UCLAMP_TASK`.
5831    /// * The `sysctl_sched_dl_period_{min,max}` bound on SCHED_DEADLINE
5832    ///   periods, which is runtime-tunable.
5833    ///
5834    /// In all three Hermit accepts the request as the same no-op as any other
5835    /// well-formed one. The same rule is why SCHED_EXT (policy 7) is accepted
5836    /// unconditionally even though `valid_policy()` admits it only with
5837    /// `CONFIG_SCHED_CLASS_EXT`: picking one answer keeps the sandbox stable
5838    /// across hosts, and accepting is the choice consistent with suppressing
5839    /// the effect rather than refusing the request.
5840    ///
5841    /// The bracketed regression test in `hermit-cli/tests/sched_setattr_abi.rs`
5842    /// compares Hermit against the running kernel case by case, and therefore
5843    /// deliberately omits exactly these cases -- including them would make its
5844    /// verdict depend on the host it runs on.
5845    pub async fn handle_sched_setattr<G: Guest<Self>>(
5846        &self,
5847        guest: &mut G,
5848        call: syscalls::SchedSetattr,
5849    ) -> Result<i64, Error> {
5850        // `if (!uattr || pid < 0 || flags)` -- one clause in the kernel, so all
5851        // three are EINVAL and none of them is ordered against the others. A
5852        // null pointer is EINVAL rather than EFAULT because it is refused
5853        // before any access is attempted.
5854        if call.flags() != 0 || call.pid() < 0 {
5855            return Err(Errno::EINVAL.into());
5856        }
5857        let attr = call.attr().ok_or(Errno::EINVAL)?;
5858
5859        // `get_user(size, &uattr->size)` -- the declared size is read before
5860        // anything else, and a fault here is EFAULT with no store back.
5861        // `AddrMut` is not `Copy` and `cast` consumes it, so each access below
5862        // re-derives the address from the syscall argument.
5863        // A fault reading guest memory is EFAULT for this syscall. The
5864        // backend's memory layer reports a failed peek as EIO, which is not an
5865        // errno `sched_setattr` can return, so it is mapped here; measured
5866        // native, an unmapped `uattr` gives EFAULT.
5867        let declared: u32 = guest
5868            .memory()
5869            .read_value(attr.cast())
5870            .map_err(|_| Errno::EFAULT)?;
5871
5872        // Both E2BIG exits below run the kernel's `err_size` path, which stores
5873        // the kernel's own struct size into `uattr->size` before returning. A
5874        // guest that reads the field back learns the size it should have sent,
5875        // which is the entire point of the store; the kernel ignores whether it
5876        // succeeds, and so do we.
5877        fn refuse_too_big<S: MemoryAccess>(mut memory: S, attr: AddrMut<libc::c_void>) -> Error {
5878            let _ = memory.write_value(attr.cast::<u32>(), &SCHED_ATTR_KERNEL_SIZE);
5879            Errno::E2BIG.into()
5880        }
5881
5882        let size = match sched_attr_effective_size(declared) {
5883            Ok(size) => size,
5884            Err(()) => {
5885                let back = call.attr().ok_or(Errno::EINVAL)?;
5886                return Err(refuse_too_big(guest.memory(), back));
5887            }
5888        };
5889
5890        // `copy_struct_from_user` with a user size past the kernel's struct:
5891        // every trailing byte must be zero, or the guest is sending a field
5892        // this kernel does not know and must not silently drop. A fault while
5893        // scanning is EFAULT and skips the store back -- only the not-all-zero
5894        // verdict is E2BIG.
5895        if size > SCHED_ATTR_KERNEL_SIZE {
5896            let base: AddrMut<u8> = call.attr().ok_or(Errno::EINVAL)?.cast();
5897            match scan_tail_is_zeroed(
5898                &guest.memory(),
5899                base,
5900                SCHED_ATTR_KERNEL_SIZE as usize,
5901                (size - SCHED_ATTR_KERNEL_SIZE) as usize,
5902            ) {
5903                TailVerdict::AllZero => {}
5904                TailVerdict::NotZeroed => {
5905                    let back = call.attr().ok_or(Errno::EINVAL)?;
5906                    return Err(refuse_too_big(guest.memory(), back));
5907                }
5908                TailVerdict::Faulted => return Err(Errno::EFAULT.into()),
5909            }
5910        }
5911
5912        // Copy the interoperable prefix, zero-filling anything the guest's
5913        // buffer is too short to carry, exactly as `copy_struct_from_user`
5914        // does for a short read.
5915        let copied = std::cmp::min(size, SCHED_ATTR_KERNEL_SIZE) as usize;
5916        let mut raw = [0u8; SCHED_ATTR_KERNEL_SIZE as usize];
5917        let base: AddrMut<u8> = call.attr().ok_or(Errno::EINVAL)?.cast();
5918        guest
5919            .memory()
5920            .read_exact(base, &mut raw[..copied])
5921            .map_err(|_| Errno::EFAULT)?;
5922        let field32 = |offset: usize| -> u32 {
5923            u32::from_ne_bytes(raw[offset..offset + 4].try_into().expect("4 bytes"))
5924        };
5925        let field64 = |offset: usize| -> u64 {
5926            u64::from_ne_bytes(raw[offset..offset + 8].try_into().expect("8 bytes"))
5927        };
5928        let fields = SchedAttrFields {
5929            policy: field32(SCHED_ATTR_OFF_POLICY),
5930            sched_flags: field64(SCHED_ATTR_OFF_FLAGS),
5931            priority: field32(SCHED_ATTR_OFF_PRIORITY),
5932            runtime: field64(SCHED_ATTR_OFF_RUNTIME),
5933            deadline: field64(SCHED_ATTR_OFF_DEADLINE),
5934            period: field64(SCHED_ATTR_OFF_PERIOD),
5935        };
5936
5937        // Everything the kernel decides before it resolves the pid.
5938        validate_sched_attr_before_lookup(size, &fields)?;
5939
5940        // `find_process_by_pid()` sits here, between the two groups of checks,
5941        // and the position is guest-visible: a request that is malformed only
5942        // in a way judged below reports ESRCH rather than EINVAL when the pid
5943        // does not exist. pid 0 means the calling thread, which always exists;
5944        // otherwise ask the scheduler's own task table, so the answer comes
5945        // from Detcore's state rather than from the host's process table.
5946        // `find_task_by_vpid` resolves ANY LIVE TASK, not just a thread-group
5947        // leader. The kill-target resolver is the wrong question here: it
5948        // models `kill(2)`, whose first act is to refuse anything that is not a
5949        // leader, so a perfectly live non-leader thread's tid reported ESRCH.
5950        // A nested caller names the thread by its own namespace's tid
5951        // (https://github.com/rrnewton/hermit/issues/4155).
5952        let named = if call.pid() > 0 && guest.thread_state().nested_pid_namespace {
5953            match crate::tool_global::guest_pid(guest, call.pid()).await {
5954                GuestPidAnswer::Task { task, .. } => Some(task),
5955                GuestPidAnswer::NoSuchPid => None,
5956                GuestPidAnswer::Untranslatable(reason) => {
5957                    return Err(self
5958                        .refuse_nested_pid(guest, reverie::syscalls::Sysno::sched_setattr, &reason)
5959                        .await);
5960                }
5961            }
5962        } else {
5963            Some(DetTid::from_raw(call.pid()))
5964        };
5965        if call.pid() != 0 {
5966            match named {
5967                Some(task) if thread_is_live(guest, task).await => {}
5968                _ => return Err(Errno::ESRCH.into()),
5969            }
5970        }
5971
5972        // And everything `__sched_setscheduler` decides after it.
5973        validate_sched_attr_after_lookup(&fields)?;
5974
5975        info!(
5976            "Suppressing sched_setattr(pid={}, flags={}); Linux scheduler attributes are virtual",
5977            call.pid(),
5978            call.flags()
5979        );
5980        Ok(0)
5981    }
5982
5983    /// ioprio_set under Hermit. Detcore serializes guest threads onto one virtual
5984    /// CPU, so the block-layer I/O scheduling class and priority cannot change
5985    /// guest-visible computation. Accept and suppress the request as a
5986    /// deterministic no-op success, mirroring how sched_setaffinity is handled.
5987    /// Re-enables `ionice` under --strict.
5988    // AUTONOMOUS-BOT-IMPLEMENTED
5989    // TODO-HUMAN-REVIEW(#791)
5990    pub async fn handle_ioprio_set<G: Guest<Self>>(
5991        &self,
5992        _guest: &mut G,
5993        call: syscalls::IoprioSet,
5994    ) -> Result<i64, Error> {
5995        info!(
5996            "Suppressing ioprio_set(which={}, who={}, priority={}); I/O priority is virtual",
5997            call.which(),
5998            call.who(),
5999            call.priority()
6000        );
6001        Ok(0)
6002    }
6003
6004    /// ioprio_get under Hermit. I/O priority is inert under Detcore's serialized
6005    /// scheduler, so process queries observe the fixed raw IOPRIO_CLASS_NONE
6006    /// value while group/user queries observe the effective SCHED_OTHER default
6007    /// of IOPRIO_CLASS_BE/4, without consulting host block-scheduler state.
6008    // AUTONOMOUS-BOT-IMPLEMENTED
6009    // TODO-HUMAN-REVIEW(PR-881)
6010    pub async fn handle_ioprio_get<G: Guest<Self>>(
6011        &self,
6012        _guest: &mut G,
6013        call: syscalls::IoprioGet,
6014    ) -> Result<i64, Error> {
6015        let priority = virtual_ioprio(call.which())?;
6016
6017        info!(
6018            "Emulating ioprio_get(which={}, who={}): fixed priority {}",
6019            call.which(),
6020            call.who(),
6021            priority
6022        );
6023        Ok(priority)
6024    }
6025}
6026
6027/// The one real implementation of [`robust_list::RobustDeathEffects`]: guest
6028/// memory plus a wake against Detcore's modeled futex waiter pool.
6029///
6030/// It exists so the walk itself never mentions `Guest`, and can therefore be
6031/// unit-tested against fake memory.
6032struct GuestRobustEffects<'a, G, T> {
6033    guest: &'a mut G,
6034    dettid: DetTid,
6035    staged_wakes: Option<&'a mut Vec<RobustListWake>>,
6036    /// The words this exit's walks found owned by their dying owner, keyed as
6037    /// futexes (so two mappings of one shared page are one word), with the
6038    /// value Linux's walk stores there. Detcore does not store it, so a later
6039    /// read of the same word, through another entry or another owner's list
6040    /// in the same group exit, sees this value, as it would after Linux's
6041    /// store.
6042    marked: &'a mut HashMap<FutexID, u32>,
6043    tool: PhantomData<T>,
6044}
6045
6046impl<G, T> robust_list::RobustDeathEffects for GuestRobustEffects<'_, G, T>
6047where
6048    G: Guest<Detcore<T>>,
6049    T: RecordOrReplay,
6050{
6051    fn read_u64(&mut self, address: usize) -> Option<u64> {
6052        let at = Addr::<u64>::from_raw(address)?;
6053        self.guest.memory().read_value::<_, u64>(at).ok()
6054    }
6055
6056    fn read_u32(&mut self, address: usize) -> Option<u32> {
6057        let at = Addr::<u32>::from_raw(address)?;
6058        let value = self.guest.memory().read_value::<_, u32>(at).ok()?;
6059        let futexid = self.guest.thread_state().futex_id(address, false);
6060        Some(self.marked.get(&futexid).copied().unwrap_or(value))
6061    }
6062
6063    fn compare_and_swap(
6064        &mut self,
6065        address: usize,
6066        expected: u32,
6067        desired: u32,
6068    ) -> robust_list::FutexCasOutcome {
6069        use robust_list::FutexCasOutcome;
6070
6071        let Some(read_at) = Addr::<u32>::from_raw(address) else {
6072            return FutexCasOutcome::Faulted;
6073        };
6074        // Detcore never stores the owner word itself. It re-reads the word to
6075        // decide whether the kernel's retry loop would take another pass, then
6076        // leaves the replacement to the native task exit, which every
6077        // [`OwnerDeathPath`] completes before another modeled thread runs.
6078        let observed = match self.guest.memory().read_value::<_, u32>(read_at) {
6079            Ok(value) => value,
6080            Err(_) => return FutexCasOutcome::Faulted,
6081        };
6082        let futexid = self.guest.thread_state().futex_id(address, false);
6083        let observed = self.marked.get(&futexid).copied().unwrap_or(observed);
6084        if observed != expected {
6085            return FutexCasOutcome::Changed(observed);
6086        }
6087        self.marked.insert(futexid, desired);
6088        // No other scheduler turn runs until Linux has repeated the owner
6089        // check and changed the word atomically. Do not perform a separate
6090        // write here: a process outside Hermit's scheduler can share the
6091        // mapping and acquire the mutex between this read and that write.
6092        // Deliberately DEBUG, not INFO: this line carries a raw guest address,
6093        // and INFO is the surface `--verify-strict` compares.
6094        debug!(
6095            "[detcore, dtid {}] robust-list owner death: leaving futex word {:#x} for backend exit cleanup",
6096            self.dettid, address,
6097        );
6098        FutexCasOutcome::Matched
6099    }
6100
6101    async fn wake_one(&mut self, address: usize, observed: u32) {
6102        // glibc always issues robust-mutex futex operations with the shared
6103        // flag, and so does the kernel's owner-death wake; resolve the same key.
6104        let futexid = self.guest.thread_state().futex_id(address, false);
6105        if let Some(wakes) = self.staged_wakes.as_mut() {
6106            wakes.push(RobustListWake { futex: futexid });
6107            debug!(
6108                "[detcore, dtid {}] staged robust-list owner-death wake until physical exit",
6109                self.dettid,
6110            );
6111            return;
6112        }
6113        let woken = match futex_action(
6114            self.guest,
6115            FutexAction::WakeRequest(1),
6116            &futexid,
6117            observed as i32,
6118            u32::MAX,
6119        )
6120        .await
6121        {
6122            Some(SchedValue::Value(count)) => count,
6123            // A FUTEX_LOCK_PI waiter first in the queue stops the wake: Linux's
6124            // `futex_wake` answers EINVAL, which `handle_futex_death` ignores.
6125            Some(SchedValue::PiWaiterReached { woken }) => woken,
6126            // A wake never carries a timeout, and a cancelled RPC wakes nobody.
6127            Some(SchedValue::TimeOut)
6128            | Some(SchedValue::Signaled(_))
6129            | Some(SchedValue::Requeued { .. })
6130            | Some(SchedValue::PiFutex(_))
6131            | Some(SchedValue::PiTaken(_))
6132            | None => 0,
6133        };
6134        // Guest-level identities only: dettid, the modeled futex key, and a
6135        // count. No host pointers and no iteration order leak into this line,
6136        // which is compared exactly under `--verify-strict`.
6137        info!(
6138            "[detcore, dtid {}] robust-list owner death woke {} waiter(s) on futex {:?}",
6139            self.dettid, woken, futexid,
6140        );
6141        let _ = futex_action(
6142            self.guest,
6143            FutexAction::WakeFinished(0),
6144            &futexid,
6145            observed as i32,
6146            u32::MAX,
6147        )
6148        .await;
6149    }
6150}
6151
6152/// How a thread group's robust-mutex owners die, for
6153/// `Detcore::read_thread_group_robust_list_wakes`.
6154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6155enum OwnerDeath {
6156    /// exit_group or a fatal signal: every thread exits with its own TID.
6157    Exit,
6158    /// A successful execve: the other threads exit with their own TIDs, and the
6159    /// caller's list is walked under the TID it holds after `de_thread`.
6160    Exec,
6161}
6162
6163/// `Detcore::robust_owner_death_path` for `cfg`.
6164///
6165/// The Barrier path needs three properties of a backend: an exit callback that
6166/// runs before the native exit, a native exit that runs Linux's own robust
6167/// walk, and a registered host pidfd for every thread, which the scheduler's
6168/// exited-thread hold polls. `requires_thread_directed_process_signals` is
6169/// the flag that makes Detcore refuse a thread without host IDs, so it implies
6170/// the third; today only DBT sets it, and DBT has the other two.
6171/// `owner_death_path_is_tied_to_the_backends_that_have_its_properties` fails
6172/// if another backend starts to set it (Claude review of
6173/// https://github.com/rrnewton/hermit/pull/4277 at 65138a17, N4).
6174fn owner_death_path(cfg: &Config) -> Option<OwnerDeathPath> {
6175    if !cfg.sequentialize_threads || cfg.debug_futex_mode != BlockingMode::Precise {
6176        return None;
6177    }
6178    if cfg.backend.runs_exit_robust_list {
6179        Some(OwnerDeathPath::Kernel)
6180    } else if cfg.backend.requires_thread_directed_process_signals {
6181        Some(OwnerDeathPath::Barrier)
6182    } else {
6183        None
6184    }
6185}
6186
6187/// How Detcore orders its modeled owner-death wakes after Linux's own walk.
6188/// In both, Linux changes each owner word to `FUTEX_OWNER_DIED` in the native
6189/// exit, and Detcore only reads the words.
6190#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6191enum OwnerDeathPath {
6192    /// The backend's exit callback comes after the native exit
6193    /// (`BackendCapabilities::runs_exit_robust_list`, ptrace). Wakes for a
6194    /// whole group's death wait for the owners' exit callbacks.
6195    Kernel,
6196    /// The backend reports a thread's exit before the native exit runs, and
6197    /// registers a pidfd for every host thread
6198    /// (`BackendCapabilities::requires_thread_directed_process_signals`, DBT).
6199    /// The scheduler then commits no turn until the kernel has ended a thread
6200    /// that exited by its own grant, or any thread of a group whose exit_group
6201    /// was granted (`Scheduler::exited_thread_barriers`). An exit_group's
6202    /// killed threads also hold their native exits until the group's walk is
6203    /// done (`ThreadState::wait_for_exit_group_robust_walk`), so that walk reads
6204    /// every word as the guest left it.
6205    Barrier,
6206}
6207
6208/// How precise and polling futex modes answer a futex command, decided as
6209/// Linux's `do_futex` (kernel/futex/syscalls.c) decides it, before the futex
6210/// address is looked at.
6211#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6212pub(crate) enum FutexCommand {
6213    /// Emulated: FUTEX_WAIT, FUTEX_WAIT_BITSET, FUTEX_WAKE, FUTEX_WAKE_BITSET,
6214    /// FUTEX_REQUEUE, FUTEX_CMP_REQUEUE, FUTEX_WAKE_OP, and the
6215    /// priority-inheritance lock commands FUTEX_LOCK_PI, FUTEX_LOCK_PI2,
6216    /// FUTEX_TRYLOCK_PI and FUTEX_UNLOCK_PI (`Detcore::handle_futex_lock_pi`,
6217    /// `Detcore::handle_futex_unlock_pi`).
6218    Served,
6219    /// `-ENOSYS`, exactly as Linux: FUTEX_CLOCK_REALTIME with any command but
6220    /// FUTEX_WAIT_BITSET, FUTEX_WAIT_REQUEUE_PI or FUTEX_LOCK_PI2; the removed
6221    /// FUTEX_FD; and any unknown command.
6222    NoSys,
6223    /// FUTEX_WAIT_REQUEUE_PI or FUTEX_CMP_REQUEUE_PI, the requeue-PI pair that
6224    /// moves a condition variable's waiters onto a PI mutex. Detcore does not
6225    /// emulate it; refused by name (`Detcore::refuse_futex_command`). glibc's
6226    /// condition variables have not issued it since glibc 2.25: they wait on
6227    /// their own words and then lock the mutex.
6228    RequeuePi(&'static str),
6229}
6230
6231/// [`FutexCommand`] for a raw `futex_op`.
6232// AUTONOMOUS-BOT-IMPLEMENTED
6233pub(crate) fn classify_futex_command(futex_op: i32) -> FutexCommand {
6234    let cmd = futex_op & libc::FUTEX_CMD_MASK;
6235    if futex_op & libc::FUTEX_CLOCK_REALTIME != 0
6236        && !matches!(
6237            cmd,
6238            libc::FUTEX_WAIT_BITSET | libc::FUTEX_WAIT_REQUEUE_PI | libc::FUTEX_LOCK_PI2
6239        )
6240    {
6241        return FutexCommand::NoSys;
6242    }
6243    match cmd {
6244        libc::FUTEX_WAIT
6245        | libc::FUTEX_WAIT_BITSET
6246        | libc::FUTEX_WAKE
6247        | libc::FUTEX_WAKE_BITSET
6248        | libc::FUTEX_REQUEUE
6249        | libc::FUTEX_CMP_REQUEUE
6250        | libc::FUTEX_WAKE_OP
6251        // AUTONOMOUS-BOT-IMPLEMENTED
6252        | libc::FUTEX_LOCK_PI
6253        | libc::FUTEX_LOCK_PI2
6254        | libc::FUTEX_TRYLOCK_PI
6255        | libc::FUTEX_UNLOCK_PI => FutexCommand::Served,
6256        libc::FUTEX_WAIT_REQUEUE_PI => FutexCommand::RequeuePi("FUTEX_WAIT_REQUEUE_PI"),
6257        libc::FUTEX_CMP_REQUEUE_PI => FutexCommand::RequeuePi("FUTEX_CMP_REQUEUE_PI"),
6258        _ => FutexCommand::NoSys,
6259    }
6260}
6261
6262/// Why `--debug-futex-mode polling` refuses FUTEX_REQUEUE, FUTEX_CMP_REQUEUE,
6263/// FUTEX_WAKE_OP and the priority-inheritance lock commands.
6264const POLLING_REFUSAL: &str = "--debug-futex-mode polling does not model futex waiter lists";
6265
6266/// The name of a priority-inheritance lock command (FUTEX_LOCK_PI,
6267/// FUTEX_LOCK_PI2, FUTEX_TRYLOCK_PI, FUTEX_UNLOCK_PI), or `None` for any other.
6268fn pi_lock_command_name(command: i32) -> Option<&'static str> {
6269    match command {
6270        libc::FUTEX_LOCK_PI => Some("FUTEX_LOCK_PI"),
6271        libc::FUTEX_LOCK_PI2 => Some("FUTEX_LOCK_PI2"),
6272        libc::FUTEX_TRYLOCK_PI => Some("FUTEX_TRYLOCK_PI"),
6273        libc::FUTEX_UNLOCK_PI => Some("FUTEX_UNLOCK_PI"),
6274        _ => None,
6275    }
6276}
6277
6278/// Whether the futex word at `address` lies in a file-backed shared mapping,
6279/// which a process outside Hermit can update concurrently. Detcore changes a
6280/// word with a separate read and write, which Linux's locked instruction does
6281/// not, so the commands that change their word refuse such a one.
6282fn futex_word_is_file_backed<T: RecordOrReplay, G: Guest<Detcore<T>>>(
6283    guest: &G,
6284    address: usize,
6285) -> bool {
6286    matches!(
6287        guest.thread_state().futex_id(address, false),
6288        FutexID::Shared {
6289            object: SharedMemoryObjectId::File { .. } | SharedMemoryObjectId::OpenFile { .. },
6290            ..
6291        }
6292    )
6293}
6294
6295/// Write a futex word as a user-mode store does, which is what Linux's
6296/// `futex_cmpxchg_value_locked` and `futex_atomic_op_inuser` do: exactly four
6297/// bytes with the guest's own permissions, `-EFAULT` if the word cannot be
6298/// written.
6299fn write_futex_word<T: RecordOrReplay, G: Guest<Detcore<T>>>(
6300    guest: &mut G,
6301    address: usize,
6302    value: u32,
6303) -> Result<(), Error> {
6304    let word = AddrMut::<u32>::from_raw(address).ok_or(Errno::EFAULT)?;
6305    guest
6306        .memory()
6307        .write_value(word, &value)
6308        .map_err(|_| Error::Errno(Errno::EFAULT))
6309}
6310
6311/// Why the priority-inheritance lock commands refuse a caller in a nested PID
6312/// namespace, whose futex words hold TIDs Detcore does not translate.
6313const NESTED_PID_NAMESPACE_REFUSAL: &str = "the caller is in a nested PID namespace, whose \
6314     priority-inheritance futex words hold TIDs Detcore does not translate";
6315
6316/// Why a command that changes its futex word refuses a word in a file-backed
6317/// shared mapping (`futex_word_is_file_backed`).
6318const FILE_BACKED_WORD_REFUSAL: &str = "its word is in a file-backed shared mapping that a process \
6319     outside Hermit could update concurrently, and Detcore cannot apply the operation atomically";
6320
6321/// The `val2` of FUTEX_REQUEUE, FUTEX_CMP_REQUEUE and FUTEX_WAKE_OP, which the
6322/// syscall passes in the timeout argument: `sys_futex` hands
6323/// `(unsigned long)utime` to `do_futex` as a `u32`, and the commands read it as
6324/// an `int`.
6325fn futex_val2(call: &syscalls::Futex) -> u32 {
6326    call.timeout().map_or(0, Addr::as_raw) as u32
6327}
6328
6329/// The raw `uaddr2` argument; NULL is address 0, as Linux keys it.
6330fn futex_uaddr2(call: &syscalls::Futex) -> usize {
6331    call.uaddr2().map_or(0, AddrMut::as_raw)
6332}
6333
6334/// The raw `uaddr` of a futex call; NULL is a valid private key for the
6335/// commands that only key their words.
6336fn futex_uaddr(call: &syscalls::Futex) -> usize {
6337    call.uaddr().map_or(0, AddrMut::as_raw)
6338}
6339
6340/// How a command acquires a futex key: Linux's `enum futex_access`.
6341#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6342enum KeyAccess {
6343    /// `FUTEX_READ`: the word is only compared or woken.
6344    Read,
6345    /// `FUTEX_WRITE`: the word is changed (WAKE_OP's second word).
6346    Write,
6347}
6348
6349/// The highest address `access_ok` accepts for a futex word on this host,
6350/// `USER_PTR_MAX` (arch/x86/include/asm/uaccess_64.h). It depends on the
6351/// paging mode the kernel booted with, so it is measured once from the host
6352/// kernel, which is also the guest's kernel: a private FUTEX_WAKE with count 0
6353/// keys the word without touching it and fails with EFAULT exactly when
6354/// `access_ok` refuses it.
6355pub(crate) fn user_ptr_max() -> usize {
6356    static MAX: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
6357    *MAX.get_or_init(|| {
6358        let accepted = |address: usize| {
6359            // SAFETY: a private FUTEX_WAKE with count 0 never accesses `address`.
6360            let ret = unsafe {
6361                libc::syscall(
6362                    libc::SYS_futex,
6363                    address,
6364                    libc::FUTEX_WAKE | libc::FUTEX_PRIVATE_FLAG,
6365                    0,
6366                    0,
6367                    0,
6368                    0,
6369                )
6370            };
6371            ret >= 0
6372        };
6373        // Binary search over 4-aligned addresses: 0 is accepted, and an address
6374        // past the largest user address space never is. The bound stays below
6375        // any tag bits (arm64's top byte, x86's bits 57 to 62), so the search
6376        // stays monotone even if Hermit itself ran with tagging enabled.
6377        let bad_start = if cfg!(target_arch = "aarch64") {
6378            1usize << 55
6379        } else {
6380            1usize << 57
6381        };
6382        let (mut ok, mut bad) = (0usize, bad_start);
6383        while bad - ok > 4 {
6384            let mid = (ok + (bad - ok) / 2) & !3;
6385            if accepted(mid) { ok = mid } else { bad = mid }
6386        }
6387        ok
6388    })
6389}
6390
6391/// The futex key of a word that a command keys, as Linux's `get_futex_key`
6392/// (kernel/futex/core.c) admits it, without reading the word:
6393/// - `-EINVAL` if it is not 4-aligned;
6394/// - `-EFAULT` if `access_ok` refuses it (`user_ptr_max`);
6395/// - a private key needs nothing more, so NULL is a valid one;
6396/// - a shared key needs its backing page, acquired for reading or writing.
6397///   Which pages the kernel can acquire (a copy-on-write page made read-only, a
6398///   page past the end of a truncated file, a read-only anonymous page) is not
6399///   visible in `/proc` metadata, so the kernel is asked with an effect-free
6400///   probe on the guest's own address (`probe_shared_futex_key`).
6401async fn futex_key<G: Guest<Detcore<T>>, T: RecordOrReplay>(
6402    guest: &mut G,
6403    address: usize,
6404    private: bool,
6405    access: KeyAccess,
6406) -> Result<FutexID, Error> {
6407    if !address.is_multiple_of(std::mem::size_of::<u32>()) {
6408        return Err(Error::Errno(Errno::EINVAL));
6409    }
6410    if address > user_ptr_max() {
6411        return Err(Error::Errno(Errno::EFAULT));
6412    }
6413    if !private {
6414        probe_shared_futex_key(guest, address, access).await?;
6415    }
6416    Ok(guest.thread_state().futex_id(address, private))
6417}
6418
6419/// Whether the kernel admits `address` as a shared futex key with `access`,
6420/// asked with a futex call injected into the guest that runs `get_futex_key`
6421/// and has no other effect, even on a waiter outside Hermit:
6422/// - a READ key: a non-private FUTEX_REQUEUE from the word to itself with both
6423///   counts 0. `futex_requeue` acquires both keys for reading, and its loop
6424///   then stops before the first waiter (`task_count - nr_wake >= nr_requeue`).
6425///   A FUTEX_WAKE with count 0 would not do: it still wakes one waiter;
6426/// - a WRITE key: a non-private FUTEX_WAKE_OP from the word to itself with an
6427///   unknown operation. `futex_wake_op` acquires the first key for reading and
6428///   the second for writing, then `futex_atomic_op_inuser` returns -ENOSYS for
6429///   the unknown operation before it touches the word or wakes anyone.
6430///
6431/// Linux's refusal (-EFAULT, or -EINVAL) is returned as is.
6432async fn probe_shared_futex_key<G: Guest<Detcore<T>>, T: RecordOrReplay>(
6433    guest: &mut G,
6434    address: usize,
6435    access: KeyAccess,
6436) -> Result<(), Error> {
6437    let probe = shared_futex_key_probe(address, access);
6438    match run_shared_futex_key_probe(guest, probe, access, |guest, probe| guest.inject(probe)).await
6439    {
6440        SharedKeyProbe::Admitted => Ok(()),
6441        SharedKeyProbe::Refused(errno) => Err(Error::Errno(errno)),
6442        SharedKeyProbe::Interrupted(errno) => {
6443            refuse_interrupted_shared_key_probe(guest, address, errno).await
6444        }
6445    }
6446}
6447
6448/// The effect-free futex call that asks the kernel whether `address` is a
6449/// shared key with `access` (see `probe_shared_futex_key`).
6450fn shared_futex_key_probe(address: usize, access: KeyAccess) -> syscalls::Futex {
6451    let word = AddrMut::<libc::c_int>::from_raw(address);
6452    match access {
6453        KeyAccess::Read => syscalls::Futex::new()
6454            .with_uaddr(word)
6455            .with_futex_op(libc::FUTEX_REQUEUE)
6456            .with_val(0)
6457            .with_uaddr2(word),
6458        KeyAccess::Write => syscalls::Futex::new()
6459            .with_uaddr(word)
6460            .with_futex_op(libc::FUTEX_WAKE_OP)
6461            .with_val(0)
6462            .with_uaddr2(word)
6463            .with_val3((7 << 28) | (1 << 12)),
6464    }
6465}
6466
6467/// The pending answer to one injected shared-key probe.
6468type KeyProbeAnswer<'a> =
6469    std::pin::Pin<Box<dyn std::future::Future<Output = Result<i64, Errno>> + Send + 'a>>;
6470
6471/// What one injected shared-key probe answered.
6472#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6473enum SharedKeyProbe {
6474    /// The kernel admitted the key: 0, or ENOSYS for a WRITE key (the unknown
6475    /// WAKE_OP operation is rejected only after both keys are acquired).
6476    Admitted,
6477    /// The kernel refused the key with this errno, which is the guest's.
6478    Refused(Errno),
6479    /// A signal stopped the probe's instruction before it ran (Reverie's
6480    /// `Guest::inject` returns ERESTARTSYS; EINTR is never a key answer either,
6481    /// since neither probe sleeps). The kernel answered nothing.
6482    Interrupted(Errno),
6483}
6484
6485/// Inject a shared-key `probe` once with `inject` and classify the answer.
6486///
6487/// The probe is never injected again: Reverie holds the signal that stopped
6488/// it in a single slot, and a second interrupted injection would replace that
6489/// signal, losing one the kernel already dequeued (Codex re-check #4 of
6490/// https://github.com/rrnewton/hermit/pull/4020). An interruption is returned
6491/// as such, never as an admission or as the guest's errno.
6492async fn run_shared_futex_key_probe<C>(
6493    context: &mut C,
6494    probe: syscalls::Futex,
6495    access: KeyAccess,
6496    inject: for<'a> fn(&'a mut C, syscalls::Futex) -> KeyProbeAnswer<'a>,
6497) -> SharedKeyProbe {
6498    match inject(context, probe).await {
6499        Ok(_) => SharedKeyProbe::Admitted,
6500        Err(errno @ (Errno::ERESTARTSYS | Errno::EINTR)) => SharedKeyProbe::Interrupted(errno),
6501        Err(Errno::ENOSYS) if access == KeyAccess::Write => SharedKeyProbe::Admitted,
6502        Err(errno) => SharedKeyProbe::Refused(errno),
6503    }
6504}
6505
6506/// Stop the run by name because a signal interrupted the shared-key probe for
6507/// the futex word at `address`.
6508///
6509/// The original call has no answer to return: the interruption token is not a
6510/// Linux result for it, retrying could lose the held signal
6511/// (`run_shared_futex_key_probe`), and admitting the key unchecked could be a
6512/// silent wrong answer. The run ends with the policy-refusal status whatever
6513/// the unsupported-operation policy, since `--allow-unsupported-syscalls` has
6514/// no errno to offer here either. A signal must arrive at exactly that
6515/// instruction, which is rare.
6516async fn refuse_interrupted_shared_key_probe<G: Guest<Detcore<T>>, T: RecordOrReplay>(
6517    guest: &mut G,
6518    address: usize,
6519    errno: Errno,
6520) -> ! {
6521    {
6522        use std::io::Write;
6523        let _ = writeln!(
6524            crate::util::RetryingStderr,
6525            "hermit: a signal interrupted Detcore's shared futex key check for word {:#x} in thread {} ({}); \
6526             refusing the run rather than retrying it, which could lose that signal \
6527             (https://github.com/rrnewton/hermit/pull/4020)",
6528            address,
6529            guest.thread_state().dettid,
6530            errno,
6531        );
6532    }
6533    crate::tool_global::unrecoverable_shutdown(guest, detcore_model::HERMIT_POLICY_REFUSAL_EXIT)
6534        .await
6535}
6536
6537/// A futex word's value as a user-mode access reads it, which is what Linux's
6538/// `futex_get_value_locked`/`get_user` and `futex_atomic_op_inuser` do:
6539/// `-EFAULT` if the word cannot be read.
6540///
6541/// The word is read as exactly four bytes with the guest's own permissions. On
6542/// x86 a writable page is also readable from user mode, but that read refuses a
6543/// mapping without `PROT_READ`, so for a writable one the 8-byte-aligned word
6544/// containing it is read instead. That cannot cross into the next page, unlike a
6545/// debugger read at the word's own address. A debugger read alone would also
6546/// ignore `PROT_NONE`.
6547fn read_futex_word<G: Guest<Detcore<T>>, T: RecordOrReplay>(
6548    guest: &mut G,
6549    address: usize,
6550) -> Result<i32, Error> {
6551    let word = Addr::<u8>::from_raw(address).ok_or(Errno::EFAULT)?;
6552    let mut bytes = [0u8; 4];
6553    if guest
6554        .memory()
6555        .read_exact_with_user_access(word, &mut bytes)
6556        .is_ok()
6557    {
6558        return Ok(i32::from_ne_bytes(bytes));
6559    }
6560    // Only a backend that runs the guest as real host threads has a host
6561    // `/proc/<pid>/maps` describing the guest; elsewhere (KVM's guest pid is
6562    // virtual) the user-access read above is the backend's whole answer.
6563    if !guest.config().backend.host_task_reads_reach_guest_threads
6564        || !mapping_is_writable(guest.pid().as_raw(), address)
6565    {
6566        return Err(Error::Errno(Errno::EFAULT));
6567    }
6568    let aligned = address & !(std::mem::size_of::<u64>() - 1);
6569    let pair: u64 = guest
6570        .memory()
6571        .read_value(Addr::<u64>::from_raw(aligned).ok_or(Errno::EFAULT)?)
6572        .map_err(|_| Error::Errno(Errno::EFAULT))?;
6573    let offset = address - aligned;
6574    let pair = pair.to_ne_bytes();
6575    bytes.copy_from_slice(&pair[offset..offset + 4]);
6576    Ok(i32::from_ne_bytes(bytes))
6577}
6578
6579/// Write only Linux's sched_param word, never an extended target-libc wrapper.
6580fn write_sched_getparam<M: MemoryAccess>(
6581    memory: &mut M,
6582    param: AddrMut<'_, syscalls::linux_abi::SchedParam>,
6583) -> Result<(), Errno> {
6584    memory.write_value(
6585        param,
6586        &syscalls::linux_abi::SchedParam { sched_priority: 0 },
6587    )
6588}
6589
6590/// Whether the guest mapping containing `address` is writable, from
6591/// `/proc/<pid>/maps`. False if no mapping contains it or the file cannot be
6592/// read.
6593fn mapping_is_writable(pid: i32, address: usize) -> bool {
6594    mapping_permissions(pid, address).is_some_and(|perms| perms[1] == b'w')
6595}
6596
6597/// The `rwxp` permissions of the guest mapping containing `address`, from
6598/// `/proc/<pid>/maps`, or `None` if no mapping contains it or the file cannot be
6599/// read.
6600fn mapping_permissions(pid: i32, address: usize) -> Option<[u8; 4]> {
6601    let maps = std::fs::read_to_string(format!("/proc/{pid}/maps")).ok()?;
6602    maps.lines().find_map(|line| {
6603        let mut fields = line.split_whitespace();
6604        let (Some(range), Some(perms)) = (fields.next(), fields.next()) else {
6605            return None;
6606        };
6607        let (start, end) = range.split_once('-')?;
6608        let (Ok(start), Ok(end)) = (
6609            usize::from_str_radix(start, 16),
6610            usize::from_str_radix(end, 16),
6611        ) else {
6612            return None;
6613        };
6614        if !(start..end).contains(&address) {
6615            return None;
6616        }
6617        perms.as_bytes().get(..4)?.try_into().ok()
6618    })
6619}
6620
6621/// A `struct timespec` copied from the guest as Linux's `get_timespec64`
6622/// (`copy_from_user`) copies it: every byte must lie in a mapping user mode can
6623/// read, which on x86 includes a write-only one, or the copy is EFAULT, with no
6624/// partial result. On a backend whose guest is real host threads, the guest's
6625/// host `/proc/<pid>/maps` decides; elsewhere the backend's user-access read. The bytes are then read through the tracer, which is only
6626/// safe because every page they span was checked first: the tracer's own read
6627/// would force its way through a PROT_NONE page.
6628fn read_user_timespec<G: Guest<Detcore<T>>, T: RecordOrReplay>(
6629    guest: &mut G,
6630    address: usize,
6631) -> Result<Timespec, Error> {
6632    let size = std::mem::size_of::<Timespec>();
6633    let last = address.checked_add(size - 1).ok_or(Errno::EFAULT)?;
6634    if last > user_ptr_max() {
6635        return Err(Error::Errno(Errno::EFAULT));
6636    }
6637    if !guest.config().backend.host_task_reads_reach_guest_threads {
6638        // No host `/proc` describes this guest (KVM's guest pid is virtual), so
6639        // the backend's own user-access read decides, all or nothing.
6640        let mut bytes = [0u8; std::mem::size_of::<Timespec>()];
6641        guest
6642            .memory()
6643            .read_exact_with_user_access(
6644                Addr::<u8>::from_raw(address).ok_or(Errno::EFAULT)?,
6645                &mut bytes,
6646            )
6647            .map_err(|_| Error::Errno(Errno::EFAULT))?;
6648        let field = |at: usize| -> i64 {
6649            let mut raw = [0u8; 8];
6650            raw.copy_from_slice(&bytes[at..at + 8]);
6651            i64::from_ne_bytes(raw)
6652        };
6653        return Ok(Timespec {
6654            tv_sec: field(0) as libc::time_t,
6655            tv_nsec: field(8) as libc::c_long,
6656        });
6657    }
6658    // 4096 divides every page size, so probing each 4 KiB step checks every page.
6659    let page = 4096;
6660    let pid = guest.pid().as_raw();
6661    let mut cursor = address & !(page - 1);
6662    while cursor <= last {
6663        let probe = cursor.max(address);
6664        let readable = mapping_permissions(pid, probe)
6665            .is_some_and(|perms| perms[0] == b'r' || perms[1] == b'w');
6666        if !readable {
6667            return Err(Error::Errno(Errno::EFAULT));
6668        }
6669        cursor += page;
6670    }
6671    // Read the bytes as aligned 8-byte words, which the tracer reads one at a
6672    // time even from a write-only page; a larger read goes through
6673    // `process_vm_readv`, which needs the page to be readable.
6674    let start = address & !7;
6675    let mut bytes = Vec::with_capacity(last + 1 - start);
6676    let mut word = start;
6677    while word <= last {
6678        let value: u64 = guest
6679            .memory()
6680            .read_value(Addr::<u64>::from_raw(word).ok_or(Errno::EFAULT)?)
6681            .map_err(|_| Error::Errno(Errno::EFAULT))?;
6682        bytes.extend_from_slice(&value.to_ne_bytes());
6683        word += 8;
6684    }
6685    let field = |at: usize| -> i64 {
6686        let mut raw = [0u8; 8];
6687        raw.copy_from_slice(&bytes[address - start + at..address - start + at + 8]);
6688        i64::from_ne_bytes(raw)
6689    };
6690    Ok(Timespec {
6691        tv_sec: field(0) as libc::time_t,
6692        tv_nsec: field(8) as libc::c_long,
6693    })
6694}
6695
6696// FUTEX_WAKE_OP operations and comparisons (include/uapi/linux/futex.h).
6697const FUTEX_OP_SET: u32 = 0;
6698const FUTEX_OP_ADD: u32 = 1;
6699const FUTEX_OP_OR: u32 = 2;
6700const FUTEX_OP_ANDN: u32 = 3;
6701const FUTEX_OP_XOR: u32 = 4;
6702const FUTEX_OP_OPARG_SHIFT: u32 = 8;
6703const FUTEX_OP_CMP_EQ: u32 = 0;
6704const FUTEX_OP_CMP_NE: u32 = 1;
6705const FUTEX_OP_CMP_LT: u32 = 2;
6706const FUTEX_OP_CMP_GE: u32 = 3;
6707const FUTEX_OP_CMP_LE: u32 = 4;
6708const FUTEX_OP_CMP_GT: u32 = 5;
6709
6710/// FUTEX_WAKE_OP's encoded operation, decoded as Linux's
6711/// `futex_atomic_op_inuser` (kernel/futex/waitwake.c) decodes it.
6712#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6713pub(crate) struct FutexWakeOp {
6714    op: u32,
6715    cmp: u32,
6716    oparg: i32,
6717    cmparg: i32,
6718}
6719
6720impl FutexWakeOp {
6721    pub(crate) fn decode(encoded: u32) -> Self {
6722        let sign_extend_12 = |v: u32| ((v << 20) as i32) >> 20;
6723        let op = (encoded & 0x7000_0000) >> 28;
6724        let cmp = (encoded & 0x0f00_0000) >> 24;
6725        let mut oparg = sign_extend_12((encoded & 0x00ff_f000) >> 12);
6726        let cmparg = sign_extend_12(encoded & 0x0000_0fff);
6727        if encoded & (FUTEX_OP_OPARG_SHIFT << 28) != 0 {
6728            // Linux masks an out-of-range shift (and logs "fix this program").
6729            if !(0..=31).contains(&oparg) {
6730                oparg &= 31;
6731            }
6732            oparg = 1i32.wrapping_shl(oparg as u32);
6733        }
6734        FutexWakeOp {
6735            op,
6736            cmp,
6737            oparg,
6738            cmparg,
6739        }
6740    }
6741
6742    /// Whether the operation is one x86's `arch_futex_atomic_op_inuser` knows;
6743    /// it returns `-ENOSYS` for any other before touching the word.
6744    pub(crate) fn has_known_operation(&self) -> bool {
6745        self.apply(0).is_some()
6746    }
6747
6748    /// The new value of the word, from its old value.
6749    pub(crate) fn apply(&self, old: i32) -> Option<i32> {
6750        Some(match self.op {
6751            FUTEX_OP_SET => self.oparg,
6752            FUTEX_OP_ADD => old.wrapping_add(self.oparg),
6753            FUTEX_OP_OR => old | self.oparg,
6754            FUTEX_OP_ANDN => old & !self.oparg,
6755            FUTEX_OP_XOR => old ^ self.oparg,
6756            _ => return None,
6757        })
6758    }
6759
6760    /// Whether to wake on `uaddr2`, from the word's old value; `None` (`-ENOSYS`)
6761    /// for an unknown comparison.
6762    pub(crate) fn compare(&self, old: i32) -> Option<bool> {
6763        Some(match self.cmp {
6764            FUTEX_OP_CMP_EQ => old == self.cmparg,
6765            FUTEX_OP_CMP_NE => old != self.cmparg,
6766            FUTEX_OP_CMP_LT => old < self.cmparg,
6767            FUTEX_OP_CMP_GE => old >= self.cmparg,
6768            FUTEX_OP_CMP_LE => old <= self.cmparg,
6769            FUTEX_OP_CMP_GT => old > self.cmparg,
6770            _ => return None,
6771        })
6772    }
6773}
6774
6775#[cfg(test)]
6776mod tests {
6777
6778    struct SchedParamMemory {
6779        bytes: [u8; 16],
6780        writable_end: usize,
6781        requested: Vec<usize>,
6782    }
6783    impl MemoryAccess for SchedParamMemory {
6784        fn read_vectored(
6785            &self,
6786            _: &[std::io::IoSlice<'_>],
6787            _: &mut [std::io::IoSliceMut<'_>],
6788        ) -> Result<usize, Errno> {
6789            panic!("sched_getparam must not read guest memory")
6790        }
6791        fn write_vectored(
6792            &mut self,
6793            _: &[std::io::IoSlice<'_>],
6794            _: &mut [std::io::IoSliceMut<'_>],
6795        ) -> Result<usize, Errno> {
6796            panic!("the offset-based mock must use write directly")
6797        }
6798        fn write(&mut self, address: AddrMut<u8>, source: &[u8]) -> Result<usize, Errno> {
6799            // The address is an offset into this mock, not a live pointer. Do
6800            // not construct a slice from it through the default write method.
6801            let start = address.as_raw();
6802            let want = source.len();
6803            self.requested.push(want);
6804            if start
6805                .checked_add(want)
6806                .is_none_or(|end| end > self.writable_end)
6807            {
6808                return Err(Errno::EFAULT);
6809            }
6810            self.bytes[start..start + want].copy_from_slice(source);
6811            Ok(want)
6812        }
6813    }
6814
6815    #[test]
6816    fn sched_getparam_writes_only_kernel_priority_before_guard() {
6817        let mut memory = SchedParamMemory {
6818            bytes: [0xA5; 16],
6819            writable_end: 5,
6820            requested: Vec::new(),
6821        };
6822        let param = AddrMut::<syscalls::linux_abi::SchedParam>::from_raw(1).unwrap();
6823        assert_eq!(write_sched_getparam(&mut memory, param), Ok(()));
6824        assert_eq!(memory.requested, vec![4]);
6825        assert_eq!(&memory.bytes[1..5], &0i32.to_ne_bytes());
6826        assert_eq!(memory.bytes[0], 0xA5);
6827        assert_eq!(&memory.bytes[5..], &[0xA5; 11]);
6828    }
6829
6830    #[test]
6831    fn sched_getparam_short_writable_kernel_word_remains_efault() {
6832        let mut memory = SchedParamMemory {
6833            bytes: [0xA5; 16],
6834            writable_end: 4,
6835            requested: Vec::new(),
6836        };
6837        let param = AddrMut::<syscalls::linux_abi::SchedParam>::from_raw(1).unwrap();
6838        assert_eq!(write_sched_getparam(&mut memory, param), Err(Errno::EFAULT));
6839        assert_eq!(memory.requested, vec![4]);
6840        assert_eq!(memory.bytes, [0xA5; 16]);
6841    }
6842
6843    /// A copy the record marks as discarded at send is left out even when the
6844    /// wake's answer names its signal (`committed`), as for a drain batch's
6845    /// copy that was flushed and sent again, discarded (R4-1 of the R2b-1
6846    /// design re-checks, https://github.com/rrnewton/hermit/issues/4273; arm
6847    /// A9). A kept copy in either queue stays.
6848    #[test]
6849    fn a_discarded_copy_is_left_out_even_when_committed() {
6850        let chld = kernel_sigset_bit(libc::SIGCHLD);
6851        let usr1 = kernel_sigset_bit(libc::SIGUSR1);
6852        let state = KernelSignalState {
6853            pending: chld | usr1,
6854            thread_pending: chld,
6855            shared_pending: usr1,
6856            ..Default::default()
6857        };
6858        assert_eq!(with_kept_copies(&state, chld, 0, chld | usr1, chld), usr1);
6859        assert_eq!(with_kept_copies(&state, 0, usr1, chld | usr1, usr1), chld);
6860        assert_eq!(with_kept_copies(&state, 0, 0, chld | usr1, 0), chld | usr1);
6861    }
6862    use std::io::Read;
6863    use std::io::Seek;
6864    use std::os::fd::AsRawFd;
6865
6866    use reverie::GlobalRPC;
6867    use reverie::GlobalTool;
6868    use reverie::Tid;
6869    use reverie::Tool;
6870
6871    use super::*;
6872
6873    /// The Kernel path is ptrace's, and the Barrier path is DBT's alone: a
6874    /// backend gets the Barrier path only by setting a flag whose other
6875    /// properties it must then have (`owner_death_path`).
6876    #[test]
6877    fn owner_death_path_is_tied_to_the_backends_that_have_its_properties() {
6878        let path = |backend: reverie::BackendCapabilities| {
6879            let mut cfg = Config::default().with_backend(|capabilities| *capabilities = backend);
6880            cfg.sequentialize_threads = true;
6881            cfg.debug_futex_mode = BlockingMode::Precise;
6882            owner_death_path(&cfg)
6883        };
6884        assert_eq!(
6885            path(reverie::BackendCapabilities::PTRACE),
6886            Some(OwnerDeathPath::Kernel)
6887        );
6888        assert_eq!(
6889            path(reverie::BackendCapabilities::DBT),
6890            Some(OwnerDeathPath::Barrier)
6891        );
6892        for backend in [
6893            reverie::BackendCapabilities::KVM,
6894            reverie::BackendCapabilities::SABRE,
6895            reverie::BackendCapabilities::LITEINST_IN_GUEST,
6896        ] {
6897            assert_eq!(path(backend), None, "{backend:?}");
6898        }
6899    }
6900
6901    /// Each configuration takes its own `sched_yield` route; only chaos with
6902    /// the timer on and delivered, no schedule replay, and a heuristic other
6903    /// than `Random` takes the yield epoch, and an ordinary run still ends its
6904    /// timeslice (https://github.com/rrnewton/hermit/issues/4068,
6905    /// https://github.com/rrnewton/hermit/issues/4150).
6906    #[test]
6907    fn each_configuration_takes_its_sched_yield_route() {
6908        let path = || Some(std::path::PathBuf::from("record.json"));
6909        let cases = [
6910            (Config::default(), SchedYieldRoute::EndTimeslice),
6911            // The ordinary ptrace run: timers delivered, no chaos.
6912            (
6913                Config {
6914                    ..Config::default()
6915                }
6916                .with_backend(|backend| {
6917                    backend.delivers_preemption_timer_events = true;
6918                    backend.runs_guest_code_between_injections = false;
6919                }),
6920                SchedYieldRoute::EndTimeslice,
6921            ),
6922            (
6923                Config {
6924                    chaos: true,
6925                    replay_schedule_from: path(),
6926                    ..Config::default()
6927                }
6928                .with_backend(|backend| {
6929                    backend.delivers_preemption_timer_events = true;
6930                    backend.runs_guest_code_between_injections = false;
6931                }),
6932                SchedYieldRoute::WeakYield,
6933            ),
6934            (
6935                Config {
6936                    chaos: true,
6937                    ..Config::default()
6938                }
6939                .with_backend(|backend| {
6940                    backend.delivers_preemption_timer_events = true;
6941                    backend.runs_guest_code_between_injections = false;
6942                }),
6943                SchedYieldRoute::ChaosYieldEpoch,
6944            ),
6945            (
6946                Config {
6947                    chaos: true,
6948                    sched_heuristic: SchedHeuristic::Random,
6949                    ..Config::default()
6950                }
6951                .with_backend(|backend| {
6952                    backend.delivers_preemption_timer_events = true;
6953                    backend.runs_guest_code_between_injections = false;
6954                }),
6955                SchedYieldRoute::WeakYield,
6956            ),
6957            (
6958                Config {
6959                    chaos: true,
6960                    sched_heuristic: SchedHeuristic::StickyRandom,
6961                    ..Config::default()
6962                }
6963                .with_backend(|backend| {
6964                    backend.delivers_preemption_timer_events = true;
6965                    backend.runs_guest_code_between_injections = false;
6966                }),
6967                SchedYieldRoute::ChaosYieldEpoch,
6968            ),
6969            (
6970                Config {
6971                    chaos: true,
6972                    sched_heuristic: SchedHeuristic::ConnectBind,
6973                    ..Config::default()
6974                }
6975                .with_backend(|backend| {
6976                    backend.delivers_preemption_timer_events = true;
6977                    backend.runs_guest_code_between_injections = false;
6978                }),
6979                SchedYieldRoute::ChaosYieldEpoch,
6980            ),
6981            (
6982                Config {
6983                    chaos: true,
6984                    max_timeslice: None,
6985                    sched_heuristic: SchedHeuristic::Random,
6986                    ..Config::default()
6987                },
6988                SchedYieldRoute::ChaosReprioritize,
6989            ),
6990            // A backend that delivers no preemption timer events.
6991            (
6992                Config {
6993                    chaos: true,
6994                    ..Config::default()
6995                }
6996                .with_backend(|backend| backend.delivers_preemption_timer_events = false),
6997                SchedYieldRoute::WeakYield,
6998            ),
6999            (
7000                Config {
7001                    chaos: true,
7002                    max_timeslice: None,
7003                    ..Config::default()
7004                },
7005                SchedYieldRoute::ChaosReprioritize,
7006            ),
7007            (
7008                Config {
7009                    chaos: true,
7010                    replay_preemptions_from: path(),
7011                    ..Config::default()
7012                }
7013                .with_backend(|backend| {
7014                    backend.delivers_preemption_timer_events = true;
7015                    backend.runs_guest_code_between_injections = false;
7016                }),
7017                SchedYieldRoute::ChaosYieldEpoch,
7018            ),
7019            (
7020                Config {
7021                    chaos: true,
7022                    replay_schedule_from: path(),
7023                    ..Config::default()
7024                },
7025                SchedYieldRoute::WeakYield,
7026            ),
7027            (
7028                Config {
7029                    replay_schedule_from: path(),
7030                    ..Config::default()
7031                },
7032                SchedYieldRoute::WeakYield,
7033            ),
7034            (
7035                Config {
7036                    replay_preemptions_from: path(),
7037                    ..Config::default()
7038                },
7039                SchedYieldRoute::ReplayStrongYield,
7040            ),
7041        ];
7042        for (cfg, route) in cases {
7043            assert_eq!(
7044                sched_yield_route(&cfg),
7045                route,
7046                "chaos={} timer delivered={} heuristic={:?} max_timeslice={:?} preemptions={:?} schedule={:?}",
7047                cfg.chaos,
7048                cfg.backend.delivers_preemption_timer_events,
7049                cfg.sched_heuristic,
7050                cfg.max_timeslice,
7051                cfg.replay_preemptions_from,
7052                cfg.replay_schedule_from
7053            );
7054        }
7055    }
7056
7057    /// A thread whose reported pid is not its thread group, as Reverie
7058    /// reports for a process that a raw `clone` without `CLONE_THREAD` made,
7059    /// still has a readable signal state and is not taken to be gone
7060    /// (https://github.com/rrnewton/hermit/issues/3982). The test's own thread
7061    /// stands in for it, with this process's parent as the wrong pid.
7062    #[test]
7063    fn a_thread_reported_under_another_pid_is_read_and_alive() {
7064        let tid = Pid::from_raw(unsafe { libc::syscall(libc::SYS_gettid) } as i32);
7065        let wrong = Pid::from_raw(unsafe { libc::getppid() });
7066        assert_ne!(wrong, tid);
7067        assert!(read_kernel_signal_state(wrong, tid).is_err());
7068        assert!(read_thread_signal_state(wrong, tid).is_ok());
7069        assert!(!thread_is_gone(wrong, tid));
7070        assert!(read_thread_wait_signal_state(wrong, tid).is_ok());
7071    }
7072
7073    /// Host `/proc` signal state reads are forbidden exactly where guest
7074    /// thread IDs do not name host tasks: KVM, and no other backend today.
7075    #[test]
7076    fn host_signal_state_reads_are_refused_only_where_guest_tids_are_not_host_tasks() {
7077        assert!(refuses_host_signal_state_reads(&BackendCapabilities::KVM));
7078        for backend in [
7079            BackendCapabilities::PTRACE,
7080            BackendCapabilities::E9PATCH,
7081            BackendCapabilities::SABRE,
7082            BackendCapabilities::DBT,
7083            BackendCapabilities::LITEINST_IN_GUEST,
7084        ] {
7085            assert!(!refuses_host_signal_state_reads(&backend));
7086        }
7087    }
7088
7089    /// Once the refusal is latched for a backend whose guest thread IDs are
7090    /// not host tasks, a signal state read never reaches `/proc`: the test's
7091    /// own live thread, whose state is readable before, is refused after.
7092    #[test]
7093    fn a_latched_refusal_never_reads_a_host_task_signal_state() {
7094        let pid = Pid::from_raw(unsafe { libc::getpid() });
7095        let tid = Pid::from_raw(unsafe { libc::syscall(libc::SYS_gettid) } as i32);
7096        refuse_host_signal_state_reads(&BackendCapabilities::PTRACE);
7097        assert!(!host_signal_state_reads_refused());
7098        assert!(read_kernel_signal_state(pid, tid).is_ok());
7099        refuse_host_signal_state_reads(&BackendCapabilities::KVM);
7100        assert!(host_signal_state_reads_refused());
7101        assert_eq!(read_kernel_signal_state(pid, tid), Err(Errno::ENOSYS));
7102        assert_eq!(read_thread_signal_state(pid, tid), Err(Errno::ENOSYS));
7103    }
7104
7105    /// A kept status file is read afresh each time: a signal blocked after
7106    /// the first read shows in the second, which reuses the kept file, and the
7107    /// thread's exit hook drops it (`forget_thread_status`).
7108    #[test]
7109    fn a_kept_status_file_reads_the_current_state_until_its_thread_is_forgotten() {
7110        std::thread::spawn(|| {
7111            status_files::enable();
7112            let pid = Pid::from_raw(unsafe { libc::getpid() });
7113            let tid = Pid::from_raw(unsafe { libc::syscall(libc::SYS_gettid) } as i32);
7114            let usr2 = crate::scheduler::kernel_signal_bit(libc::SIGUSR2);
7115            let before = read_kernel_signal_state(pid, tid).unwrap();
7116            assert_eq!(before.blocked & usr2, 0);
7117            assert!(status_files::keeps(pid, tid));
7118            // SAFETY: changes only this spawned thread's own mask.
7119            unsafe {
7120                let mut set: libc::sigset_t = std::mem::zeroed();
7121                libc::sigemptyset(&mut set);
7122                libc::sigaddset(&mut set, libc::SIGUSR2);
7123                assert_eq!(
7124                    libc::pthread_sigmask(libc::SIG_BLOCK, &set, std::ptr::null_mut()),
7125                    0
7126                );
7127            }
7128            let after = read_kernel_signal_state(pid, tid).unwrap();
7129            assert_eq!(after.blocked & usr2, usr2);
7130            assert!(status_files::keeps(pid, tid));
7131            forget_thread_status(tid);
7132            assert!(!status_files::keeps(pid, tid));
7133        })
7134        .join()
7135        .unwrap();
7136    }
7137
7138    /// Until keeping is enabled, which only Hermit's own process does, a read
7139    /// keeps no file: no descriptor of this process names the thread's status
7140    /// file afterwards. Detcore linked into a guest is in this state, so its
7141    /// reads leave nothing in the guest's descriptor table.
7142    #[test]
7143    #[allow(clippy::disallowed_methods)] // a host unit test lists its own descriptors
7144    fn without_keeping_a_read_leaves_no_descriptor_open() {
7145        std::thread::spawn(|| {
7146            let pid = Pid::from_raw(unsafe { libc::getpid() });
7147            let tid = Pid::from_raw(unsafe { libc::syscall(libc::SYS_gettid) } as i32);
7148            assert!(read_kernel_signal_state(pid, tid).is_ok());
7149            assert!(read_kernel_signal_state(pid, tid).is_ok());
7150            assert!(status_files::kept_file(pid, tid).is_none());
7151            let status = format!("/proc/{}/task/{}/status", pid.as_raw(), tid.as_raw());
7152            for entry in std::fs::read_dir("/proc/self/fd").unwrap() {
7153                let target = std::fs::read_link(entry.unwrap().path()).unwrap_or_default();
7154                assert_ne!(target.to_str(), Some(status.as_str()));
7155            }
7156            forget_thread_status(tid);
7157        })
7158        .join()
7159        .unwrap();
7160    }
7161
7162    /// A kept status file stays bound to the thread it was opened for: once
7163    /// that thread has exited, the kept file is unreadable (`ESRCH`), so a
7164    /// read never takes a later thread's state from it. The read then drops
7165    /// the file and reads the path, as it did before files were kept: `ESRCH`
7166    /// while the TID is free, or the state of a thread that reused it (other
7167    /// tests in this process start threads concurrently).
7168    #[test]
7169    fn a_kept_status_file_of_an_exited_thread_is_unreadable_and_dropped() {
7170        status_files::enable();
7171        let pid = Pid::from_raw(unsafe { libc::getpid() });
7172        let (tid, kept) = std::thread::spawn(move || {
7173            status_files::enable();
7174            let tid = Pid::from_raw(unsafe { libc::syscall(libc::SYS_gettid) } as i32);
7175            assert!(read_kernel_signal_state(pid, tid).is_ok());
7176            let kept = status_files::kept_file(pid, tid).expect("the read keeps its file");
7177            (tid, kept)
7178        })
7179        .join()
7180        .unwrap();
7181        // join() returns before the kernel has finished tearing the thread
7182        // down; until then the kept file still reads it (as its path would).
7183        // Every read names that thread, and the file becomes unreadable.
7184        let deadline = std::time::Instant::now() + Duration::from_secs(5);
7185        let error = loop {
7186            match status_files::read_kept(&kept) {
7187                Ok(text) => {
7188                    let own = format!("\nPid:\t{}\n", tid.as_raw());
7189                    assert!(text.contains(&own), "{text}");
7190                    assert!(std::time::Instant::now() < deadline, "{text}");
7191                    std::thread::sleep(Duration::from_millis(1));
7192                }
7193                Err(error) => break error,
7194            }
7195        };
7196        assert_eq!(error.raw_os_error(), Some(libc::ESRCH));
7197        match read_kernel_signal_state(pid, tid) {
7198            Err(errno) => {
7199                assert_eq!(errno, Errno::ESRCH);
7200                assert!(!status_files::keeps(pid, tid));
7201            }
7202            // A thread of another test took the TID: the read is the path's.
7203            Ok(_) => {
7204                // The kept file, if any, is the reopened one (the cache may be
7205                // full of other tests' threads, which never run the exit hook).
7206                if let Some(now) = status_files::kept_file(pid, tid) {
7207                    assert!(!Arc::ptr_eq(&now, &kept));
7208                }
7209            }
7210        }
7211        assert!(status_files::kept() <= status_files::CAPACITY);
7212    }
7213    use crate::config::Config;
7214    use crate::tool_global::GlobalRequest;
7215    use crate::tool_global::GlobalState;
7216    use crate::types::MmId;
7217
7218    /// A new process whose exit signal is not SIGCHLD makes its creator hold
7219    /// that signal; a thread, a `CLONE_PARENT` child, a SIGCHLD child, and an
7220    /// out-of-range signal add nothing
7221    /// (https://github.com/rrnewton/hermit/issues/3146).
7222    #[test]
7223    fn a_child_exit_signal_other_than_sigchld_is_host_timed_for_its_creator() {
7224        let usr1 = 1_u64 << (libc::SIGUSR1 - 1);
7225        let none = CloneFlags::empty();
7226        assert_eq!(clone_host_timed_signals(none, libc::SIGUSR1), usr1);
7227        assert_eq!(
7228            clone_host_timed_signals(CloneFlags::CLONE_VFORK, libc::SIGUSR1),
7229            usr1
7230        );
7231        assert_eq!(clone_host_timed_signals(none, libc::SIGCHLD), 0);
7232        assert_eq!(clone_host_timed_signals(none, 0), 0);
7233        assert_eq!(clone_host_timed_signals(none, 65), 0);
7234        let thread = CloneFlags::CLONE_THREAD | CloneFlags::CLONE_SIGHAND | CloneFlags::CLONE_VM;
7235        assert_eq!(clone_host_timed_signals(thread, libc::SIGUSR1), 0);
7236        assert_eq!(
7237            clone_host_timed_signals(CloneFlags::CLONE_PARENT, libc::SIGUSR1),
7238            0
7239        );
7240    }
7241
7242    struct FailedExecStack;
7243    struct FailedExecStackGuard;
7244
7245    impl Drop for FailedExecStackGuard {
7246        fn drop(&mut self) {}
7247    }
7248
7249    impl reverie::Stack for FailedExecStack {
7250        type StackGuard = FailedExecStackGuard;
7251
7252        fn size(&self) -> usize {
7253            panic!("failed exec must not use the guest stack")
7254        }
7255        fn capacity(&self) -> usize {
7256            panic!("failed exec must not use the guest stack")
7257        }
7258        fn push<'stack, T>(&mut self, _: T) -> Addr<'stack, T> {
7259            panic!("failed exec must not use the guest stack")
7260        }
7261        fn reserve<'stack, T>(&mut self) -> AddrMut<'stack, T> {
7262            panic!("failed exec must not use the guest stack")
7263        }
7264        fn commit(self) -> Result<Self::StackGuard, Errno> {
7265            panic!("failed exec must not use the guest stack")
7266        }
7267    }
7268
7269    // Inject only the backend's errno and, when the backend refuses the exec
7270    // as unsupported, its typed refusal. Preparation, rollback, cancellation,
7271    // unsupported reporting and the configured refusal policy are real code.
7272    struct FailedExecGuest<'a> {
7273        config: &'a Config,
7274        global: &'a GlobalState,
7275        thread: crate::ThreadState<()>,
7276        sender: Tid,
7277        process: Tid,
7278        old_mm: MmId,
7279        errno: Errno,
7280        backend_refuses: bool,
7281        refusal: Option<reverie::UnsupportedRefusal>,
7282        refusal_takes: usize,
7283        injections: usize,
7284        requests: Mutex<Vec<GlobalRequest>>,
7285    }
7286
7287    #[reverie::tool]
7288    impl GlobalRPC<GlobalState> for FailedExecGuest<'_> {
7289        async fn send_rpc(
7290            &self,
7291            message: <GlobalState as GlobalTool>::Request,
7292        ) -> <GlobalState as GlobalTool>::Response {
7293            assert_eq!(message.1, self.old_mm, "RPC follows restored exec identity");
7294            self.requests.lock().unwrap().push(message.2.clone());
7295            self.global.receive_rpc(self.sender, message).await
7296        }
7297        fn config(&self) -> &Config {
7298            self.config
7299        }
7300    }
7301
7302    #[reverie::tool]
7303    impl Guest<Detcore> for FailedExecGuest<'_> {
7304        type Memory = reverie::syscalls::LocalMemory;
7305        type Stack = FailedExecStack;
7306
7307        fn tid(&self) -> Tid {
7308            self.sender
7309        }
7310        fn pid(&self) -> Tid {
7311            self.process
7312        }
7313        fn ppid(&self) -> Option<Tid> {
7314            None
7315        }
7316        fn memory(&self) -> Self::Memory {
7317            panic!("failed exec rollback must not read guest memory")
7318        }
7319        fn thread_state(&self) -> &crate::ThreadState<()> {
7320            &self.thread
7321        }
7322        fn thread_state_mut(&mut self) -> &mut crate::ThreadState<()> {
7323            &mut self.thread
7324        }
7325        async fn regs(&mut self) -> libc::user_regs_struct {
7326            panic!("failed exec rollback must not read registers")
7327        }
7328        async fn stack(&mut self) -> Self::Stack {
7329            panic!("failed exec rollback must not use a guest stack")
7330        }
7331        async fn daemonize(&mut self) {
7332            panic!("failed exec rollback must not daemonize")
7333        }
7334        async fn inject<S: SyscallInfo>(&mut self, call: S) -> Result<i64, Errno> {
7335            assert_eq!(call.number(), syscalls::Sysno::execveat);
7336            assert_eq!(
7337                self.thread.mm_id,
7338                self.old_mm.for_exec(self.thread.detpid.unwrap())
7339            );
7340            assert_eq!(self.thread.robust_list_head, None);
7341            self.injections += 1;
7342            // As reverie-kvm does, every injection replaces the record.
7343            self.refusal = self.backend_refuses.then(|| {
7344                reverie::UnsupportedRefusal::new(
7345                    reverie::UnsupportedOperation::NonLeaderExec,
7346                    self.errno,
7347                    "KVM nonleader exec is unsupported",
7348                )
7349            });
7350            Err(self.errno)
7351        }
7352        fn take_unsupported_refusal(&mut self) -> Option<reverie::UnsupportedRefusal> {
7353            assert_eq!(
7354                self.thread.mm_id,
7355                self.old_mm.for_exec(self.thread.detpid.unwrap()),
7356                "the refusal is taken before the failed exec is rolled back"
7357            );
7358            self.refusal_takes += 1;
7359            self.refusal.take()
7360        }
7361        async fn tail_inject<S: SyscallInfo>(&mut self, _: S) -> reverie::Never {
7362            panic!("failed exec must not retire a live guest")
7363        }
7364        fn set_timer(&mut self, _: reverie::TimerSchedule) -> Result<(), Error> {
7365            panic!("failed exec rollback must not replace the timer")
7366        }
7367        fn set_timer_precise(&mut self, _: reverie::TimerSchedule) -> Result<(), Error> {
7368            panic!("failed exec rollback must not replace the timer")
7369        }
7370        fn read_clock(&mut self) -> Result<u64, Error> {
7371            panic!("failed exec rollback must not sample the clock")
7372        }
7373    }
7374
7375    #[tokio::test]
7376    async fn backend_exec_refusal_preserves_failed_exec_rollback_and_policy() {
7377        let process = DetPid::from_raw(17);
7378        let worker = DetTid::from_raw(18);
7379        // `backend_refuses` is whether the backend reports a typed refusal for
7380        // the exec. reverie-kvm reports one for every worker exec that returns
7381        // ENOSYS, whether its own refusal after preflight or an ENOSYS from a
7382        // host syscall the preflight makes: the first two rows. The last row
7383        // is a worker ENOSYS for which a backend with KVM's other behaviours
7384        // reports no refusal (as KvmGuest::inject's early admission refusals
7385        // do); Detcore must not infer a refusal from the errno, so it is an
7386        // ordinary exec failure.
7387        for (kvm_behaviours, caller, errno, backend_refuses, fail_closed, refused, reported) in [
7388            (true, worker, Errno::ENOSYS, true, true, true, false),
7389            (true, worker, Errno::ENOSYS, true, false, false, true),
7390            (true, worker, Errno::ENOENT, false, true, false, false),
7391            (true, worker, Errno::ENOEXEC, false, true, false, false),
7392            (true, worker, Errno::EFAULT, false, true, false, false),
7393            (true, worker, Errno::EACCES, false, true, false, false),
7394            (true, worker, Errno::EOPNOTSUPP, false, true, false, false),
7395            (true, process, Errno::ENOSYS, false, true, false, false),
7396            (false, worker, Errno::ENOSYS, false, true, false, false),
7397            (true, worker, Errno::ENOSYS, false, true, false, false),
7398        ] {
7399            let mut report = tempfile::tempfile().unwrap();
7400            let config = Config {
7401                sequentialize_threads: false,
7402                panic_on_unsupported_syscalls: fail_closed,
7403                exit_on_unsupported_syscall: true,
7404                shutdown_on_unsupported_syscall: false,
7405                unsupported_syscall_report_fd: Some(report.as_raw_fd()),
7406                ..Config::default().with_backend(|backend| {
7407                    if kvm_behaviours {
7408                        // The behaviours the old `backend_is_kvm` identity flag
7409                        // selected, other than the exec refusal, which the
7410                        // backend now reports per exec (`backend_refuses`), and
7411                        // the user address limit, which it reports per guest.
7412                        backend.provides_process_signal_control = true;
7413                        backend.emulates_child_waits = true;
7414                    }
7415                })
7416            };
7417            let global = GlobalState::init_global_state(&config).await;
7418            let tool = Detcore::new(Tid::from_raw(process.as_raw()), &config);
7419            let mut thread = crate::ThreadState::new(caller, &config, ());
7420            thread.detpid = Some(process);
7421            thread.mm_id = MmId::initial(process);
7422            thread.record_robust_list_head(Some(0x12340));
7423            thread.thread_logical_time.add_syscall_with_cost(123);
7424            let old_time = thread.thread_logical_time.as_nanos();
7425            let old_files = Arc::clone(&thread.file_metadata);
7426            let old_memory = Arc::clone(&thread.memory_metadata);
7427            let old_mm = thread.mm_id;
7428            let mut guest = FailedExecGuest {
7429                config: &config,
7430                global: &global,
7431                thread,
7432                sender: Tid::from_raw(caller.as_raw()),
7433                process: Tid::from_raw(process.as_raw()),
7434                old_mm,
7435                errno,
7436                backend_refuses,
7437                refusal: None,
7438                refusal_takes: 0,
7439                injections: 0,
7440                requests: Mutex::new(Vec::new()),
7441            };
7442            let result = tool
7443                .handle_execveat(&mut guest, syscalls::Execveat::new())
7444                .await;
7445            match result {
7446                Err(Error::Tool(error)) if refused => assert_eq!(
7447                    error
7448                        .downcast_ref::<crate::UnsupportedSyscallError>()
7449                        .unwrap()
7450                        .0,
7451                    syscalls::Sysno::execveat
7452                ),
7453                Err(Error::Errno(actual)) if !refused => assert_eq!(actual, errno),
7454                result => panic!("wrong failed-exec policy: {result:?}"),
7455            }
7456            assert_eq!(guest.injections, 1, "backend preflight must run first");
7457            assert_eq!(guest.refusal_takes, 1, "the refusal must be asked for once");
7458            assert_eq!(guest.refusal, None, "a reported refusal must be taken");
7459            assert_eq!(guest.thread.dettid, caller);
7460            assert_eq!(guest.thread.detpid, Some(process));
7461            assert_eq!(guest.thread.mm_id, old_mm);
7462            assert_eq!(guest.thread.robust_list_head, Some(0x12340));
7463            assert_eq!(guest.thread.thread_logical_time.as_nanos(), old_time);
7464            assert!(Arc::ptr_eq(&guest.thread.file_metadata, &old_files));
7465            assert!(Arc::ptr_eq(&guest.thread.memory_metadata, &old_memory));
7466            let requests = guest.requests.lock().unwrap();
7467            assert!(matches!(requests[0], GlobalRequest::PrepareExec(..)));
7468            assert!(matches!(requests[1], GlobalRequest::CancelExec(..)));
7469            assert_eq!(requests.len(), if reported { 3 } else { 2 });
7470            if reported {
7471                assert!(
7472                    matches!(&requests[2], GlobalRequest::ReportUnsupportedSyscall(name) if name == "execveat")
7473                );
7474            }
7475            report.rewind().unwrap();
7476            let mut aggregate = String::new();
7477            report.read_to_string(&mut aggregate).unwrap();
7478            assert_eq!(aggregate, if reported { "execveat\n" } else { "" });
7479        }
7480    }
7481
7482    /// A guest whose backend fails every clone with `errno`, recording a typed
7483    /// refusal when `backend_refuses`, as an in-guest backend does for a
7484    /// thread it cannot create.
7485    struct FailedCloneGuest<'a> {
7486        config: &'a Config,
7487        global: &'a GlobalState,
7488        thread: crate::ThreadState<()>,
7489        sender: Tid,
7490        errno: Errno,
7491        backend_refuses: bool,
7492        refusal: Option<reverie::UnsupportedRefusal>,
7493        refusal_takes: usize,
7494        injections: usize,
7495        requests: Mutex<Vec<GlobalRequest>>,
7496    }
7497
7498    #[reverie::tool]
7499    impl GlobalRPC<GlobalState> for FailedCloneGuest<'_> {
7500        async fn send_rpc(
7501            &self,
7502            message: <GlobalState as GlobalTool>::Request,
7503        ) -> <GlobalState as GlobalTool>::Response {
7504            self.requests.lock().unwrap().push(message.2.clone());
7505            self.global.receive_rpc(self.sender, message).await
7506        }
7507        fn config(&self) -> &Config {
7508            self.config
7509        }
7510    }
7511
7512    #[reverie::tool]
7513    impl Guest<Detcore> for FailedCloneGuest<'_> {
7514        type Memory = reverie::syscalls::LocalMemory;
7515        type Stack = FailedExecStack;
7516
7517        fn tid(&self) -> Tid {
7518            self.sender
7519        }
7520        fn pid(&self) -> Tid {
7521            self.sender
7522        }
7523        fn ppid(&self) -> Option<Tid> {
7524            None
7525        }
7526        fn memory(&self) -> Self::Memory {
7527            reverie::syscalls::LocalMemory::new()
7528        }
7529        fn thread_state(&self) -> &crate::ThreadState<()> {
7530            &self.thread
7531        }
7532        fn thread_state_mut(&mut self) -> &mut crate::ThreadState<()> {
7533            &mut self.thread
7534        }
7535        async fn regs(&mut self) -> libc::user_regs_struct {
7536            panic!("a failed clone must not read registers")
7537        }
7538        async fn stack(&mut self) -> Self::Stack {
7539            panic!("a failed clone must not use a guest stack")
7540        }
7541        async fn daemonize(&mut self) {
7542            panic!("a failed clone must not daemonize")
7543        }
7544        async fn inject<S: SyscallInfo>(&mut self, call: S) -> Result<i64, Errno> {
7545            assert_eq!(call.number(), syscalls::Sysno::clone);
7546            self.injections += 1;
7547            self.refusal = self.backend_refuses.then(|| {
7548                reverie::UnsupportedRefusal::new(
7549                    reverie::UnsupportedOperation::NativeExec,
7550                    self.errno,
7551                    "this backend cannot create a thread",
7552                )
7553            });
7554            Err(self.errno)
7555        }
7556        fn take_unsupported_refusal(&mut self) -> Option<reverie::UnsupportedRefusal> {
7557            self.refusal_takes += 1;
7558            self.refusal.take()
7559        }
7560        async fn tail_inject<S: SyscallInfo>(&mut self, _: S) -> reverie::Never {
7561            panic!("a failed clone must not retire a live guest")
7562        }
7563        fn set_timer(&mut self, _: reverie::TimerSchedule) -> Result<(), Error> {
7564            panic!("a failed clone must not replace the timer")
7565        }
7566        fn set_timer_precise(&mut self, _: reverie::TimerSchedule) -> Result<(), Error> {
7567            panic!("a failed clone must not replace the timer")
7568        }
7569        fn read_clock(&mut self) -> Result<u64, Error> {
7570            panic!("a failed clone must not sample the clock")
7571        }
7572    }
7573
7574    /// A clone the backend refuses with a typed refusal is refused by the
7575    /// unsupported-operation policy, never answered with the refusal's errno in
7576    /// a fail-closed run; a clone that merely fails keeps its errno.
7577    #[tokio::test]
7578    async fn backend_clone_refusal_follows_the_unsupported_operation_policy() {
7579        let process = DetPid::from_raw(23);
7580        for (errno, backend_refuses, fail_closed, refused, reported) in [
7581            (Errno::EOPNOTSUPP, true, true, true, false),
7582            (Errno::EOPNOTSUPP, true, false, false, true),
7583            (Errno::EAGAIN, false, true, false, false),
7584            (Errno::EOPNOTSUPP, false, true, false, false),
7585        ] {
7586            let mut report = tempfile::tempfile().unwrap();
7587            let config = Config {
7588                sequentialize_threads: false,
7589                panic_on_unsupported_syscalls: fail_closed,
7590                exit_on_unsupported_syscall: true,
7591                shutdown_on_unsupported_syscall: false,
7592                unsupported_syscall_report_fd: Some(report.as_raw_fd()),
7593                ..Config::default()
7594            };
7595            let global = GlobalState::init_global_state(&config).await;
7596            let tool = Detcore::new(Tid::from_raw(process.as_raw()), &config);
7597            let mut thread = crate::ThreadState::new(process, &config, ());
7598            thread.detpid = Some(process);
7599            let mut guest = FailedCloneGuest {
7600                config: &config,
7601                global: &global,
7602                thread,
7603                sender: Tid::from_raw(process.as_raw()),
7604                errno,
7605                backend_refuses,
7606                refusal: None,
7607                refusal_takes: 0,
7608                injections: 0,
7609                requests: Mutex::new(Vec::new()),
7610            };
7611            let flags = (libc::CLONE_VM
7612                | libc::CLONE_FS
7613                | libc::CLONE_FILES
7614                | libc::CLONE_SIGHAND
7615                | libc::CLONE_THREAD
7616                | libc::CLONE_SYSVSEM) as u64;
7617            let clone = syscalls::Clone::new()
7618                .with_flags(nix::sched::CloneFlags::from_bits_truncate(flags as _));
7619            let result = tool
7620                .handle_clone_family(&mut guest, syscalls::family::CloneFamily::Clone(clone))
7621                .await;
7622            match result {
7623                Err(Error::Tool(error)) if refused => assert_eq!(
7624                    error
7625                        .downcast_ref::<crate::UnsupportedSyscallError>()
7626                        .unwrap()
7627                        .0,
7628                    syscalls::Sysno::clone
7629                ),
7630                Err(Error::Errno(actual)) if !refused => assert_eq!(actual, errno),
7631                result => panic!("wrong failed-clone policy: {result:?}"),
7632            }
7633            assert_eq!(guest.injections, 1);
7634            assert_eq!(guest.refusal_takes, 1, "the refusal must be asked for once");
7635            assert_eq!(guest.refusal, None, "a reported refusal must be taken");
7636            assert_eq!(guest.thread.clone_flags, None);
7637            let requests = guest.requests.lock().unwrap();
7638            assert_eq!(
7639                requests
7640                    .iter()
7641                    .filter(|request| matches!(request, GlobalRequest::ReportUnsupportedSyscall(name) if name == "clone"))
7642                    .count(),
7643                usize::from(reported)
7644            );
7645            report.rewind().unwrap();
7646            let mut aggregate = String::new();
7647            report.read_to_string(&mut aggregate).unwrap();
7648            assert_eq!(aggregate, if reported { "clone\n" } else { "" });
7649        }
7650    }
7651
7652    fn bits(signals: &[i32]) -> KernelSigset {
7653        signals
7654            .iter()
7655            .fold(0, |set, &signal| set | kernel_sigset_bit(signal))
7656    }
7657
7658    #[test]
7659    fn a_precise_futex_wait_holds_a_committed_signal_only_if_a_host_timed_source_can_post_it() {
7660        let chld = kernel_sigset_bit(libc::SIGCHLD);
7661        let usr1 = kernel_sigset_bit(libc::SIGUSR1);
7662        // Nothing committed: SIGCHLD and the host-timed signals are held.
7663        assert_eq!(futex_wait_held_after_wake(0, 0), chld);
7664        assert_eq!(futex_wait_held_after_wake(usr1, 0), chld | usr1);
7665        // A committed SIGCHLD is not held (https://github.com/rrnewton/hermit/issues/4005).
7666        assert_eq!(futex_wait_held_after_wake(0, chld), 0);
7667        assert_eq!(futex_wait_held_after_wake(usr1, chld), usr1);
7668        // Unless a host-timed source can post it too, and a committed
7669        // host-timed signal stays held.
7670        assert_eq!(futex_wait_held_after_wake(chld, chld), chld);
7671        assert_eq!(futex_wait_held_after_wake(usr1, usr1 | chld), usr1);
7672    }
7673
7674    #[test]
7675    fn kernel_signal_state_parses_proc_status() {
7676        let status = "Name:\tguest\nSigQ:\t1/1024\nSigPnd:\t0000000000000200\n\
7677                      ShdPnd:\t0000000000000400\nSigBlk:\t0000000000000800\n\
7678                      SigIgn:\t0000000000001000\nSigCgt:\t0000000000000400\n";
7679        assert_eq!(
7680            KernelSignalState::parse(status),
7681            Some(KernelSignalState {
7682                pending: bits(&[libc::SIGUSR1, libc::SIGSEGV]),
7683                thread_pending: bits(&[libc::SIGUSR1]),
7684                shared_pending: bits(&[libc::SIGSEGV]),
7685                blocked: bits(&[libc::SIGUSR2]),
7686                ignored: bits(&[libc::SIGPIPE]),
7687                caught: bits(&[libc::SIGSEGV]),
7688            })
7689        );
7690        // Every line is required: a missing one is not an empty set.
7691        assert_eq!(
7692            KernelSignalState::parse("SigPnd:\t0\nShdPnd:\t0\nSigBlk:\t0\nSigIgn:\t0\n"),
7693            None
7694        );
7695        assert_eq!(
7696            KernelSignalState::parse(
7697                "SigPnd:\tzz\nShdPnd:\t0\nSigBlk:\t0\nSigIgn:\t0\nSigCgt:\t0\n"
7698            ),
7699            None
7700        );
7701    }
7702
7703    #[test]
7704    fn only_unblocked_caught_or_fatal_signals_interrupt_a_wait() {
7705        let state = KernelSignalState {
7706            pending: bits(&[
7707                libc::SIGUSR1,
7708                libc::SIGUSR2,
7709                libc::SIGPIPE,
7710                libc::SIGTERM,
7711                libc::SIGCHLD,
7712                libc::SIGWINCH,
7713                libc::SIGURG,
7714                libc::SIGCONT,
7715            ]),
7716            blocked: bits(&[libc::SIGUSR2]),
7717            ignored: bits(&[libc::SIGPIPE]),
7718            caught: bits(&[libc::SIGUSR1, libc::SIGUSR2, libc::SIGWINCH]),
7719            ..Default::default()
7720        };
7721        // Caught and unblocked (SIGUSR1, SIGWINCH) and default-fatal (SIGTERM) end the
7722        // wait; blocked (SIGUSR2), ignored (SIGPIPE), and default-ignored ones do not.
7723        assert_eq!(
7724            state.pending_interrupting(state.blocked, false),
7725            bits(&[libc::SIGUSR1, libc::SIGWINCH, libc::SIGTERM])
7726        );
7727        // The mask the guest had when the wait began decides, not the current one.
7728        assert_eq!(
7729            state.pending_interrupting(0, false),
7730            bits(&[libc::SIGUSR1, libc::SIGUSR2, libc::SIGWINCH, libc::SIGTERM])
7731        );
7732        let interrupting = state.interrupting(state.blocked);
7733        assert_ne!(interrupting & kernel_sigset_bit(libc::SIGKILL), 0);
7734        assert_ne!(interrupting & kernel_sigset_bit(libc::SIGSTOP), 0);
7735        assert_eq!(
7736            interrupting & kernel_sigset_bit(reverie::PERF_EVENT_SIGNAL as i32),
7737            0
7738        );
7739        assert_eq!(kernel_sigset_bit(0), 0);
7740        assert_eq!(kernel_sigset_bit(65), 0);
7741    }
7742
7743    /// A wait whose restart starts its relative timeout again does not end for a
7744    /// default `SIGTSTP`, `SIGTTIN`, or `SIGTTOU`, which Linux discards in an
7745    /// orphaned process group; every other wait still does. `SIGSTOP`, a caught
7746    /// stop signal, and a fatal signal end both kinds, and an ignored stop signal
7747    /// ends neither (review of https://github.com/rrnewton/hermit/pull/3361 at
7748    /// `cbb36408`, finding 4).
7749    #[test]
7750    fn a_rearming_wait_is_not_ended_by_a_default_job_control_stop() {
7751        let stops = bits(&[libc::SIGTSTP, libc::SIGTTIN, libc::SIGTTOU]);
7752        let state = KernelSignalState {
7753            pending: stops | bits(&[libc::SIGSTOP, libc::SIGTERM]),
7754            ..Default::default()
7755        };
7756        assert_eq!(state.default_job_control_stops(), stops);
7757        assert_eq!(state.pending_interrupting(0, false), state.pending);
7758        assert_eq!(
7759            state.pending_interrupting(0, true),
7760            bits(&[libc::SIGSTOP, libc::SIGTERM])
7761        );
7762        let rearming = state.interrupting_wait(0, true);
7763        assert_eq!(rearming & stops, 0);
7764        assert_eq!(rearming, state.interrupting(0) & !stops);
7765
7766        // A handler makes SIGTSTP an ordinary caught signal, and SIG_IGN takes
7767        // SIGTTIN out of every wait; SIGTTOU keeps its default action.
7768        let state = KernelSignalState {
7769            pending: stops,
7770            caught: bits(&[libc::SIGTSTP]),
7771            ignored: bits(&[libc::SIGTTIN]),
7772            ..Default::default()
7773        };
7774        assert_eq!(state.default_job_control_stops(), bits(&[libc::SIGTTOU]));
7775        assert_eq!(state.pending_interrupting(0, true), bits(&[libc::SIGTSTP]));
7776        assert_eq!(
7777            state.pending_interrupting(0, false),
7778            bits(&[libc::SIGTSTP, libc::SIGTTOU])
7779        );
7780        // The mask still decides first.
7781        assert_eq!(state.pending_interrupting(bits(&[libc::SIGTSTP]), true), 0);
7782    }
7783
7784    #[test]
7785    fn kernel_blocked_mask_preserves_libc_signal_membership() {
7786        let mask = blocked_signal_mask();
7787        let mut libc_mask: libc::sigset_t = unsafe { std::mem::zeroed() };
7788        unsafe {
7789            libc::sigfillset(&mut libc_mask);
7790            libc::sigdelset(&mut libc_mask, reverie::PERF_EVENT_SIGNAL as i32);
7791        }
7792        for raw_signal in 1..=KernelSigset::BITS as i32 {
7793            assert_eq!(
7794                signal_is_blocked(&mask, SigWrapper(raw_signal)),
7795                unsafe { libc::sigismember(&libc_mask, raw_signal) == 1 },
7796                "signal {raw_signal} membership changed while converting to the kernel ABI"
7797            );
7798        }
7799    }
7800
7801    #[test]
7802    fn clone_without_child_cleartid_does_not_register_the_pointer() {
7803        assert_eq!(
7804            child_tid_clear_address(CloneFlags::CLONE_CHILD_SETTID, 0x1234),
7805            0
7806        );
7807    }
7808
7809    #[test]
7810    fn clone_with_child_cleartid_registers_the_pointer() {
7811        assert_eq!(
7812            child_tid_clear_address(CloneFlags::CLONE_CHILD_CLEARTID, 0x1234),
7813            0x1234
7814        );
7815    }
7816
7817    #[test]
7818    fn linux_default_dispositions_that_do_not_interrupt_child_waits() {
7819        for signal in [libc::SIGCHLD, libc::SIGCONT, libc::SIGURG, libc::SIGWINCH] {
7820            assert!(signal_default_disposition_does_not_interrupt_child_wait(
7821                SigWrapper(signal)
7822            ));
7823        }
7824        for signal in [libc::SIGALRM, libc::SIGSTOP, libc::SIGUSR1] {
7825            assert!(!signal_default_disposition_does_not_interrupt_child_wait(
7826                SigWrapper(signal)
7827            ));
7828        }
7829    }
7830
7831    /// Only a serialized run on a backend whose kernel signal state is the
7832    /// guest's own, with no gdbserver attached, retires threads at a fatal
7833    /// delivery.
7834    #[test]
7835    fn only_serialized_ptrace_runs_without_a_gdbserver_retire_at_a_fatal_delivery() {
7836        let ptrace = Config {
7837            sequentialize_threads: true,
7838            ..Config::default()
7839        }
7840        .with_backend(|backend| *backend = reverie::BackendCapabilities::PTRACE);
7841        assert!(retires_others_at_fatal_delivery(&ptrace));
7842        for (case, cfg) in [
7843            (
7844                "gdbserver",
7845                Config {
7846                    gdbserver: true,
7847                    ..ptrace.clone()
7848                },
7849            ),
7850            (
7851                "unserialized",
7852                Config {
7853                    sequentialize_threads: false,
7854                    ..ptrace.clone()
7855                },
7856            ),
7857            (
7858                "DBT",
7859                ptrace
7860                    .clone()
7861                    .with_backend(|backend| *backend = reverie::BackendCapabilities::DBT),
7862            ),
7863        ] {
7864            assert!(!retires_others_at_fatal_delivery(&cfg), "{case}");
7865        }
7866    }
7867
7868    /// A delivery stop's signal ends the process only when it is unblocked
7869    /// and left at a default that terminates or dumps core.
7870    #[test]
7871    fn only_an_unblocked_default_fatal_delivery_ends_the_process() {
7872        let abrt = kernel_sigset_bit(libc::SIGABRT);
7873        let default = KernelSignalState::default();
7874        assert!(default.delivery_carries_out_a_fatal_default(libc::SIGABRT));
7875        assert!(default.delivery_carries_out_a_fatal_default(libc::SIGSEGV));
7876        for signal in [libc::SIGCHLD, libc::SIGWINCH, libc::SIGTSTP, libc::SIGCONT] {
7877            assert!(
7878                !default.delivery_carries_out_a_fatal_default(signal),
7879                "{signal}"
7880            );
7881        }
7882        for state in [
7883            KernelSignalState {
7884                blocked: abrt,
7885                ..Default::default()
7886            },
7887            KernelSignalState {
7888                caught: abrt,
7889                ..Default::default()
7890            },
7891            KernelSignalState {
7892                ignored: abrt,
7893                ..Default::default()
7894            },
7895        ] {
7896            assert!(
7897                !state.delivery_carries_out_a_fatal_default(libc::SIGABRT),
7898                "{state:?}"
7899            );
7900        }
7901    }
7902
7903    /// signal(7): the signals whose default action is to terminate or dump
7904    /// core end the process; those ignored, stopping or continuing by
7905    /// default do not, and SIGKILL never reaches a delivery callback
7906    /// (https://github.com/rrnewton/hermit/issues/4278).
7907    #[test]
7908    fn default_actions_that_end_the_process() {
7909        for signal in [
7910            libc::SIGTERM,
7911            libc::SIGINT,
7912            libc::SIGHUP,
7913            libc::SIGQUIT,
7914            libc::SIGABRT,
7915            libc::SIGSEGV,
7916            libc::SIGBUS,
7917            libc::SIGFPE,
7918            libc::SIGILL,
7919            libc::SIGPIPE,
7920            libc::SIGALRM,
7921            libc::SIGUSR1,
7922            libc::SIGUSR2,
7923            libc::SIGRTMIN(),
7924            libc::SIGRTMAX(),
7925        ] {
7926            assert!(
7927                signal_default_action_ends_the_process(SigWrapper(signal)),
7928                "{signal}"
7929            );
7930        }
7931        for signal in [
7932            libc::SIGCHLD,
7933            libc::SIGCONT,
7934            libc::SIGURG,
7935            libc::SIGWINCH,
7936            libc::SIGSTOP,
7937            libc::SIGTSTP,
7938            libc::SIGTTIN,
7939            libc::SIGTTOU,
7940            libc::SIGKILL,
7941        ] {
7942            assert!(
7943                !signal_default_action_ends_the_process(SigWrapper(signal)),
7944                "{signal}"
7945            );
7946        }
7947    }
7948
7949    #[test]
7950    fn uncatchable_signals_do_not_require_a_sigaction_query() {
7951        assert!(signal_has_uncatchable_default_disposition(SigWrapper(
7952            libc::SIGKILL
7953        )));
7954        assert!(signal_has_uncatchable_default_disposition(SigWrapper(
7955            libc::SIGSTOP
7956        )));
7957        assert!(!signal_has_uncatchable_default_disposition(SigWrapper(
7958            libc::SIGUSR1
7959        )));
7960    }
7961
7962    #[test]
7963    fn waitid_ready_child_wins_when_scheduler_also_reports_a_signal() {
7964        assert_eq!(
7965            exact_wait_poll_decision(true, true, Some(ExactChildWaitState::Running)),
7966            ExactWaitPollDecision::ChildReady
7967        );
7968        assert_eq!(
7969            exact_wait_poll_decision(false, true, Some(ExactChildWaitState::LogicallyExited)),
7970            ExactWaitPollDecision::ReapAfterLogicalExit
7971        );
7972        assert_eq!(
7973            exact_wait_poll_decision(false, true, Some(ExactChildWaitState::PhysicalExitPending)),
7974            ExactWaitPollDecision::AwaitPhysicalExit
7975        );
7976        assert_eq!(
7977            exact_wait_poll_decision(false, true, Some(ExactChildWaitState::Running)),
7978            ExactWaitPollDecision::Interrupted
7979        );
7980        assert_eq!(
7981            exact_wait_poll_decision(false, false, Some(ExactChildWaitState::Running)),
7982            ExactWaitPollDecision::Retry
7983        );
7984    }
7985
7986    #[test]
7987    fn wait4_argument_validation_follows_linux_precedence() {
7988        let valid_bits = [0, 1, 3, 29, 30, 31];
7989        for bit in valid_bits {
7990            let options = WaitPidFlag::from_bits_retain((1_u32 << bit) as libc::c_int);
7991            assert_eq!(validate_wait4_arguments(-1, options), Ok(()), "bit {bit}");
7992        }
7993
7994        for bit in (0..u32::BITS).filter(|bit| !valid_bits.contains(bit)) {
7995            let options = WaitPidFlag::from_bits_retain((1_u32 << bit) as libc::c_int);
7996            assert_eq!(
7997                validate_wait4_arguments(-1, options),
7998                Err(Errno::EINVAL),
7999                "bit {bit}"
8000            );
8001        }
8002
8003        // Only INT_MIN is refused: every other pid, including the nearest
8004        // process-group selector, must still reach the wait.
8005        for pid in [libc::pid_t::MIN + 1, -2, 0, 1, libc::pid_t::MAX] {
8006            assert_eq!(
8007                validate_wait4_arguments(pid, WaitPidFlag::empty()),
8008                Ok(()),
8009                "pid {pid}"
8010            );
8011        }
8012        assert_eq!(
8013            validate_wait4_arguments(libc::pid_t::MIN, WaitPidFlag::empty()),
8014            Err(Errno::ESRCH)
8015        );
8016        assert_eq!(
8017            validate_wait4_arguments(libc::pid_t::MIN, WaitPidFlag::from_bits_retain(0x10)),
8018            Err(Errno::EINVAL),
8019            "invalid options must win over the INT_MIN selector"
8020        );
8021    }
8022
8023    #[test]
8024    fn stale_any_child_preserves_interrupt_until_no_ready_child_remains() {
8025        let next_child = DetPid::from_raw(200);
8026
8027        assert!(
8028            !stale_any_wait_must_interrupt(true, Some(next_child)),
8029            "another ready child must retain child-ready precedence"
8030        );
8031        assert!(
8032            stale_any_wait_must_interrupt(true, None),
8033            "a pending signal must interrupt before the wait parks again"
8034        );
8035        assert!(!stale_any_wait_must_interrupt(false, None));
8036    }
8037
8038    #[test]
8039    fn ioprio_query_reports_fixed_raw_and_effective_defaults() {
8040        assert_eq!(virtual_ioprio(IOPRIO_WHO_PROCESS), Ok(0));
8041        assert_eq!(
8042            virtual_ioprio(IOPRIO_WHO_PGRP),
8043            Ok(i64::from(IOPRIO_DEFAULT_EFFECTIVE))
8044        );
8045        assert_eq!(
8046            virtual_ioprio(IOPRIO_WHO_USER),
8047            Ok(i64::from(IOPRIO_DEFAULT_EFFECTIVE))
8048        );
8049        assert_eq!(virtual_ioprio(0), Err(Errno::EINVAL));
8050        assert_eq!(virtual_ioprio(4), Err(Errno::EINVAL));
8051    }
8052
8053    #[test]
8054    fn waitid_siginfo_canonicalization_clears_only_cpu_accounting() {
8055        let mut info: libc::siginfo_t = unsafe { std::mem::zeroed() };
8056        info.si_signo = libc::SIGCHLD;
8057        info.si_code = libc::CLD_EXITED;
8058        // SAFETY: This uses the same Linux SIGCHLD layout mirror validated by
8059        // canonicalize_waitid_siginfo.
8060        let fields = unsafe {
8061            &mut (*(std::ptr::addr_of_mut!(info)).cast::<WaitidSiginfoHead>())
8062                .fields
8063                .sigchld
8064        };
8065        fields.pid = 123;
8066        fields.uid = 456;
8067        fields.status = 7;
8068        fields.utime = 8;
8069        fields.stime = 9;
8070
8071        canonicalize_waitid_siginfo(&mut info);
8072
8073        assert_eq!(info.si_signo, libc::SIGCHLD);
8074        assert_eq!(info.si_code, libc::CLD_EXITED);
8075        assert_eq!(unsafe { info.si_pid() }, 123);
8076        assert_eq!(unsafe { info.si_uid() }, 456);
8077        assert_eq!(unsafe { info.si_status() }, 7);
8078        assert_eq!(unsafe { info.si_utime() }, 0);
8079        assert_eq!(unsafe { info.si_stime() }, 0);
8080    }
8081
8082    #[test]
8083    fn wait_status_rollup_only_accepts_process_termination() {
8084        assert!(wait_status_is_termination(0));
8085        assert!(wait_status_is_termination(libc::SIGTERM));
8086        assert!(!wait_status_is_termination((libc::SIGSTOP << 8) | 0x7f));
8087        assert!(!wait_status_is_termination(0xffff));
8088
8089        assert!(waitid_code_is_termination(libc::CLD_EXITED));
8090        assert!(waitid_code_is_termination(libc::CLD_KILLED));
8091        assert!(waitid_code_is_termination(libc::CLD_DUMPED));
8092        assert!(!waitid_code_is_termination(libc::CLD_STOPPED));
8093        assert!(!waitid_code_is_termination(libc::CLD_CONTINUED));
8094        assert!(!waitid_code_is_termination(libc::CLD_TRAPPED));
8095    }
8096
8097    /// `do_futex` (kernel/futex/syscalls.c): FUTEX_CLOCK_REALTIME only with
8098    /// FUTEX_WAIT_BITSET, FUTEX_WAIT_REQUEUE_PI or FUTEX_LOCK_PI2, else -ENOSYS;
8099    /// the removed FUTEX_FD and unknown commands are -ENOSYS; the requeue-PI
8100    /// commands are refused by name; the rest, the PI lock commands included,
8101    /// are emulated (https://github.com/rrnewton/hermit/issues/3958).
8102    #[test]
8103    fn futex_commands_are_classified_as_linux_dispatches_them() {
8104        use FutexCommand::*;
8105        let private = libc::FUTEX_PRIVATE_FLAG;
8106        let realtime = libc::FUTEX_CLOCK_REALTIME;
8107        for cmd in [
8108            libc::FUTEX_WAIT,
8109            libc::FUTEX_WAIT_BITSET,
8110            libc::FUTEX_WAKE,
8111            libc::FUTEX_WAKE_BITSET,
8112            libc::FUTEX_REQUEUE,
8113            libc::FUTEX_CMP_REQUEUE,
8114            libc::FUTEX_WAKE_OP,
8115        ] {
8116            assert_eq!(classify_futex_command(cmd), Served, "cmd {cmd}");
8117            assert_eq!(classify_futex_command(cmd | private), Served, "cmd {cmd}");
8118        }
8119        assert_eq!(
8120            classify_futex_command(libc::FUTEX_WAIT_BITSET | realtime),
8121            Served
8122        );
8123        for cmd in [
8124            libc::FUTEX_WAIT,
8125            libc::FUTEX_WAKE,
8126            libc::FUTEX_REQUEUE,
8127            libc::FUTEX_WAKE_OP,
8128        ] {
8129            assert_eq!(classify_futex_command(cmd | realtime), NoSys, "cmd {cmd}");
8130        }
8131        for cmd in [libc::FUTEX_FD, 14, 99, libc::FUTEX_CMD_MASK] {
8132            assert_eq!(classify_futex_command(cmd), NoSys, "cmd {cmd}");
8133        }
8134        for cmd in [
8135            libc::FUTEX_LOCK_PI,
8136            libc::FUTEX_LOCK_PI2,
8137            libc::FUTEX_TRYLOCK_PI,
8138            libc::FUTEX_UNLOCK_PI,
8139        ] {
8140            assert_eq!(classify_futex_command(cmd), Served, "cmd {cmd}");
8141            assert_eq!(classify_futex_command(cmd | private), Served, "cmd {cmd}");
8142        }
8143        assert_eq!(
8144            classify_futex_command(libc::FUTEX_LOCK_PI2 | realtime),
8145            Served
8146        );
8147        for cmd in [
8148            libc::FUTEX_LOCK_PI,
8149            libc::FUTEX_TRYLOCK_PI,
8150            libc::FUTEX_UNLOCK_PI,
8151        ] {
8152            assert_eq!(classify_futex_command(cmd | realtime), NoSys, "cmd {cmd}");
8153        }
8154        assert_eq!(
8155            classify_futex_command(libc::FUTEX_WAIT_REQUEUE_PI | realtime),
8156            RequeuePi("FUTEX_WAIT_REQUEUE_PI")
8157        );
8158        for (cmd, name) in [
8159            (libc::FUTEX_WAIT_REQUEUE_PI, "FUTEX_WAIT_REQUEUE_PI"),
8160            (libc::FUTEX_CMP_REQUEUE_PI, "FUTEX_CMP_REQUEUE_PI"),
8161        ] {
8162            assert_eq!(classify_futex_command(cmd), RequeuePi(name));
8163        }
8164        assert_eq!(
8165            classify_futex_command(libc::FUTEX_CMP_REQUEUE_PI | realtime),
8166            NoSys
8167        );
8168    }
8169
8170    /// `futex_init_timeout` and `do_futex` (kernel/futex/syscalls.c:114-116,
8171    /// 147-157 at v6.16): FUTEX_WAIT's timeout is relative; FUTEX_WAIT_BITSET's,
8172    /// FUTEX_LOCK_PI's and FUTEX_LOCK_PI2's are absolute. FUTEX_LOCK_PI's is
8173    /// always on CLOCK_REALTIME, the others' only with FUTEX_CLOCK_REALTIME.
8174    #[test]
8175    fn futex_deadlines_are_measured_as_linux_measures_them() {
8176        let one_second = Timespec {
8177            tv_sec: 1,
8178            tv_nsec: 0,
8179        };
8180        let realtime = libc::FUTEX_CLOCK_REALTIME;
8181        assert_eq!(
8182            parse_futex_timeout(libc::FUTEX_WAIT, one_second),
8183            Ok(FutexTimeout::Relative(1_000_000_000))
8184        );
8185        for cmd in [
8186            libc::FUTEX_WAIT_BITSET,
8187            libc::FUTEX_LOCK_PI,
8188            libc::FUTEX_LOCK_PI2,
8189            libc::FUTEX_LOCK_PI | libc::FUTEX_PRIVATE_FLAG,
8190        ] {
8191            assert_eq!(
8192                parse_futex_timeout(cmd, one_second),
8193                Ok(FutexTimeout::Absolute(LogicalTime::from_nanos(
8194                    1_000_000_000
8195                ))),
8196                "cmd {cmd}"
8197            );
8198        }
8199        assert_eq!(
8200            futex_deadline_clock(libc::FUTEX_LOCK_PI),
8201            syscalls::ClockId::CLOCK_REALTIME
8202        );
8203        assert_eq!(
8204            futex_deadline_clock(libc::FUTEX_LOCK_PI2),
8205            syscalls::ClockId::CLOCK_MONOTONIC
8206        );
8207        assert_eq!(
8208            futex_deadline_clock(libc::FUTEX_LOCK_PI2 | realtime),
8209            syscalls::ClockId::CLOCK_REALTIME
8210        );
8211        assert_eq!(
8212            futex_deadline_clock(libc::FUTEX_WAIT_BITSET),
8213            syscalls::ClockId::CLOCK_MONOTONIC
8214        );
8215    }
8216
8217    /// FUTEX_WAKE_OP's encoding, as `futex_atomic_op_inuser` decodes it: 12-bit
8218    /// signed arguments, the shift flag (with an out-of-range shift masked to
8219    /// 0..31), and -ENOSYS for an unknown operation or comparison.
8220    /// An injected probe's futex op, uaddr, uaddr2, val and val3.
8221    type ProbeArgs = (i32, usize, usize, i32, i32);
8222
8223    /// A guest standing in for the kernel: each injected probe is recorded and
8224    /// answered with the next scripted result.
8225    struct ScriptedProbes {
8226        answers: std::collections::VecDeque<Result<i64, Errno>>,
8227        probes: Vec<ProbeArgs>,
8228    }
8229
8230    impl ScriptedProbes {
8231        fn inject(&mut self, probe: syscalls::Futex) -> KeyProbeAnswer<'_> {
8232            self.probes.push((
8233                probe.futex_op(),
8234                probe.uaddr().map_or(0, AddrMut::as_raw),
8235                probe.uaddr2().map_or(0, AddrMut::as_raw),
8236                probe.val(),
8237                probe.val3(),
8238            ));
8239            let answer = self
8240                .answers
8241                .pop_front()
8242                .expect("the probe was injected more often than scripted");
8243            Box::pin(async move { answer })
8244        }
8245    }
8246
8247    fn run_scripted(
8248        access: KeyAccess,
8249        answer: Result<i64, Errno>,
8250    ) -> (SharedKeyProbe, Vec<ProbeArgs>) {
8251        let mut guest = ScriptedProbes {
8252            answers: [answer].into_iter().collect(),
8253            probes: Vec::new(),
8254        };
8255        let result = futures::executor::block_on(run_shared_futex_key_probe(
8256            &mut guest,
8257            shared_futex_key_probe(0x1000, access),
8258            access,
8259            |guest, probe| guest.inject(probe),
8260        ));
8261        (result, guest.probes)
8262    }
8263
8264    /// The shared-key admission probe is injected exactly once
8265    /// (https://github.com/rrnewton/hermit/pull/4020). When a signal stops it
8266    /// before it runs (`Guest::inject` returns ERESTARTSYS, or EINTR), it is not
8267    /// injected again, because Reverie holds that signal in a single slot that a
8268    /// second interruption would overwrite, and the interruption is reported as
8269    /// such: never as an admitted key and never as the guest's errno. The
8270    /// caller stops the run by name on it. The kernel's own answers keep their
8271    /// meaning: 0 admits, ENOSYS admits only a WRITE key, EFAULT is the
8272    /// guest's.
8273    #[test]
8274    fn an_interrupted_shared_key_probe_is_never_retried_or_taken_as_an_answer() {
8275        let read = (libc::FUTEX_REQUEUE, 0x1000, 0x1000, 0, 0);
8276        let write = (
8277            libc::FUTEX_WAKE_OP,
8278            0x1000,
8279            0x1000,
8280            0,
8281            (7 << 28) | (1 << 12),
8282        );
8283        for (access, args) in [(KeyAccess::Read, read), (KeyAccess::Write, write)] {
8284            for errno in [Errno::ERESTARTSYS, Errno::EINTR] {
8285                assert_eq!(
8286                    run_scripted(access, Err(errno)),
8287                    (SharedKeyProbe::Interrupted(errno), vec![args]),
8288                    "{access:?} {errno}"
8289                );
8290            }
8291            assert_eq!(
8292                run_scripted(access, Ok(0)),
8293                (SharedKeyProbe::Admitted, vec![args])
8294            );
8295            assert_eq!(
8296                run_scripted(access, Err(Errno::EFAULT)),
8297                (SharedKeyProbe::Refused(Errno::EFAULT), vec![args])
8298            );
8299        }
8300        assert_eq!(
8301            run_scripted(KeyAccess::Read, Err(Errno::ENOSYS)),
8302            (SharedKeyProbe::Refused(Errno::ENOSYS), vec![read])
8303        );
8304        assert_eq!(
8305            run_scripted(KeyAccess::Write, Err(Errno::ENOSYS)),
8306            (SharedKeyProbe::Admitted, vec![write])
8307        );
8308    }
8309
8310    #[test]
8311    fn futex_wake_op_decodes_and_applies_as_linux() {
8312        let encode = |op: u32, cmp: u32, oparg: u32, cmparg: u32| {
8313            ((op & 0xf) << 28) | ((cmp & 0xf) << 24) | ((oparg & 0xfff) << 12) | (cmparg & 0xfff)
8314        };
8315        let set5_eq0 = FutexWakeOp::decode(encode(FUTEX_OP_SET, FUTEX_OP_CMP_EQ, 5, 0));
8316        assert_eq!(set5_eq0.apply(0), Some(5));
8317        assert_eq!(set5_eq0.compare(0), Some(true));
8318        assert_eq!(set5_eq0.compare(1), Some(false));
8319        let add_minus1 = FutexWakeOp::decode(encode(FUTEX_OP_ADD, FUTEX_OP_CMP_LT, 0xfff, 0x800));
8320        assert_eq!(add_minus1.apply(10), Some(9));
8321        assert_eq!(add_minus1.apply(i32::MIN), Some(i32::MAX));
8322        assert_eq!(add_minus1.compare(-2048), Some(false));
8323        assert_eq!(add_minus1.compare(-2049), Some(true));
8324        let or = FutexWakeOp::decode(encode(FUTEX_OP_OR, FUTEX_OP_CMP_NE, 0b100, 7));
8325        assert_eq!(or.apply(0b011), Some(0b111));
8326        assert_eq!(or.compare(7), Some(false));
8327        let andn = FutexWakeOp::decode(encode(FUTEX_OP_ANDN, FUTEX_OP_CMP_GE, 0b110, 3));
8328        assert_eq!(andn.apply(0b111), Some(0b001));
8329        assert_eq!(andn.compare(3), Some(true));
8330        let xor = FutexWakeOp::decode(encode(FUTEX_OP_XOR, FUTEX_OP_CMP_LE, 0xff, 0));
8331        assert_eq!(xor.apply(0x0f), Some(0xf0));
8332        assert_eq!(xor.compare(1), Some(false));
8333        let shift = FutexWakeOp::decode(encode(
8334            FUTEX_OP_OPARG_SHIFT | FUTEX_OP_OR,
8335            FUTEX_OP_CMP_GT,
8336            3,
8337            0,
8338        ));
8339        assert_eq!(shift.apply(0), Some(8));
8340        assert_eq!(shift.compare(1), Some(true));
8341        // A shift of -1 (0xfff) is masked to 31, as Linux does.
8342        let masked = FutexWakeOp::decode(encode(FUTEX_OP_OPARG_SHIFT | FUTEX_OP_SET, 0, 0xfff, 0));
8343        assert_eq!(masked.apply(0), Some(i32::MIN));
8344        let unknown_op = FutexWakeOp::decode(encode(7, FUTEX_OP_CMP_EQ, 1, 0));
8345        assert!(!unknown_op.has_known_operation());
8346        assert_eq!(unknown_op.apply(0), None);
8347        let unknown_cmp = FutexWakeOp::decode(encode(FUTEX_OP_SET, 6, 1, 0));
8348        assert!(unknown_cmp.has_known_operation());
8349        assert_eq!(unknown_cmp.compare(0), None);
8350    }
8351
8352    #[test]
8353    fn futex_timeout_units_and_modes_match_linux() {
8354        let timeout = Timespec {
8355            tv_sec: 2,
8356            tv_nsec: 3,
8357        };
8358        assert_eq!(
8359            parse_futex_timeout(libc::FUTEX_WAIT, timeout),
8360            Ok(FutexTimeout::Relative(2_000_000_003))
8361        );
8362        assert_eq!(
8363            parse_futex_timeout(libc::FUTEX_WAIT_BITSET, timeout),
8364            Ok(FutexTimeout::Absolute(LogicalTime::from_nanos(
8365                2_000_000_003
8366            )))
8367        );
8368        // The command bits must be matched after masking off FUTEX_PRIVATE_FLAG
8369        // (and FUTEX_CLOCK_REALTIME): a private FUTEX_WAIT_BITSET still uses an
8370        // absolute deadline, and a private FUTEX_WAIT still uses a relative one.
8371        assert_eq!(
8372            parse_futex_timeout(libc::FUTEX_WAIT_BITSET | libc::FUTEX_PRIVATE_FLAG, timeout),
8373            Ok(FutexTimeout::Absolute(LogicalTime::from_nanos(
8374                2_000_000_003
8375            )))
8376        );
8377        assert_eq!(
8378            parse_futex_timeout(libc::FUTEX_WAIT | libc::FUTEX_PRIVATE_FLAG, timeout),
8379            Ok(FutexTimeout::Relative(2_000_000_003))
8380        );
8381    }
8382
8383    #[test]
8384    fn absolute_futex_timeout_is_rebased_to_logical_time() {
8385        let logical_now = LogicalTime::from_secs(100);
8386        let clock_now = LogicalTime::from_secs(5_000);
8387        let deadline = clock_now + Duration::from_millis(100);
8388        assert_eq!(
8389            rebase_absolute_timeout(deadline, clock_now, logical_now),
8390            logical_now + Duration::from_millis(100)
8391        );
8392        assert_eq!(
8393            rebase_absolute_timeout(
8394                clock_now - LogicalTime::from_nanos(1),
8395                clock_now,
8396                logical_now
8397            ),
8398            logical_now
8399        );
8400    }
8401
8402    #[test]
8403    fn absolute_futex_timeout_detects_host_and_logical_clock_domains() {
8404        let host_monotonic_now = LogicalTime::from_secs(374_766);
8405        let logical_now = LogicalTime::from_secs(1_640_995_199);
8406        let delta = Duration::from_millis(100);
8407
8408        assert!(absolute_timeout_uses_host_clock(
8409            host_monotonic_now + delta,
8410            host_monotonic_now,
8411            logical_now
8412        ));
8413        assert!(!absolute_timeout_uses_host_clock(
8414            logical_now + delta,
8415            host_monotonic_now,
8416            logical_now
8417        ));
8418
8419        let host_realtime_now = LogicalTime::from_secs(1_785_142_800);
8420        assert!(absolute_timeout_uses_host_clock(
8421            host_realtime_now + delta,
8422            host_realtime_now,
8423            logical_now
8424        ));
8425    }
8426
8427    /// Linux's `futex_init_timeout` refuses only an invalid timespec, and
8428    /// `timespec64_to_ktime` clamps a valid one beyond `KTIME_MAX` instead of
8429    /// refusing it (Codex re-check of https://github.com/rrnewton/hermit/pull/4061).
8430    #[test]
8431    fn futex_timeout_clamps_a_huge_valid_timespec_like_ktime() {
8432        let huge = Timespec {
8433            tv_sec: libc::time_t::MAX,
8434            tv_nsec: 0,
8435        };
8436        assert!(futex_timespec_is_valid(huge));
8437        assert_eq!(
8438            parse_futex_timeout(libc::FUTEX_WAIT, huge),
8439            Ok(FutexTimeout::Relative(FUTEX_KTIME_MAX_NANOS))
8440        );
8441        assert_eq!(
8442            parse_futex_timeout(libc::FUTEX_WAIT_BITSET, huge),
8443            Ok(FutexTimeout::Absolute(LogicalTime::from_nanos(
8444                FUTEX_KTIME_MAX_NANOS
8445            )))
8446        );
8447        let at_ktime_sec_max = Timespec {
8448            tv_sec: (FUTEX_KTIME_MAX_NANOS / 1_000_000_000) as libc::time_t,
8449            tv_nsec: 0,
8450        };
8451        assert_eq!(
8452            parse_futex_timeout(libc::FUTEX_WAIT, at_ktime_sec_max),
8453            Ok(FutexTimeout::Relative(FUTEX_KTIME_MAX_NANOS))
8454        );
8455        let just_below = Timespec {
8456            tv_sec: (FUTEX_KTIME_MAX_NANOS / 1_000_000_000 - 1) as libc::time_t,
8457            tv_nsec: 999_999_999,
8458        };
8459        assert_eq!(
8460            parse_futex_timeout(libc::FUTEX_WAIT, just_below),
8461            Ok(FutexTimeout::Relative(
8462                (FUTEX_KTIME_MAX_NANOS / 1_000_000_000) * 1_000_000_000 - 1
8463            ))
8464        );
8465        for invalid in [(-1, 0), (0, -1), (0, 1_000_000_000)] {
8466            assert!(!futex_timespec_is_valid(Timespec {
8467                tv_sec: invalid.0,
8468                tv_nsec: invalid.1,
8469            }));
8470        }
8471    }
8472
8473    #[test]
8474    fn futex_timeout_rejects_invalid_timespecs() {
8475        assert_eq!(
8476            parse_futex_timeout(
8477                libc::FUTEX_WAIT,
8478                Timespec {
8479                    tv_sec: -1,
8480                    tv_nsec: 0,
8481                },
8482            ),
8483            Err(Errno::EINVAL)
8484        );
8485        assert_eq!(
8486            parse_futex_timeout(
8487                libc::FUTEX_WAIT_BITSET,
8488                Timespec {
8489                    tv_sec: 0,
8490                    tv_nsec: 1_000_000_000,
8491                },
8492            ),
8493            Err(Errno::EINVAL)
8494        );
8495    }
8496
8497    // The expectations below are not guesses at the kernel's behaviour: each is
8498    // a row measured natively on this host (Linux 6.19, x86-64) with a raw
8499    // `sched_setattr` probe. Where a row differs from what the handler used to
8500    // do, the difference is called out.
8501
8502    /// A well-formed VER0 SCHED_OTHER descriptor, as the decoded fields.
8503    fn plain_attr() -> SchedAttrFields {
8504        SchedAttrFields {
8505            policy: 0,
8506            sched_flags: 0,
8507            priority: 0,
8508            runtime: 0,
8509            deadline: 0,
8510            period: 0,
8511        }
8512    }
8513
8514    #[test]
8515    fn size_zero_is_a_well_formed_ver0_request() {
8516        // ABI compatibility quirk: `if (!size) size = SCHED_ATTR_SIZE_VER0;`.
8517        // Measured native: ret 0. PR #2288 returned E2BIG.
8518        assert_eq!(sched_attr_effective_size(0), Ok(SCHED_ATTR_SIZE_VER0));
8519    }
8520
8521    #[test]
8522    fn size_below_ver0_or_past_a_page_is_too_big() {
8523        // Measured native: size 1, 47 and 4097 all give E2BIG.
8524        assert_eq!(sched_attr_effective_size(1), Err(()));
8525        assert_eq!(sched_attr_effective_size(SCHED_ATTR_SIZE_VER0 - 1), Err(()));
8526        assert_eq!(
8527            sched_attr_effective_size(sched_attr_max_size() + 1),
8528            Err(())
8529        );
8530    }
8531
8532    #[test]
8533    fn size_from_ver0_through_one_page_is_accepted_unchanged() {
8534        // Measured native: 48, 56, 57 and 4096 all give ret 0.
8535        for size in [
8536            SCHED_ATTR_SIZE_VER0,
8537            SCHED_ATTR_SIZE_VER1,
8538            SCHED_ATTR_SIZE_VER1 + 1,
8539            sched_attr_max_size(),
8540        ] {
8541            assert_eq!(sched_attr_effective_size(size), Ok(size), "size {}", size);
8542        }
8543    }
8544
8545    #[test]
8546    fn sched_ext_is_a_valid_policy_and_sched_iso_is_not() {
8547        // Measured native: policy 7 gives ret 0; policy 4 gives EINVAL. PR
8548        // #2288 refused 7.
8549        assert!(is_valid_sched_policy(SCHED_EXT), "SCHED_EXT is accepted");
8550        assert!(!is_valid_sched_policy(4), "SCHED_ISO is reserved");
8551        for policy in [0, 1, 2, 3, 5, 6] {
8552            assert!(is_valid_sched_policy(policy), "policy {}", policy);
8553        }
8554        // Measured native: policy 8 and 99 both give EINVAL.
8555        for policy in [8, 99] {
8556            assert!(!is_valid_sched_policy(policy), "policy {}", policy);
8557        }
8558    }
8559
8560    // ---- which side of the pid lookup each rule falls on --------------------
8561    // The kernel resolves the pid between `sched_setattr()` and
8562    // `__sched_setscheduler()`, so a request naming a nonexistent pid reports
8563    // ESRCH for every rule on the far side and its own errno for every rule on
8564    // the near side. Measured natively with pid 0x3fffffff:
8565    //     bad sched_flags    -> ESRCH    (far side)
8566    //     bad priority       -> ESRCH    (far side)
8567    //     negative policy    -> EINVAL   (near side)
8568    //     util-clamp size 48 -> EINVAL   (near side)
8569    // The split between the two functions below is exactly that boundary.
8570
8571    #[test]
8572    fn util_clamp_size_rule_is_decided_before_the_pid_lookup() {
8573        // Measured native: EINVAL even with a nonexistent pid.
8574        let mut attr = plain_attr();
8575        attr.sched_flags = SCHED_FLAG_UTIL_CLAMP_MIN;
8576        assert_eq!(
8577            validate_sched_attr_before_lookup(SCHED_ATTR_SIZE_VER0, &attr),
8578            Err(Errno::EINVAL)
8579        );
8580        assert_eq!(
8581            validate_sched_attr_before_lookup(SCHED_ATTR_SIZE_VER1, &attr),
8582            Ok(())
8583        );
8584        // ...and it is not re-decided on the far side.
8585        assert_eq!(validate_sched_attr_after_lookup(&attr), Ok(()));
8586    }
8587
8588    #[test]
8589    fn a_negative_policy_is_decided_before_the_pid_lookup() {
8590        // Measured native: EINVAL even with a nonexistent pid, and EINVAL with
8591        // KEEP_POLICY too -- the signed test sits above the substitution.
8592        let mut attr = plain_attr();
8593        attr.policy = 0x8000_0000;
8594        assert_eq!(
8595            validate_sched_attr_before_lookup(SCHED_ATTR_SIZE_VER0, &attr),
8596            Err(Errno::EINVAL)
8597        );
8598        attr.sched_flags = SCHED_FLAG_KEEP_POLICY;
8599        assert_eq!(
8600            validate_sched_attr_before_lookup(SCHED_ATTR_SIZE_VER0, &attr),
8601            Err(Errno::EINVAL),
8602            "KEEP_POLICY must not hide a negative policy"
8603        );
8604    }
8605
8606    #[test]
8607    fn policy_and_flag_rules_are_decided_after_the_pid_lookup() {
8608        // Measured native: with a nonexistent pid both of these report ESRCH,
8609        // so neither may be decided on the near side.
8610        let mut bad_policy = plain_attr();
8611        bad_policy.policy = 99;
8612        assert_eq!(
8613            validate_sched_attr_before_lookup(SCHED_ATTR_SIZE_VER0, &bad_policy),
8614            Ok(()),
8615            "an undefined policy must survive the near side so ESRCH can win"
8616        );
8617        assert_eq!(
8618            validate_sched_attr_after_lookup(&bad_policy),
8619            Err(Errno::EINVAL)
8620        );
8621
8622        let mut bad_flag = plain_attr();
8623        bad_flag.sched_flags = 0x80;
8624        assert_eq!(
8625            validate_sched_attr_before_lookup(SCHED_ATTR_SIZE_VER0, &bad_flag),
8626            Ok(()),
8627            "an undefined sched_flags bit must survive the near side"
8628        );
8629        assert_eq!(
8630            validate_sched_attr_after_lookup(&bad_flag),
8631            Err(Errno::EINVAL)
8632        );
8633    }
8634
8635    #[test]
8636    fn keep_policy_makes_the_policy_field_irrelevant() {
8637        // Measured native: policy 99 alone is EINVAL, but policy 99 with
8638        // SCHED_FLAG_KEEP_POLICY is ret 0 -- the kernel overwrites the field
8639        // with SETPARAM_POLICY and never runs valid_policy(). PR #2288 refused
8640        // it.
8641        let mut attr = plain_attr();
8642        attr.policy = 99;
8643        assert_eq!(validate_sched_attr_after_lookup(&attr), Err(Errno::EINVAL));
8644        attr.sched_flags = SCHED_FLAG_KEEP_POLICY;
8645        assert_eq!(validate_sched_attr_after_lookup(&attr), Ok(()));
8646    }
8647
8648    #[test]
8649    fn defined_sched_flags_bits_are_accepted_and_undefined_ones_are_not() {
8650        // Measured native: sched_flags 0x80 gives EINVAL; RESET_ON_FORK,
8651        // KEEP_PARAMS, KEEP_ALL, RECLAIM and DL_OVERRUN are all ret 0 at
8652        // size 48. PR #2288 ignored sched_flags entirely.
8653        let mut attr = plain_attr();
8654        attr.sched_flags = 0x80;
8655        assert_eq!(validate_sched_attr_after_lookup(&attr), Err(Errno::EINVAL));
8656        for flag in [
8657            SCHED_FLAG_RESET_ON_FORK,
8658            SCHED_FLAG_RECLAIM,
8659            SCHED_FLAG_DL_OVERRUN,
8660            SCHED_FLAG_KEEP_PARAMS,
8661            SCHED_FLAG_KEEP_POLICY,
8662        ] {
8663            let mut ok = plain_attr();
8664            ok.sched_flags = flag;
8665            assert_eq!(
8666                validate_sched_attr_after_lookup(&ok),
8667                Ok(()),
8668                "flag {:#x}",
8669                flag
8670            );
8671        }
8672        // The whole mask at once is refused on the NEAR side, and for the
8673        // util-clamp size reason rather than the flag-validity one: measured
8674        // native EINVAL at size 48.
8675        let mut all = plain_attr();
8676        all.sched_flags = SCHED_FLAG_ALL;
8677        assert_eq!(
8678            validate_sched_attr_before_lookup(SCHED_ATTR_SIZE_VER0, &all),
8679            Err(Errno::EINVAL)
8680        );
8681        assert_eq!(
8682            validate_sched_attr_before_lookup(SCHED_ATTR_SIZE_VER1, &all),
8683            Ok(())
8684        );
8685        assert_eq!(validate_sched_attr_after_lookup(&all), Ok(()));
8686    }
8687
8688    #[test]
8689    fn priority_must_agree_with_the_policy() {
8690        // The kernel states this as `rt_policy(policy) != (prio != 0)`.
8691        // Measured native, all EINVAL: OTHER/BATCH/IDLE with priority 1, and
8692        // FIFO/RR with priority 0. PR #2288 accepted every one of them.
8693        for policy in [0u32, 3, 5] {
8694            let mut attr = plain_attr();
8695            attr.policy = policy;
8696            attr.priority = 1;
8697            assert_eq!(
8698                validate_sched_attr_after_lookup(&attr),
8699                Err(Errno::EINVAL),
8700                "policy {} with priority 1",
8701                policy
8702            );
8703            attr.priority = 0;
8704            assert_eq!(
8705                validate_sched_attr_after_lookup(&attr),
8706                Ok(()),
8707                "policy {} with priority 0",
8708                policy
8709            );
8710        }
8711        for policy in [SCHED_FIFO, SCHED_RR] {
8712            let mut attr = plain_attr();
8713            attr.policy = policy;
8714            attr.priority = 0;
8715            assert_eq!(
8716                validate_sched_attr_after_lookup(&attr),
8717                Err(Errno::EINVAL),
8718                "rt policy {} with priority 0",
8719                policy
8720            );
8721            // A real-time priority is accepted here; whether the *caller* may
8722            // ask for it is EPERM, which depends on privilege and is not
8723            // emulated.
8724            attr.priority = 1;
8725            assert_eq!(validate_sched_attr_after_lookup(&attr), Ok(()));
8726        }
8727    }
8728
8729    #[test]
8730    fn a_priority_past_max_rt_prio_is_refused() {
8731        // Measured native: FIFO with priority 100 is EINVAL rather than the
8732        // EPERM that priorities 1..=99 give, so the range test runs first.
8733        let mut attr = plain_attr();
8734        attr.policy = SCHED_FIFO;
8735        attr.priority = MAX_RT_PRIO;
8736        assert_eq!(validate_sched_attr_after_lookup(&attr), Err(Errno::EINVAL));
8737        attr.priority = MAX_RT_PRIO - 1;
8738        assert_eq!(validate_sched_attr_after_lookup(&attr), Ok(()));
8739    }
8740
8741    #[test]
8742    fn deadline_parameters_are_checked() {
8743        // Measured native: SCHED_DEADLINE with all-zero parameters is EINVAL,
8744        // and with runtime > deadline is EINVAL. PR #2288 accepted both.
8745        let mut zeroed = plain_attr();
8746        zeroed.policy = SCHED_DEADLINE;
8747        assert_eq!(
8748            validate_sched_attr_after_lookup(&zeroed),
8749            Err(Errno::EINVAL),
8750            "all-zero deadline parameters"
8751        );
8752
8753        let mut inverted = plain_attr();
8754        inverted.policy = SCHED_DEADLINE;
8755        inverted.runtime = 90_000_000;
8756        inverted.deadline = 30_000_000;
8757        inverted.period = 30_000_000;
8758        assert_eq!(
8759            validate_sched_attr_after_lookup(&inverted),
8760            Err(Errno::EINVAL),
8761            "runtime > deadline"
8762        );
8763
8764        // Sane parameters pass the ABI rules. Natively this host answers EPERM,
8765        // which is a privilege question rather than an ABI one.
8766        let mut sane = plain_attr();
8767        sane.policy = SCHED_DEADLINE;
8768        sane.runtime = 10_000_000;
8769        sane.deadline = 30_000_000;
8770        sane.period = 30_000_000;
8771        assert_eq!(validate_sched_attr_after_lookup(&sane), Ok(()));
8772    }
8773
8774    #[test]
8775    fn deadline_parameter_edges_follow_checkparam_dl() {
8776        // deadline == 0 is refused whatever else is set.
8777        assert!(!deadline_params_are_valid(1 << 20, 0, 0));
8778        // runtime below the DL_SCALE truncation floor (1 << 10) is refused.
8779        assert!(!deadline_params_are_valid((1 << 10) - 1, 1 << 20, 1 << 20));
8780        assert!(deadline_params_are_valid(1 << 10, 1 << 20, 1 << 20));
8781        // The MSB is reserved on both deadline and period.
8782        assert!(!deadline_params_are_valid(1 << 20, 1 << 63, 0));
8783        assert!(!deadline_params_are_valid(1 << 20, 1 << 20, 1 << 63));
8784        // A zero period means "same as the deadline", so this is runtime <=
8785        // deadline <= deadline and is accepted.
8786        assert!(deadline_params_are_valid(1 << 20, 1 << 21, 0));
8787        // deadline > period is refused.
8788        assert!(!deadline_params_are_valid(1 << 20, 1 << 22, 1 << 21));
8789    }
8790
8791    #[test]
8792    fn the_size_written_back_on_refusal_is_the_kernel_struct_not_the_libc_mirror() {
8793        // Measured native: every E2BIG row leaves uattr->size holding 56, not
8794        // 48. Deriving that from the libc crate's `sched_attr` would report 48
8795        // and tell the guest to retry with a size the kernel already accepted.
8796        assert_eq!(SCHED_ATTR_KERNEL_SIZE, 56);
8797        assert_eq!(std::mem::size_of::<libc::sched_attr>(), 48);
8798        assert_ne!(
8799            SCHED_ATTR_KERNEL_SIZE as usize,
8800            std::mem::size_of::<libc::sched_attr>()
8801        );
8802    }
8803
8804    /// A `MemoryAccess` whose readable region ends at `readable_len`, recording
8805    /// every read length it is asked for.
8806    struct BoundedMemory {
8807        bytes: Vec<u8>,
8808        readable_len: usize,
8809        reads: std::cell::RefCell<Vec<usize>>,
8810    }
8811
8812    impl reverie::syscalls::MemoryAccess for BoundedMemory {
8813        fn read_vectored(
8814            &self,
8815            read_from: &[std::io::IoSlice<'_>],
8816            write_to: &mut [std::io::IoSliceMut<'_>],
8817        ) -> Result<usize, Errno> {
8818            let start = read_from[0].as_ptr() as usize;
8819            let want = read_from.iter().map(|slice| slice.len()).sum::<usize>();
8820            self.reads.borrow_mut().push(want);
8821            if start >= self.readable_len {
8822                return Err(Errno::EFAULT);
8823            }
8824            let avail = (self.readable_len - start).min(want);
8825            let mut copied = 0;
8826            for out in write_to.iter_mut() {
8827                if copied == avail {
8828                    break;
8829                }
8830                let take = out.len().min(avail - copied);
8831                out[..take].copy_from_slice(&self.bytes[start + copied..start + copied + take]);
8832                copied += take;
8833            }
8834            Ok(copied)
8835        }
8836
8837        fn write_vectored(
8838            &mut self,
8839            _read_from: &[std::io::IoSlice<'_>],
8840            _write_to: &mut [std::io::IoSliceMut<'_>],
8841        ) -> Result<usize, Errno> {
8842            unimplemented!("this fixture never writes")
8843        }
8844    }
8845
8846    fn bounded(bytes: Vec<u8>, readable_len: usize) -> BoundedMemory {
8847        BoundedMemory {
8848            bytes,
8849            readable_len,
8850            reads: std::cell::RefCell::new(Vec::new()),
8851        }
8852    }
8853
8854    fn tail_base() -> AddrMut<'static, u8> {
8855        AddrMut::<u8>::from_raw(1).expect("nonzero base")
8856    }
8857
8858    /// ITEM 2 REGRESSION, ORDERING HALF: a non-zero byte BEFORE an unreadable
8859    /// page is E2BIG, not EFAULT.
8860    ///
8861    /// `check_zeroed_user` scans forward and stops at the first thing it finds,
8862    /// so it never reaches the fault. Reading the whole tail up front and
8863    /// judging afterwards turns this into EFAULT and changes the errno the
8864    /// guest sees.
8865    #[test]
8866    fn a_nonzero_byte_before_a_fault_is_e2big_not_efault() {
8867        let off = SCHED_ATTR_KERNEL_SIZE as usize;
8868        let mut bytes = vec![0u8; off + 4096];
8869        bytes[off + 1] = 0xAA; // non-zero, well before the boundary
8870        let readable = off + 512; // everything past this faults
8871        let memory = bounded(bytes, readable);
8872        assert_eq!(
8873            scan_tail_is_zeroed(&memory, tail_base(), off, 4096),
8874            TailVerdict::NotZeroed,
8875            "a non-zero byte the scan reaches first must win over a later fault"
8876        );
8877    }
8878
8879    /// ITEM 2 REGRESSION, the other side of the same order: a fault with
8880    /// nothing non-zero before it is EFAULT.
8881    #[test]
8882    fn a_fault_before_any_nonzero_byte_is_efault() {
8883        let off = SCHED_ATTR_KERNEL_SIZE as usize;
8884        let bytes = vec![0u8; off + 4096];
8885        let memory = bounded(bytes, off + 512);
8886        assert_eq!(
8887            scan_tail_is_zeroed(&memory, tail_base(), off, 4096),
8888            TailVerdict::Faulted,
8889            "an unreadable byte reached before anything non-zero must be EFAULT"
8890        );
8891    }
8892
8893    /// ITEM 2 REGRESSION, PROTECTION HALF: never issue a read of eight bytes or
8894    /// fewer.
8895    ///
8896    /// safeptrace serves those through `PTRACE_PEEKDATA`, which reads a whole
8897    /// aligned word and bypasses guest page protections, so a small tail would
8898    /// be readable under Hermit where Linux reports EFAULT. The assertion is on
8899    /// the READ LENGTHS ASKED FOR, because that is the mechanism; the returned
8900    /// verdict cannot show it.
8901    #[test]
8902    fn tail_reads_are_never_small_enough_to_bypass_guest_protection() {
8903        let off = SCHED_ATTR_KERNEL_SIZE as usize;
8904        for tail_len in 1..=16usize {
8905            let bytes = vec![0u8; off + tail_len + 64];
8906            let memory = bounded(bytes, off + tail_len + 64);
8907            assert_eq!(
8908                scan_tail_is_zeroed(&memory, tail_base(), off, tail_len),
8909                TailVerdict::AllZero
8910            );
8911            let reads = memory.reads.borrow().clone();
8912            assert!(!reads.is_empty(), "tail_len {tail_len} issued no read");
8913            for length in reads {
8914                assert!(
8915                    length > std::mem::size_of::<u64>(),
8916                    "tail_len {tail_len} issued a {length}-byte read, which safeptrace \
8917                     serves with PTRACE_PEEKDATA and which therefore bypasses guest \
8918                     page protections"
8919                );
8920            }
8921        }
8922    }
8923
8924    /// ITEM 3 REGRESSION: KEEP_POLICY reuses the CURRENT policy; it does not
8925    /// switch the policy-dependent rules off.
8926    ///
8927    /// Detcore's current policy is the one `handle_sched_getattr` reports for
8928    /// every thread, SCHED_OTHER, and a non-real-time policy requires priority
8929    /// 0. Skipping the rules accepted this.
8930    #[test]
8931    fn keep_policy_validates_against_the_virtual_current_policy() {
8932        let attr = SchedAttrFields {
8933            policy: 99, // ignored under KEEP_POLICY, and deliberately invalid
8934            sched_flags: SCHED_FLAG_KEEP_POLICY,
8935            priority: 1,
8936            runtime: 0,
8937            deadline: 0,
8938            period: 0,
8939        };
8940        assert_eq!(
8941            validate_sched_attr_after_lookup(&attr),
8942            Err(Errno::EINVAL),
8943            "priority 1 under the virtual SCHED_OTHER current policy must be refused"
8944        );
8945
8946        // The companion that must keep passing: the ignored policy field really
8947        // is ignored, so the same request at priority 0 is accepted.
8948        let ok = SchedAttrFields {
8949            priority: 0,
8950            ..attr
8951        };
8952        assert_eq!(
8953            validate_sched_attr_after_lookup(&ok),
8954            Ok(()),
8955            "KEEP_POLICY must still ignore the policy field itself"
8956        );
8957    }
8958
8959    /// ITEM 3 COHERENCE: the substituted policy is the one the guest can
8960    /// actually observe, so the two sites cannot drift apart silently.
8961    #[test]
8962    fn the_virtual_current_policy_is_what_sched_getattr_reports() {
8963        assert_eq!(
8964            VIRTUAL_CURRENT_POLICY,
8965            libc::SCHED_OTHER as u32,
8966            "handle_sched_getattr writes SCHED_OTHER into sched_policy for every thread; \
8967             KEEP_POLICY must substitute that same value"
8968        );
8969    }
8970}
8971
8972/// The real uid of thread `tid`, from `/proc/<tid>/status`.
8973pub(crate) fn real_uid(tid: i32) -> Option<u32> {
8974    let status = std::fs::read_to_string(format!("/proc/{tid}/status")).ok()?;
8975    status
8976        .lines()
8977        .find_map(|line| line.strip_prefix("Uid:"))?
8978        .split_whitespace()
8979        .next()?
8980        .parse()
8981        .ok()
8982}
8983
8984/// The sizes of the three versions of `struct clone_args`
8985/// (`CLONE_ARGS_SIZE_VER0` to `_VER2`, include/uapi/linux/sched.h): the
8986/// smallest `clone3` accepts, the one with `set_tid`, and the one v6.16 reads.
8987const CLONE_ARGS_SIZE_VER0: usize = 64;
8988const CLONE_ARGS_SIZE_VER1: usize = 80;
8989const CLONE_ARGS_SIZE_VER2: usize = 88;
8990
8991/// The most bytes of a `struct clone_args` [`inject_traced_clone3`] stages,
8992/// within the guest stack scratch's capacity.
8993const CLONE3_STAGED_MAX: usize = 512;
8994
8995/// What [`inject_traced_clone`] did with a clone-family call.
8996enum CloneInjection {
8997    /// The call ran, with this result.
8998    Ran(Result<i64, Errno>),
8999    /// The call did not run: the reason it cannot be made traced.
9000    Refused(String),
9001}
9002
9003/// Injects the guest's `clone_family` call with `CLONE_UNTRACED` cleared.
9004///
9005/// `CLONE_UNTRACED` keeps a tracer from attaching to the child
9006/// (`kernel_clone`, kernel/fork.c). No guest task has a tracer of its own:
9007/// Detcore answers every guest `ptrace` with `EPERM`
9008/// (`is_privileged_observation_refused_syscall`), and natively an untraced
9009/// program has none either, so for the guest the flag changes nothing. Under
9010/// a tracing backend every guest task must be traced: Detcore admits the
9011/// child to its scheduler as the clone returns and waits for it to take its
9012/// first turn, which an untraced child never does, so the run waited forever.
9013/// Cleared, the child is traced like every other and runs as it would
9014/// natively. A backend that does not trace children ignores the flag, so the
9015/// call is the same there. `flags` are the call's, as
9016/// [`Detcore::handle_clone_family`] read them.
9017async fn inject_traced_clone<T: RecordOrReplay, G: Guest<Detcore<T>>>(
9018    guest: &mut G,
9019    clone_family: syscalls::family::CloneFamily,
9020    flags: CloneFlags,
9021) -> CloneInjection {
9022    use syscalls::family::CloneFamily;
9023    if !flags.contains(CloneFlags::CLONE_UNTRACED) {
9024        return CloneInjection::Ran(
9025            crate::reserved_input::inject_original_clone(guest, Syscall::from(clone_family)).await,
9026        );
9027    }
9028    match clone_family {
9029        CloneFamily::Clone(clone) => {
9030            let traced = clone.with_flags(clone.flags() - nix::sched::CloneFlags::CLONE_UNTRACED);
9031            CloneInjection::Ran(
9032                crate::reserved_input::inject_original_clone(guest, Syscall::Clone(traced)).await,
9033            )
9034        }
9035        CloneFamily::Clone3(clone) => inject_traced_clone3(guest, clone).await,
9036        #[cfg(not(target_arch = "aarch64"))]
9037        CloneFamily::Fork(_) | CloneFamily::Vfork(_) => {
9038            unreachable!("fork and vfork take no flags")
9039        }
9040    }
9041}
9042
9043/// [`inject_traced_clone`] of a `clone3` call, whose flags are in the guest's
9044/// `struct clone_args`. That struct is left as it is: a copy without
9045/// `CLONE_UNTRACED` is staged in the guest stack scratch, with the bytes under
9046/// it saved and put back after the call, as Detcore stages a pre-call lookup
9047/// buffer there. On ptrace that is below the red zone; a child that does not
9048/// share the caller's memory keeps the copy there, below its stack pointer,
9049/// where the ABI lets a signal frame write. Other backends lend an arena of
9050/// their own.
9051///
9052/// The copy holds every byte up to the guest's last nonzero one, and at
9053/// least those the kernel reads, so the kernel judges it as it would the
9054/// guest's struct (`copy_clone_args_from_user`, kernel/fork.c). A call the
9055/// kernel fails before reading the struct (no struct, one it cannot read, a
9056/// size it refuses) is injected as the guest made it and fails as natively.
9057/// Two calls are refused by name rather than run untraced: one whose nonzero
9058/// bytes do not fit the scratch, and one whose pointers lead into it.
9059async fn inject_traced_clone3<T: RecordOrReplay, G: Guest<Detcore<T>>>(
9060    guest: &mut G,
9061    clone: syscalls::Clone3,
9062) -> CloneInjection {
9063    let size = clone.size();
9064    let page = usize::try_from(unsafe { libc::sysconf(libc::_SC_PAGESIZE) }).unwrap_or(4096);
9065    let mut bytes = vec![0_u8; size];
9066    let readable = clone.args().is_some_and(|args| {
9067        (CLONE_ARGS_SIZE_VER0..=page).contains(&size)
9068            && crate::syscalls::read_guest_exact(&guest.memory(), args.cast(), &mut bytes).is_ok()
9069    });
9070    if !readable {
9071        return CloneInjection::Ran(
9072            crate::reserved_input::inject_original_clone(guest, Syscall::Clone3(clone)).await,
9073        );
9074    }
9075    let refused = |why: &str| {
9076        CloneInjection::Refused(format!(
9077            "clone3 with CLONE_UNTRACED: hermit must trace the child, so it clears the flag in a \
9078             copy of clone_args staged below the stack pointer, but {why}"
9079        ))
9080    };
9081    let nonzero = bytes
9082        .iter()
9083        .rposition(|byte| *byte != 0)
9084        .map_or(0, |last| last + 1);
9085    let len = nonzero.max(size.min(CLONE_ARGS_SIZE_VER2));
9086    if len > CLONE3_STAGED_MAX {
9087        return refused(&format!(
9088            "its nonzero bytes run past the {CLONE3_STAGED_MAX} that can be staged"
9089        ));
9090    }
9091    let mut copy = [0_u8; CLONE3_STAGED_MAX];
9092    copy[..len].copy_from_slice(&bytes[..len]);
9093    let field = |copy: &[u8], offset: usize| {
9094        u64::from_ne_bytes(copy[offset..offset + 8].try_into().unwrap())
9095    };
9096    let traced = field(&copy, 0) & !CloneFlags::CLONE_UNTRACED.bits();
9097    copy[..8].copy_from_slice(&traced.to_ne_bytes());
9098
9099    // `GuestStack` starts its allocations 128 bytes below the stack pointer.
9100    let rsp = guest.regs().await.rsp as usize;
9101    let Some(start) = rsp.checked_sub(128 + CLONE3_STAGED_MAX) else {
9102        return refused("the stack pointer is too low to stage it");
9103    };
9104    let staged = start..start + CLONE3_STAGED_MAX;
9105    // What the kernel writes through the struct's pointers (pidfd, child_tid,
9106    // parent_tid) or reads through set_tid, which the restore below would
9107    // erase or the copy has already overwritten.
9108    let set_tid_bytes = if len >= CLONE_ARGS_SIZE_VER1 {
9109        field(&copy, 72).saturating_mul(4)
9110    } else {
9111        0
9112    };
9113    let pointed = [
9114        (field(&copy, 8), 4),
9115        (field(&copy, 16), 4),
9116        (field(&copy, 24), 4),
9117        (field(&copy, 64), set_tid_bytes),
9118    ];
9119    if pointed.iter().any(|&(address, bytes)| {
9120        let address = address as usize;
9121        bytes > 0 && address < staged.end && address.saturating_add(bytes as usize) > staged.start
9122    }) {
9123        return refused("one of its pointers leads into the staged copy");
9124    }
9125
9126    let mut stack = guest.stack().await;
9127    let scratch: Addr<[u8; CLONE3_STAGED_MAX]> = stack.push(copy);
9128    let mut saved = [0_u8; CLONE3_STAGED_MAX];
9129    if let Err(errno) = crate::syscalls::read_guest_exact(
9130        &guest.memory(),
9131        AddrMut::from_raw(scratch.as_raw()).expect("a nonzero address"),
9132        &mut saved,
9133    ) {
9134        return refused(&format!("the stack below it cannot be read ({errno})"));
9135    }
9136    let guard = match stack.commit() {
9137        Ok(guard) => guard,
9138        Err(errno) => {
9139            restore_staged_clone_args(guest, scratch, &saved);
9140            return refused(&format!("the stack below it cannot be written ({errno})"));
9141        }
9142    };
9143    let result = crate::reserved_input::inject_original_clone(
9144        guest,
9145        Syscall::Clone3(
9146            clone
9147                .with_args(AddrMut::from_raw(scratch.as_raw()))
9148                .with_size(len),
9149        ),
9150    )
9151    .await;
9152    restore_staged_clone_args(guest, scratch, &saved);
9153    drop(guard);
9154    CloneInjection::Ran(result)
9155}
9156
9157/// Puts back the guest bytes that [`inject_traced_clone3`] staged its copy
9158/// over.
9159fn restore_staged_clone_args<T: RecordOrReplay, G: Guest<Detcore<T>>>(
9160    guest: &mut G,
9161    scratch: Addr<[u8; CLONE3_STAGED_MAX]>,
9162    saved: &[u8; CLONE3_STAGED_MAX],
9163) {
9164    let restored = AddrMut::<u8>::from_raw(scratch.as_raw())
9165        .is_some_and(|scratch| guest.memory().write_exact(scratch, saved).is_ok());
9166    if !restored {
9167        info!("Could not restore the guest bytes under a staged clone_args.");
9168    }
9169}
9170
9171/// The 8-byte field at `offset` of a clone3 call's `struct clone_args`,
9172/// which may be any size the kernel accepts from 64 bytes (its first
9173/// version) up: read alone, so that a shorter struct at the end of a mapping
9174/// is read as the kernel reads it. `None` for a size below 64, a field past
9175/// the size, or an unreadable field.
9176fn clone3_field<M: MemoryAccess>(
9177    memory: &M,
9178    clone: &syscalls::Clone3,
9179    offset: usize,
9180) -> Option<u64> {
9181    if clone.size() < CLONE_ARGS_SIZE_VER0 || offset + 8 > clone.size() {
9182        return None;
9183    }
9184    let field = clone.args()?.as_raw().checked_add(offset)?;
9185    // Past user memory there is nothing to read, and a read's address
9186    // arithmetic near the top of the address space would overflow.
9187    if !super::chosen_ids::within_user_memory(field, std::mem::size_of::<u64>()) {
9188        return None;
9189    }
9190    memory.read_value(Addr::<u64>::from_raw(field)?).ok()
9191}
9192
9193/// Whether a `clone3`'s exit signal and the low byte of its flags pass the
9194/// kernel's checks: the exit signal is a signal number, and the flags hold
9195/// nothing in `CSIGNAL` but `CLONE_NEWTIME`, which `clone3` accepts from
9196/// Linux 6.3 (`clone3_args_valid`). On an older kernel, which refuses
9197/// `CLONE_NEWTIME` with EINVAL, a refusal after this is the conservative
9198/// side.
9199fn clone3_signal_fields_valid(flags: u64, exit_signal: u64) -> bool {
9200    const CSIGNAL: u64 = 0xff;
9201    const CLONE_NEWTIME: u64 = 0x80;
9202    exit_signal <= 64 && flags & CSIGNAL & !CLONE_NEWTIME == 0
9203}
9204
9205#[cfg(test)]
9206mod clone3_signal_fields_tests {
9207    /// CLONE_NEWTIME is the one CSIGNAL bit clone3 accepts in its flags;
9208    /// every other one, or an exit signal above 64, is the kernel's EINVAL.
9209    #[test]
9210    fn clone3_accepts_only_clone_newtime_among_the_csignal_bits() {
9211        assert!(super::clone3_signal_fields_valid(0x80, 17));
9212        assert!(super::clone3_signal_fields_valid(0, 64));
9213        for bit in 0..7 {
9214            assert!(!super::clone3_signal_fields_valid(1 << bit, 17), "{bit}");
9215        }
9216        assert!(!super::clone3_signal_fields_valid(0, 65));
9217    }
9218}
9219
9220/// Whether Linux refuses `clone_family` with EINVAL, from its flags and its
9221/// `struct clone_args` alone, before it reads anything else or creates
9222/// anything. A `clone3` is decided only where no check the kernel makes first
9223/// could fail another way: its struct, of a size the kernel takes, is read
9224/// whole with the guest's own access, as `copy_from_user` reads it, has no
9225/// nonzero tail past the fields known here (E2BIG), and names no `set_tid`
9226/// array, whose copy could fault (EFAULT). Linux's EINVALs that depend on the
9227/// caller's state, `CLONE_PARENT` from a namespace's init and a thread after
9228/// `unshare(CLONE_NEWPID)`, are left to the backend. `user_limit` is the
9229/// guest's user address limit, as its backend reports it
9230/// (`Guest::user_address_limit`), if it does.
9231fn linux_refuses_clone_flags<M: MemoryAccess>(
9232    memory: &M,
9233    clone_family: &syscalls::family::CloneFamily,
9234    user_limit: Option<u64>,
9235) -> bool {
9236    match clone_family {
9237        // `clone` takes the low 32 bits of its argument, less the exit signal
9238        // in the low byte, and writes a pidfd where it writes the parent's tid.
9239        syscalls::family::CloneFamily::Clone(clone) => {
9240            clone_flags_einval(u64::from(clone.flags().bits() as u32) & !0xff, true)
9241        }
9242        syscalls::family::CloneFamily::Clone3(clone) => {
9243            let size = clone.size();
9244            let page = usize::try_from(unsafe { libc::sysconf(libc::_SC_PAGESIZE) }).unwrap_or(0);
9245            let mut bytes = vec![0u8; size.min(page)];
9246            (CLONE_ARGS_SIZE_VER0..=page).contains(&size)
9247                && clone
9248                    .args()
9249                    .and_then(|args| Addr::<u8>::from_raw(args.as_raw()))
9250                    .filter(|args| super::chosen_ids::within_user_memory(args.as_raw(), size))
9251                    .is_some_and(|args| {
9252                        memory.read_exact_with_user_access(args, &mut bytes).is_ok()
9253                    })
9254                && clone3_args_einval(&bytes, user_limit)
9255        }
9256        #[cfg(not(target_arch = "aarch64"))]
9257        _ => false,
9258    }
9259}
9260
9261/// Whether Linux refuses a clone with these `flags` with EINVAL, whatever the
9262/// caller's state: the check of `kernel_clone` for a pidfd written where the
9263/// parent's tid goes (`pidfd_is_parent_tid`), and the checks `copy_process`
9264/// makes first, at Linux v6.17:
9265/// https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/kernel/fork.c?h=v6.17#n2582
9266/// https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/kernel/fork.c?h=v6.17#n1935
9267fn clone_flags_einval(flags: u64, pidfd_is_parent_tid: bool) -> bool {
9268    let flags = CloneFlags::from_bits_retain(flags);
9269    let thread = flags.contains(CloneFlags::CLONE_THREAD);
9270    (flags.contains(CloneFlags::CLONE_PIDFD | CloneFlags::CLONE_PARENT_SETTID)
9271        && pidfd_is_parent_tid)
9272        || flags.contains(CloneFlags::CLONE_NEWNS | CloneFlags::CLONE_FS)
9273        || flags.contains(CloneFlags::CLONE_NEWUSER | CloneFlags::CLONE_FS)
9274        || (thread && !flags.contains(CloneFlags::CLONE_SIGHAND))
9275        || (flags.contains(CloneFlags::CLONE_SIGHAND) && !flags.contains(CloneFlags::CLONE_VM))
9276        || (thread && flags.intersects(CloneFlags::CLONE_NEWUSER | CloneFlags::CLONE_NEWPID))
9277        || flags.contains(CloneFlags::CLONE_PIDFD | CloneFlags::CLONE_DETACHED)
9278}
9279
9280/// Whether Linux refuses a `clone3` whose `struct clone_args` is `bytes` with
9281/// EINVAL before it creates anything: the checks of
9282/// `copy_clone_args_from_user` and `clone3_args_valid`, then
9283/// [`clone_flags_einval`]. False wherever another error could come first, a
9284/// nonzero tail (E2BIG) or a `set_tid` copy (EFAULT):
9285/// https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/kernel/fork.c?h=v6.17#n2768
9286/// https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/kernel/fork.c?h=v6.17#n2849
9287///
9288/// `clone3_stack_valid` refuses a stack range that `access_ok` refuses,
9289/// one whose end passes the user address limit (`__access_ok` and
9290/// `valid_user_address`, arch/x86/include/asm/uaccess_64.h:56-57 and 98-107
9291/// at v6.17). That limit is the kernel's (four- or five-level paging on a
9292/// host), so it is decided only from `user_limit`, the guest's limit as its
9293/// backend reports it, and left to the backend without one:
9294/// https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/kernel/fork.c?h=v6.17#n2829
9295fn clone3_args_einval(bytes: &[u8], user_limit: Option<u64>) -> bool {
9296    const CLONE_CLEAR_SIGHAND: u64 = 1 << 32;
9297    const CLONE_INTO_CGROUP: u64 = 1 << 33;
9298    if bytes
9299        .iter()
9300        .skip(CLONE_ARGS_SIZE_VER2)
9301        .any(|byte| *byte != 0)
9302    {
9303        return false;
9304    }
9305    let field = |offset: usize| {
9306        let mut word = [0u8; 8];
9307        for (i, byte) in word.iter_mut().enumerate() {
9308            *byte = bytes.get(offset + i).copied().unwrap_or(0);
9309        }
9310        u64::from_ne_bytes(word)
9311    };
9312    let (flags, exit_signal, stack, stack_size) = (field(0), field(32), field(40), field(48));
9313    let (set_tid, set_tid_size) = (field(64), field(72));
9314    let into_cgroup = flags & CLONE_INTO_CGROUP != 0;
9315    if set_tid_size > 32
9316        || (set_tid == 0) != (set_tid_size == 0)
9317        || exit_signal > 64
9318        || (into_cgroup && (field(80) > i32::MAX as u64 || bytes.len() < CLONE_ARGS_SIZE_VER2))
9319    {
9320        return true;
9321    }
9322    if set_tid != 0 {
9323        return false;
9324    }
9325    let sighand = libc::CLONE_SIGHAND as u64 | CLONE_CLEAR_SIGHAND;
9326    flags & !(0xffff_ffff | CLONE_CLEAR_SIGHAND | CLONE_INTO_CGROUP) != 0
9327        || !clone3_signal_fields_valid(flags, exit_signal)
9328        || flags & libc::CLONE_DETACHED as u64 != 0
9329        || flags & sighand == sighand
9330        || (flags & (libc::CLONE_THREAD | libc::CLONE_PARENT) as u64 != 0 && exit_signal != 0)
9331        || (stack == 0) != (stack_size == 0)
9332        || stack.checked_add(stack_size).is_none()
9333        || (stack != 0 && user_limit.is_some_and(|limit| stack + stack_size > limit))
9334        || clone_flags_einval(flags, field(8) == field(24))
9335}
9336
9337#[cfg(test)]
9338mod linux_clone_einval_tests {
9339    use super::clone_flags_einval;
9340    use super::clone3_args_einval;
9341
9342    const VM: u64 = libc::CLONE_VM as u64;
9343    const FS: u64 = libc::CLONE_FS as u64;
9344    const FILES: u64 = libc::CLONE_FILES as u64;
9345    const SIGHAND: u64 = libc::CLONE_SIGHAND as u64;
9346    const THREAD: u64 = libc::CLONE_THREAD as u64;
9347    const PIDFD: u64 = 0x1000;
9348    const PARENT_SETTID: u64 = libc::CLONE_PARENT_SETTID as u64;
9349    /// glibc's pthread_create flags, less the tid and TLS flags.
9350    const PTHREAD: u64 = VM | FS | FILES | SIGHAND | THREAD | libc::CLONE_SYSVSEM as u64;
9351
9352    /// Exactly copy_process's first checks and kernel_clone's pidfd check
9353    /// refuse: never the sharing Linux allows, such as a process with
9354    /// CLONE_VM and CLONE_FS, CLONE_FILES or CLONE_SIGHAND, or a thread
9355    /// without CLONE_FS, CLONE_FILES or CLONE_SYSVSEM.
9356    #[test]
9357    fn only_the_kernels_flag_rules_refuse() {
9358        let newns = libc::CLONE_NEWNS as u64;
9359        let newuser = libc::CLONE_NEWUSER as u64;
9360        let newpid = libc::CLONE_NEWPID as u64;
9361        let detached = libc::CLONE_DETACHED as u64;
9362        for (flags, pidfd_is_parent_tid, einval) in [
9363            (0, true, false),
9364            (VM | libc::CLONE_VFORK as u64, true, false),
9365            (PTHREAD, true, false),
9366            (VM | SIGHAND | THREAD, true, false),
9367            (VM | FS, true, false),
9368            (VM | FILES, true, false),
9369            (VM | SIGHAND, true, false),
9370            (VM | SIGHAND | THREAD | newns, true, false),
9371            (PIDFD | PTHREAD, true, false),
9372            (PIDFD | PARENT_SETTID, false, false),
9373            (newns | FS, true, true),
9374            (newuser | FS, true, true),
9375            (VM | FS | FILES | THREAD, true, true),
9376            (SIGHAND, true, true),
9377            (PTHREAD | newpid, true, true),
9378            (VM | SIGHAND | THREAD | newuser, true, true),
9379            (PIDFD | PARENT_SETTID, true, true),
9380            (PIDFD | detached, true, true),
9381        ] {
9382            assert_eq!(
9383                clone_flags_einval(flags, pidfd_is_parent_tid),
9384                einval,
9385                "{flags:#x} {pidfd_is_parent_tid}"
9386            );
9387        }
9388    }
9389
9390    /// A clone_args of `len` bytes with these fields, the rest zero.
9391    fn args(len: usize, fields: &[(usize, u64)]) -> Vec<u8> {
9392        let mut bytes = vec![0u8; len];
9393        for (offset, value) in fields {
9394            bytes[*offset..*offset + 8].copy_from_slice(&value.to_ne_bytes());
9395        }
9396        bytes
9397    }
9398
9399    /// clone3's own checks refuse first, in their order: a set_tid shape,
9400    /// the exit signal, CLONE_INTO_CGROUP's field, then flags clone3 does not
9401    /// take, CLONE_SIGHAND with CLONE_CLEAR_SIGHAND, an exit signal for a
9402    /// thread and an unpaired or overflowing stack. Where the kernel could
9403    /// fail another way first, a nonzero tail (E2BIG) or a set_tid array to
9404    /// copy (EFAULT), nothing is decided.
9405    #[test]
9406    fn clone3_decides_only_what_the_kernel_checks_first() {
9407        let sigchld = libc::SIGCHLD as u64;
9408        let fork = [(32, sigchld)];
9409        for (bytes, einval) in [
9410            (args(64, &fork), false),
9411            (args(88, &[(0, PTHREAD), (40, 0x1000), (48, 0x1000)]), false),
9412            (args(88, &[(0, 0x80), (32, sigchld)]), false),
9413            (args(128, &[(0, VM | FS), (32, sigchld)]), false),
9414            (args(88, &[(0, PTHREAD), (32, sigchld)]), true),
9415            (
9416                args(88, &[(0, libc::CLONE_PARENT as u64), (32, sigchld)]),
9417                true,
9418            ),
9419            (args(88, &[(0, 0x11)]), true),
9420            (args(88, &[(0, libc::CLONE_DETACHED as u64)]), true),
9421            (args(88, &[(0, VM | SIGHAND | (1 << 32))]), true),
9422            (args(88, &[(0, 1 << 34)]), true),
9423            (args(88, &[(32, 65)]), true),
9424            (args(88, &[(40, 0x1000)]), true),
9425            (args(88, &[(48, 0x1000)]), true),
9426            (args(88, &[(40, u64::MAX), (48, 2)]), true),
9427            (args(88, &[(64, 0x1000)]), true),
9428            (args(88, &[(72, 1)]), true),
9429            (args(88, &[(64, 0x1000), (72, 33)]), true),
9430            (args(80, &[(0, 1 << 33)]), true),
9431            (args(88, &[(0, 1 << 33), (80, 1 << 31)]), true),
9432            (args(88, &[(0, 1 << 33), (80, 3)]), false),
9433            (args(88, &[(0, THREAD), (64, 0x1000), (72, 1)]), false),
9434            (args(96, &[(0, THREAD), (88, 1)]), false),
9435            (args(96, &[(0, THREAD)]), true),
9436            (
9437                args(88, &[(0, PIDFD | PARENT_SETTID), (8, 8), (24, 8)]),
9438                true,
9439            ),
9440            (
9441                args(88, &[(0, PIDFD | PARENT_SETTID), (8, 8), (24, 16)]),
9442                false,
9443            ),
9444            (
9445                args(64, &[(0, libc::CLONE_NEWNS as u64 | FS), (32, sigchld)]),
9446                true,
9447            ),
9448        ] {
9449            assert_eq!(clone3_args_einval(&bytes, None), einval, "{bytes:x?}");
9450        }
9451    }
9452
9453    /// A stack whose end passes the user address limit the backend reports
9454    /// is refused, as `access_ok` refuses it: one in the kernel's half, and
9455    /// one a byte past the limit. One that ends at the limit is not, and
9456    /// without a reported limit nothing is decided from the stack's place.
9457    #[test]
9458    fn clone3_refuses_a_stack_past_the_reported_user_limit() {
9459        let sigchld = libc::SIGCHLD as u64;
9460        let limit = (1_u64 << 47) - 4096;
9461        let stack = |base: u64, size: u64| args(88, &[(32, sigchld), (40, base), (48, size)]);
9462        for (bytes, user_limit, einval) in [
9463            (stack(0xffff_8000_0000_0000, 0x1000), Some(limit), true),
9464            (stack(0xffff_8000_0000_0000, 0x1000), None, false),
9465            (stack(limit - 0x1000, 0x1001), Some(limit), true),
9466            (stack(limit - 0x1000, 0x1000), Some(limit), false),
9467            (stack(0x7000_0000, 0x1000), Some(limit), false),
9468            (args(88, &[(32, sigchld)]), Some(limit), false),
9469        ] {
9470            assert_eq!(
9471                clone3_args_einval(&bytes, user_limit),
9472                einval,
9473                "{bytes:x?} {user_limit:?}"
9474            );
9475        }
9476    }
9477}
9478
9479/// Where a `CLONE_PIDFD` clone writes the new process's pidfd (`clone`'s
9480/// `parent_tid` argument, or `clone3`'s `pidfd` field), for a child Detcore
9481/// tracks the pidfd of (`Detcore::register_clone_pidfd`): one that shares
9482/// neither the caller's memory nor its descriptor table and is no vfork
9483/// child. Such a child could rewrite the word, or close or replace the
9484/// descriptor, before the caller tracks it, and a vfork parent resumes, and
9485/// tracks it, only after its child is done, while a sibling may already
9486/// use the pidfd; Detcore leaves those pidfds untracked, as before
9487/// https://github.com/rrnewton/hermit/issues/4430
9488/// (https://github.com/rrnewton/hermit/issues/4443).
9489fn clone_pidfd_address<M: MemoryAccess>(
9490    clone_family: &syscalls::family::CloneFamily,
9491    memory: &M,
9492    flags: CloneFlags,
9493) -> Option<usize> {
9494    let (flags, address) = match clone_family {
9495        syscalls::family::CloneFamily::Clone(clone) => {
9496            (flags, clone.ptid().map(|ptid| ptid.as_raw()))
9497        }
9498        syscalls::family::CloneFamily::Clone3(clone) => (
9499            flags,
9500            Some(usize::try_from(clone3_field(memory, clone, 8)?).ok()?),
9501        ),
9502        #[cfg(not(target_arch = "aarch64"))]
9503        _ => return None,
9504    };
9505    if !flags.contains(CloneFlags::CLONE_PIDFD)
9506        || flags
9507            .intersects(CloneFlags::CLONE_VM | CloneFlags::CLONE_FILES | CloneFlags::CLONE_VFORK)
9508    {
9509        return None;
9510    }
9511    address
9512}
9513
9514/// A5 step 3c.1e B (<https://github.com/rrnewton/hermit/issues/4485>): a
9515/// managed child wait under the in-guest delivery gate reads the guest's mask
9516/// without replacing it and restores nothing, so the mask its requests publish
9517/// while it parks, and the one the guest resumes under, is the guest's own,
9518/// never "every signal blocked".
9519#[cfg(test)]
9520mod disposition_mask_tests {
9521    use std::sync::Mutex;
9522
9523    use reverie::Errno;
9524    use reverie::Error;
9525    use reverie::GlobalRPC;
9526    use reverie::GlobalTool;
9527    use reverie::Guest;
9528    use reverie::Pid;
9529    use reverie::syscalls::Addr;
9530    use reverie::syscalls::AddrMut;
9531    use reverie::syscalls::LocalMemory;
9532    use reverie::syscalls::MemoryAccess;
9533    use reverie::syscalls::SyscallInfo;
9534
9535    use super::*;
9536    use crate::tool_global::GlobalState;
9537
9538    const GUEST_MASK: KernelSigset = (1 << (libc::SIGUSR1 - 1)) | (1 << (libc::SIGHUP - 1));
9539
9540    struct UnusedStack;
9541    struct UnusedStackGuard;
9542    impl Drop for UnusedStackGuard {
9543        fn drop(&mut self) {}
9544    }
9545    impl reverie::Stack for UnusedStack {
9546        type StackGuard = UnusedStackGuard;
9547        fn size(&self) -> usize {
9548            panic!("the disposition mask uses the caller's addresses")
9549        }
9550        fn capacity(&self) -> usize {
9551            panic!("the disposition mask uses the caller's addresses")
9552        }
9553        fn push<'a, V>(&mut self, _: V) -> Addr<'a, V> {
9554            panic!("the disposition mask uses the caller's addresses")
9555        }
9556        fn reserve<'a, V>(&mut self) -> AddrMut<'a, V> {
9557            panic!("the disposition mask uses the caller's addresses")
9558        }
9559        fn commit(self) -> Result<Self::StackGuard, Errno> {
9560            panic!("the disposition mask uses the caller's addresses")
9561        }
9562    }
9563
9564    /// A thread whose backend answers each injected `rt_sigprocmask` as the
9565    /// kernel would for a thread whose mask is `mask`, and records the
9566    /// `(how, set given)` of each.
9567    struct MaskGuest {
9568        config: Config,
9569        thread: crate::ThreadState<()>,
9570        mask: KernelSigset,
9571        calls: Mutex<Vec<(i32, bool)>>,
9572    }
9573
9574    #[reverie::tool]
9575    impl GlobalRPC<GlobalState> for MaskGuest {
9576        async fn send_rpc(
9577            &self,
9578            message: <GlobalState as GlobalTool>::Request,
9579        ) -> <GlobalState as GlobalTool>::Response {
9580            panic!("the disposition mask sends no RPC here: {:?}", message.2)
9581        }
9582        fn config(&self) -> &Config {
9583            &self.config
9584        }
9585    }
9586
9587    #[reverie::tool]
9588    impl Guest<Detcore> for MaskGuest {
9589        type Memory = LocalMemory;
9590        type Stack = UnusedStack;
9591        fn tid(&self) -> Pid {
9592            Pid::from_raw(41)
9593        }
9594        fn pid(&self) -> Pid {
9595            Pid::from_raw(41)
9596        }
9597        fn ppid(&self) -> Option<Pid> {
9598            None
9599        }
9600        fn memory(&self) -> LocalMemory {
9601            LocalMemory::new()
9602        }
9603        fn thread_state(&self) -> &crate::ThreadState<()> {
9604            &self.thread
9605        }
9606        fn thread_state_mut(&mut self) -> &mut crate::ThreadState<()> {
9607            &mut self.thread
9608        }
9609        async fn regs(&mut self) -> libc::user_regs_struct {
9610            panic!("no registers are read")
9611        }
9612        async fn stack(&mut self) -> UnusedStack {
9613            panic!("the disposition mask uses the caller's addresses")
9614        }
9615        async fn daemonize(&mut self) {
9616            panic!("no daemonize")
9617        }
9618        async fn inject<S: SyscallInfo>(&mut self, call: S) -> Result<i64, Errno> {
9619            assert_eq!(call.number(), syscalls::Sysno::rt_sigprocmask);
9620            let args = call.into_parts().1;
9621            let (how, set, old) = (args.arg0 as i32, args.arg1, args.arg2);
9622            self.calls.lock().unwrap().push((how, set != 0));
9623            let previous = self.mask;
9624            if set != 0 {
9625                let set: KernelSigset = LocalMemory::new()
9626                    .read_value(Addr::<KernelSigset>::from_raw(set).unwrap())
9627                    .unwrap();
9628                self.mask = set;
9629            }
9630            if old != 0 {
9631                LocalMemory::new()
9632                    .write_value(AddrMut::<KernelSigset>::from_raw(old).unwrap(), &previous)
9633                    .unwrap();
9634            }
9635            Ok(0)
9636        }
9637        async fn tail_inject<S: SyscallInfo>(&mut self, _: S) -> reverie::Never {
9638            panic!("no tail injection")
9639        }
9640        fn set_timer(&mut self, _: reverie::TimerSchedule) -> Result<(), Error> {
9641            panic!("no timer")
9642        }
9643        fn set_timer_precise(&mut self, _: reverie::TimerSchedule) -> Result<(), Error> {
9644            panic!("no timer")
9645        }
9646        fn read_clock(&mut self) -> Result<u64, Error> {
9647            panic!("no clock")
9648        }
9649    }
9650
9651    fn mask_guest(gate: bool) -> MaskGuest {
9652        let mut config = Config {
9653            sequentialize_threads: true,
9654            max_timeslice: None,
9655            in_guest_signal_delivery: gate,
9656            ..Config::default()
9657        };
9658        config.backend = reverie::BackendCapabilities::LITEINST_IN_GUEST;
9659        let detpid = DetPid::from_raw(41);
9660        let mut thread = crate::ThreadState::new(detpid, &config, ());
9661        thread.detpid = Some(detpid);
9662        MaskGuest {
9663            config,
9664            thread,
9665            mask: GUEST_MASK,
9666            calls: Mutex::new(Vec::new()),
9667        }
9668    }
9669
9670    /// Under the gate the wait's mask step is a query and its restore is
9671    /// nothing, so the guest's mask stays its own throughout; with the gate
9672    /// off the in-guest backend keeps today's block and restore. Mutation:
9673    /// "the block left in place under the gate" (the gated row's mask is the
9674    /// blocked set while the wait runs, and its calls show a set).
9675    #[tokio::test]
9676    async fn a_gated_managed_wait_reads_the_guest_mask_without_replacing_it() {
9677        for (gate, calls_during, mask_during, calls_after) in [
9678            (
9679                true,
9680                vec![(libc::SIG_SETMASK, false)],
9681                GUEST_MASK,
9682                vec![(libc::SIG_SETMASK, false)],
9683            ),
9684            (
9685                false,
9686                vec![(libc::SIG_SETMASK, true)],
9687                blocked_signal_mask(),
9688                vec![(libc::SIG_SETMASK, true), (libc::SIG_SETMASK, true)],
9689            ),
9690        ] {
9691            let mut guest = mask_guest(gate);
9692            let blocked = blocked_signal_mask();
9693            let mut old = 0 as KernelSigset;
9694            let blocked_addr =
9695                Addr::<KernelSigset>::from_raw(&blocked as *const _ as usize).unwrap();
9696            let old_addr = AddrMut::<KernelSigset>::from_raw(&mut old as *mut _ as usize).unwrap();
9697            let read = block_signals_for_disposition(&mut guest, blocked_addr, old_addr)
9698                .await
9699                .unwrap();
9700            assert_eq!(read, GUEST_MASK, "gate={gate}");
9701            assert_eq!(*guest.calls.lock().unwrap(), calls_during, "gate={gate}");
9702            assert_eq!(
9703                guest.mask, mask_during,
9704                "gate={gate}: the mask while the wait runs"
9705            );
9706            restore_signals_after_disposition(&mut guest, old_addr)
9707                .await
9708                .unwrap();
9709            assert_eq!(*guest.calls.lock().unwrap(), calls_after, "gate={gate}");
9710            assert_eq!(
9711                guest.mask, GUEST_MASK,
9712                "gate={gate}: the mask after the wait"
9713            );
9714        }
9715    }
9716}