Skip to main content

detcore/syscalls/
misc.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//! Miscellaneous virtualized syscalls.
10
11use std::collections::hash_map::DefaultHasher;
12use std::hash::Hash;
13use std::hash::Hasher;
14
15use reverie::BackendCapabilities;
16use reverie::Error;
17use reverie::Guest;
18use reverie::syscalls;
19use reverie::syscalls::AddrMut;
20#[cfg(target_arch = "x86_64")]
21use reverie::syscalls::ArchPrctlCmd;
22use reverie::syscalls::Errno;
23use reverie::syscalls::MemoryAccess;
24use reverie::syscalls::Sysno;
25
26use crate::consts::DEFAULT_HOSTNAME;
27use crate::detlog;
28use crate::fd::RandomDeviceStream;
29use crate::random::RANDOM_FILL_CHUNK_BYTES;
30#[cfg(test)]
31use crate::random::getrandom_request_len;
32use crate::random::splitmix64;
33#[cfg(test)]
34use crate::random::validate_getrandom_flags;
35use crate::random::write_random_chunk;
36use crate::record_or_replay::RecordOrReplay;
37use crate::scheduler::HostTimedSignalScope;
38use crate::tool_global::SigalrmControl;
39use crate::tool_global::create_session;
40use crate::tool_global::record_host_timed_signals;
41use crate::tool_global::refuse_sigalrm;
42use crate::tool_global::registered_process_of;
43use crate::tool_global::set_child_subreaper;
44use crate::tool_global::set_process_group;
45use crate::tool_local::Detcore;
46use crate::types::DetPid;
47
48#[cfg(target_arch = "x86_64")]
49const ARCH_GET_XCOMP_SUPP: libc::c_int = 0x1021;
50#[cfg(target_arch = "x86_64")]
51const ARCH_GET_XCOMP_PERM: libc::c_int = 0x1022;
52#[cfg(target_arch = "x86_64")]
53const ARCH_REQ_XCOMP_PERM: libc::c_int = 0x1023;
54#[cfg(target_arch = "x86_64")]
55const ARCH_GET_XCOMP_GUEST_PERM: libc::c_int = 0x1024;
56#[cfg(target_arch = "x86_64")]
57const ARCH_REQ_XCOMP_GUEST_PERM: libc::c_int = 0x1025;
58
59#[cfg(target_arch = "x86_64")]
60const ARCH_SHSTK_ENABLE: libc::c_int = 0x5001;
61#[cfg(target_arch = "x86_64")]
62const ARCH_SHSTK_DISABLE: libc::c_int = 0x5002;
63#[cfg(target_arch = "x86_64")]
64const ARCH_SHSTK_LOCK: libc::c_int = 0x5003;
65#[cfg(target_arch = "x86_64")]
66const ARCH_SHSTK_UNLOCK: libc::c_int = 0x5004;
67#[cfg(target_arch = "x86_64")]
68const ARCH_SHSTK_STATUS: libc::c_int = 0x5005;
69#[cfg(target_arch = "x86_64")]
70const ARCH_SHSTK_VALID_MASK: usize = 0b11;
71
72const SECCOMP_SET_MODE_STRICT: u32 = 0;
73const SECCOMP_SET_MODE_FILTER: u32 = 1;
74const SECCOMP_GET_ACTION_AVAIL: u32 = 2;
75const SECCOMP_GET_NOTIF_SIZES: u32 = 3;
76const SECCOMP_FILTER_FLAG_TSYNC: u32 = 1;
77
78fn seccomp_result(op: u32, flags: u32, has_args: bool) -> Result<i64, Errno> {
79    if op > SECCOMP_GET_NOTIF_SIZES {
80        return Err(Errno::EINVAL);
81    }
82    if op == SECCOMP_SET_MODE_STRICT && (flags != 0 || has_args) {
83        return Err(Errno::EINVAL);
84    }
85    if op == SECCOMP_SET_MODE_FILTER && flags & !SECCOMP_FILTER_FLAG_TSYNC != 0 {
86        return Err(Errno::EINVAL);
87    }
88    if matches!(
89        op,
90        SECCOMP_SET_MODE_FILTER | SECCOMP_GET_ACTION_AVAIL | SECCOMP_GET_NOTIF_SIZES
91    ) && !has_args
92    {
93        return Err(Errno::EFAULT);
94    }
95
96    // Hermit cannot enforce a guest-installed BPF policy in every backend.
97    // Report the operation as unsupported instead of claiming a filter exists.
98    Err(Errno::EOPNOTSUPP)
99}
100
101/// Whether Detcore sends parent-death signals itself
102/// (https://github.com/rrnewton/hermit/issues/4440): only where a thread's
103/// exit reaches the scheduler in order (sequentialized threads) and the
104/// guest's tasks are host tasks it can signal (ptrace).
105fn delivers_parent_death_signal(config: &crate::config::Config) -> bool {
106    config.sequentialize_threads && config.backend.host_task_reads_reach_guest_threads
107}
108
109/// Whether Detcore holds parent-death signals itself, arming the kernel with
110/// 0 and answering `PR_GET_PDEATHSIG` from its own record: wherever it sends
111/// them ([`delivers_parent_death_signal`]), and on an in-guest run's default
112/// route (`Config::in_guest_refuses_host_timed_producers`), where it cannot
113/// send them yet and refuses the run when one would fire
114/// (`Scheduler::deliver_parent_death_signals`; D28a of A5 step 3c,
115/// https://github.com/rrnewton/hermit/issues/4485).
116fn holds_parent_death_signal(config: &crate::config::Config) -> bool {
117    delivers_parent_death_signal(config) || config.in_guest_refuses_host_timed_producers
118}
119
120/// Where `prctl(PR_SET_PDEATHSIG, signal)` goes ([`parent_death_signal_route`]).
121#[derive(Clone, Copy, Debug, PartialEq, Eq)]
122enum ParentDeathSignalRoute {
123    /// Detcore keeps the value (0 disarms) and arms the kernel with 0.
124    Hold,
125    /// The kernel takes the call, as on Linux.
126    Kernel,
127    /// EPERM and a WARN: a signal Detcore cannot hold, which the kernel would
128    /// send at a host-timed moment.
129    Refuse,
130}
131
132/// Where `prctl(PR_SET_PDEATHSIG, signal)` goes, where Detcore holds
133/// parent-death signals (`holds`) and refuses the ones it cannot hold
134/// (`refuses_unheld`, an in-guest run's default route). Detcore holds 0 and
135/// every signal it sends itself ([`detcore_sends_parent_death_signal`]).
136/// SIGKILL keeps the kernel's path everywhere: it cannot be blocked or left
137/// pending, so the in-guest runtime's mask does not make it host-timed in the
138/// way the refusal is about (design section 16.32). Any other signal (SIGSTOP,
139/// SIGALRM, a realtime signal) is refused where unheld signals are; a value
140/// above 64 always goes to the kernel, which refuses it with EINVAL.
141fn parent_death_signal_route(
142    signal: libc::c_ulong,
143    holds: bool,
144    refuses_unheld: bool,
145) -> ParentDeathSignalRoute {
146    if holds && (signal == 0 || detcore_sends_parent_death_signal(signal)) {
147        ParentDeathSignalRoute::Hold
148    } else if refuses_unheld
149        && (1..=64).contains(&signal)
150        && signal != libc::SIGKILL as libc::c_ulong
151    {
152        ParentDeathSignalRoute::Refuse
153    } else {
154        ParentDeathSignalRoute::Kernel
155    }
156}
157
158/// Whether parent-death signal `signal` is one Detcore sends itself: a
159/// standard signal (1 to 31) other than SIGKILL and SIGSTOP, which the
160/// kernel keeps acting on as before (a SIGKILL'd child is never counted as
161/// stuck, `Scheduler::child_is_stuck`), and SIGALRM, whose scheduler sends go
162/// to the process's alarm ledger.
163fn detcore_sends_parent_death_signal(signal: libc::c_ulong) -> bool {
164    (1..=31).contains(&signal)
165        && ![libc::SIGKILL, libc::SIGSTOP, libc::SIGALRM].contains(&(signal as libc::c_int))
166}
167
168fn is_supported_prctl_option(option: libc::c_int) -> bool {
169    matches!(
170        option,
171        libc::PR_SET_NAME
172            | libc::PR_GET_NAME
173            | libc::PR_SET_THP_DISABLE
174            | libc::PR_GET_THP_DISABLE
175            // AUTONOMOUS-BOT-IMPLEMENTED
176            // TODO-HUMAN-REVIEW(#919)
177            //
178            // Dumpability is per-process state initialized deterministically by
179            // ordinary exec and subsequently controlled only by the guest. Keep
180            // Linux's 0/1 validation and state transitions rather than refusing
181            // applications that explicitly disable or restore core-dump access.
182            | libc::PR_SET_DUMPABLE
183            | libc::PR_GET_DUMPABLE
184            // PR_SET_TIMERSLACK and PR_GET_TIMERSLACK are deliberately absent:
185            // `handle_prctl` emulates them against per-thread Detcore state.
186            // AUTONOMOUS-BOT-IMPLEMENTED
187            // TODO-HUMAN-REVIEW(#802)
188            //
189            // PR_{SET,GET}_KEEPCAPS only read/toggle the calling thread's
190            // "keep capabilities across a UID change" flag. The result is a pure
191            // function of the guest's own prior prctl calls (0/1), never host
192            // state, so passthrough is deterministic and bitwise-identical across
193            // runs. Supporting it lets `setpriv` (and the `date`/privilege-drop
194            // wrappers that call it) run under --strict instead of aborting with
195            // "keep process capabilities failed: Function not implemented".
196            | libc::PR_SET_KEEPCAPS
197            | libc::PR_GET_KEEPCAPS
198            // AUTONOMOUS-BOT-IMPLEMENTED
199            // TODO-HUMAN-REVIEW(#824)
200            //
201            // PR_{SET,GET}_PDEATHSIG only read/toggle the calling thread's
202            // parent-death-signal attribute. PR_SET_PDEATHSIG validates its
203            // signal argument (a valid signal or 0 succeeds, anything else
204            // faults EINVAL) and PR_GET_PDEATHSIG reports the value the guest
205            // previously set. The result is a pure function of the guest's own
206            // prior prctl calls and its argument, never host state, so
207            // passthrough is deterministic and bitwise-identical across runs.
208            // The registered signal fires when the parent thread exits, and the
209            // kernel posts it at a moment set by host timing, so `handle_prctl`
210            // first records a nonzero PR_SET_PDEATHSIG signal as host-timed for
211            // the caller's process: no gated wait of that process ends for it
212            // (https://github.com/rrnewton/hermit/issues/3146). Supporting it lets
213            // `setpriv --pdeathsig` run under --strict instead of aborting with
214            // "set parent death signal failed: Function not implemented".
215            | libc::PR_SET_PDEATHSIG
216            | libc::PR_GET_PDEATHSIG
217            // AUTONOMOUS-BOT-IMPLEMENTED
218            // TODO-HUMAN-REVIEW(PR-1125): Review KVM capability-control prctl forwarding.
219            //
220            // PR_CAPBSET_DROP and PR_CAP_AMBIENT change and read the calling
221            // thread's capability bounding and ambient sets. They reach the
222            // kernel on every backend (the host's, or KVM's provider), as
223            // PR_CAPBSET_READ and capget already do, so each backend answers as
224            // native Linux does for the same credentials: a drop or a raise
225            // the caller is not entitled to fails with EPERM, an unknown
226            // capability with EINVAL. The result is a function of the
227            // container's initial credentials and the guest's own earlier
228            // calls. They used to reach the kernel on KVM only, and fail with
229            // ENOSYS everywhere else.
230            | libc::PR_CAPBSET_DROP
231            | libc::PR_CAP_AMBIENT
232            // AUTONOMOUS-BOT-IMPLEMENTED
233            // TODO-HUMAN-REVIEW(PR-4763)
234            //
235            // PR_SET_PTRACER names which process may ptrace the caller's
236            // process under the Yama LSM's ptrace scope 1, or any process
237            // (PR_SET_PTRACER_ANY), or none (0). It changes only that
238            // per-process exception, and the answer is a function of the
239            // argument and the host kernel: 0 with Yama, EINVAL for a pid
240            // that names no process, and EINVAL without Yama, where the option
241            // reaches prctl's default case. Android's crash handler (bionic's
242            // debuggerd) calls it with PR_SET_PTRACER_ANY on every fatal
243            // signal and treats EINVAL as "no Yama", but any other error as
244            // fatal: with ENOSYS from here, every crashing guest on Android
245            // exited 1 from inside its own signal handler instead of dying of
246            // the signal.
247            | libc::PR_SET_PTRACER
248    )
249}
250
251/// Whether Detcore can admit `PR_SET_CHILD_SUBREAPER`: threads are
252/// sequentialized, so every exit reaches the scheduler through
253/// `deregister_thread` and re-parents there (`Scheduler::reparent_orphans`);
254/// without that, Detcore's record never learns the orphan moved while it still
255/// decides ECHILD. And on `backend`, its own record of children decides waits
256/// (`tracks_process_children`), the kernel rather than the backend performs
257/// them (`!emulates_child_waits`), and a process's exit reaches Detcore only
258/// after the kernel's `exit_notify` has re-parented its children (neither
259/// asynchronous nor separately reported physical exits), so the record agrees
260/// with the kernel. Today that is ptrace, and e9patch preprocessing run under
261/// ptrace, in a sequentialized run.
262fn models_child_subreapers(sequentialize_threads: bool, backend: &BackendCapabilities) -> bool {
263    sequentialize_threads
264        && backend.tracks_process_children
265        && !backend.emulates_child_waits
266        && !backend.process_exits_complete_asynchronously
267        && !backend.reports_physical_process_exits
268}
269
270/// The name `PR_SET_NAME` stores from the caller's string at `address`
271/// (`strncpy_from_user(comm, arg2, sizeof(me->comm) - 1)`): its bytes through
272/// the first NUL, at most 15. The bytes are read with the vectored,
273/// protection-respecting copy (`files::read_guest_prefix`, never
274/// `PTRACE_PEEKDATA`, which reads whole words and could fault on the page
275/// after a string that ends before it), so a string that ends just before an
276/// unreadable page is read whole, as the kernel reads it. `None` where a byte
277/// before the string's end or its 15th byte cannot be read.
278fn prctl_task_name<M: reverie::syscalls::MemoryAccess>(
279    memory: &M,
280    address: libc::c_ulong,
281) -> Option<crate::procfs::task_stat::TaskComm> {
282    let mut bytes = [0_u8; crate::procfs::task_stat::TASK_COMM_LEN - 1];
283    let address = AddrMut::<u8>::from_raw(usize::try_from(address).ok()?)?;
284    let read = crate::syscalls::files::read_guest_prefix(memory, address, &mut bytes).ok()?;
285    let readable = &bytes[..read];
286    if read < bytes.len() && !readable.contains(&0) {
287        return None;
288    }
289    Some(crate::procfs::task_stat::TaskComm::new(readable))
290}
291
292/// Is `which` one of the Linux `PRIO_*` target selectors for get/setpriority?
293fn is_valid_prio_which(which: i32) -> bool {
294    which == libc::PRIO_PROCESS as i32
295        || which == libc::PRIO_PGRP as i32
296        || which == libc::PRIO_USER as i32
297}
298
299// AUTONOMOUS-BOT-IMPLEMENTED
300// TODO-HUMAN-REVIEW(#806)
301/// Deterministic raw `getpriority(2)` result under Hermit's inert-nice model.
302///
303/// Returns `20 - nice` (nice 0 -> 20) for any valid target regardless of `who`,
304/// because nice is inert under the virtualized scheduler and `getpriority` never
305/// checks permissions; an unknown `which` faults with `EINVAL`, matching Linux.
306fn getpriority_result(which: i32) -> Result<i64, Errno> {
307    if is_valid_prio_which(which) {
308        Ok(20)
309    } else {
310        Err(Errno::EINVAL)
311    }
312}
313
314// AUTONOMOUS-BOT-IMPLEMENTED
315// TODO-HUMAN-REVIEW(#806)
316/// Deterministic raw `setpriority(2)` result: accept any priority change for a
317/// valid target as an inert no-op (returns 0), `EINVAL` for an unknown `which`.
318fn setpriority_result(which: i32) -> Result<i64, Errno> {
319    if is_valid_prio_which(which) {
320        Ok(0)
321    } else {
322        Err(Errno::EINVAL)
323    }
324}
325
326// AUTONOMOUS-BOT-IMPLEMENTED
327// TODO-HUMAN-REVIEW(PR-857): Deterministic empty kernel-log action policy.
328fn deterministic_syslog_result(action: i32, len: usize) -> Result<i64, Errno> {
329    const SYSLOG_ACTION_CONSOLE_LEVEL: i32 = 8;
330
331    match action {
332        0..=7 | 9..=10 => Ok(0),
333        SYSLOG_ACTION_CONSOLE_LEVEL if (1..=8).contains(&len) => Ok(0),
334        _ => Err(Errno::EINVAL),
335    }
336}
337
338/// Write get_mempolicy's output as Linux's `put_user`/`copy_to_user` do:
339/// through the guest's page protections, so a buffer that cannot take all the
340/// bytes is `EFAULT`. Any other failure is the backend's, not a guest fault,
341/// and is reported as a tool error rather than handed to the guest as an
342/// errno get_mempolicy never returns (Claude re-check of
343/// https://github.com/rrnewton/hermit/pull/4249).
344fn write_get_mempolicy_output<M: MemoryAccess>(
345    memory: &mut M,
346    address: AddrMut<'_, u8>,
347    bytes: &[u8],
348) -> Result<(), Error> {
349    match memory.write_with_user_access(address, bytes) {
350        Ok(written) if written == bytes.len() => Ok(()),
351        Ok(_) | Err(Errno::EFAULT) => Err(Errno::EFAULT.into()),
352        Err(errno) => Err(Error::Tool(anyhow::anyhow!(
353            "get_mempolicy output write failed: {errno}"
354        ))),
355    }
356}
357
358fn from_str(s: &str) -> [libc::c_char; 65] {
359    let mut ret: [libc::c_char; 65] = [0; 65];
360    for (i, ch) in s.bytes().take(64).enumerate() {
361        ret[i] = ch as libc::c_char;
362    }
363    ret
364}
365
366const RANDOM_DEVICE_BYTE_STRIDE: u8 = 73;
367const RANDOM_DEVICE_FIRST_BYTE: u8 = 41;
368
369// AUTONOMOUS-BOT-IMPLEMENTED
370// TODO-HUMAN-REVIEW(PR-1096): Review the backend-independent random-device stream.
371fn canonical_random_device_byte(seed: u64, index: u64) -> u8 {
372    let seed_byte = seed.rotate_right(((index % 8) * 8) as u32) as u8;
373    (index as u8)
374        .wrapping_mul(RANDOM_DEVICE_BYTE_STRIDE)
375        .wrapping_add(RANDOM_DEVICE_FIRST_BYTE)
376        ^ seed_byte
377}
378
379/// The seed of `stream`'s bytes in a run seeded `run_seed`. Distinct streams
380/// of a run, like distinct run seeds, almost surely get distinct seeds, so two
381/// open files of a random device read different bytes
382/// (`crate::fd::RandomDeviceStream`).
383fn random_device_stream_seed(run_seed: u64, stream: RandomDeviceStream) -> u64 {
384    [stream.creator.as_raw() as u32 as u64, stream.sequence]
385        .into_iter()
386        .fold(run_seed, |seed, word| splitmix64(seed ^ splitmix64(word)))
387}
388
389/// Scatter one stream through an already-imported array. Scratch space is
390/// independent of request size; only EFAULT becomes a successful copied prefix.
391fn fill_canonical_random_iovecs(
392    memory: &mut impl MemoryAccess,
393    iovecs: &[crate::iovecs::ImportedIovec],
394    seed: u64,
395    stream_offset: u64,
396    hasher: &mut DefaultHasher,
397) -> Result<usize, Error> {
398    let mut local_words = [0_u64; RANDOM_FILL_CHUNK_BYTES / std::mem::size_of::<u64>()];
399    let mut written = 0_usize;
400    for iov in iovecs {
401        let mut segment_written = 0;
402        while segment_written < iov.len {
403            let remote_chunk = match iov
404                .base
405                .checked_add(segment_written)
406                .and_then(AddrMut::<u8>::from_raw)
407            {
408                Some(address) => address,
409                None if written == 0 => return Err(Errno::EFAULT.into()),
410                None => return Ok(written),
411            };
412            let chunk_len = (iov.len - segment_written).min(RANDOM_FILL_CHUNK_BYTES);
413            let local_buf = unsafe {
414                std::slice::from_raw_parts_mut(local_words.as_mut_ptr().cast::<u8>(), chunk_len)
415            };
416            for (index, byte) in local_buf.iter_mut().enumerate() {
417                *byte = canonical_random_device_byte(
418                    seed,
419                    stream_offset
420                        .saturating_add(written as u64)
421                        .saturating_add(index as u64),
422                );
423            }
424            let n = match write_random_chunk(memory, remote_chunk, local_buf) {
425                Ok(n) => n,
426                Err(Errno::EFAULT) if written > 0 => return Ok(written),
427                Err(error) => return Err(crate::random::copy_error(error)),
428            };
429            if n == 0 && written == 0 {
430                return Err(Errno::EFAULT.into());
431            }
432            if cfg!(debug_assertions) {
433                Hash::hash_slice(&local_buf[..n], hasher);
434            }
435            written += n;
436            segment_written += n;
437            if n < chunk_len {
438                return Ok(written);
439            }
440        }
441    }
442    Ok(written)
443}
444
445impl<T: RecordOrReplay> Detcore<T> {
446    /// Validates seccomp capability probes without installing guest filters.
447    // TODO-HUMAN-REVIEW(PR-874): Review deterministic seccomp probe validation.
448    pub async fn handle_seccomp<G: Guest<Self>>(
449        &self,
450        _guest: &mut G,
451        call: syscalls::Seccomp,
452    ) -> Result<i64, Error> {
453        seccomp_result(call.op(), call.flags(), call.args().is_some()).map_err(Into::into)
454    }
455
456    /// The host xstate facts of an x86-64-v3 run, or `None` under v2.
457    #[cfg(target_arch = "x86_64")]
458    fn v3_host_xstate(&self) -> Option<&detcore_model::cpu_profile::HostXstate> {
459        (self.cfg.cpu_profile == detcore_model::cpu_profile::CpuProfile::X86_64V3)
460            .then_some(self.cfg.cpu_profile_host.as_ref())
461            .flatten()
462    }
463
464    #[cfg(target_arch = "x86_64")]
465    fn write_arch_prctl_u64<G: Guest<Self>>(
466        &self,
467        guest: &mut G,
468        raw_addr: usize,
469        value: u64,
470    ) -> Result<i64, Error> {
471        let addr = AddrMut::<u64>::from_raw(raw_addr).ok_or(Errno::EFAULT)?;
472        guest.memory().write_value(addr, &value)?;
473        Ok(0)
474    }
475
476    // AUTONOMOUS-BOT-IMPLEMENTED
477    // TODO-HUMAN-REVIEW(#539): Confirm the virtual arch_prctl control policy.
478    /// Preserve thread-local bases while hiding host CPU feature controls.
479    #[cfg(target_arch = "x86_64")]
480    pub async fn handle_arch_prctl<G: Guest<Self>>(
481        &self,
482        guest: &mut G,
483        call: syscalls::ArchPrctl,
484    ) -> Result<i64, Error> {
485        let cpuid_uses_faulting = self.cfg.virtualize_cpuid && guest.has_cpuid_interception();
486        match call.cmd() {
487            ArchPrctlCmd::ARCH_SET_FS(_)
488            | ArchPrctlCmd::ARCH_SET_GS(_)
489            | ArchPrctlCmd::ARCH_GET_FS(_)
490            | ArchPrctlCmd::ARCH_GET_GS(_) => Ok(guest.inject(call).await?),
491
492            // When Reverie successfully disables native CPUID, Detcore answers its fault from a
493            // fixed table. Preserve that backend state and reject attempts to re-enable CPUID.
494            ArchPrctlCmd::ARCH_GET_CPUID(_) if cpuid_uses_faulting => Ok(0),
495            ArchPrctlCmd::ARCH_SET_CPUID(value) if cpuid_uses_faulting => {
496                if value == 0 {
497                    Ok(0)
498                } else {
499                    Err(Errno::EPERM.into())
500                }
501            }
502            // Reverie cannot faithfully deliver a CPUID fault requested by the tracee. In
503            // explicit host-CPUID mode, expose a fixed enabled control state and reject disable.
504            ArchPrctlCmd::ARCH_GET_CPUID(_) if !self.cfg.virtualize_cpuid => Ok(1),
505            ArchPrctlCmd::ARCH_SET_CPUID(value) if !self.cfg.virtualize_cpuid => {
506                if value == 0 {
507                    Err(Errno::EPERM.into())
508                } else {
509                    Ok(0)
510                }
511            }
512
513            // Ptrace hosts without CPUID-faulting support retain the kernel's honest state.
514            ArchPrctlCmd::ARCH_GET_CPUID(_) | ArchPrctlCmd::ARCH_SET_CPUID(_) => {
515                Ok(guest.inject(call).await?)
516            }
517
518            // The host profile exposes the native XSAVE layout, including
519            // dynamic components, so Linux must answer its permission queries
520            // and apply requests to the guest task itself. Preserve its masks,
521            // pointer checks and errno rather than the fixed-profile policy:
522            // Linux v7.1.3, fpu_xstate_prctl, arch/x86/kernel/fpu/xstate.c:1840.
523            // https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/arch/x86/kernel/fpu/xstate.c?h=v7.1.3#n1840
524            ArchPrctlCmd::Other(
525                ARCH_GET_XCOMP_SUPP
526                | ARCH_GET_XCOMP_PERM
527                | ARCH_GET_XCOMP_GUEST_PERM
528                | ARCH_REQ_XCOMP_PERM
529                | ARCH_REQ_XCOMP_GUEST_PERM,
530                _,
531            ) if self.cfg.cpu_profile == detcore_model::cpu_profile::CpuProfile::HostNoRdrand => {
532                Ok(guest.inject(call).await?)
533            }
534
535            // Under x86-64-v3 the guest's XGETBV and XSAVE run on the host, so
536            // the xstate queries answer with the host's masks, captured at
537            // startup on a host without dynamic features and bound in the
538            // recording; the requests answer as Linux does on such a host:
539            // EOPNOTSUPP below XFEATURE_MAX and EINVAL from it up
540            // (https://github.com/rrnewton/hermit/issues/4243).
541            ArchPrctlCmd::Other(
542                cmd @ (ARCH_GET_XCOMP_SUPP | ARCH_GET_XCOMP_PERM | ARCH_GET_XCOMP_GUEST_PERM),
543                addr,
544            ) if let Some(host) = self.v3_host_xstate() => {
545                let mask = match cmd {
546                    ARCH_GET_XCOMP_SUPP => host.xcomp_supp,
547                    ARCH_GET_XCOMP_PERM => host.xcomp_perm,
548                    _ => host.xcomp_guest_perm,
549                };
550                self.write_arch_prctl_u64(guest, addr, mask)
551            }
552            ArchPrctlCmd::Other(ARCH_REQ_XCOMP_PERM | ARCH_REQ_XCOMP_GUEST_PERM, index)
553                if let Some(host) = self.v3_host_xstate() =>
554            {
555                if index >= host.xfeature_max as usize {
556                    Err(Errno::EINVAL.into())
557                } else {
558                    Err(Errno::EOPNOTSUPP.into())
559                }
560            }
561            // Expose a conservative virtual CPU with no optional extended-state permissions.
562            ArchPrctlCmd::Other(
563                ARCH_GET_XCOMP_SUPP | ARCH_GET_XCOMP_PERM | ARCH_GET_XCOMP_GUEST_PERM,
564                addr,
565            ) => self.write_arch_prctl_u64(guest, addr, 0),
566            ArchPrctlCmd::Other(ARCH_REQ_XCOMP_PERM | ARCH_REQ_XCOMP_GUEST_PERM, _) => {
567                Err(Errno::EINVAL.into())
568            }
569
570            // Keep shadow stacks disabled in the virtual policy. Disabling an already-disabled
571            // feature is idempotent; enable/lock/unlock requests cannot be honored.
572            ArchPrctlCmd::Other(ARCH_SHSTK_STATUS, addr) => {
573                self.write_arch_prctl_u64(guest, addr, 0)
574            }
575            ArchPrctlCmd::Other(ARCH_SHSTK_DISABLE, features)
576                if features != 0 && features & !ARCH_SHSTK_VALID_MASK == 0 =>
577            {
578                Ok(0)
579            }
580            ArchPrctlCmd::Other(ARCH_SHSTK_DISABLE, _)
581            | ArchPrctlCmd::Other(ARCH_SHSTK_ENABLE | ARCH_SHSTK_LOCK | ARCH_SHSTK_UNLOCK, _) => {
582                Err(Errno::EINVAL.into())
583            }
584
585            ArchPrctlCmd::Other(_, _) => Err(Errno::EINVAL.into()),
586        }
587    }
588
589    // AUTONOMOUS-BOT-IMPLEMENTED
590    // TODO-HUMAN-REVIEW(#663)
591    /// Preserve deterministic Ruby thread controls, report the container's fixed
592    /// capability bounding set, and reject options that expose unmodeled process
593    /// or host state.
594    pub async fn handle_prctl<G: Guest<Self>>(
595        &self,
596        guest: &mut G,
597        call: syscalls::Prctl,
598    ) -> Result<i64, Error> {
599        match call.option() {
600            // The capability bounding set is fixed by the container launch policy.
601            libc::PR_CAPBSET_READ => Ok(self.record_or_replay(guest, call).await?),
602            // AUTONOMOUS-BOT-IMPLEMENTED
603            // TODO-HUMAN-REVIEW(PR-2150): Timer slack is shared with the
604            // virtual `/proc/<tid>/timerslack_ns` channel.  Never pass either
605            // setter through to the physical tracee: a large physical slack
606            // changes wake timing on Detcore's remaining host-timed waits.
607            libc::PR_SET_TIMERSLACK => {
608                let requested = call.arg2();
609                let state = guest.thread_state_mut();
610                // Linux treats zero as reset-to-default. Hermit virtualizes the
611                // Linux scheduling policy as SCHED_OTHER, so the kernel's
612                // RT/DL no-op branch is unreachable in the guest model.
613                state.timer_slack_ns = if requested == 0 {
614                    state.default_timer_slack_ns
615                } else {
616                    requested
617                };
618                Ok(0)
619            }
620            libc::PR_GET_TIMERSLACK => Ok(guest.thread_state().timer_slack_ns as i64),
621            // The kernel sends a parent-death signal process-directed
622            // (`group_send_sig_info` with `PIDTYPE_TGID`) when the parent thread
623            // exits, at a moment set by host timing. Record it in this turn,
624            // before the call reaches the kernel, for every thread of the
625            // caller's process, and never forget it: a later disarm or `exec`
626            // does not remove it (`Scheduler::record_host_timed_signals`). Linux
627            // accepts 0 (disarm) and 1 to 64; anything else fails with EINVAL in
628            // the kernel and arms nothing.
629            // A parent-death signal Detcore sends itself is held by Detcore,
630            // not the kernel (armed with 0); any other comes from the kernel.
631            libc::PR_GET_PDEATHSIG if holds_parent_death_signal(guest.config()) => {
632                let held = crate::tool_global::parent_death_signal(guest, None).await;
633                if held == 0 {
634                    return self.passthrough(guest, call.into()).await;
635                }
636                let target =
637                    AddrMut::<libc::c_int>::from_raw(call.arg2() as usize).ok_or(Errno::EFAULT)?;
638                guest
639                    .memory()
640                    .write_value(target, &held)
641                    .map_err(|_| Errno::EFAULT)?;
642                Ok(0)
643            }
644            libc::PR_SET_PDEATHSIG => {
645                let signal = call.arg2();
646                // A reserved in-guest signal as the parent-death signal; 0
647                // disarms and is never reserved, nor is a value the kernel
648                // refuses with EINVAL (above 64).
649                Self::refuse_reserved_signal_arming(
650                    guest,
651                    crate::refused_signal_controls::ReservedArming::PrctlPdeathsig,
652                    i64::try_from(signal).unwrap_or(-1),
653                )?;
654                if signal == libc::SIGALRM as libc::c_ulong {
655                    refuse_sigalrm(guest, SigalrmControl::ArmProducer).await?;
656                }
657                // A signal Detcore sends itself when the parent thread leaves
658                // (`Scheduler::deliver_parent_death_signals`,
659                // https://github.com/rrnewton/hermit/issues/4440), or, on an
660                // in-guest run's default route, one it holds and refuses the
661                // run over if it would fire (D28a): the kernel is armed with
662                // 0, so it never posts a copy of its own at a host-timed
663                // moment, and Detcore keeps the value. There, a signal Detcore
664                // cannot hold is refused. Any other value takes the kernel's
665                // path below, and disarms Detcore's only once the kernel has
666                // accepted it (an invalid one, which the kernel refuses with
667                // EINVAL, changes nothing).
668                let held = holds_parent_death_signal(guest.config());
669                let route = parent_death_signal_route(
670                    signal,
671                    held,
672                    guest.config().in_guest_refuses_host_timed_producers,
673                );
674                if route == ParentDeathSignalRoute::Refuse {
675                    tracing::warn!(
676                        "{}",
677                        crate::host_timed_producers::parent_death_signal_refused_warning(
678                            signal as i32,
679                            guest.thread_state().dettid.as_raw(),
680                        )
681                    );
682                    return Err(Errno::EPERM.into());
683                }
684                if route == ParentDeathSignalRoute::Hold {
685                    // The call is Detcore's, not recorded: replay decides it
686                    // the same way. The kernel may hold a value an earlier
687                    // kernel-path call armed, which this one replaces, so it
688                    // is disarmed (retried if interrupted). Detcore's record
689                    // changes first: a parent that dies at a host-timed moment
690                    // between the two then sends the new signal and the old
691                    // (a death just before the call, on Linux, sends the old,
692                    // which a fatal old one makes the outcome), never neither.
693                    // A disarm that fails restores Detcore's previous value.
694                    let previous = crate::tool_global::parent_death_signal(guest, None).await;
695                    crate::tool_global::parent_death_signal(guest, Some(signal as i32)).await;
696                    if let Err(errno) = guest
697                        .inject_with_retry(syscalls::Syscall::from(call.with_arg2(0)))
698                        .await
699                    {
700                        crate::tool_global::parent_death_signal(guest, Some(previous)).await;
701                        return Err(errno.into());
702                    }
703                    return Ok(0);
704                }
705                if (1..=64).contains(&signal)
706                    && guest.config().backend.host_timed_signals_are_physical
707                {
708                    record_host_timed_signals(
709                        guest,
710                        HostTimedSignalScope::Caller,
711                        1_u64 << (signal - 1),
712                    )
713                    .await;
714                }
715                if signal == libc::SIGKILL as libc::c_ulong {
716                    crate::reserved_input::expose_global(guest, "kernel-parent-death-sigkill")?;
717                }
718                let result = self.passthrough(guest, call.into()).await?;
719                if held {
720                    crate::tool_global::parent_death_signal(guest, Some(0)).await;
721                }
722                Ok(result)
723            }
724            // AUTONOMOUS-BOT-IMPLEMENTED
725            // TODO-HUMAN-REVIEW(PR-4000)
726            //
727            // The subreaper flag is per-process state only the guest sets, so
728            // the kernel call is deterministic. It is injected in replay too,
729            // like setpgid, rather than replayed from the log: the kernel must
730            // hold the flag for orphans to become this process's children, so
731            // that the waits Detcore injects for them succeed. On success
732            // Detcore mirrors it in its thread tree, in this turn, so that an
733            // orphan re-parented to this process becomes its child for
734            // Detcore's waits too (`Scheduler::reparent_orphans`). The process
735            // is the one Detcore registered the calling thread in, resolved
736            // before the call: a raw clone child with a non-SIGCHLD exit
737            // signal is its own process there, although the backend reports
738            // its creator's pid. A thread with no registered process is
739            // refused before any effect. Linux treats any nonzero argument as
740            // "on". PR_GET_CHILD_SUBREAPER reads the caller's own flag back
741            // from the kernel.
742            libc::PR_SET_CHILD_SUBREAPER
743                if models_child_subreapers(
744                    guest.config().sequentialize_threads,
745                    &guest.config().backend,
746                ) =>
747            {
748                let dettid = guest.thread_state().dettid;
749                let Some(process) = registered_process_of(guest, dettid).await else {
750                    return Err(Errno::ENOSYS.into());
751                };
752                let result = guest.inject(call).await?;
753                set_child_subreaper(guest, process, call.arg2() != 0).await;
754                Ok(result)
755            }
756            libc::PR_GET_CHILD_SUBREAPER
757                if models_child_subreapers(
758                    guest.config().sequentialize_threads,
759                    &guest.config().backend,
760                ) =>
761            {
762                Ok(guest.inject(call).await?)
763            }
764            // PR_SET_NAME reaches the kernel, as before, and Detcore then
765            // records the name the kernel took, which a rendered
766            // `/proc/<pid>/stat` shows where the tool cannot read the kernel's
767            // own (`procfs::task_stat`): the caller's string through its first
768            // NUL, at most 15 bytes (`strncpy_from_user(comm, arg2,
769            // sizeof(me->comm) - 1)`, kernel/sys.c:2514-2521 in 6.17), read
770            // byte-exactly through the guest's own protections
771            // (`prctl_task_name`). A string Detcore cannot read where the
772            // kernel could keeps the previous record.
773            libc::PR_SET_NAME => {
774                let result = self.passthrough(guest, call.into()).await?;
775                let dettid = guest.thread_state().dettid;
776                match prctl_task_name(&guest.memory(), call.arg2()) {
777                    Some(comm) => crate::tool_global::set_task_comm(guest, dettid, comm).await,
778                    None => tracing::warn!(
779                        "[detcore, dtid {dettid}] PR_SET_NAME succeeded but its name could not \
780                         be read again; the previous recorded name stands"
781                    ),
782                }
783                Ok(result)
784            }
785            option if is_supported_prctl_option(option) => {
786                self.passthrough(guest, call.into()).await
787            }
788            _ => Err(Errno::ENOSYS.into()),
789        }
790    }
791
792    // AUTONOMOUS-BOT-IMPLEMENTED
793    // TODO-HUMAN-REVIEW(#806)
794    /// Report the deterministic default nice value for any scheduling target.
795    ///
796    /// Under Hermit the Linux nice value is inert: the scheduler is virtualized
797    /// and guest threads are serialized onto one virtual CPU, so a process's,
798    /// group's, or user's scheduling priority never affects guest-visible
799    /// computation. Report the deterministic default nice (0) for every valid
800    /// target regardless of `who` — real tools such as `renice -p <pid>` always
801    /// pass an explicit pid, and the raw `getpriority(2)` never checks
802    /// permissions on a read, so it must never return `EPERM`. An unknown
803    /// `which` still faults with `EINVAL`, matching Linux.
804    pub async fn handle_getpriority<G: Guest<Self>>(
805        &self,
806        _guest: &mut G,
807        call: syscalls::Getpriority,
808    ) -> Result<i64, Error> {
809        Ok(getpriority_result(call.which())?)
810    }
811
812    // AUTONOMOUS-BOT-IMPLEMENTED
813    // TODO-HUMAN-REVIEW(PR-857): Deterministic syslog(2) virtualization.
814    /// Present an empty kernel ring buffer. Reads and size queries return zero,
815    /// controls are inert, and invalid actions preserve Linux's EINVAL boundary.
816    pub async fn handle_syslog<G: Guest<Self>>(
817        &self,
818        _guest: &mut G,
819        call: syscalls::Syslog,
820    ) -> Result<i64, Error> {
821        Ok(deterministic_syslog_result(call.priority(), call.len())?)
822    }
823
824    // AUTONOMOUS-BOT-IMPLEMENTED
825    // TODO-HUMAN-REVIEW(#806)
826    /// Accept any priority change as a deterministic no-op.
827    ///
828    /// Nice values are inert under Hermit's virtualized, serialized scheduler,
829    /// so accept the request without touching host scheduling. The guest runs as
830    /// a single uid-0 container principal, so a real `setpriority(2)` from the
831    /// caller would succeed anyway; never fabricate `EPERM` for tools such as
832    /// `nice -n 5 <cmd>`, `renice -p <pid>`, or Python's `os.nice`. An unknown
833    /// `which` still faults with `EINVAL`, matching Linux.
834    pub async fn handle_setpriority<G: Guest<Self>>(
835        &self,
836        _guest: &mut G,
837        call: syscalls::Setpriority,
838    ) -> Result<i64, Error> {
839        Ok(setpriority_result(call.which())?)
840    }
841
842    // AUTONOMOUS-BOT-IMPLEMENTED
843    // TODO-HUMAN-REVIEW(#663)
844    /// Reject cross-process memory advice without consulting host process state.
845    pub fn handle_process_madvise(pidfd: usize, flags: usize) -> Result<i64, Error> {
846        if flags != 0 {
847            return Err(Errno::EINVAL.into());
848        }
849
850        // Linux interprets pidfd as an int. Preserve its deterministic invalid-fd
851        // rejection, but never let a valid host pidfd alter another process's memory.
852        if (pidfd as libc::c_int) < 0 {
853            Err(Errno::EBADF.into())
854        } else {
855            Err(Errno::EPERM.into())
856        }
857    }
858
859    // AUTONOMOUS-BOT-IMPLEMENTED
860    // TODO-HUMAN-REVIEW(PR-1096): Review the backend-independent random-device stream.
861    /// Fill guest memory from `stream`, one of the canonical streams that every
862    /// backend's `/dev/random` and `/dev/urandom` virtualization reads.
863    pub(super) fn fill_random_device_bytes<G: Guest<Self>>(
864        &self,
865        guest: &mut G,
866        stream: RandomDeviceStream,
867        remote_buf: AddrMut<u8>,
868        len: usize,
869        stream_offset: u64,
870    ) -> Result<usize, Error> {
871        self.fill_random_device_iovecs(
872            guest,
873            stream,
874            &[crate::iovecs::ImportedIovec {
875                base: remote_buf.as_raw(),
876                len,
877            }],
878            stream_offset,
879        )
880    }
881
882    pub(super) fn fill_random_device_iovecs<G: Guest<Self>>(
883        &self,
884        guest: &mut G,
885        stream: RandomDeviceStream,
886        iovecs: &[crate::iovecs::ImportedIovec],
887        stream_offset: u64,
888    ) -> Result<usize, Error> {
889        let seed = random_device_stream_seed(guest.config().rng_seed(), stream);
890        let mut hasher = DefaultHasher::new();
891        let written = fill_canonical_random_iovecs(
892            &mut guest.memory(),
893            iovecs,
894            seed,
895            stream_offset,
896            &mut hasher,
897        )?;
898        if cfg!(debug_assertions) {
899            detlog!(
900                "[dtid {}] USER RAND [/dev/[u]random] Filled guest memory with {} canonical random bytes of {:?} at offset {}, hash of bytes: {}",
901                guest.thread_state().dettid,
902                written,
903                stream,
904                stream_offset,
905                hasher.finish()
906            );
907        }
908        Ok(written)
909    }
910
911    /// uname syscall
912    pub async fn handle_uname<G: Guest<Self>>(
913        &self,
914        guest: &mut G,
915        call: syscalls::Uname,
916    ) -> Result<i64, Error> {
917        let ret = self.record_or_replay(guest, call).await?;
918        if let Some(buf) = call.buf() {
919            let mut un = guest.memory().read_value(buf)?;
920            let kernel = crate::machine::MachineDescription::of(guest.config()).kernel;
921
922            if !guest.config().has_uts_namespace {
923                // FIXME: It should be possible to remove this once all tests
924                // are also using namespaces.
925                un.nodename = from_str(DEFAULT_HOSTNAME);
926                un.domainname = from_str(DEFAULT_HOSTNAME.split('.').next_back().unwrap_or(""));
927            }
928
929            // The kernel procfs describes too (`procfs::fixed`).
930            un.sysname = from_str(&kernel.sysname);
931            un.release = from_str(&kernel.release);
932            un.version = from_str(&kernel.version());
933            guest.memory().write_value(buf, &un)?;
934        }
935
936        Ok(ret)
937    }
938
939    /// Fill `getrandom(2)` requests from the current thread's seeded deterministic PRNG.
940    /// Supported blocking/source-selection flags share that always-ready stream; invalid Linux
941    /// flag combinations are rejected before guest memory is touched.
942    pub async fn handle_getrandom<G: Guest<Self>>(
943        &self,
944        guest: &mut G,
945        call: syscalls::Getrandom,
946    ) -> Result<i64, Error> {
947        let memory = guest.memory();
948        let dettid = guest.thread_state().dettid;
949        crate::random::getrandom(guest.thread_state_mut().thread_prng(), memory, dettid, call)
950    }
951
952    /// setsid system call, answered as Linux answers it from Detcore's model
953    /// of the guest's sessions and process groups (`ThreadTree`), the same on
954    /// every backend (slice B3a-4c of
955    /// <https://github.com/rrnewton/hermit/issues/4266>). The call then goes to
956    /// the backend too, so that a kernel that keeps process groups (ptrace's)
957    /// stays in step; a backend that keeps none answers ENOSYS (KVM's
958    /// executor), and the model is the guest's answer. Any other disagreement
959    /// is refused by name, never answered with the backend's errno. A caller
960    /// in a nested PID namespace, whose ids are not the model's, is answered
961    /// by the kernel and mirrored, as before.
962    // TODO-HUMAN-REVIEW(PR-b3a4c): Review setsid from the model.
963    pub async fn handle_setsid<G: Guest<Self>>(
964        &self,
965        guest: &mut G,
966        call: syscalls::Setsid,
967    ) -> Result<i64, Error> {
968        let process = guest.thread_state().detpid.expect("detpid unset");
969        let res = if guest.thread_state().nested_pid_namespace {
970            let res = guest.inject(call).await?;
971            let _ = create_session(guest, process).await;
972            res
973        } else {
974            crate::tool_global::setsid_decision(guest, process).await?;
975            self.mirror_group_change(guest, reverie::syscalls::Syscall::Setsid(call))
976                .await?;
977            let _ = create_session(guest, process).await;
978            i64::from(process.as_raw())
979        };
980
981        // task is trying to become a daemon process. for more details
982        // see: https://notes.shichao.io/apue/ch13/
983        if guest.config().kill_daemons {
984            guest.daemonize().await;
985        }
986        Ok(res)
987    }
988
989    /// `getpgrp`, `getpgid` and `getsid`, answered from Detcore's model of the
990    /// guest's process groups and sessions (`ThreadTree`), which `setpgid` and
991    /// `setsid` keep, the same on every backend. The root process's group and
992    /// session come from outside the container's PID namespace and read as 0,
993    /// as on Linux (`scheduler::OUTSIDE_NAMESPACE`), and so do the namespace
994    /// init's, pid 1. A pid that names no task Linux still has (none
995    /// registered, an exited thread, a process already reaped, automatically
996    /// or by the namespace's init) is ESRCH; a negative one too, as Linux
997    /// finds no task for it (`Scheduler::group_or_session_of`). A caller in a
998    /// nested PID namespace, whose ids are not the model's, is answered by the
999    /// kernel as before.
1000    // TODO-HUMAN-REVIEW(PR-b3a4c): Review the group and session model answers
1001    // (slice B3a-4c of https://github.com/rrnewton/hermit/issues/4266).
1002    pub async fn handle_group_or_session_getter<G: Guest<Self>>(
1003        &self,
1004        guest: &mut G,
1005        call: reverie::syscalls::Syscall,
1006    ) -> Result<i64, Error> {
1007        if guest.thread_state().nested_pid_namespace {
1008            return Ok(guest.inject(call).await?);
1009        }
1010        let (sysno, args) = reverie::syscalls::SyscallInfo::into_parts(call);
1011        #[cfg(target_arch = "x86_64")]
1012        let pid = if sysno == Sysno::getpgrp {
1013            0
1014        } else {
1015            args.arg0 as libc::pid_t
1016        };
1017        #[cfg(not(target_arch = "x86_64"))]
1018        let pid = {
1019            let _ = sysno;
1020            args.arg0 as libc::pid_t
1021        };
1022        let task = match pid {
1023            0 => guest.thread_state().detpid.expect("detpid unset"),
1024            pid if pid < 0 => return Err(Errno::ESRCH.into()),
1025            pid => DetPid::from_raw(pid),
1026        };
1027        let answer =
1028            crate::tool_global::group_or_session_of(guest, task, sysno == Sysno::getsid).await;
1029        Ok(i64::from(answer.ok_or(Errno::ESRCH)?.as_raw()))
1030    }
1031
1032    /// setpgid system call, answered as Linux answers it from Detcore's model
1033    /// (`ThreadTree::setpgid_decision`), and mirrored to the backend, as
1034    /// `handle_setsid`. A caller in a nested PID namespace is answered by the
1035    /// kernel and the model mirrors it, as before: the kernel validates, and
1036    /// group-selecting waits read the model rather than host `/proc` state.
1037    // TODO-HUMAN-REVIEW(PR-b3a4c): Review setpgid from the model.
1038    pub async fn handle_setpgid<G: Guest<Self>>(
1039        &self,
1040        guest: &mut G,
1041        call: syscalls::Setpgid,
1042    ) -> Result<i64, Error> {
1043        let caller = guest.thread_state().detpid.expect("detpid unset");
1044        if !guest.thread_state().nested_pid_namespace {
1045            let (process, group) =
1046                crate::tool_global::setpgid_decision(guest, caller, call.pid(), call.pgid())
1047                    .await?;
1048            self.mirror_group_change(guest, reverie::syscalls::Syscall::Setpgid(call))
1049                .await?;
1050            let _ = set_process_group(guest, process, group).await;
1051            return Ok(0);
1052        }
1053        let res = guest.inject(call).await?;
1054        // A nested caller's ids are its own PID namespace's; the kernel took
1055        // them so, and the model records the tasks they name
1056        // (https://github.com/rrnewton/hermit/issues/4155).
1057        let process = if call.pid() == 0 {
1058            caller
1059        } else {
1060            self.task_named_after_success(guest, Sysno::setpgid, call.pid())
1061                .await?
1062        };
1063        let group = if call.pgid() == 0 {
1064            process
1065        } else {
1066            self.task_named_after_success(guest, Sysno::setpgid, call.pgid())
1067                .await?
1068        };
1069        let _ = set_process_group(guest, process, group).await;
1070        Ok(res)
1071    }
1072
1073    /// Give the backend a `setsid` or `setpgid` the model accepted, so that a
1074    /// kernel that keeps process groups stays in step with the model. A
1075    /// backend that keeps none says so with a typed refusal
1076    /// (`UnsupportedOperation::ProcessGroups`), and the model's answer stands.
1077    /// Any other failure, whatever its errno, means the backend's process
1078    /// groups disagree with the model's: refused by name rather than answered
1079    /// with the backend's errno. An errno alone never stands for a refusal: a
1080    /// kernel ENOSYS for another reason would otherwise read as success.
1081    async fn mirror_group_change<G: Guest<Self>>(
1082        &self,
1083        guest: &mut G,
1084        call: reverie::syscalls::Syscall,
1085    ) -> Result<(), Error> {
1086        let result = guest.inject(call).await;
1087        // Ask before anything else is injected: the next injection replaces
1088        // the backend's record.
1089        let refusal = result
1090            .is_err()
1091            .then(|| guest.take_unsupported_refusal())
1092            .flatten();
1093        match result {
1094            Ok(_) => Ok(()),
1095            Err(_)
1096                if refusal.is_some_and(|refusal| {
1097                    refusal.operation == reverie::UnsupportedOperation::ProcessGroups
1098                }) =>
1099            {
1100                Ok(())
1101            }
1102            Err(errno) => {
1103                let sysno = reverie::syscalls::SyscallInfo::number(&call);
1104                tracing::error!(
1105                    "[detcore, dtid {}] {} was accepted by Detcore's process-group model but the backend answered {}: its process groups disagree with the model's (slice B3a-4c of https://github.com/rrnewton/hermit/issues/4266)",
1106                    guest.thread_state().dettid,
1107                    sysno,
1108                    errno,
1109                );
1110                self.refuse_unserviceable_operation(guest, sysno, errno)
1111                    .await
1112                    .map(|_| ())
1113            }
1114        }
1115    }
1116
1117    /// The task a positive id the calling thread passed names, after the
1118    /// kernel accepted it: the id itself at depth 0, translated from the
1119    /// caller's PID namespace below that. An id the record cannot name is
1120    /// refused by name; the kernel found it, so the record is incomplete.
1121    async fn task_named_after_success<G: Guest<Self>>(
1122        &self,
1123        guest: &mut G,
1124        sysno: Sysno,
1125        id: libc::pid_t,
1126    ) -> Result<DetPid, Error> {
1127        if !guest.thread_state().nested_pid_namespace {
1128            return Ok(DetPid::from_raw(id));
1129        }
1130        let reason = match crate::tool_global::guest_pid(guest, id).await {
1131            crate::scheduler::pid_namespaces::GuestPidAnswer::Task { task, .. } => return Ok(task),
1132            crate::scheduler::pid_namespaces::GuestPidAnswer::NoSuchPid => {
1133                format!(
1134                    "the kernel accepted id {id}, which no recorded task of the caller's namespace has"
1135                )
1136            }
1137            crate::scheduler::pid_namespaces::GuestPidAnswer::Untranslatable(reason) => reason,
1138        };
1139        tracing::error!(
1140            "[detcore, dtid {}] {} from a nested PID namespace is not supported: {} (https://github.com/rrnewton/hermit/issues/4155)",
1141            guest.thread_state().dettid,
1142            sysno,
1143            reason,
1144        );
1145        match self
1146            .refuse_unserviceable_operation(guest, sysno, Errno::ENOSYS)
1147            .await
1148        {
1149            Err(error) => Err(error),
1150            Ok(_) => Err(Errno::ENOSYS.into()),
1151        }
1152    }
1153
1154    /// membarrier (system call).
1155    ///
1156    /// `membarrier(2)` issues process-wide memory barriers so that userspace can
1157    /// use asymmetric fences (e.g. CPython's QSBR, RCU-style reclamation).
1158    /// Detcore serializes all guest threads onto a single logical CPU with a
1159    /// total memory order, so any requested barrier is *already* satisfied and
1160    /// every command is a deterministic no-op. For `MEMBARRIER_CMD_QUERY` we
1161    /// report the set of commands we emulate so the guest stays on this
1162    /// controlled path instead of a host-dependent fallback; every other command
1163    /// returns success without doing anything.
1164    pub async fn handle_membarrier<G: Guest<Self>>(
1165        &self,
1166        guest: &mut G,
1167        call: syscalls::Membarrier,
1168    ) -> Result<i64, Error> {
1169        // Values from <linux/membarrier.h>.
1170        const MEMBARRIER_CMD_QUERY: i32 = 0;
1171        const MEMBARRIER_CMD_GLOBAL: i32 = 1 << 0;
1172        const MEMBARRIER_CMD_GLOBAL_EXPEDITED: i32 = 1 << 1;
1173        const MEMBARRIER_CMD_REGISTER_GLOBAL_EXPEDITED: i32 = 1 << 2;
1174        const MEMBARRIER_CMD_PRIVATE_EXPEDITED: i32 = 1 << 3;
1175        const MEMBARRIER_CMD_REGISTER_PRIVATE_EXPEDITED: i32 = 1 << 4;
1176        const SUPPORTED: i32 = MEMBARRIER_CMD_GLOBAL
1177            | MEMBARRIER_CMD_GLOBAL_EXPEDITED
1178            | MEMBARRIER_CMD_REGISTER_GLOBAL_EXPEDITED
1179            | MEMBARRIER_CMD_PRIVATE_EXPEDITED
1180            | MEMBARRIER_CMD_REGISTER_PRIVATE_EXPEDITED;
1181
1182        let cmd = call.cmd();
1183        if cmd == MEMBARRIER_CMD_QUERY {
1184            detlog!(
1185                "[dtid {}] membarrier(QUERY) => reporting emulated commands {:#x}",
1186                guest.thread_state().dettid,
1187                SUPPORTED,
1188            );
1189            Ok(SUPPORTED as i64)
1190        } else {
1191            detlog!(
1192                "[dtid {}] membarrier(cmd={}) no-op (threads are serialized on one CPU)",
1193                guest.thread_state().dettid,
1194                cmd,
1195            );
1196            Ok(0)
1197        }
1198    }
1199
1200    /// getcpu system call: CPU 0 of Hermit's historical single CPU, or under
1201    /// `--virtual-cpus` the thread's virtual CPU (`Detcore::virtual_cpu`).
1202    /// The vDSO's `getcpu` reaches here too: Reverie turns it into this
1203    /// system call.
1204    pub async fn handle_getcpu<G: Guest<Self>>(
1205        &self,
1206        guest: &mut G,
1207        call: syscalls::Getcpu,
1208    ) -> Result<i64, Error> {
1209        let current = match self.cfg.virtual_cpus {
1210            Some(cpus) => self.virtual_cpu(guest, cpus.get()).await,
1211            None => 0,
1212        };
1213        if let Some(cpu) = call.cpu() {
1214            guest.memory().write_value(cpu, &current)?;
1215        }
1216
1217        // Always set the NUMA node to 0.
1218        if let Some(node) = call.node() {
1219            guest.memory().write_value(node, &0)?;
1220        }
1221
1222        Ok(0)
1223    }
1224
1225    /// getresuid under Hermit. Detcore presents a fixed virtual-root identity, so
1226    /// the real, effective, and saved user IDs are all the constant 0. Under the
1227    /// ptrace backend the guest runs inside a CLONE_NEWUSER namespace that already
1228    /// maps the host uid to 0; emulating the same constant here makes in-process
1229    /// backends (DBT) agree with that golden reference instead of leaking the host
1230    /// uid, and the fully emulated result is bitwise-identical across --verify and
1231    /// record/replay.
1232    // AUTONOMOUS-BOT-IMPLEMENTED
1233    // TODO-HUMAN-REVIEW(#1549)
1234    pub async fn handle_getresuid<G: Guest<Self>>(
1235        &self,
1236        guest: &mut G,
1237        call: syscalls::Getresuid,
1238    ) -> Result<i64, Error> {
1239        if let Some(ruid) = call.ruid() {
1240            guest.memory().write_value(ruid, &0)?;
1241        }
1242        if let Some(euid) = call.euid() {
1243            guest.memory().write_value(euid, &0)?;
1244        }
1245        if let Some(suid) = call.suid() {
1246            guest.memory().write_value(suid, &0)?;
1247        }
1248        Ok(0)
1249    }
1250
1251    /// getresgid under Hermit. The group-ID counterpart of `handle_getresuid`:
1252    /// the real, effective, and saved group IDs are all the fixed virtual-root
1253    /// constant 0, matching the ptrace CLONE_NEWUSER identity and deterministic
1254    /// across --verify and record/replay.
1255    // AUTONOMOUS-BOT-IMPLEMENTED
1256    // TODO-HUMAN-REVIEW(#1549)
1257    pub async fn handle_getresgid<G: Guest<Self>>(
1258        &self,
1259        guest: &mut G,
1260        call: syscalls::Getresgid,
1261    ) -> Result<i64, Error> {
1262        if let Some(rgid) = call.rgid() {
1263            guest.memory().write_value(rgid, &0)?;
1264        }
1265        if let Some(egid) = call.egid() {
1266            guest.memory().write_value(egid, &0)?;
1267        }
1268        if let Some(sgid) = call.sgid() {
1269            guest.memory().write_value(sgid, &0)?;
1270        }
1271        Ok(0)
1272    }
1273
1274    /// get_mempolicy under Hermit. The container exposes a single virtual NUMA
1275    /// node, so the effective policy is always the default and every address
1276    /// resolves to node 0. The result is fully emulated (never injected), so it
1277    /// is bitwise-identical across the two --verify runs and under record/replay,
1278    /// removing the host-NUMA-topology dependence a passthrough would introduce.
1279    ///
1280    /// It follows Linux's `kernel_get_mempolicy` and `do_get_mempolicy`
1281    /// (mm/mempolicy.c) for a kernel with one node (`nr_node_ids` 1):
1282    /// - a node mask with `maxnode` below 1 is EINVAL;
1283    /// - an unknown flag (any bit beyond `MPOL_F_NODE`, `MPOL_F_ADDR` and
1284    ///   `MPOL_F_MEMS_ALLOWED`, including those above bit 31 of the raw
1285    ///   argument, which Reverie's typed call drops), `MPOL_F_MEMS_ALLOWED`
1286    ///   with `MPOL_F_NODE` or `MPOL_F_ADDR`, and an address without
1287    ///   `MPOL_F_ADDR` are EINVAL;
1288    /// - `*policy` is 0: `MPOL_DEFAULT`, or node 0 for `MPOL_F_NODE`;
1289    /// - the node mask is written as `copy_nodes_to_user` writes it: node 0
1290    ///   alone for `MPOL_F_MEMS_ALLOWED`, as `/proc/<pid>/status` reports
1291    ///   `Mems_allowed_list` (<https://github.com/rrnewton/hermit/issues/4214>),
1292    ///   and no node for the default policy, in `ALIGN(maxnode - 1, 64) / 8`
1293    ///   bytes (none for a `maxnode` of 1), EINVAL beyond a page;
1294    /// - both are written with the guest's own access rights, so a read-only or
1295    ///   unmapped buffer is EFAULT, as from `put_user` and `copy_to_user`.
1296    ///
1297    /// Limits: `set_mempolicy` and `mbind` succeed without recording a policy,
1298    /// so this always reports the default policy, where Linux reports, say,
1299    /// `MPOL_INTERLEAVE` after `set_mempolicy(MPOL_INTERLEAVE)`. For the same
1300    /// reason `MPOL_F_NODE` without `MPOL_F_ADDR` answers node 0, as Linux does
1301    /// for an interleave policy (libnuma's `numa_get_interleave_node`), where
1302    /// it refuses it under the default policy. `MPOL_F_ADDR` does not check
1303    /// that the address is mapped, where Linux answers EFAULT for an unmapped
1304    /// one.
1305    // AUTONOMOUS-BOT-IMPLEMENTED
1306    // TODO-HUMAN-REVIEW(#720)
1307    pub async fn handle_get_mempolicy<G: Guest<Self>>(
1308        &self,
1309        guest: &mut G,
1310        call: syscalls::GetMempolicy,
1311    ) -> Result<i64, Error> {
1312        const MPOL_F_NODE: u64 = 1;
1313        const MPOL_F_ADDR: u64 = 2;
1314        const MPOL_F_MEMS_ALLOWED: u64 = 4;
1315        // `copy_nodes_to_user` refuses a mask longer than a page.
1316        let page = crate::page::page_size();
1317        let mask = call.nodemask();
1318        if mask.is_some() && call.maxnode() < 1 {
1319            return Err(Errno::EINVAL.into());
1320        }
1321        // The flags argument is an unsigned long; Reverie decodes it as u32.
1322        let flags = crate::arch::syscall_arguments(&guest.regs().await)[4];
1323        if flags & !(MPOL_F_NODE | MPOL_F_ADDR | MPOL_F_MEMS_ALLOWED) != 0 {
1324            return Err(Errno::EINVAL.into());
1325        }
1326        let mems_allowed = flags & MPOL_F_MEMS_ALLOWED != 0;
1327        if mems_allowed {
1328            if flags & (MPOL_F_NODE | MPOL_F_ADDR) != 0 {
1329                return Err(Errno::EINVAL.into());
1330            }
1331        } else if flags & MPOL_F_ADDR == 0 && call.addr().is_some() {
1332            return Err(Errno::EINVAL.into());
1333        }
1334        if let Some(policy) = call.policy() {
1335            let policy = AddrMut::<u8>::from_raw(policy.as_raw()).ok_or(Errno::EFAULT)?;
1336            write_get_mempolicy_output(&mut guest.memory(), policy, &0i32.to_ne_bytes())?;
1337        }
1338        if let Some(mask) = mask {
1339            let bytes = (call.maxnode() - 1).div_ceil(64) * 8;
1340            if bytes > page.max(8) {
1341                return Err(Errno::EINVAL.into());
1342            }
1343            let mut words = vec![0u8; bytes as usize];
1344            if mems_allowed && !words.is_empty() {
1345                words[0] = 1;
1346            }
1347            if !words.is_empty() {
1348                let mask = AddrMut::<u8>::from_raw(mask.as_raw()).ok_or(Errno::EFAULT)?;
1349                write_get_mempolicy_output(&mut guest.memory(), mask, &words)?;
1350            }
1351        }
1352        Ok(0)
1353    }
1354
1355    /// move_pages under Hermit. On a single virtual NUMA node nothing can be
1356    /// relocated, so report every page as residing on node 0 and succeed. The
1357    /// answer is a fixed constant, so it is deterministic across --verify and
1358    /// record/replay.
1359    // AUTONOMOUS-BOT-IMPLEMENTED
1360    // TODO-HUMAN-REVIEW(#720)
1361    pub async fn handle_move_pages<G: Guest<Self>>(
1362        &self,
1363        guest: &mut G,
1364        call: syscalls::MovePages,
1365    ) -> Result<i64, Error> {
1366        // When a status buffer is supplied (either a real move request or a
1367        // location query with nodes == NULL), report node 0 for every page.
1368        if let Some(status) = call.status() {
1369            let count = call.nr_pages() as usize;
1370            let zeros = vec![0i32; count];
1371            guest.memory().write_values(status, &zeros)?;
1372        }
1373        Ok(0)
1374    }
1375}
1376
1377#[cfg(test)]
1378mod tests {
1379    /// PR_SET_NAME's name: the caller's string through its first NUL, at
1380    /// most 15 bytes, read exactly: a string that ends just before an
1381    /// unmapped page is read whole, and one whose bytes run into it is not.
1382    #[test]
1383    fn prctl_set_name_reads_exactly_up_to_15_bytes_and_the_first_nul() {
1384        let memory = reverie::syscalls::LocalMemory::new();
1385        let name = |address: usize| {
1386            super::prctl_task_name(&memory, address as libc::c_ulong)
1387                .map(|comm| comm.as_bytes().to_vec())
1388        };
1389        let text = |bytes: &[u8]| name(bytes.as_ptr() as usize);
1390        assert_eq!(text(b"worker\0junk"), Some(b"worker".to_vec()));
1391        assert_eq!(
1392            text(b"0123456789abcdefghij\0"),
1393            Some(b"0123456789abcde".to_vec())
1394        );
1395        assert_eq!(text(b"\0"), Some(Vec::new()));
1396        assert_eq!(text(b"\xff\n\\L\0"), Some(b"\xff\n\\L".to_vec()));
1397        // Two pages, the second made unreadable: "x\0" in the last two bytes
1398        // of the first page is a whole name; "xy" there runs into the
1399        // unreadable page.
1400        const PAGE: usize = 4096;
1401        // SAFETY: a private anonymous mapping this test owns and unmaps.
1402        let base = unsafe {
1403            libc::mmap(
1404                std::ptr::null_mut(),
1405                2 * PAGE,
1406                libc::PROT_READ | libc::PROT_WRITE,
1407                libc::MAP_PRIVATE | libc::MAP_ANONYMOUS,
1408                -1,
1409                0,
1410            )
1411        };
1412        assert_ne!(base, libc::MAP_FAILED);
1413        let base = base as usize;
1414        // SAFETY: inside the first, writable page; then the second page is
1415        // made unreadable.
1416        unsafe {
1417            *((base + PAGE - 2) as *mut u8) = b'x';
1418            *((base + PAGE - 1) as *mut u8) = 0;
1419            assert_eq!(
1420                libc::mprotect((base + PAGE) as *mut _, PAGE, libc::PROT_NONE),
1421                0
1422            );
1423        }
1424        assert_eq!(name(base + PAGE - 2), Some(b"x".to_vec()));
1425        // SAFETY: the first page is still writable.
1426        unsafe { *((base + PAGE - 1) as *mut u8) = b'y' };
1427        assert_eq!(name(base + PAGE - 2), None);
1428        // SAFETY: the mapping this test made.
1429        assert_eq!(unsafe { libc::munmap(base as *mut _, 2 * PAGE) }, 0);
1430    }
1431
1432    use super::*;
1433
1434    /// D28a: on an in-guest run's default route Detcore holds 0 and every
1435    /// signal it sends itself, as on ptrace; SIGKILL keeps the kernel's path;
1436    /// SIGSTOP, SIGALRM and the realtime signals are refused; a value above 64
1437    /// is the kernel's EINVAL. Where Detcore delivers them (ptrace), nothing
1438    /// is refused. Where it does neither (every other backend), everything is
1439    /// the kernel's.
1440    #[test]
1441    fn parent_death_signals_are_held_refused_or_the_kernels() {
1442        use ParentDeathSignalRoute::*;
1443        for signal in 0..=66_u64 {
1444            let held = signal == 0 || detcore_sends_parent_death_signal(signal);
1445            let in_guest = parent_death_signal_route(signal, true, true);
1446            let ptrace = parent_death_signal_route(signal, true, false);
1447            let other = parent_death_signal_route(signal, false, false);
1448            assert_eq!(other, Kernel, "{signal}");
1449            assert_eq!(ptrace, if held { Hold } else { Kernel }, "{signal}");
1450            let expected = if held {
1451                Hold
1452            } else if signal == libc::SIGKILL as u64 || signal > 64 {
1453                Kernel
1454            } else {
1455                Refuse
1456            };
1457            assert_eq!(in_guest, expected, "{signal}");
1458        }
1459        // The values the brief names.
1460        for (signal, route) in [
1461            (libc::SIGUSR1, Hold),
1462            (libc::SIGUSR2, Hold),
1463            (0, Hold),
1464            (libc::SIGKILL, Kernel),
1465            (libc::SIGSTOP, Refuse),
1466            (libc::SIGALRM, Refuse),
1467            (32, Refuse),
1468            (64, Refuse),
1469            (65, Kernel),
1470        ] {
1471            assert_eq!(
1472                parent_death_signal_route(signal as u64, true, true),
1473                route,
1474                "{signal}"
1475            );
1476        }
1477    }
1478
1479    /// Detcore holds parent-death signals where it delivers them (ptrace,
1480    /// sequentialized) and on an in-guest run's default route, and only
1481    /// delivers them on ptrace.
1482    #[test]
1483    fn holding_a_parent_death_signal_is_split_from_delivering_it() {
1484        let ptrace = crate::config::Config {
1485            sequentialize_threads: true,
1486            backend: BackendCapabilities::PTRACE,
1487            ..Default::default()
1488        };
1489        assert!(delivers_parent_death_signal(&ptrace));
1490        assert!(holds_parent_death_signal(&ptrace));
1491        let unsequentialized = crate::config::Config {
1492            sequentialize_threads: false,
1493            ..ptrace.clone()
1494        };
1495        assert!(!delivers_parent_death_signal(&unsequentialized));
1496        assert!(!holds_parent_death_signal(&unsequentialized));
1497        let in_guest = crate::config::Config {
1498            sequentialize_threads: true,
1499            backend: BackendCapabilities::LITEINST_IN_GUEST,
1500            in_guest_refuses_host_timed_producers: true,
1501            ..Default::default()
1502        };
1503        assert!(!delivers_parent_death_signal(&in_guest));
1504        assert!(holds_parent_death_signal(&in_guest));
1505        let off = crate::config::Config {
1506            in_guest_refuses_host_timed_producers: false,
1507            ..in_guest
1508        };
1509        assert!(!holds_parent_death_signal(&off));
1510    }
1511
1512    mod group_mirror {
1513        use std::sync::Mutex;
1514
1515        use reverie::GlobalRPC;
1516        use reverie::GlobalTool;
1517        use reverie::Tid;
1518        use reverie::Tool;
1519        use reverie::syscalls::Addr;
1520        use reverie::syscalls::AddrMut;
1521        use reverie::syscalls::SyscallInfo;
1522
1523        use super::*;
1524        use crate::config::Config;
1525        use crate::tool_global::GlobalState;
1526
1527        /// A guest whose backend answers the mirrored `setsid` with `answer`,
1528        /// recording `refusal` as its typed refusal when the answer fails.
1529        struct MirrorGuest<'a> {
1530            config: &'a Config,
1531            global: &'a GlobalState,
1532            thread: crate::ThreadState<()>,
1533            sender: Tid,
1534            answer: Result<i64, Errno>,
1535            refusal: Option<reverie::UnsupportedOperation>,
1536            recorded: Option<reverie::UnsupportedRefusal>,
1537            injections: Mutex<usize>,
1538            /// The one call this guest expects to be injected.
1539            expected: Sysno,
1540        }
1541
1542        struct NoStack;
1543        struct NoStackGuard;
1544        impl Drop for NoStackGuard {
1545            fn drop(&mut self) {}
1546        }
1547        impl reverie::Stack for NoStack {
1548            type StackGuard = NoStackGuard;
1549            fn size(&self) -> usize {
1550                panic!("a mirror must not use the guest stack")
1551            }
1552            fn capacity(&self) -> usize {
1553                panic!("a mirror must not use the guest stack")
1554            }
1555            fn push<'stack, T>(&mut self, _: T) -> Addr<'stack, T> {
1556                panic!("a mirror must not use the guest stack")
1557            }
1558            fn reserve<'stack, T>(&mut self) -> AddrMut<'stack, T> {
1559                panic!("a mirror must not use the guest stack")
1560            }
1561            fn commit(self) -> Result<Self::StackGuard, Errno> {
1562                panic!("a mirror must not use the guest stack")
1563            }
1564        }
1565
1566        #[reverie::tool]
1567        impl GlobalRPC<GlobalState> for MirrorGuest<'_> {
1568            async fn send_rpc(
1569                &self,
1570                message: <GlobalState as GlobalTool>::Request,
1571            ) -> <GlobalState as GlobalTool>::Response {
1572                self.global.receive_rpc(self.sender, message).await
1573            }
1574            fn config(&self) -> &Config {
1575                self.config
1576            }
1577        }
1578
1579        #[reverie::tool]
1580        impl Guest<Detcore> for MirrorGuest<'_> {
1581            type Memory = reverie::syscalls::LocalMemory;
1582            type Stack = NoStack;
1583
1584            fn tid(&self) -> Tid {
1585                self.sender
1586            }
1587            fn pid(&self) -> Tid {
1588                self.sender
1589            }
1590            fn ppid(&self) -> Option<Tid> {
1591                None
1592            }
1593            fn memory(&self) -> Self::Memory {
1594                reverie::syscalls::LocalMemory::new()
1595            }
1596            fn thread_state(&self) -> &crate::ThreadState<()> {
1597                &self.thread
1598            }
1599            fn thread_state_mut(&mut self) -> &mut crate::ThreadState<()> {
1600                &mut self.thread
1601            }
1602            async fn regs(&mut self) -> libc::user_regs_struct {
1603                panic!("a mirror must not read registers")
1604            }
1605            async fn stack(&mut self) -> Self::Stack {
1606                panic!("a mirror must not use a guest stack")
1607            }
1608            async fn daemonize(&mut self) {
1609                panic!("a mirror must not daemonize")
1610            }
1611            async fn inject<S: SyscallInfo>(&mut self, call: S) -> Result<i64, Errno> {
1612                assert_eq!(call.number(), self.expected);
1613                *self.injections.lock().unwrap() += 1;
1614                if let Err(errno) = self.answer {
1615                    self.recorded = self.refusal.map(|operation| {
1616                        reverie::UnsupportedRefusal::new(operation, errno, "test backend")
1617                    });
1618                }
1619                self.answer
1620            }
1621            fn take_unsupported_refusal(&mut self) -> Option<reverie::UnsupportedRefusal> {
1622                self.recorded.take()
1623            }
1624            async fn tail_inject<S: SyscallInfo>(&mut self, _: S) -> reverie::Never {
1625                panic!("a mirror must not retire the guest")
1626            }
1627            fn set_timer(&mut self, _: reverie::TimerSchedule) -> Result<(), Error> {
1628                panic!("a mirror must not set a timer")
1629            }
1630            fn set_timer_precise(&mut self, _: reverie::TimerSchedule) -> Result<(), Error> {
1631                panic!("a mirror must not set a timer")
1632            }
1633            fn read_clock(&mut self) -> Result<u64, Error> {
1634                panic!("a mirror must not read the clock")
1635            }
1636        }
1637
1638        /// The negative control for the mirror of a group change the model
1639        /// accepted: the backend's success, or its typed statement that it
1640        /// keeps no process groups, lets the model's answer stand; any other
1641        /// failure, a bare ENOSYS (as a guest's seccomp filter could give)
1642        /// or an errno the model did not predict, refuses the run by name in
1643        /// a fail-closed run, never passing the errno on.
1644        #[tokio::test]
1645        async fn a_failed_mirror_stands_only_with_the_typed_process_group_refusal() {
1646            use reverie::UnsupportedOperation::Flock;
1647            use reverie::UnsupportedOperation::ProcessGroups;
1648            let process = DetPid::from_raw(23);
1649            for (answer, refusal, accepted) in [
1650                (Ok(23), None, true),
1651                (Err(Errno::ENOSYS), Some(ProcessGroups), true),
1652                (Err(Errno::ENOSYS), None, false),
1653                (Err(Errno::EPERM), None, false),
1654                (Err(Errno::ENOSYS), Some(Flock), false),
1655            ] {
1656                let config = Config {
1657                    panic_on_unsupported_syscalls: true,
1658                    exit_on_unsupported_syscall: true,
1659                    shutdown_on_unsupported_syscall: false,
1660                    ..Config::default()
1661                };
1662                let global = GlobalState::init_global_state(&config).await;
1663                let tool = Detcore::new(Tid::from_raw(process.as_raw()), &config);
1664                let mut thread = crate::ThreadState::new(process, &config, ());
1665                thread.detpid = Some(process);
1666                let mut guest = MirrorGuest {
1667                    config: &config,
1668                    global: &global,
1669                    thread,
1670                    sender: Tid::from_raw(process.as_raw()),
1671                    answer,
1672                    refusal,
1673                    recorded: None,
1674                    injections: Mutex::new(0),
1675                    expected: Sysno::setsid,
1676                };
1677                let result = tool
1678                    .mirror_group_change(
1679                        &mut guest,
1680                        reverie::syscalls::Syscall::Setsid(reverie::syscalls::Setsid::new()),
1681                    )
1682                    .await;
1683                match result {
1684                    Ok(()) if accepted => {}
1685                    Err(Error::Tool(error)) if !accepted => assert_eq!(
1686                        error
1687                            .downcast_ref::<crate::UnsupportedSyscallError>()
1688                            .unwrap()
1689                            .0,
1690                        Sysno::setsid,
1691                        "{answer:?} {refusal:?}"
1692                    ),
1693                    result => panic!("wrong mirror policy for {answer:?} {refusal:?}: {result:?}"),
1694                }
1695                assert_eq!(*guest.injections.lock().unwrap(), 1);
1696                assert_eq!(guest.recorded, None, "a reported refusal must be taken");
1697            }
1698        }
1699
1700        /// The tripwire for the calls KVM leaves to the Tool, on the path of
1701        /// the clone3 race (https://github.com/rrnewton/hermit/issues/4531):
1702        /// a getpgrp from a nested PID namespace, the one Detcore handler
1703        /// that passes one of them through to the backend, comes back with
1704        /// the backend's `ToolDecided` refusal, and the tripwire then panics
1705        /// with its internal-failure marker. The test calls the handler and
1706        /// then the tripwire directly; it does not cover the tripwire's call
1707        /// site in `handle_syscall_event`.
1708        #[tokio::test]
1709        #[should_panic(expected = "HERMIT_INTERNAL_FAILURE class=tool-decided-injection")]
1710        async fn an_injected_tool_decided_call_ends_the_run() {
1711            let process = DetPid::from_raw(23);
1712            let config = Config::default();
1713            let global = GlobalState::init_global_state(&config).await;
1714            let tool = Detcore::new(Tid::from_raw(process.as_raw()), &config);
1715            let mut thread = crate::ThreadState::new(process, &config, ());
1716            thread.detpid = Some(process);
1717            thread.nested_pid_namespace = true;
1718            let mut guest = MirrorGuest {
1719                config: &config,
1720                global: &global,
1721                thread,
1722                sender: Tid::from_raw(process.as_raw()),
1723                answer: Err(Errno::ENOSYS),
1724                refusal: Some(reverie::UnsupportedOperation::ToolDecided),
1725                recorded: None,
1726                injections: Mutex::new(0),
1727                expected: Sysno::getpgrp,
1728            };
1729            let result = tool
1730                .handle_group_or_session_getter(
1731                    &mut guest,
1732                    reverie::syscalls::Syscall::Getpgrp(reverie::syscalls::Getpgrp::new()),
1733                )
1734                .await;
1735            assert_eq!(*guest.injections.lock().unwrap(), 1);
1736            assert!(
1737                matches!(result, Err(Error::Errno(Errno::ENOSYS))),
1738                "{result:?}"
1739            );
1740            crate::syscalls::helpers::fail_on_tool_decided_injection::<
1741                crate::record_or_replay::NoopTool,
1742                _,
1743            >(&mut guest, Sysno::getpgrp);
1744        }
1745    }
1746
1747    struct ScatterMemory {
1748        bytes: Vec<u8>,
1749        outcomes: std::collections::VecDeque<Result<usize, Errno>>,
1750        writes: usize,
1751    }
1752
1753    impl ScatterMemory {
1754        fn new(size: usize, outcomes: impl IntoIterator<Item = Result<usize, Errno>>) -> Self {
1755            Self {
1756                bytes: vec![0xa5; size],
1757                outcomes: outcomes.into_iter().collect(),
1758                writes: 0,
1759            }
1760        }
1761    }
1762
1763    impl MemoryAccess for ScatterMemory {
1764        fn read_vectored(
1765            &self,
1766            _: &[std::io::IoSlice],
1767            _: &mut [std::io::IoSliceMut],
1768        ) -> Result<usize, Errno> {
1769            panic!("canonical scatter must not read guest bytes")
1770        }
1771        fn write_vectored(
1772            &mut self,
1773            _: &[std::io::IoSlice],
1774            _: &mut [std::io::IoSliceMut],
1775        ) -> Result<usize, Errno> {
1776            panic!("canonical scatter must use the user-access capability")
1777        }
1778        fn write_with_user_access(
1779            &mut self,
1780            address: AddrMut<u8>,
1781            bytes: &[u8],
1782        ) -> Result<usize, Errno> {
1783            self.writes += 1;
1784            let count = self.outcomes.pop_front().unwrap_or(Ok(bytes.len()))?;
1785            assert!(count <= bytes.len());
1786            let offset = address.as_raw() - 0x1000;
1787            self.bytes[offset..offset + count].copy_from_slice(&bytes[..count]);
1788            Ok(count)
1789        }
1790    }
1791
1792    fn assert_copy_failure(error: Error, expected: Errno) {
1793        let Error::Tool(error) = error else {
1794            panic!("copy failure became a guest errno: {error:?}")
1795        };
1796        assert_eq!(
1797            error
1798                .downcast_ref::<crate::random::RandomCopyFailure>()
1799                .expect("typed copy failure")
1800                .errno(),
1801            expected
1802        );
1803    }
1804
1805    fn scatter(memory: &mut ScatterMemory, lengths: &[usize], offset: u64) -> Result<usize, Error> {
1806        let mut base = 0x1000;
1807        let vectors: Vec<_> = lengths
1808            .iter()
1809            .map(|&len| {
1810                let vector = crate::iovecs::ImportedIovec { base, len };
1811                base += len;
1812                vector
1813            })
1814            .collect();
1815        fill_canonical_random_iovecs(memory, &vectors, 0, offset, &mut DefaultHasher::new())
1816    }
1817
1818    #[test]
1819    fn get_mempolicy_output_faults_are_efault_and_backend_errors_are_tool_errors() {
1820        let address = AddrMut::<u8>::from_raw(0x1000).unwrap();
1821        let write = |outcome| {
1822            write_get_mempolicy_output(&mut ScatterMemory::new(8, [outcome]), address, &[1; 8])
1823        };
1824        assert!(write(Ok(8)).is_ok());
1825        for fault in [Ok(0), Ok(3), Err(Errno::EFAULT)] {
1826            assert!(
1827                matches!(write(fault), Err(Error::Errno(Errno::EFAULT))),
1828                "{fault:?}"
1829            );
1830        }
1831        for errno in [Errno::ENOSYS, Errno::ESRCH, Errno::EIO] {
1832            assert!(
1833                matches!(write(Err(errno)), Err(Error::Tool(_))),
1834                "{errno:?}"
1835            );
1836        }
1837    }
1838
1839    #[test]
1840    fn random_scatter_distinguishes_fault_prefixes_from_backend_errors() {
1841        // Exercise both an earlier complete iovec and an earlier complete chunk.
1842        for lengths in [vec![3, 5], vec![RANDOM_FILL_CHUNK_BYTES + 5]] {
1843            let prefix = if lengths.len() == 2 {
1844                3
1845            } else {
1846                RANDOM_FILL_CHUNK_BYTES
1847            };
1848            for error in [Errno::EFAULT, Errno::EIO, Errno::ENOMEM] {
1849                let mut memory = ScatterMemory::new(prefix + 5, [Ok(prefix), Err(error)]);
1850                let result = scatter(&mut memory, &lengths, 7);
1851                match error {
1852                    Errno::EFAULT => assert_eq!(result.unwrap(), prefix),
1853                    _ => assert_copy_failure(result.unwrap_err(), error),
1854                }
1855                let expected: Vec<_> = (7..7 + prefix as u64)
1856                    .map(|index| canonical_random_device_byte(0, index))
1857                    .collect();
1858                assert_eq!(&memory.bytes[..prefix], expected);
1859                assert_eq!(&memory.bytes[prefix..], &[0xa5; 5]);
1860                assert_eq!(memory.writes, 2);
1861            }
1862        }
1863    }
1864
1865    #[test]
1866    fn random_scatter_short_or_zero_copy_stops_without_touching_later_segments() {
1867        for first in [0, 2] {
1868            let mut memory = ScatterMemory::new(10, [Ok(first)]);
1869            let result = scatter(&mut memory, &[5, 5], 0);
1870            if first == 0 {
1871                assert!(matches!(result, Err(Error::Errno(Errno::EFAULT))));
1872            } else {
1873                assert_eq!(result.unwrap(), first);
1874                assert_eq!(&memory.bytes[..2], &[41, 114]);
1875            }
1876            assert!(memory.bytes[first..].iter().all(|&byte| byte == 0xa5));
1877            assert_eq!(memory.writes, 1);
1878        }
1879        let mut memory = ScatterMemory::new(10, [Ok(5), Ok(0)]);
1880        assert_eq!(scatter(&mut memory, &[5, 5], 0).unwrap(), 5);
1881        assert!(memory.bytes[5..].iter().all(|&byte| byte == 0xa5));
1882        assert_eq!(memory.writes, 2);
1883    }
1884
1885    #[test]
1886    fn random_scatter_saturated_bytes_match_partitioned_calls() {
1887        for (offset, expected) in [(u64::MAX - 2, [78, 151, 224, 224]), (u64::MAX, [224; 4])] {
1888            let mut single = ScatterMemory::new(4, []);
1889            assert_eq!(scatter(&mut single, &[4], offset).unwrap(), 4);
1890            let mut partitioned = ScatterMemory::new(4, []);
1891            let mut hash = DefaultHasher::new();
1892            assert_eq!(
1893                fill_canonical_random_iovecs(
1894                    &mut partitioned,
1895                    &[crate::iovecs::ImportedIovec {
1896                        base: 0x1000,
1897                        len: 1
1898                    }],
1899                    0,
1900                    offset,
1901                    &mut hash
1902                )
1903                .unwrap(),
1904                1
1905            );
1906            assert_eq!(
1907                fill_canonical_random_iovecs(
1908                    &mut partitioned,
1909                    &[crate::iovecs::ImportedIovec {
1910                        base: 0x1001,
1911                        len: 3
1912                    }],
1913                    0,
1914                    offset.saturating_add(1),
1915                    &mut hash
1916                )
1917                .unwrap(),
1918                3
1919            );
1920            assert_eq!(single.bytes, expected);
1921            assert_eq!(partitioned.bytes, expected);
1922        }
1923    }
1924
1925    #[test]
1926    fn random_scatter_backend_error_rolls_back_shared_cursor_after_eight_byte_prefix() {
1927        let fd = crate::fd::DetFd::new(
1928            3,
1929            nix::fcntl::OFlag::O_RDONLY,
1930            crate::fd::FdType::Rng,
1931            crate::types::OpenFileId::new(crate::types::DetTid::from_raw(1), 0),
1932        );
1933        for error in [Errno::EFAULT, Errno::EIO] {
1934            let mut memory = ScatterMemory::new(8, [Ok(4), Err(error)]);
1935            let result = fd.with_random_device_stream(|offset| scatter(&mut memory, &[8], offset));
1936            if error == Errno::EFAULT {
1937                assert_eq!(result.unwrap(), 4);
1938            } else {
1939                assert_copy_failure(result.unwrap_err(), Errno::EIO);
1940            }
1941            // First iteration commits four; the EIO iteration must not commit
1942            // its physically copied prefix or fabricate a successful result.
1943            assert_eq!(fd.random_device_offset(), 4);
1944            assert_eq!(&memory.bytes[4..], &[0xa5; 4]);
1945            assert_eq!(memory.writes, 2);
1946        }
1947    }
1948
1949    #[test]
1950    fn prctl_support_covers_deterministic_controls() {
1951        for option in [
1952            libc::PR_SET_NAME,
1953            libc::PR_GET_NAME,
1954            libc::PR_SET_THP_DISABLE,
1955            libc::PR_GET_THP_DISABLE,
1956            // Deterministic per-process dumpability state.
1957            libc::PR_SET_DUMPABLE,
1958            libc::PR_GET_DUMPABLE,
1959            // Deterministic per-thread capability-retention flag used by setpriv.
1960            libc::PR_SET_KEEPCAPS,
1961            libc::PR_GET_KEEPCAPS,
1962            // Deterministic per-thread parent-death-signal flag used by setpriv.
1963            libc::PR_SET_PDEATHSIG,
1964            libc::PR_GET_PDEATHSIG,
1965        ] {
1966            assert!(is_supported_prctl_option(option));
1967        }
1968
1969        assert!(!is_supported_prctl_option(libc::PR_SET_NO_NEW_PRIVS));
1970        // Detcore's serialized-execution contract
1971        // (`Tool::enforces_serialized_execution`) rests on refusing it.
1972        assert!(!is_supported_prctl_option(libc::PR_SET_SECCOMP));
1973        assert!(!is_supported_prctl_option(libc::PR_SET_TIMERSLACK));
1974        assert!(!is_supported_prctl_option(libc::PR_GET_TIMERSLACK));
1975    }
1976
1977    /// The subreaper prctls are admitted only where Detcore's record of
1978    /// children decides waits and the kernel has re-parented by the time an
1979    /// exit reaches Detcore: ptrace, and e9patch preprocessing under ptrace,
1980    /// with threads sequentialized. Without sequentialization no exit reaches
1981    /// the scheduler, nothing re-parents, and a subreaper's wait would see
1982    /// ECHILD while its orphan lives, so the call keeps failing with ENOSYS.
1983    #[test]
1984    fn child_subreaper_prctls_are_admitted_only_where_detcore_models_them() {
1985        for backend in [BackendCapabilities::PTRACE, BackendCapabilities::E9PATCH] {
1986            assert!(models_child_subreapers(true, &backend), "{backend:?}");
1987            assert!(!models_child_subreapers(false, &backend), "{backend:?}");
1988        }
1989        for backend in [
1990            BackendCapabilities::LITEINST_IN_GUEST,
1991            BackendCapabilities::SABRE,
1992            BackendCapabilities::DBT,
1993            BackendCapabilities::KVM,
1994        ] {
1995            assert!(!models_child_subreapers(true, &backend), "{backend:?}");
1996            assert!(!models_child_subreapers(false, &backend), "{backend:?}");
1997        }
1998        assert!(!is_supported_prctl_option(libc::PR_SET_CHILD_SUBREAPER));
1999        assert!(!is_supported_prctl_option(libc::PR_GET_CHILD_SUBREAPER));
2000    }
2001
2002    #[test]
2003    fn capability_set_prctls_are_supported_on_every_backend() {
2004        for option in [libc::PR_CAPBSET_DROP, libc::PR_CAP_AMBIENT] {
2005            assert!(is_supported_prctl_option(option));
2006        }
2007        assert!(!is_supported_prctl_option(libc::PR_SET_SECUREBITS));
2008    }
2009
2010    /// PR_SET_PTRACER reaches the kernel, which answers as Linux does: 0 with
2011    /// Yama, EINVAL without it. Before, Detcore answered ENOSYS, which
2012    /// bionic's crash handler takes as fatal.
2013    #[test]
2014    fn set_ptracer_reaches_the_kernel() {
2015        assert_eq!(libc::PR_SET_PTRACER, 0x5961_6d61);
2016        assert!(is_supported_prctl_option(libc::PR_SET_PTRACER));
2017    }
2018
2019    #[test]
2020    fn getrandom_accepts_linux_flags() {
2021        for flags in [
2022            0,
2023            libc::GRND_NONBLOCK as usize,
2024            libc::GRND_RANDOM as usize,
2025            (libc::GRND_NONBLOCK | libc::GRND_RANDOM) as usize,
2026            libc::GRND_INSECURE as usize,
2027            (libc::GRND_NONBLOCK | libc::GRND_INSECURE) as usize,
2028            1_usize << 32,
2029        ] {
2030            assert!(
2031                validate_getrandom_flags(flags).is_ok(),
2032                "valid flags rejected: {flags:#x}"
2033            );
2034        }
2035    }
2036
2037    #[test]
2038    fn getrandom_rejects_invalid_flags() {
2039        for flags in [
2040            0x8000_0000,
2041            (1_usize << 32) | 0x8000_0000,
2042            (libc::GRND_RANDOM | libc::GRND_INSECURE) as usize,
2043        ] {
2044            assert_eq!(validate_getrandom_flags(flags), Err(Errno::EINVAL));
2045        }
2046    }
2047
2048    #[test]
2049    fn getrandom_caps_requests_at_linux_max_rw_count() {
2050        assert_eq!(getrandom_request_len(16), 16);
2051        assert_eq!(
2052            getrandom_request_len(usize::MAX),
2053            crate::page::max_rw_count()
2054        );
2055    }
2056
2057    #[test]
2058    fn canonical_random_device_stream_matches_kvm_root_contract() {
2059        let first: Vec<_> = (0..8)
2060            .map(|index| canonical_random_device_byte(0, index))
2061            .collect();
2062        assert_eq!(first, [41, 114, 187, 4, 77, 150, 223, 40]);
2063
2064        let continued: Vec<_> = (8..16)
2065            .map(|index| canonical_random_device_byte(0, index))
2066            .collect();
2067        assert_eq!(continued, [113, 186, 3, 76, 149, 222, 39, 112]);
2068
2069        let seeded: Vec<_> = (0..16)
2070            .map(|index| canonical_random_device_byte(17, index))
2071            .collect();
2072        assert_eq!(
2073            seeded,
2074            [
2075                56, 114, 187, 4, 77, 150, 223, 40, 96, 186, 3, 76, 149, 222, 39, 112
2076            ]
2077        );
2078        assert_ne!(seeded, [first, continued].concat());
2079    }
2080
2081    /// Distinct random-device open files read distinct bytes, and each one's
2082    /// bytes depend only on the run seed and its name. The pinned seeds catch
2083    /// an accidental change to the derivation, which would change every
2084    /// guest's random-device bytes and stop older recordings replaying.
2085    #[test]
2086    fn random_device_streams_are_distinct_per_open_file_and_pinned() {
2087        let named = |creator, sequence| RandomDeviceStream {
2088            creator: DetPid::from_raw(creator),
2089            sequence,
2090        };
2091        assert_eq!(
2092            random_device_stream_seed(0, named(3, 0)),
2093            0x0581_cd8e_1945_b98c
2094        );
2095        assert_eq!(
2096            random_device_stream_seed(0, named(3, 1)),
2097            0xbd1c_08a3_03e1_67c7
2098        );
2099        assert_eq!(
2100            random_device_stream_seed(17, named(3, 0)),
2101            0x149b_5f6d_a3c3_0dbc
2102        );
2103        let first_bytes = |run_seed, stream| -> Vec<u8> {
2104            let seed = random_device_stream_seed(run_seed, stream);
2105            (0..8)
2106                .map(|index| canonical_random_device_byte(seed, index))
2107                .collect()
2108        };
2109        assert_eq!(
2110            first_bytes(0, named(3, 0)),
2111            [165, 203, 254, 29, 195, 91, 94, 45]
2112        );
2113        let mut seen = std::collections::BTreeSet::new();
2114        for run_seed in [0, 1, 17] {
2115            for creator in [3, 5, 7] {
2116                for sequence in 0..4 {
2117                    assert!(
2118                        seen.insert(first_bytes(run_seed, named(creator, sequence))),
2119                        "seed {run_seed}, creator {creator}, sequence {sequence}"
2120                    );
2121                }
2122            }
2123        }
2124    }
2125
2126    #[test]
2127    fn getpriority_reports_default_nice_for_every_target() {
2128        // Every valid PRIO_* selector reports the default nice (raw 20 = nice 0),
2129        // regardless of `who`. Real tools such as `renice -p <pid>` pass an
2130        // explicit pid, and getpriority never checks permissions, so this must
2131        // never be EPERM (the pre-fix stub only accepted PRIO_PROCESS/who==0).
2132        for which in [libc::PRIO_PROCESS, libc::PRIO_PGRP, libc::PRIO_USER] {
2133            assert_eq!(getpriority_result(which as i32), Ok(20));
2134        }
2135    }
2136
2137    #[test]
2138    fn setpriority_accepts_any_change_for_valid_target() {
2139        // Nice is inert under Hermit, so any priority change for a valid target
2140        // succeeds as a no-op — including nonzero nice (`nice -n 5`, os.nice).
2141        for which in [libc::PRIO_PROCESS, libc::PRIO_PGRP, libc::PRIO_USER] {
2142            assert_eq!(setpriority_result(which as i32), Ok(0));
2143        }
2144    }
2145
2146    #[test]
2147    fn get_and_set_priority_reject_unknown_which_with_einval() {
2148        // Match Linux: an unknown target selector faults with EINVAL, not EPERM.
2149        for which in [-1, 3, 42] {
2150            assert_eq!(getpriority_result(which), Err(Errno::EINVAL));
2151            assert_eq!(setpriority_result(which), Err(Errno::EINVAL));
2152        }
2153    }
2154
2155    #[test]
2156    fn seccomp_tsync_null_probe_matches_linux_validation() {
2157        assert_eq!(
2158            seccomp_result(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_TSYNC, false,),
2159            Err(Errno::EFAULT)
2160        );
2161        assert_eq!(
2162            seccomp_result(SECCOMP_SET_MODE_FILTER, 1 << 31, false),
2163            Err(Errno::EINVAL)
2164        );
2165        assert_eq!(
2166            seccomp_result(SECCOMP_SET_MODE_FILTER, 0, true),
2167            Err(Errno::EOPNOTSUPP)
2168        );
2169    }
2170
2171    #[test]
2172    fn process_madvise_is_rejected_deterministically() {
2173        assert!(matches!(
2174            Detcore::<crate::record_or_replay::NoopTool>::handle_process_madvise(
2175                (-10_000_i32) as usize,
2176                0
2177            ),
2178            Err(Error::Errno(Errno::EBADF))
2179        ));
2180        assert!(matches!(
2181            Detcore::<crate::record_or_replay::NoopTool>::handle_process_madvise(3, 1),
2182            Err(Error::Errno(Errno::EINVAL))
2183        ));
2184        assert!(matches!(
2185            Detcore::<crate::record_or_replay::NoopTool>::handle_process_madvise(3, 0),
2186            Err(Error::Errno(Errno::EPERM))
2187        ));
2188    }
2189
2190    #[test]
2191    fn syslog_exposes_an_empty_log_and_validates_actions() {
2192        for action in 0..=7 {
2193            assert_eq!(deterministic_syslog_result(action, 0), Ok(0));
2194        }
2195        for action in [9, 10] {
2196            assert_eq!(deterministic_syslog_result(action, 0), Ok(0));
2197        }
2198        assert_eq!(deterministic_syslog_result(8, 1), Ok(0));
2199        assert_eq!(deterministic_syslog_result(8, 8), Ok(0));
2200        assert_eq!(deterministic_syslog_result(8, 0), Err(Errno::EINVAL));
2201        assert_eq!(deterministic_syslog_result(8, 9), Err(Errno::EINVAL));
2202        assert_eq!(deterministic_syslog_result(11, 0), Err(Errno::EINVAL));
2203    }
2204}