Skip to main content

detcore/
lib.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//! Detcore is a Reverie tool that determinizes the execution of a process.
10//!
11//! # Backend-abstraction commandment
12//!
13//! Detcore is a *tool* written against Reverie's **abstract** instrumentation
14//! interface (the `reverie` crate). It depends only on those traits and types
15//! and is deliberately ignorant of how a guest is actually traced.
16//!
17//! Detcore MUST NEVER depend on or import a concrete Reverie backend or support
18//! crate -- any `reverie-*` crate other than the abstract `reverie-core`
19//! interface. Choosing and instantiating a backend, and running a detcore tool
20//! against it, is the sole responsibility of the `hermit-cli` package. There
21//! are no backend-specific hacks in detcore: any tracing-mechanism-specific
22//! behavior belongs behind the Reverie abstraction, not here.
23//!
24//! Why: Hermit follows Reverie's abstract model. A backend dependency in
25//! detcore would couple the determinism engine to one tracing mechanism and
26//! break the clean abstraction boundary that lets the same tool run over any
27//! backend.
28//!
29//! The one allowed exception is test-only: detcore's own integration tests
30//! (under `detcore/tests/`, wired via the `reverie-ptrace` **dev-dependency**)
31//! drive a real tracer to exercise the tool. That coupling never reaches the
32//! shipped library. This invariant is enforced in CI by
33//! `scripts/check-detcore-backend-abstraction.sh`.
34
35#![deny(clippy::all)]
36#![deny(missing_docs)]
37#![allow(clippy::uninlined_format_args)]
38
39pub mod access_trace;
40mod arch;
41mod config;
42mod consts;
43mod cpu_clock;
44mod cpu_set;
45mod cpuid;
46mod digest;
47mod dirents;
48#[cfg(feature = "dispatch-observer")]
49pub mod dispatch_observer;
50/// Schedule-alignment and edit-distance algorithms shared by Hermit tools.
51#[allow(missing_docs)]
52pub mod edit_distance;
53#[cfg(test)]
54mod edited_replay_hook_tests;
55pub mod exec_record;
56pub mod exit_dependencies;
57mod fatal_core;
58mod fd;
59mod fifo_peer;
60mod finalization;
61pub mod guest_log;
62mod host_inputs;
63pub mod host_timed_producers;
64mod ignored_signals;
65pub mod in_guest_launch;
66pub mod in_guest_site_patching;
67mod io_buffers;
68mod iovecs;
69#[allow(unused)]
70mod ivar;
71pub mod logdiff;
72pub mod machine;
73mod memory;
74pub mod native_exec;
75pub mod netlink_route;
76pub mod nonwaiting_write;
77mod page;
78mod pipe_wake;
79#[cfg(test)]
80mod predicted_death_tests;
81mod procfs;
82mod procfs_inode;
83mod procmaps;
84pub mod race_probe;
85pub mod random;
86mod record_or_replay;
87pub mod refused_signal_controls;
88mod reserved_input;
89mod resources;
90mod scheduler;
91mod shared_open_files;
92mod sigalrm_phase1;
93#[cfg(test)]
94mod signal_announcement_tests;
95pub mod signal_restart;
96mod signal_state;
97pub mod slice_boundary;
98mod sock_diag;
99mod stat;
100mod store_exclusive;
101mod syscall_classification;
102mod syscall_histogram;
103#[cfg(test)]
104mod syscall_hot_path_tests;
105mod syscall_time;
106mod syscalls;
107#[cfg(test)]
108mod test_pages;
109mod tool_global;
110#[cfg(feature = "physical-mm-test-support")]
111pub use tool_global::initial_mm_probe;
112
113/// Keep each traced thread's `/proc/<pid>/task/<tid>/status` open in this
114/// process and re-read it with pread(2), instead of opening it for every
115/// signal-state read. Call it only from a process that never shares a guest's
116/// descriptor table: Hermit's own binary, at startup. Detcore linked into a
117/// guest (the in-guest runtimes) must never call it, or the kept descriptors
118/// would be the guest's.
119pub fn keep_host_status_files_open() {
120    crate::syscalls::keep_status_files_open();
121}
122
123/// Whether a run on the backend whose capabilities are `backend` needs the
124/// placeholder directory hermit-cli mounts for Detcore
125/// (`Config::directory_placeholder`): where the guest's procfs is not the
126/// kernel's, as one capability says (`procfs_inode::tool_can_name_guest_descriptors`).
127/// Where it is the kernel's, Detcore's served `/proc` directories are open on
128/// that procfs's root, and hermit-cli mounts nothing for them.
129pub fn needs_directory_placeholder(backend: &reverie::BackendCapabilities) -> bool {
130    !procfs_inode::tool_can_name_guest_descriptors(backend)
131}
132mod tool_local;
133pub mod util;
134mod wait_signal_end;
135
136pub mod detlog;
137pub mod preemptions;
138pub mod types;
139use std::fs::File;
140use std::io::Write;
141use std::os::unix::io::RawFd;
142use std::sync::Arc;
143use std::sync::Mutex;
144use std::time::Duration;
145
146pub use config::BlockingMode;
147pub use config::CONFIG_FINGERPRINT_ENV;
148pub use config::Config;
149pub use config::FatalCoreCapture;
150pub use config::InheritedSeccomp;
151pub use config::LITEINST_CONFIG_FINGERPRINT_ENV;
152pub use config::LaunchImage;
153pub use config::RunsPostFork;
154pub use config::SchedHeuristic;
155pub use config::config_fingerprint_mismatch;
156pub use config::config_wire_fingerprint;
157pub use config::decode_backend_config_json;
158pub use config::encode_backend_config_json;
159pub use config::from_legacy_backend_json;
160pub use config::set_kernel_signal_facts;
161pub use config::to_legacy_backend_json;
162// AUTONOMOUS-BOT-IMPLEMENTED
163// TODO-HUMAN-REVIEW(PR-1120): Review the public canonical Detcore root identity.
164pub use consts::ROOT_DETPID;
165pub use detcore_model::pid::GuestIds;
166pub use digest::Digest;
167pub use finalization::FinalizationFailure;
168pub use finalization::PriorityHistoryRefusal;
169pub use finalization::ReplayDepartureError;
170pub use finalization::TerminalReplayRefusal;
171pub use finalization::combine_finalization;
172#[cfg(test)]
173use rand::RngExt as _;
174#[cfg(target_arch = "x86_64")]
175use raw_cpuid::CpuIdResult;
176#[cfg(target_arch = "x86_64")]
177use raw_cpuid::cpuid;
178pub use record_or_replay::ChosenBinaryInputRefusal;
179pub use record_or_replay::RecordOrReplay;
180pub use record_or_replay::admit_chosen_input_stream;
181pub use record_or_replay::require_chosen_input_stream;
182use reverie::Error;
183use reverie::ExitStatus;
184use reverie::GlobalRPC;
185use reverie::Guest;
186use reverie::Pid;
187#[cfg(target_arch = "x86_64")]
188use reverie::Rdtsc;
189#[cfg(target_arch = "x86_64")]
190use reverie::RdtscResult;
191use reverie::RegDisplay;
192use reverie::Signal;
193use reverie::Subscription;
194use reverie::Tid;
195use reverie::TimerSchedule;
196use reverie::Tool;
197pub use reverie::process::Namespace;
198use reverie::syscalls::CloneFlags;
199use reverie::syscalls::Displayable;
200#[cfg(target_arch = "x86_64")]
201use reverie::syscalls::EpollCreate1;
202use reverie::syscalls::Errno;
203#[cfg(target_arch = "x86_64")]
204use reverie::syscalls::InotifyInit1;
205use reverie::syscalls::MemoryAccess;
206use reverie::syscalls::Syscall;
207use reverie::syscalls::SyscallInfo;
208use reverie::syscalls::Sysno;
209pub use scheduler::Priority;
210pub use scheduler::ReplayRefusalExitHook;
211pub use scheduler::TerminalDeadlockHook;
212pub use scheduler::TerminalOutcome;
213pub use scheduler::TerminalOutcomeUnwind;
214pub use scheduler::TerminalOutcomeUnwindGuard;
215pub use scheduler::VirtualTimeoutExitHook;
216pub use scheduler::runqueue::DEFAULT_PRIORITY;
217pub use scheduler::runqueue::FIRST_PRIORITY;
218pub use scheduler::runqueue::LAST_PRIORITY;
219pub use scheduler::set_replay_refusal_exit_hook;
220pub use scheduler::set_terminal_deadlock_hook;
221pub use scheduler::set_virtual_timeout_exit_hook;
222pub use syscalls::chosen_ids::holder_of as chosen_id_holder;
223pub use tool_global::BackendFailureCleanup;
224pub use tool_global::GlobalState;
225pub use tool_global::SyscallObservationGap;
226use tool_global::ThreadDeregistration;
227use tool_global::acknowledge_robust_list_exit_time;
228pub use tool_global::cancel_exec;
229use tool_global::create_child_thread;
230use tool_global::create_vfork_child_thread;
231use tool_global::deregister_thread;
232pub use tool_global::format_unsupported_syscall_warning;
233pub use tool_global::native_exec as native_exec_control;
234pub use tool_global::prepare_exec;
235use tool_global::report_unsupported_syscall;
236use tool_global::robust_list_wakes_after_exec;
237use tool_global::robust_list_wakes_after_exit;
238pub use tool_global::shared_open_file_reply;
239pub use tool_global::shared_open_file_request;
240pub use tool_local::initial_resource_limit;
241
242fn select_thread_exit_detpid(
243    thread_detpid: Option<DetPid>,
244    process_detpid: DetPid,
245) -> (DetPid, bool) {
246    match thread_detpid {
247        Some(detpid) => (detpid, false),
248        None => (process_detpid, true),
249    }
250}
251
252fn select_thread_start_detpid(thread_detpid: Option<DetPid>, guest_pid: Pid) -> DetPid {
253    thread_detpid.unwrap_or_else(|| DetPid::from_raw(guest_pid.into()))
254}
255
256/// What a stop of thread `tid` of `pid` did with a copy of the standard
257/// `signal` (`kept`: resumed into the thread, or else dropped), for the
258/// scheduler's record of copies Linux discarded when they were sent
259/// (`scheduler::discarded_at_send`, <https://github.com/rrnewton/hermit/issues/4293>).
260/// Reads, after the stop, the thread's queues and its mask, which decides
261/// whether a kept copy is requeued at the resume (`ptrace_signal`). `None`
262/// for a signal the record does not track, or a state that cannot be read,
263/// which changes nothing.
264fn consumed_signal_control(
265    pid: Pid,
266    tid: Pid,
267    signal: i32,
268    kept: bool,
269) -> Option<scheduler::discarded_at_send::DiscardedSignalControl> {
270    use scheduler::discarded_at_send::Consumption;
271    use scheduler::discarded_at_send::DiscardedSignalControl;
272    if !(1..32).contains(&signal) {
273        return None;
274    }
275    let state = syscalls::read_kernel_signal_state(pid, tid).ok()?;
276    let bit = scheduler::kernel_signal_bit(signal);
277    let how = if kept {
278        Consumption::KeptResume {
279            blocked: state.blocked & bit != 0,
280        }
281    } else {
282        Consumption::Dropped
283    };
284    Some(DiscardedSignalControl::Consumed {
285        signal,
286        how,
287        thread_holds: state.thread_pending & bit != 0,
288        process_holds: state.shared_pending & bit != 0,
289    })
290}
291
292fn thread_start_needs_registration(
293    is_root_process: bool,
294    past_first_execve: bool,
295    dettid: DetTid,
296    detpid: DetPid,
297) -> bool {
298    dettid == detpid && (is_root_process || !past_first_execve)
299}
300
301// AUTONOMOUS-BOT-IMPLEMENTED
302// TODO-HUMAN-REVIEW(PR-644): Review the typed fail-closed backend signal.
303/// Identifies an unsupported syscall that a backend must terminate without unwinding.
304#[derive(Debug)]
305pub struct UnsupportedSyscallError(pub Sysno);
306
307impl std::fmt::Display for UnsupportedSyscallError {
308    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
309        write!(formatter, "unsupported syscall: {:?}", self.0)
310    }
311}
312
313impl std::error::Error for UnsupportedSyscallError {}
314pub use fd::OpenFileLease;
315pub use fd::OpenFileModel;
316pub use fd::SharedOpenFileChannel;
317pub use fd::SharedOpenFileError;
318pub use fd::install_shared_open_file_channel;
319pub use shared_open_files::ChunkedOpenFileChannel;
320pub use shared_open_files::OpenFileControl;
321pub use shared_open_files::OpenFileControlReply;
322pub use shared_open_files::OpenFileControlTransport;
323pub use tool_local::Detcore;
324pub use tool_local::FileMetadata;
325/// Returns whether the audited runtime policy classifies `sysno` as unsupported.
326// AUTONOMOUS-BOT-IMPLEMENTED
327// TODO-HUMAN-REVIEW(PR-644): Review the copied-DBT-child classification surface.
328pub fn is_unsupported_syscall(sysno: Sysno) -> bool {
329    matches!(
330        syscall_classification::classify_syscall(sysno),
331        syscall_classification::SyscallClassification::Unsupported
332    )
333}
334
335/// Every syscall in the pinned x86_64 table, including the final entry.
336///
337/// Backends that sweep the classification table must use this rather than
338/// `Sysno::iter()`, which stops one short and silently drops the last row.
339pub fn all_pinned_syscalls() -> impl Iterator<Item = Sysno> {
340    syscall_classification::all_pinned_syscalls()
341}
342
343/// Returns whether the audited runtime policy classifies `sysno` as
344/// `Determinized` — that is, Detcore either models the syscall with a handler
345/// or applies an explicit deterministic refusal policy to it.
346///
347/// This is the complement of the refusal boundary below. A backend that
348/// executes guest syscalls outside `handle_syscall_event` needs BOTH: the
349/// refusal set tells it which syscalls to answer with a fixed errno, and this
350/// predicate tells it which syscalls Detcore claims to determinize at all.
351/// Running a `Determinized` syscall natively is a determinism hole even when it
352/// is not in the refusal set, because the modelling that makes it deterministic
353/// lives in a handler the backend never entered.
354pub fn is_determinized_syscall(sysno: Sysno) -> bool {
355    matches!(
356        syscall_classification::classify_syscall(sysno),
357        syscall_classification::SyscallClassification::Determinized
358    )
359}
360
361/// Returns whether `sysno` is a kernel-keyring syscall (`add_key`,
362/// `request_key`, `keyctl`) that Detcore hides behind a deterministic
363/// `CONFIG_KEYS`-absent boundary under the default fail-closed policy.
364// AUTONOMOUS-BOT-IMPLEMENTED
365// TODO-HUMAN-REVIEW(PR-916): Exposed so the copied-DBT-child policy can preserve
366// the same keyring isolation boundary that the reclassification (PR-848) moved
367// out of the Unsupported set.
368pub fn is_kernel_keyring_syscall(sysno: Sysno) -> bool {
369    syscall_classification::is_kernel_keyring_syscall(sysno)
370}
371
372/// Returns whether Detcore deterministically refuses `sysno` with a fixed
373/// errno when the fail-closed policy is active, without consulting the host.
374///
375/// A backend that lets a guest execute a syscall outside Detcore's
376/// `handle_syscall_event` dispatcher consults this to enforce the same fixed
377/// refusal the ptrace path enforces. The only production consumer is the DBT
378/// copied pre-exec child policy in `detcore-dbt`, which runs without a Detcore
379/// tool; the KVM backend, like ptrace, delivers these syscalls to
380/// `handle_syscall_event`. It deliberately excludes emulated / no-op /
381/// host-forwarding families (credential no-ops, `timer_create`, AF_UNIX
382/// autobind), because fail-closing a copied child for those would diverge from
383/// the ptrace path rather than match it.
384// AUTONOMOUS-BOT-IMPLEMENTED
385// TODO-HUMAN-REVIEW(PR-978): Review the copied-DBT-child deterministic-refusal surface.
386pub fn is_deterministically_refused_syscall(sysno: Sysno) -> bool {
387    syscall_classification::is_deterministically_refused_syscall(sysno)
388}
389
390/// Returns whether `sysno` is refused by the default fail-closed policy but
391/// forwarded under the explicit compatibility opt-out. The legacy
392/// `strict_only` name is retained for API compatibility.
393pub fn is_strict_only_deterministic_refusal_syscall(sysno: Sysno) -> bool {
394    syscall_classification::is_strict_only_deterministic_refusal_syscall(sysno)
395}
396
397use tool_local::PosixTimers;
398use tool_local::ProcessCpuTime;
399pub use tool_local::ThreadState;
400pub use tool_local::ThreadStats;
401pub use tool_local::thread_rng_from_parent;
402use tracing::debug;
403use tracing::error;
404use tracing::info;
405use tracing::trace;
406use tracing::warn;
407pub use types::DetTid;
408use types::*;
409pub use util::punch_out_print;
410
411use crate::resources::Permission;
412use crate::resources::ResourceID;
413use crate::scheduler::itimer_coalesce::GuestSigalrmTarget;
414use crate::syscall_classification::SyscallClassification;
415use crate::syscall_classification::classify_syscall;
416use crate::syscall_classification::is_credential_identity_noop_syscall;
417use crate::syscall_classification::is_futex2_enosys_syscall;
418use crate::syscall_classification::is_host_kernel_probe_syscall;
419use crate::syscall_classification::is_host_security_identity_probe_syscall;
420use crate::syscall_classification::is_landlock_sandbox_syscall;
421use crate::syscall_classification::is_mount_introspection_enosys_syscall;
422use crate::syscall_classification::is_mount_ns_admin_refused_syscall;
423use crate::syscall_classification::is_optional_memory_feature_syscall;
424use crate::syscall_classification::is_ownership_change_noop_syscall;
425use crate::syscall_classification::is_perf_event_enosys_syscall;
426use crate::syscall_classification::is_privileged_admin_refused_syscall;
427use crate::syscall_classification::is_privileged_observation_refused_syscall;
428use crate::syscall_classification::is_process_isolation_refused_syscall;
429use crate::syscall_classification::is_remap_file_pages_enosys_syscall;
430use crate::syscall_classification::is_unimplemented_enosys_syscall;
431use crate::syscall_classification::is_unsupported_async_ipc_syscall;
432use crate::syscall_classification::is_zero_copy_pipe_syscall;
433use crate::syscalls::helpers::with_guest_rip;
434use crate::syscalls::helpers::with_guest_time;
435#[cfg(target_arch = "x86_64")]
436use crate::syscalls::time::guest_clock_time;
437use crate::tool_global::resource_request;
438use crate::tool_global::trace_schedevent;
439use crate::tool_global::trace_schedevent_with_access;
440use crate::tool_global::unrecoverable_shutdown;
441use crate::types::SigWrapper;
442
443#[macro_use]
444extern crate bitflags;
445
446#[cold]
447fn report_rcb_overshoot(
448    panic_on_rcb_overshoot: bool,
449    clock_value: u64,
450    delta_rcbs: u64,
451    last_timer: u64,
452) {
453    let message = format!(
454        "{} prehook: PMU RCB overshoot! Clock_value: {}. Stepped forward {} RCBs, but should have trapped at {}",
455        reverie::SKID_OVERSHOOT_MARKER,
456        clock_value,
457        delta_rcbs,
458        last_timer
459    );
460    if panic_on_rcb_overshoot {
461        panic!("{}", message);
462    }
463    reverie::record_skid_overshoot();
464    error!("{}", message);
465}
466
467fn rcb_timer_overshot(delta_rcbs: u64, last_timer: u64) -> bool {
468    delta_rcbs > last_timer
469}
470
471/// The branches from `now` to the point the last handler armed the thread's
472/// timer for: `armed` branches past `committed`, that handler's clock value
473/// (`keep_timer_across_dropped_signal`). At least one: a stop at or past the
474/// point fires the event at once, where the backend records any overshoot.
475/// Timeslice ends processed without a stop (`Detcore::catch_up_lone_slice_ends`),
476/// counted for the run summary (`PreemptionSummary::stopless_slice_ends`).
477/// Process-wide: a run reports its growth since it started, so two runs in
478/// one process at the same time would each count both.
479pub(crate) static STOPLESS_SLICE_ENDS: std::sync::atomic::AtomicU64 =
480    std::sync::atomic::AtomicU64::new(0);
481
482/// How far ahead, in retired conditional branches, the backstop timer of a
483/// thread running alone fires at the earliest (`arm_lone_backstop`): 20 ms of
484/// virtual time at the default clock multiplier. A stop this rare costs
485/// nothing next to the slice ends it replaces at small `--max-timeslice`
486/// values, and it bounds how long a host input waits for a scheduler turn.
487const LONE_BACKSTOP_RCBS: u64 = 2_000_000;
488
489/// Arm the timer of a thread running alone: an imprecise one, at least
490/// [`LONE_BACKSTOP_RCBS`] ahead and never before the pending slice end
491/// (`rcbs_to_end` away), so it adds no stop a stop at every end would not.
492/// It ends no slice. Its event processes the slice ends the thread ran past
493/// (`Detcore::catch_up_lone_slice_ends`) and arms the next timer, so a long
494/// stretch without a syscall still enters Detcore now and then. Where it
495/// lands does not matter: each end is processed at its own branch count.
496fn arm_lone_backstop<G, T>(guest: &mut G, rcbs_to_end: u64)
497where
498    G: Guest<Detcore<T>>,
499    T: RecordOrReplay,
500{
501    guest
502        .set_timer(TimerSchedule::Rcbs(LONE_BACKSTOP_RCBS.max(rcbs_to_end)))
503        .expect("Failed to set timer");
504}
505
506/// Stop skipping the pending slice end of a thread that no longer runs
507/// alone (a reply without `GlobalResponse::WithLoneGrant`): arm the precise
508/// timer for it, as the post-hook arms it for a thread that never ran alone.
509/// The counter has not passed it: every callback first processes the ends
510/// the thread ran past.
511pub(crate) fn stop_skipping_lone_slice_end<G, T>(guest: &mut G)
512where
513    G: Guest<Detcore<T>>,
514    T: RecordOrReplay,
515{
516    let Some(end) = guest.thread_state_mut().lone_slice_end.take() else {
517        return;
518    };
519    let now = guest.read_clock().expect("Couldn't read clock");
520    assert!(
521        end > now,
522        "a slice end at branch {end} that the counter ({now}) passed was not processed"
523    );
524    {
525        let thread_state = guest.thread_state_mut();
526        let committed = thread_state.committed_clock_value;
527        thread_state.last_rcb_timer = Some(end - committed);
528        thread_state.last_rcb_timer_is_max = true;
529    }
530    guest
531        .set_timer_precise(TimerSchedule::Rcbs(end - now))
532        .expect("Failed to set timer");
533}
534
535fn kept_timer_rcbs(committed: u64, armed: u64, now: u64) -> u64 {
536    committed.saturating_add(armed).saturating_sub(now).max(1)
537}
538
539fn choose_rcb_timer(
540    max_rcbs_remaining: u64,
541    current_rcbs: u64,
542    next_interrupt: Option<u64>,
543) -> (u64, bool) {
544    if let Some(next_interrupt) = next_interrupt {
545        let interrupt_rcbs = next_interrupt - current_rcbs;
546        if interrupt_rcbs < max_rcbs_remaining {
547            return (interrupt_rcbs, false);
548        }
549    }
550    (max_rcbs_remaining, true)
551}
552
553/// The actual callback check-in at which a slice ends, carried into
554/// `end_timeslice_with_checkpoint` so that priority-history replay of a
555/// callback-annotated record (`Config::annotated_priority_replay`) decides
556/// each row that check-in installs from the same callback and site, with
557/// freshly observed progress, rather than from the legacy RCB expiry.
558#[derive(Clone, Copy)]
559struct PriorityCheckin<'a> {
560    token: Option<&'a preemptions::callback_history::Token>,
561    site: preemptions::callback_history::Site,
562}
563
564/// Why this configuration cannot keep the two-phase delivery protocol
565/// (`Tool::announce_signal_event`), if it cannot: a part of the commit would
566/// file a scheduler request, or would read what only exists after the
567/// kernel's dequeue (the thread's queues, or the delivery's siginfo).
568///
569/// Public because the host's in-guest delivery gate requires `None` on the
570/// final prepared configuration (`Config::in_guest_signal_delivery`, D35 of
571/// the A5 design), and must ask Detcore rather than restate the rows.
572pub fn two_phase_refusal(cfg: &Config) -> Option<&'static str> {
573    if scheduler::discarded_at_send::records(cfg) {
574        return Some(
575            "the record of copies Linux discarded at send reads the thread's queues after the dequeue",
576        );
577    }
578    if cfg.backend.reports_kernel_child_exit_signals {
579        return Some("a child-exit SIGCHLD is told apart from the scheduler's copy by its siginfo");
580    }
581    if cfg.sequentialize_threads && cfg.backend.requires_thread_directed_process_signals {
582        return Some("a fatal delivery files its thread group's exit");
583    }
584    if cfg.target_timeslice.or(cfg.max_timeslice).is_some() {
585        return Some("the post-handler hook can end the timeslice");
586    }
587    if cfg.observes_schedevents() {
588        return Some(
589            "a recorded or replayed schedule traces the delivery, and a replay can hand off",
590        );
591    }
592    if cfg.pipe_wake_latency_in_effect().is_some() {
593        return Some("the post-handler hook can hand the pipe off");
594    }
595    None
596}
597
598/// A5 step 3c.1e A, IR-15: whether delivering `signal` takes its default
599/// action (neither a handler nor `SIG_IGN`), as Detcore's copy of the
600/// dispositions holds them or else as the kernel reports them. A thread whose
601/// state cannot be read counts as taking the default action, so the IR-15
602/// guard fails closed.
603fn delivery_takes_default_action<G, T>(guest: &G, signal: i32) -> bool
604where
605    G: Guest<Detcore<T>>,
606    T: RecordOrReplay,
607{
608    let bit = crate::syscalls::kernel_sigset_bit(signal);
609    match crate::syscalls::signal_shadow::copied_dispositions_for(guest, signal) {
610        Some(copied) => (copied.caught | copied.ignored) & bit == 0,
611        None => crate::syscalls::read_kernel_signal_state(guest.pid(), guest.tid())
612            .map_or(true, |state| (state.caught | state.ignored) & bit == 0),
613    }
614}
615
616/// Whether delivering `signal` now runs a handler the guest installed, as
617/// Detcore's copy of the dispositions holds them or else as the kernel
618/// reports them (`ThreadState::delivered_since_request`). A thread whose state
619/// cannot be read counts as running one, which keeps the continuation;
620/// `MAX_SIGRETURN_CONTINUATIONS` still bounds it.
621fn delivery_runs_handler<G, T>(guest: &G, signal: i32) -> bool
622where
623    G: Guest<Detcore<T>>,
624    T: RecordOrReplay,
625{
626    let bit = crate::syscalls::kernel_sigset_bit(signal);
627    match crate::syscalls::signal_shadow::copied_dispositions_for(guest, signal) {
628        Some(copied) => copied.caught & bit != 0,
629        None => crate::syscalls::read_kernel_signal_state(guest.pid(), guest.tid())
630            .map_or(true, |state| state.caught & bit != 0),
631    }
632}
633
634/// The installed disposition observed before submitting the signal event.
635/// A replay handoff can let another thread change it before kernel delivery.
636/// This metadata never determines the signal's scheduling or continuation.
637fn traced_disposition<G, T>(
638    guest: &G,
639    signal: i32,
640) -> detcore_model::access_trace::ObservedSignalDisposition
641where
642    G: Guest<Detcore<T>>,
643    T: RecordOrReplay,
644{
645    let dispositions = match crate::syscalls::signal_shadow::copied_dispositions_for(guest, signal)
646    {
647        Some(copied) => Some((copied.ignored, copied.caught)),
648        None => crate::syscalls::read_kernel_signal_state(guest.pid(), guest.tid())
649            .ok()
650            .map(|state| (state.ignored, state.caught)),
651    };
652    classify_traced_disposition(signal, dispositions)
653}
654
655/// Classify the disposition sample used by the signal trace payload.
656fn classify_traced_disposition(
657    signal: i32,
658    dispositions: Option<(u64, u64)>,
659) -> detcore_model::access_trace::ObservedSignalDisposition {
660    use detcore_model::access_trace::ObservedSignalDisposition;
661
662    let Some((ignored, caught)) = dispositions else {
663        return ObservedSignalDisposition::Unknown;
664    };
665    let bit = crate::syscalls::kernel_sigset_bit(signal);
666    if caught & bit != 0 {
667        ObservedSignalDisposition::Handler
668    } else if ignored & bit != 0 {
669        ObservedSignalDisposition::Ignored
670    } else {
671        ObservedSignalDisposition::Default
672    }
673}
674
675/// Whether a delivery's turn settles what only a delivery justifies
676/// (`Detcore::inbound_turn`).
677#[derive(Clone, Copy, Debug, PartialEq, Eq)]
678enum TurnScope {
679    /// A delivery the kernel already dequeued: everything (ptrace's stop).
680    Delivery,
681    /// An announcement made before the kernel's dequeue
682    /// (`Tool::announce_signal_event`): the turn alone; its commit settles
683    /// the rest.
684    Announcement,
685}
686
687/// What returned control to Detcore, as far as ending a timeslice is concerned.
688#[derive(Clone, Copy, Debug, PartialEq, Eq)]
689enum CheckIn {
690    /// A syscall, signal, timer, exec or thread start: any expired slice ends.
691    Event,
692    /// A trapped `rdtsc` or `cpuid` in the middle of user code: see
693    /// [`ThreadState::instruction_trap_ends_timeslice`]. Only x86 traps them.
694    #[cfg(target_arch = "x86_64")]
695    InstructionTrap,
696}
697
698/// The feature words the x86-64-v3 table advertises, as `(leaf, subleaf,
699/// register, bits)` with registers 0-3 for EAX-EDX, for the CLI's check that
700/// the host has every one of them natively
701/// (<https://github.com/rrnewton/hermit/issues/4243>). Leaf 0xD, which comes
702/// from the host, is not among them.
703pub fn v3_feature_words() -> Vec<(u32, u32, usize, u32)> {
704    cpuid::v3_feature_words()
705}
706
707/// Why a CPU profile configuration cannot run at all, or `None`. Host mode
708/// requires CPUID and time virtualization; Detcore answers a v3
709/// guest from the qualified host's facts, so a configuration without them is
710/// refused rather than given a guessed leaf 0xD. Whether those facts came from
711/// a real qualification is Hermit's to check, at its launch functions
712/// (`cpu_profile_admission::refuse_unadmitted_launch`), before any backend
713/// starts (<https://github.com/rrnewton/hermit/issues/4243>).
714pub fn cpu_profile_admission_refusal(cfg: &Config) -> Option<&'static str> {
715    use detcore_model::cpu_profile::CpuProfile;
716    match cfg.cpu_profile {
717        CpuProfile::X86_64V3 if cfg.cpu_profile_host.is_none() => {
718            Some("the configuration carries no qualified host facts")
719        }
720        CpuProfile::HostNoRdrand if !cfg.virtualize_cpuid => {
721            Some("CPUID interception is required; remove --no-virtualize-cpuid")
722        }
723        CpuProfile::HostNoRdrand if !cfg.virtualize_time => {
724            Some("virtual time is required for RDTSC/RDTSCP; remove --no-virtualize-time")
725        }
726        CpuProfile::HostNoRdrand if cfg.cpu_profile_source.is_none() => {
727            Some("the configuration carries no admitted host CPUID source CPU")
728        }
729        _ => None,
730    }
731}
732
733/// The digest of the CPUID table and bound host xstate facts `cfg` selects,
734/// which a recording carries so that replay can refuse a different table
735/// (<https://github.com/rrnewton/hermit/issues/4243>).
736pub fn virtual_cpuid_digest(cfg: &Config) -> [u8; 32] {
737    cpuid::InterceptedCpuid::for_config(cfg).digest(cfg.cpu_profile_host.as_ref())
738}
739
740/// Whether Detcore under `cfg` reads the backend's reports of the guest's
741/// `rt_sigreturn`, which a gdbserver turns off. A recording and its replays
742/// must all answer no, or they could take different signal continuations
743/// (`ThreadState::returned_from_sigreturn`).
744pub fn observes_sigreturns(cfg: &Config) -> bool {
745    syscalls::signal_shadow::observes_sigreturns(cfg)
746}
747
748impl<T: RecordOrReplay> Detcore<T> {
749    /// A5 step 3c.1e A, IR-15 (Detcore's half): whether the calling thread's
750    /// process is the init of a PID namespace it created with `CLONE_NEWPID`
751    /// (`Scheduler`'s `pid_namespace_inits`, through
752    /// `GlobalRequest::InPidNamespaceInit`), in which case `hook` for
753    /// `signal`, a delivery that takes its default action, is refused with a
754    /// named WARN. Linux marks such an init `SIGNAL_UNKILLABLE`
755    /// (kernel/fork.c:2558-2560 at v6.12): a signal whose action is
756    /// `SIG_DFL` is ignored when it is sent, unless blocked
757    /// (`sig_task_ignored`, kernel/signal.c:92-94, reached through
758    /// `sig_ignored`, :104-120, which keeps a blocked one, :111), and a
759    /// blocked one is queued and then discarded at dequeue (`get_signal`,
760    /// :2843-2845). SIGKILL and SIGSTOP are ignored the same way unless the
761    /// send is forced (`!(force && sig_kernel_only(sig))`, :93): forced when
762    /// it comes from an ancestor PID namespace or from the kernel
763    /// (`send_signal_locked`, :1217-1224), so whether one reaches the init
764    /// depends on its sender, which the hook does not see. No default-action
765    /// delivery or death can be predicted for any of them, SIGKILL and
766    /// SIGSTOP included. A caught signal is delivered to an init as to any
767    /// process.
768    /// `unshare(CLONE_NEWPID)`, the other way a process becomes an init
769    /// (kernel/fork.c:2558-2560 for its next child), is refused with `EPERM`
770    /// on every backend (`is_mount_ns_admin_refused_syscall`), so only
771    /// `clone(CLONE_NEWPID)` children are inits. Called only under the
772    /// in-guest delivery gate.
773    async fn pid_namespace_init_refused<G: Guest<Self>>(
774        &self,
775        guest: &mut G,
776        hook: &str,
777        signal: i32,
778    ) -> bool {
779        let dettid = guest.thread_state().dettid;
780        if !tool_global::in_pid_namespace_init(guest, dettid).await {
781            return false;
782        }
783        warn!(
784            "{}",
785            crate::refused_signal_controls::pid_namespace_init_prediction_refused_warning(
786                dettid.as_raw(),
787                hook,
788                signal,
789            )
790        );
791        true
792    }
793
794    /// Whether `signal`, passed on to a guest thread without a report, could
795    /// have entered a handler the thread's signal-state copy never saw: where
796    /// the copy is kept against the kernel's own state, for any signal but
797    /// Reverie's preemption signal, SIGKILL and SIGSTOP, which never reach a
798    /// guest handler.
799    fn copy_misses_unreported(&self, signal: i32) -> bool {
800        self.cfg.signal_state_copy
801            && self.cfg.sequentialize_threads
802            && self.cfg.backend.kernel_signal_state_is_guests
803            && signal != reverie::PERF_EVENT_SIGNAL as i32
804            && signal != libc::SIGKILL
805            && signal != libc::SIGSTOP
806    }
807
808    /// Rule 4 for the coalescing ITIMER_REAL: tell the scheduler about a
809    /// committed guest SIGALRM send (https://github.com/rrnewton/hermit/issues/4139).
810    async fn note_guest_sigalrm<G: Guest<Self>>(&self, guest: &mut G, target: GuestSigalrmTarget) {
811        if guest.config().sequentialize_threads {
812            crate::tool_global::note_sigalrm_event(
813                guest,
814                crate::tool_global::GlobalRequest::NoteGuestSigalrm(target),
815            )
816            .await;
817        }
818    }
819
820    /// The table Detcore answers a guest's CPUID instruction from. Both the
821    /// instruction (`handle_cpuid_event`) and `/proc/cpuinfo` take it from
822    /// here, so a CPU profile such as
823    /// <https://github.com/rrnewton/hermit/issues/4243> proposes cannot give
824    /// the two views different CPUs.
825    pub(crate) fn virtual_cpuid(&self) -> cpuid::InterceptedCpuid {
826        cpuid::InterceptedCpuid::for_config(&self.cfg)
827    }
828
829    /// The procfs files this session renders (`procfs::RenderedFiles`):
830    /// `/proc/cpuinfo` among them where the guest's CPUID instruction is
831    /// answered from [`Self::virtual_cpuid`], which is where CPUID is
832    /// virtualized and the backend delivers the instruction to Detcore.
833    ///
834    /// Recomputed at each lookup from this run's configuration and the
835    /// guest's backend: one capability read, no injected call, and no state
836    /// shared with another session in the process. The answer is the same at
837    /// every lookup of a run: Reverie arms CPUID interception before the
838    /// guest's first callback (ptrace in its post-spawn preinit, before
839    /// `handle_thread_start`, and again at each `exec`; KVM from the Tool's
840    /// subscription before the first entry; DBT always intercepts), and a new
841    /// task takes its parent's.
842    pub(crate) fn rendered_files<G: Guest<Self>>(&self, guest: &G) -> procfs::RenderedFiles {
843        procfs::RenderedFiles::of(
844            self.cfg.virtualize_cpuid,
845            guest.has_cpuid_interception(),
846            self.cfg.container_mount_table.is_some(),
847            !procfs_inode::tool_can_name_guest_descriptors(&self.cfg.backend),
848        )
849    }
850
851    /// Give this image's descriptor table the session's answer to whether it
852    /// renders `/proc/cpuinfo` ([`Self::rendered_files`]), at the image's
853    /// first guest callback and before guest code runs, so that a placeholder
854    /// the table rediscovered without a guest at hand (an in-guest backend's
855    /// image after `exec`) is trusted exactly where the session renders the
856    /// file (`FileMetadata::decide_cpuinfo_placeholders`).
857    fn decide_cpuinfo_placeholders<G: Guest<Self>>(&self, guest: &G) -> Result<(), Errno> {
858        let rendered = self.rendered_files(guest).cpuinfo;
859        let mut metadata = guest.thread_state().file_metadata.lock().unwrap();
860        // The run's `--proc` level too, which a table restored across `exec`
861        // does not carry (`FileMetadata::note_proc_level`).
862        metadata.note_proc_level(self.cfg.proc_level);
863        metadata.decide_cpuinfo_placeholders(rendered)
864    }
865
866    /// Signal phase 1, step I4 (design section 4, "The commit"): when the
867    /// caller's process holds a SIGALRM entry that is due for it, run the
868    /// signal pipeline ptrace runs at a signal-delivery stop (step 2), take
869    /// the entry inside that pipeline's `InboundSignal` turn (step 3), and
870    /// hand the delivery to the backend (steps 4 to 8). An entry taken that
871    /// the backend cannot deliver is a determinism loss.
872    ///
873    /// `before_syscall` is the I4 addendum's point c: the delivery happens at
874    /// the current syscall's entry and the syscall, not yet run, starts again
875    /// after the handler. Returns whether a delivery was handed to the
876    /// backend; at a syscall's entry the caller then must not run the syscall.
877    ///
878    /// `completed` is the syscall's result at its completion (None at its
879    /// entry): the signal pipeline then reads the completed register
880    /// projection (design section 4).
881    async fn deliver_due_sigalrm<G: Guest<Self>>(
882        &self,
883        guest: &mut G,
884        before_syscall: bool,
885        completed: Option<i64>,
886    ) -> bool {
887        if !tool_global::sigalrm_refuses(guest, tool_global::SigalrmControl::DueEntry).await {
888            return false;
889        }
890        if let Some(result) = completed {
891            let entry = guest.regs().await;
892            guest.thread_state_mut().signal_register_projection =
893                Some(arch::completed_syscall_projection(entry, result));
894        }
895        let pipeline = self.handle_signal_event(guest, Signal::SIGALRM).await;
896        guest.thread_state_mut().signal_register_projection = None;
897        let delivering = matches!(pipeline, Ok(Some(_)));
898        // A suppressed signal's entry is dropped and nothing is delivered.
899        if !tool_global::sigalrm_refuses(guest, tool_global::SigalrmControl::TakeEntry).await
900            || !delivering
901        {
902            return false;
903        }
904        let info = scheduler::discarded_at_send::itimer_real_siginfo();
905        let target = reverie::SignalTarget::Process { pid: guest.pid() };
906        let delivered = match reverie::SignalEvent::new(libc::SIGALRM, info, target) {
907            Ok(event) if before_syscall => guest
908                .defer_signal_delivery_before_syscall(event)
909                .await
910                .is_ok(),
911            Ok(event) => guest.defer_signal_delivery(event).await.is_ok(),
912            Err(_) => false,
913        };
914        if !delivered {
915            self.stop_after_sigalrm_loss(guest).await;
916        }
917        if before_syscall {
918            // Faithful in its position relative to the guest's syscalls, but
919            // not in the interrupted registers the handler sees: recorded as a
920            // determinism loss, so verification refuses the run rather than
921            // compare it (round 4 of the I4 review; the faithful alternative
922            // is a delivery from the trampoline's return, before the syscall
923            // instruction runs).
924            tool_global::sigalrm_refuses(
925                guest,
926                tool_global::SigalrmControl::EntryDeliveryUnfaithful,
927            )
928            .await;
929        }
930        delivered
931    }
932
933    /// A SIGALRM entry this thread took cannot be delivered (or its virtual
934    /// mask cannot be read): record the determinism loss in the coordinator,
935    /// where the verifier reads it, and stop the process before any guest
936    /// instruction runs. Linux would have delivered the signal; continuing
937    /// would run the guest as if it never fired.
938    ///
939    /// The stop is the backend's `exit_group`, not `std::process::exit`: under
940    /// in-guest LiteInst the latter is the guest's own `exit`, which runs the
941    /// guest's `atexit` handlers and ELF finalizers.
942    async fn stop_after_sigalrm_loss<G: Guest<Self>>(&self, guest: &mut G) -> ! {
943        tool_global::sigalrm_refuses(guest, tool_global::SigalrmControl::DeliveryLost).await;
944        guest_log::flush_unsent();
945        match guest
946            .tail_inject(
947                reverie::syscalls::ExitGroup::new()
948                    .with_status(detcore_model::HERMIT_POLICY_REFUSAL_EXIT),
949            )
950            .await {}
951    }
952
953    /// Signal phase 1: before a callback can file a scheduler request, make
954    /// the scheduler's SIGALRM-blocked bit match the runtime's virtual mask. A
955    /// handler's start and return change that mask with no syscall, so the bit
956    /// may be stale at any callback (a syscall, or a trapped instruction whose
957    /// hooks may end a timeslice). Reads the runtime's virtual bit (one query
958    /// it answers from its virtual state) and publishes it when it differs from
959    /// the bit this thread last published, inside the thread's turn (no
960    /// firing commits while it holds it). Returns the bit.
961    async fn sync_sigalrm_eligibility<G: Guest<Self>>(&self, guest: &mut G) -> bool {
962        let Ok(blocked) = crate::syscalls::virtual_sigalrm_blocked(guest).await else {
963            self.stop_after_sigalrm_loss(guest).await
964        };
965        if blocked != guest.thread_state().sigalrm_blocked_published {
966            tool_global::sigalrm_refuses(
967                guest,
968                tool_global::SigalrmControl::PublishBlocked(blocked),
969            )
970            .await;
971            guest.thread_state_mut().sigalrm_blocked_published = blocked;
972        }
973        blocked
974    }
975
976    /// Publish the posthook happens-before anchors the current syscall entry
977    /// reached at its prehook, now that `res` is the result the guest will
978    /// get: after the last observer that can replace it, on every path that
979    /// returns a guest result, including an observer's own final errno (the
980    /// Codex review of https://github.com/rrnewton/hermit/pull/4048). Which
981    /// results are a completion is decided by [`posthook_checkpoint`]; the
982    /// pending names are consumed either way.
983    ///
984    /// `interrupted` is the restart code of a wait a default-fatal signal
985    /// ended under the in-guest delivery gate (`wait_end_checkpoint`, A5
986    /// design D40 and IR-2): the guest never resumes after such a call, as on
987    /// ptrace, so the anchors fire as interrupted with that code, although the
988    /// guest's result is `-EINTR`.
989    async fn publish_posthook_checkpoint<G: Guest<Self>>(
990        &self,
991        guest: &mut G,
992        count: u64,
993        res: &Result<i64, Error>,
994        interrupted: Option<Errno>,
995    ) {
996        let posthook = std::mem::take(&mut guest.thread_state_mut().stats.hb_pending_posthook);
997        let interrupted = interrupted.map(|code| Err(Error::Errno(code)));
998        if let Some(checkpoint) =
999            posthook_checkpoint(count, posthook, interrupted.as_ref().unwrap_or(res))
1000        {
1001            let request = guest.thread_state().mk_request(checkpoint, Permission::R);
1002            resource_request(guest, request).await;
1003        }
1004    }
1005
1006    /// Publish the posthook checkpoint for a syscall that ends with
1007    /// `res` instead of its result, and complete it with that result.
1008    /// Boxed at its post-dispatch call sites so those rare error paths
1009    /// do not grow every syscall's future
1010    /// (`hot_path_futures_stay_small`).
1011    async fn complete_after_posthook<G: Guest<Self>>(
1012        &self,
1013        guest: &mut G,
1014        count: u64,
1015        res: Result<i64, Error>,
1016        interrupted: Option<Errno>,
1017    ) -> Result<i64, Error> {
1018        self.publish_posthook_checkpoint(guest, count, &res, interrupted)
1019            .await;
1020        self.complete_syscall(guest, res).await
1021    }
1022
1023    /// A5 step 3c.1c, D40: resolve a `WaitSignalEnd::BehindCaught` end, in
1024    /// the thread's own turn (`wait_end_checkpoint`), by reading the caught
1025    /// actions Linux's delivery loop sets up, one query `rt_sigaction` each,
1026    /// in the loop's order and only while a later delivery could still end
1027    /// the process (`DequeueWalk::resolve`). A loop it cannot decide files a
1028    /// named stopping refusal (D49), which ends the run under the gate:
1029    /// HERMIT_CAUGHT_ACTION_UNREADABLE for an action it cannot read,
1030    /// HERMIT_SIGNAL_RESET_TO_STOP for an instance reset to a stop,
1031    /// HERMIT_REALTIME_INSTANCES_UNKNOWN for a real-time signal whose queued
1032    /// instances would meet its reset default. Were it to return, the end is
1033    /// `Fatal`.
1034    async fn resolve_wait_end<G: Guest<Self>>(
1035        &self,
1036        guest: &mut G,
1037        call: &Syscall,
1038        walk: crate::wait_signal_end::DequeueWalk,
1039    ) -> crate::wait_signal_end::WaitSignalEnd {
1040        let mut actions: Vec<(i32, Option<crate::wait_signal_end::CaughtAction>)> = Vec::new();
1041        let resolved = loop {
1042            let mut missing = None;
1043            let resolved =
1044                walk.resolve(
1045                    |signal| match actions.iter().find(|(known, _)| *known == signal) {
1046                        Some((_, action)) => *action,
1047                        None => {
1048                            missing.get_or_insert(signal);
1049                            None
1050                        }
1051                    },
1052                );
1053            match (resolved, missing) {
1054                (Err(_), Some(signal)) => {
1055                    let action = crate::syscalls::helpers::caught_action(guest, signal)
1056                        .await
1057                        .ok();
1058                    actions.push((signal, action));
1059                }
1060                (resolved, _) => break resolved,
1061            }
1062        };
1063        use crate::refused_signal_controls as refusals;
1064        use crate::wait_signal_end::WalkRefusal;
1065        let caller = guest.thread_state().dettid.as_raw();
1066        let name = call.number().name();
1067        let (reason, signal) = match resolved {
1068            Ok(end) => return end,
1069            Err(WalkRefusal::ActionUnknown { caught, behind }) => (
1070                refusals::caught_action_unreadable_refusal(name, caught, behind, caller),
1071                behind,
1072            ),
1073            Err(WalkRefusal::ResetToStop { signal }) => (
1074                refusals::signal_reset_to_stop_refusal(name, signal, caller),
1075                signal,
1076            ),
1077            Err(WalkRefusal::RealtimeInstances { signal }) => (
1078                refusals::realtime_instances_unknown_refusal(name, signal, caller),
1079                signal,
1080            ),
1081        };
1082        tracing::error!("{}", reason);
1083        crate::tool_global::stopping_refusal(guest, reason).await;
1084        crate::wait_signal_end::WaitSignalEnd::Fatal(signal)
1085    }
1086
1087    /// The restart checkpoint (A5 step 3c.1c, design D40, with IR-2's
1088    /// placement): run straight after the dispatch `match`, before every
1089    /// observer of the result (`fail_on_tool_decided_injection`,
1090    /// `delivery_interruption`, DETLOG's `SyscallResult`, the posthook
1091    /// happens-before checkpoint, the post-handler hook and the completion),
1092    /// so each of them, and the backend, sees the result it returns. Under the
1093    /// in-guest delivery gate only, and on the current call's D40 flag
1094    /// (`ThreadState::wait_signal_end`), which the path that ended a wait for
1095    /// a signal set:
1096    ///
1097    /// | `res` | flag | guest result | posthook records |
1098    /// |---|---|---|---|
1099    /// | a restart code | `Fatal` | `-EINTR` | interrupted, with the code |
1100    /// | a restart code | `Caught(sig)` | the x86 restart table with `sig`'s flags (a restart of `-ERESTARTSYS` passed through, not honoured before 3c.2; any other restart refused, below) | a checkpoint |
1101    /// | a restart code | `BehindCaught` | resolved first by the caught handlers' masks (below), then the `Fatal` or `Caught` row | as that row |
1102    /// | `-ERESTARTSYS` | none | unchanged: the in-guest runtime re-runs it | interrupted |
1103    /// | `-ERESTARTNOINTR`, `-ERESTARTNOHAND`, `-ERESTART_RESTARTBLOCK` | none | the run ends (D49) | — |
1104    /// | anything else | any | unchanged | as before |
1105    ///
1106    /// The `-ERESTARTSYS` re-run is Reverie's restart protocol
1107    /// (<https://github.com/rrnewton/reverie/issues/362>), which in-guest's
1108    /// parked `wait4` and pipe reads reach today (3c.1.0's finding F6).
1109    ///
1110    /// A default-fatal end is `-EINTR` because the process dies at the return
1111    /// and the guest never sees the value; DETLOG then shows `-EINTR` where
1112    /// ptrace's record shows the kernel's restart code (a named record
1113    /// difference). A caught end that the table restarts with `-ERESTARTSYS`
1114    /// (`SA_RESTART`) is passed through, but not yet honoured: the in-guest
1115    /// Tool host re-runs the call at once (reverie-inguest
1116    /// `drive_tool_syscall`, src/tool_host.rs:283-301 at 5d9d4c4f), with no
1117    /// handler run in between, where Linux runs the handler and then restarts
1118    /// the call. It is unreachable in 3c.1: phase 1 admits only `pause` in a
1119    /// process that handles SIGALRM (F8), and `pause` ends with
1120    /// `-ERESTARTNOHAND`. Running the handler before the host's re-run is a
1121    /// step-3c.2 obligation. Any other restart code (`-ERESTARTNOINTR`, from
1122    /// D41's not-run call when the caught handlers' masks hold the
1123    /// default-fatal signal behind them) would need
1124    /// the register rewind and post-handler delivery of step 3c.2, and the
1125    /// pinned in-guest host would hand the raw code to the guest. It fails
1126    /// closed: the stopping refusal HERMIT_RESTART_AFTER_HANDLER (D49), which
1127    /// ends the run under the gate; were it to return, the result is `-EINTR`.
1128    /// (In that schedule Linux runs the handler with the default-fatal signal
1129    /// blocked and, unless the handler changes it, ends the process at the
1130    /// handler's `rt_sigreturn`, before the call is made again.) Returns the
1131    /// guest's result and, for the fatal row, the original code for the
1132    /// posthook checkpoint. With the gate off it returns `res` and nothing,
1133    /// and the flag is never set.
1134    ///
1135    /// A `BehindCaught` end (a caught signal first, and a later delivery that
1136    /// could end the process) is resolved here, in the thread's own turn just
1137    /// before the guest would resume, where Linux's delivery loop runs: each
1138    /// caught action the loop needs is read with a query `rt_sigaction`, in
1139    /// that order, and applied as Linux applies it (`sa_mask`, the signal
1140    /// unless `SA_NODEFER`, the reset to `SIG_DFL` under `SA_RESETHAND`;
1141    /// `DequeueWalk::resolve`, `resolve_wait_end`). A loop it cannot decide
1142    /// fails closed by a named stopping refusal (D49), which ends the run under
1143    /// the gate; were it to return, the end is `Fatal`.
1144    async fn wait_end_checkpoint<G: Guest<Self>>(
1145        &self,
1146        guest: &mut G,
1147        call: &Syscall,
1148        res: Result<i64, Error>,
1149    ) -> (Result<i64, Error>, Option<Errno>) {
1150        use crate::signal_restart::CaughtSignalReturn;
1151        use crate::wait_signal_end::WaitSignalEnd;
1152        let end = guest.thread_state_mut().wait_signal_end.take();
1153        if !self.cfg.in_guest_signal_delivery {
1154            return (res, None);
1155        }
1156        let code = match &res {
1157            Err(Error::Errno(errno)) if crate::wait_signal_end::is_restart_code(*errno) => {
1158                Some(*errno)
1159            }
1160            _ => None,
1161        };
1162        let end = match (end, code) {
1163            (Some(WaitSignalEnd::BehindCaught(walk)), Some(_)) => {
1164                Some(self.resolve_wait_end(guest, call, walk).await)
1165            }
1166            (end, _) => end,
1167        };
1168        match (end, code) {
1169            (Some(WaitSignalEnd::Fatal(_)), Some(code)) => (Err(Errno::EINTR.into()), Some(code)),
1170            (Some(WaitSignalEnd::Caught(signal)), Some(code)) => {
1171                let flags = if code == Errno::ERESTARTSYS {
1172                    match crate::syscalls::helpers::caught_action_flags(guest, signal).await {
1173                        Ok(flags) => flags,
1174                        Err(error) => return (Err(error), None),
1175                    }
1176                } else {
1177                    0
1178                };
1179                match crate::signal_restart::caught_signal_return(
1180                    -i64::from(code.into_raw()),
1181                    flags,
1182                ) {
1183                    CaughtSignalReturn::Returns(value) if value < 0 => (
1184                        Err(Errno::new(i32::try_from(-value).unwrap_or(libc::EINVAL)).into()),
1185                        None,
1186                    ),
1187                    CaughtSignalReturn::Returns(value) => (Ok(value), None),
1188                    CaughtSignalReturn::Restarts if code == Errno::ERESTARTSYS => (res, None),
1189                    CaughtSignalReturn::Restarts => {
1190                        let reason = crate::refused_signal_controls::restart_after_handler_refusal(
1191                            call.number().name(),
1192                            signal,
1193                            code.name().unwrap_or("a restart code"),
1194                            guest.thread_state().dettid.as_raw(),
1195                        );
1196                        tracing::error!("{}", reason);
1197                        crate::tool_global::stopping_refusal(guest, reason).await;
1198                        (Err(Errno::EINTR.into()), None)
1199                    }
1200                }
1201            }
1202            (None, Some(code)) if code != Errno::ERESTARTSYS => {
1203                let reason = crate::refused_signal_controls::restart_code_without_signal_refusal(
1204                    call.number().name(),
1205                    code.name().unwrap_or("a restart code"),
1206                    guest.thread_state().dettid.as_raw(),
1207                );
1208                tracing::error!("{}", reason);
1209                crate::tool_global::stopping_refusal(guest, reason).await;
1210                (res, None)
1211            }
1212            _ => (res, None),
1213        }
1214    }
1215
1216    /// The completion of a syscall that can resume the guest (a result or an
1217    /// errno, not a backend failure): in a process that handles SIGALRM, a due
1218    /// entry is delivered here, where Linux would deliver a signal that became
1219    /// pending while the call ran (design section 4, point a).
1220    async fn complete_syscall<G: Guest<Self>>(
1221        &self,
1222        guest: &mut G,
1223        res: Result<i64, Error>,
1224    ) -> Result<i64, Error> {
1225        if self.cfg.backend.virtualizes_guest_sigalrm && guest.thread_state().sigalrm_handled {
1226            let completed = match &res {
1227                Ok(value) => Some(*value),
1228                Err(Error::Errno(errno)) => Some(-i64::from(errno.into_raw())),
1229                Err(_) => None,
1230            };
1231            if completed.is_some() {
1232                self.deliver_due_sigalrm(guest, false, completed).await;
1233            }
1234        }
1235        res
1236    }
1237
1238    /// Signal phase 1 at a syscall's entry, before its first scheduler
1239    /// request, in a process that handles SIGALRM: sync the scheduler's
1240    /// blocked bit ([`Self::sync_sigalrm_eligibility`]), and while SIGALRM is
1241    /// unblocked deliver an entry that is already due before this syscall (the
1242    /// I4 addendum's point c). An entry is due here when it became deliverable
1243    /// with no syscall completing (a handler returned and unblocked SIGALRM),
1244    /// or committed during a scheduler yield inside a trapped instruction's
1245    /// hooks; Linux delivers it before any further guest instruction. Returns
1246    /// whether a delivery was prepared; the syscall then must not run.
1247    async fn sigalrm_at_syscall_entry<G: Guest<Self>>(&self, guest: &mut G) -> bool {
1248        let blocked = self.sync_sigalrm_eligibility(guest).await;
1249        !blocked && self.deliver_due_sigalrm(guest, true, None).await
1250    }
1251
1252    /// Registers a child whose native backend executed the clone syscall.
1253    ///
1254    /// The caller must initialize the child's local thread state from the same
1255    /// parent state and clone flags before the child enters its start hook.
1256    /// No backend that reaches this keeps a signal-state copy; one that does
1257    /// must call `signal_shadow::before_clone` before its native clone, as
1258    /// `handle_clone_family` does.
1259    // TODO-HUMAN-REVIEW(PR-743): Review the backend-neutral native child registration API.
1260    pub async fn register_external_child<G: Guest<Self>>(
1261        &self,
1262        guest: &mut G,
1263        child_tid: Tid,
1264        child_tid_addr: usize,
1265        flags: CloneFlags,
1266        exit_signal: libc::c_int,
1267        physical_ids: Option<(i32, i32)>,
1268    ) {
1269        let child_dettid = DetTid::from_raw(child_tid.into());
1270        guest.thread_state_mut().clone_flags = Some(flags);
1271        if !flags.contains(CloneFlags::CLONE_THREAD) {
1272            guest
1273                .thread_state()
1274                .prepare_child_process_cpu_time(child_dettid);
1275        }
1276        let parent_dettid = guest.thread_state().dettid;
1277        let parent_pedigree = &mut guest.thread_state_mut().pedigree;
1278        let child_pedigree = parent_pedigree.fork_mut();
1279        debug!(
1280            "[dtid {}] after registering external child (tid {}, pedigree {}) parent's pedigree becomes {}",
1281            parent_dettid, child_dettid, child_pedigree, parent_pedigree,
1282        );
1283        // The kernel clone has already succeeded before an external backend
1284        // calls this method, so these parent updates are not speculative. If
1285        // registration observes a retired parent, create_child_thread exits
1286        // that parent by tail injection; no continuing caller can retry or
1287        // roll the successful clone back.
1288        tool_global::create_child_thread(
1289            guest,
1290            child_dettid,
1291            tool_global::child_tid_clear_address(flags, child_tid_addr),
1292            Some(flags),
1293            exit_signal,
1294            physical_ids,
1295        )
1296        .await;
1297        guest.thread_state_mut().clone_flags = None;
1298    }
1299    async fn passthrough<G: Guest<Self>>(
1300        &self,
1301        guest: &mut G,
1302        call: Syscall,
1303    ) -> Result<i64, Error> {
1304        self.record_or_replay_preserving_tool_errors(guest, call)
1305            .await
1306    }
1307
1308    /// pkey_alloc, pkey_mprotect and pkey_free, passed through.
1309    ///
1310    /// An address space that allocates or applies a protection key may hold
1311    /// pages its guest cannot write although Hermit's copies can
1312    /// (`MemoryMetadata::protection_keys`). A key exists only once pkey_alloc
1313    /// returns one: a failed allocation (EINVAL, ENOSPC, or a backend without
1314    /// protection keys answering ENOSYS) allocates nothing, so it is not
1315    /// recorded.
1316    ///
1317    /// pkey_mprotect is recorded after it returns, unless it cannot have
1318    /// applied a key: key -1 (plain mprotect), ENOSYS (the backend ran
1319    /// nothing), no length, or a failure Linux returns before it changes any
1320    /// mapping. Every other outcome is recorded, failures included
1321    /// ([`pkey_mprotect_may_apply_a_key`]); a record is never cleared.
1322    /// Recording after the call is the same as before it for the reads that
1323    /// consult the record: they renumber only with sequentialized threads, so
1324    /// no other guest thread reads between the call and the record.
1325    ///
1326    /// A backend that does not implement protection keys declines all three
1327    /// calls with a typed refusal, which is named rather than handed to the
1328    /// guest as its errno ([`Detcore::report_backend_refusal`]).
1329    async fn handle_protection_key_call<G: Guest<Self>>(
1330        &self,
1331        guest: &mut G,
1332        call: Syscall,
1333    ) -> Result<i64, Error> {
1334        let sysno = call.number();
1335        let args = call.into_parts().1;
1336        let result = self.passthrough(guest, call).await;
1337        let records_a_key = match sysno {
1338            Sysno::pkey_alloc => matches!(result, Ok(key) if key >= 0),
1339            Sysno::pkey_mprotect => {
1340                pkey_mprotect_may_apply_a_key(args.arg0, args.arg1, args.arg2, args.arg3, &result)
1341            }
1342            _ => false,
1343        };
1344        if records_a_key {
1345            guest.thread_state().note_protection_keys();
1346        }
1347        self.report_backend_refusal(
1348            guest,
1349            sysno,
1350            reverie::UnsupportedOperation::ProtectionKeys,
1351            result,
1352        )
1353        .await
1354    }
1355
1356    // AUTONOMOUS-BOT-IMPLEMENTED
1357    // TODO-HUMAN-REVIEW(PR-643): Review unsupported-syscall reporting and fail-fast behavior.
1358    /// Applies the legacy policy to an explicitly listed but unsupported syscall.
1359    async fn handle_unsupported_syscall<G: Guest<Self>>(
1360        &self,
1361        guest: &mut G,
1362        call: Syscall,
1363        dettid: DetTid,
1364        panic_on_unsupported_syscalls: bool,
1365    ) -> Result<i64, Error> {
1366        if panic_on_unsupported_syscalls {
1367            error!(
1368                "[detcore, dtid {}] unsupported syscall: {} = ?",
1369                dettid,
1370                call.display(&guest.memory()),
1371            );
1372            if guest.config().shutdown_on_unsupported_syscall {
1373                // A fail-closed policy decision: the run named a syscall hermit
1374                // cannot service and `shutdown_on_unsupported_syscall` says stop.
1375                unrecoverable_shutdown(guest, detcore_model::HERMIT_POLICY_REFUSAL_EXIT).await;
1376            }
1377            if guest.config().exit_on_unsupported_syscall {
1378                return Err(Error::Tool(anyhow::Error::new(UnsupportedSyscallError(
1379                    call.number(),
1380                ))));
1381            }
1382            panic!("unsupported syscall: {:?}", call);
1383        }
1384        report_unsupported_syscall(guest, call.number()).await;
1385        self.passthrough(guest, call).await
1386    }
1387
1388    /// Linux's `restart_syscall`. When no handler runs, the kernel restarts a wait
1389    /// that returned `ERESTART_RESTARTBLOCK` by running this call at the
1390    /// interrupted call's address, and the wait resumes with its original deadline
1391    /// (https://github.com/rrnewton/hermit/issues/3358). Detcore emulates those
1392    /// waits, so it resumes them from the record the wait kept
1393    /// (`syscalls::helpers::RestartBlock`), as it does a `poll` with a positive
1394    /// timeout that it ran in the kernel. Any other `restart_syscall` keeps the
1395    /// unsupported-syscall policy.
1396    // TODO-HUMAN-REVIEW(https://github.com/rrnewton/hermit/pull/3361): Review the
1397    // emulated `restart_syscall` resumption and the record it consumes.
1398    /// Observe completed incoming vectored calls using saved actual-attempt
1399    /// metadata for locations and real returned bytes for payload. The old
1400    /// outbound observation contract is unchanged: a send can have read the
1401    /// temporarily reinstated metadata itself as payload. Boxed at the call
1402    /// site to keep every syscall's future small.
1403    #[allow(clippy::too_many_arguments)]
1404    async fn detlog_io_buffers_in_entry_view<G: Guest<Self>>(
1405        &self,
1406        guest: &mut G,
1407        call: &Syscall,
1408        ret: i64,
1409        dettid: DetTid,
1410        rng_readv_output: Option<&[io_buffers::BufferExtent]>,
1411        count: u64,
1412        interrupted: Option<Errno>,
1413    ) -> Option<Result<i64, Error>> {
1414        if !guest.config().detlog_io_buffers || !io_buffers::observed() {
1415            return None;
1416        }
1417        let entry = entry_view(guest, call);
1418        let observed = if matches!(call, Syscall::Readv(_) | Syscall::Recvmsg(_)) {
1419            let geometry = entry
1420                .as_deref()
1421                .and_then(|entry| entry.observation_geometry());
1422            io_buffers::detlog_io_buffers_with_entry_geometry(
1423                guest,
1424                call,
1425                ret,
1426                dettid,
1427                rng_readv_output,
1428                geometry,
1429            )
1430        } else {
1431            // A write may have sent the restored metadata as payload. Its
1432            // existing observer contract is outside this read-side repair.
1433            let view = entry
1434                .as_deref()
1435                .map(|entry| (entry, entry.reinstate_returned(&mut guest.memory())));
1436            let observed =
1437                io_buffers::detlog_io_buffers(guest, call, ret, dettid, rng_readv_output);
1438            restore_entry_view(guest, view).and(observed)
1439        };
1440        match observed {
1441            Err(error) => Some(
1442                self.complete_after_posthook(guest, count, Err(error), interrupted)
1443                    .await,
1444            ),
1445            Ok(()) => None,
1446        }
1447    }
1448
1449    async fn handle_restart_syscall<G: Guest<Self>>(
1450        &self,
1451        guest: &mut G,
1452        call: Syscall,
1453        restart_block: Option<syscalls::helpers::RestartBlock>,
1454        dettid: DetTid,
1455        panic_on_unsupported_syscalls: bool,
1456    ) -> Result<i64, Error> {
1457        let rip = arch::instruction_pointer(&guest.regs().await);
1458        let Some(block) = restart_block.and_then(|block| block.resumed_at(rip)) else {
1459            trace!(
1460                "[detcore, dtid {}] restart_syscall at {:#x} restarts no wait Detcore kept a record for ({:?})",
1461                dettid, rip, restart_block
1462            );
1463            return self
1464                .handle_unsupported_syscall(guest, call, dettid, panic_on_unsupported_syscalls)
1465                .await;
1466        };
1467        trace!(
1468            "[detcore, dtid {}] restart_syscall resumes {:?}",
1469            dettid, block
1470        );
1471        match block.call {
1472            #[cfg(target_arch = "x86_64")]
1473            syscalls::helpers::RestartCall::Poll(poll) => {
1474                self.handle_internal_poll_until(guest, poll, block.deadline)
1475                    .await
1476            }
1477            #[cfg(target_arch = "x86_64")]
1478            syscalls::helpers::RestartCall::KernelPoll(poll, deadline) => {
1479                self.resume_kernel_poll(guest, poll, deadline, call).await
1480            }
1481            syscalls::helpers::RestartCall::Futex(futex) => {
1482                self.handle_futex_from(
1483                    guest,
1484                    futex,
1485                    syscalls::FutexDeadline::Restart(block.deadline),
1486                )
1487                .await
1488            }
1489            syscalls::helpers::RestartCall::Nanosleep(call) => {
1490                let deadline = block
1491                    .deadline
1492                    .expect("a claimed nanosleep keeps its absolute deadline");
1493                self.resume_nanosleep(guest, call, deadline).await
1494            }
1495            // A wait Detcore ran in the kernel as the guest made it resumes in
1496            // the kernel, from the restart block that wait left.
1497            syscalls::helpers::RestartCall::InKernel(sysno) => {
1498                syscalls::helpers::resume_kernel_wait(guest, call, sysno).await
1499            }
1500        }
1501    }
1502
1503    /// A precise timer event, or a skipped slice end of a lone thread
1504    /// processed at its own branch count (`catch_up_lone_slice_ends`, with
1505    /// `ThreadState::replaying_lone_slice_end` set): the same code either way.
1506    async fn take_timer_event<G: Guest<Self>>(&self, guest: &mut G) {
1507        // A preemption stops the guest in user code (A5 step 3.2).
1508        guest.thread_state_mut().delivery_interruption = Some(reverie::Interruption::NotInSyscall);
1509
1510        info!(
1511            "[detcore, dtid {}] inbound timer preemption event",
1512            guest.thread_state().dettid
1513        );
1514        if guest.config().preemption_stacktrace {
1515            let mut file_writer: Box<dyn Write> =
1516                match &guest.config().preemption_stacktrace_log_file {
1517                    Some(path) => Box::new(
1518                        File::create(path).expect("Failed to open preemption stacktrace log file"),
1519                    ),
1520                    // Under --max-log-bytes a full stderr must not stop the
1521                    // run before the cap; see `util::DroppingStderr`.
1522                    None if crate::nonwaiting_write::diagnostics_must_not_wait() => {
1523                        Box::new(crate::util::DroppingStderr)
1524                    }
1525                    None => Box::new(std::io::stderr()),
1526                };
1527            let ts = guest.thread_state();
1528            writeln!(
1529                file_writer,
1530                "\n>>> Guest tid {} preempted at thread time {} with stack trace:",
1531                ts.dettid,
1532                ts.thread_logical_time.as_nanos(),
1533            )
1534            .unwrap();
1535            if let Some(backtrace) = guest.backtrace() {
1536                if let Ok(pbt) = backtrace.pretty() {
1537                    writeln!(file_writer, "{}", pbt).unwrap();
1538                } else {
1539                    writeln!(file_writer, "{}", backtrace).unwrap();
1540                }
1541            } else {
1542                warn!("Could not read backtrace!");
1543            }
1544        }
1545        // This may LOOK like a noop, but actually all of the logic for ending the timeslice is in
1546        // the prehook.  All the timer has to do is interrupt the guest and generate an extra call
1547        // to this prehook.
1548        let boundary_entry = if guest.config().no_rcb_time {
1549            slice_boundary::callbacks::Callback::NoRcbTimer
1550        } else {
1551            slice_boundary::callbacks::Callback::PmuTimer
1552        };
1553        let boundary_token = self
1554            .pre_handler_hook_observed(guest, true, CheckIn::Event, Some(boundary_entry))
1555            .await;
1556        if guest.config().no_rcb_time && guest.thread_state().last_rcb_timer_is_max {
1557            let max_timeslice_end = guest
1558                .thread_state()
1559                .max_timeslice_end
1560                .expect("PMU maximum timer requires a deadline");
1561            guest
1562                .thread_state_mut()
1563                .thread_logical_time
1564                .advance_to(max_timeslice_end);
1565            if self.cfg.observes_schedevents() {
1566                let dettid = guest.thread_state().dettid;
1567                let ev = with_guest_time(
1568                    guest,
1569                    SchedEvent {
1570                        dettid,
1571                        op: Op::OtherInstructions,
1572                        count: 1,
1573                        start_rip: None,
1574                        end_rip: None,
1575                        end_time: None,
1576                    },
1577                );
1578                let ev = with_guest_rip(guest, ev).await;
1579                trace_schedevent(guest, ev, false).await;
1580            }
1581            if guest.thread_state().follows_replayed_schedule(&self.cfg)
1582                && guest.thread_state().slice_boundary.is_none()
1583            {
1584                let fallback_deadline = max_timeslice_end
1585                    + Duration::from_nanos(u64::from(
1586                        self.cfg
1587                            .max_timeslice
1588                            .expect("PMU maximum must be configured"),
1589                    ));
1590                let thread_state = guest.thread_state_mut();
1591                let replay_deadline = thread_state
1592                    .end_of_timeslice
1593                    .filter(|deadline| *deadline > max_timeslice_end)
1594                    .unwrap_or(fallback_deadline);
1595                thread_state.end_of_timeslice = Some(replay_deadline);
1596                thread_state.max_timeslice_end = Some(replay_deadline);
1597                thread_state.last_rcb_timer = None;
1598                thread_state.last_rcb_timer_is_max = false;
1599                thread_state.stats.reset_timeslice();
1600            } else if guest.thread_state().own_signal_turn_hold.is_none() {
1601                slice_boundary::no_rcb_maximum(guest.thread_state_mut())
1602                    .unwrap_or_else(|error| crate::scheduler::exit_on_scheduler_refusal(&error));
1603                self.end_timeslice(guest).await;
1604            }
1605        }
1606        self.post_handler_hook_observed(guest, CheckIn::Event, boundary_token)
1607            .await;
1608    }
1609
1610    /// Process, in order, every slice end a lone thread ran past without a
1611    /// stop (`ThreadState::lone_slice_end`), each at its own branch count and
1612    /// through the same code as a timer event there (`take_timer_event`).
1613    /// Called first in every callback that sees the guest stopped, before
1614    /// that callback changes anything, so these ends come where the stops
1615    /// would have put them: before what the guest did next.
1616    ///
1617    /// Equivalent to the stops: the scheduler marked each grant lone
1618    /// (`Scheduler::runs_alone_until_it_enters_detcore`), so until now nothing
1619    /// but this thread's own requests could change what any pass decides; a
1620    /// slice end reads nothing from the guest but its branch count, which
1621    /// the clock update takes from the end itself; and the next end follows
1622    /// from the same deadline arithmetic in the post-hook. Afterwards the
1623    /// thread keeps running alone, or, if the last grant was not marked
1624    /// lone, gets the precise timer for its next end.
1625    async fn catch_up_lone_slice_ends<G: Guest<Self>>(&self, guest: &mut G) {
1626        let Some(mut end) = guest.thread_state().lone_slice_end else {
1627            return;
1628        };
1629        let reached = guest.read_clock().expect("Couldn't read clock");
1630        while end <= reached {
1631            {
1632                let thread_state = guest.thread_state_mut();
1633                thread_state.lone_slice_end = None;
1634                thread_state.replaying_lone_slice_end = Some(end);
1635            }
1636            self.take_timer_event(guest).await;
1637            STOPLESS_SLICE_ENDS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1638            if std::mem::take(&mut guest.thread_state_mut().lone_replay_unsound) {
1639                // Another thread could run at an end this thread had already
1640                // run past: the run is no longer the one a stop gives.
1641                let reason = format!(
1642                    "HERMIT_LONE_SLICE_END_UNSOUND: thread {} was granted at a skipped slice end \
1643                     (branch {end}) while another thread could run; that end was processed \
1644                     after the thread reached branch {reached}",
1645                    guest.thread_state().dettid
1646                );
1647                tracing::error!("{reason}");
1648                crate::detlog::record_determinism_loss(&reason);
1649            }
1650            let thread_state = guest.thread_state_mut();
1651            thread_state.replaying_lone_slice_end = None;
1652            end = thread_state
1653                .lone_slice_end
1654                .expect("a processed slice end notes the next one");
1655        }
1656        if !guest.thread_state().lone_grant {
1657            stop_skipping_lone_slice_end(guest);
1658        }
1659    }
1660
1661    /// Whether the post-hook may leave the slice end it is about to arm
1662    /// without a stop (`Config::replays_lone_preemptions`): the latest grant
1663    /// found the thread alone, the timer is for the maximum (not a manual
1664    /// interrupt), and the thread replays nothing, holds no turn for a
1665    /// signal to its own process and owes no pipe hand-off.
1666    fn skips_lone_slice_end<G: Guest<Self>>(&self, guest: &G, timer_is_max: bool) -> bool {
1667        let thread_state = guest.thread_state();
1668        self.cfg.replays_lone_preemptions()
1669            && thread_state.lone_grant
1670            && timer_is_max
1671            && thread_state.replay_rcb_end.is_none()
1672            && thread_state.preemption_points.is_none()
1673            && thread_state.slice_boundary.is_none()
1674            && thread_state.own_signal_turn_hold.is_none()
1675            && thread_state.pipe_handoff_due.is_none()
1676    }
1677
1678    /// Update logical thread time with any outstanding ticks of the Reverie clock.  Returns a list
1679    /// of corresponding Branch/OtherInstructions events if schedule recording is enabled.
1680    ///
1681    /// # Arguments
1682    ///
1683    /// * `precise_branch`: if true, there were no non-branch instructions since the last recorded branch instruction.
1684    async fn update_logical_time_rcbs<G: Guest<Self>>(
1685        &self,
1686        guest: &mut G,
1687        precise_branch: bool,
1688    ) -> Option<Vec<SchedEvent>> {
1689        if self.cfg.max_timeslice.is_some() {
1690            let precise_timers = !guest.config().imprecise_timers;
1691            // TODO(T86591083): we might need to not always increment as a hack fix
1692            // for deterministic virtual time without sequentialize threads.
1693            // A skipped slice end of a lone thread is processed at its own
1694            // branch count (`catch_up_lone_slice_ends`), not the counter's.
1695            let clock_value = match guest.thread_state().replaying_lone_slice_end {
1696                Some(end) => end,
1697                None => guest.read_clock().expect("Couldn't read clock"),
1698            };
1699            // N.B. clock_value does not yet include any updates for the inbound
1700            // syscall/instruction because this function is the very first thing that
1701            // happens in each type of handler.
1702            let thread_state = guest.thread_state_mut();
1703            let dettid = thread_state.dettid;
1704            assert!(thread_state.committed_clock_value <= clock_value);
1705            let delta_rcbs: u64 = clock_value - thread_state.committed_clock_value;
1706            if self.cfg.use_rcb_time() {
1707                // AUTONOMOUS-BOT-IMPLEMENTED
1708                // TODO-HUMAN-REVIEW(PR-1151)
1709                if thread_state.chaos_slowdown_active {
1710                    let factor = thread_state.rcb_time_multiplier();
1711                    thread_state
1712                        .thread_logical_time
1713                        .add_rcbs_with_multiplier(delta_rcbs, factor);
1714                } else {
1715                    thread_state.thread_logical_time.add_rcbs(delta_rcbs);
1716                }
1717            }
1718            thread_state.account_process_cpu_time();
1719            thread_state.committed_clock_value = clock_value;
1720
1721            if thread_state.end_of_timeslice.is_some() {
1722                if let Some(last_timer) = thread_state.last_rcb_timer
1723                    && rcb_timer_overshot(delta_rcbs, last_timer)
1724                    && precise_timers
1725                {
1726                    report_rcb_overshoot(
1727                        self.cfg.panic_on_rcb_overshoot,
1728                        clock_value,
1729                        delta_rcbs,
1730                        last_timer,
1731                    );
1732                    // Preserve timer state. `pre_handler_hook` will yield through the normal
1733                    // scheduler path if the slice expired; `post_handler_hook` will otherwise
1734                    // re-arm an overshot `interrupt_at` timer.
1735                }
1736                // Otherwise we're very early, at the prehook of handle_thread_start.
1737            } else {
1738                panic!(
1739                    "Invariant violation: end_of_timeslice is None during update_logical_time_rcbs..."
1740                )
1741            }
1742
1743            trace!(
1744                "[dtid {}] updated rcb clock, new logical time: {:?}, i.e. {}, timeslice end: {}, local rcb clock_value {:?}",
1745                dettid,
1746                &thread_state.thread_logical_time,
1747                thread_state.thread_logical_time.as_nanos(),
1748                thread_state
1749                    .end_of_timeslice
1750                    .map_or_else(|| "".to_string(), |x| format!("{}", x)),
1751                clock_value,
1752            );
1753            if self.cfg.use_rcb_time() && self.cfg.observes_schedevents() {
1754                let mut vec = Vec::new();
1755                let ev = with_guest_time(
1756                    guest,
1757                    SchedEvent::branches(
1758                        dettid,
1759                        delta_rcbs
1760                            .try_into()
1761                            .expect("should not have more than 2^32 branches at once"),
1762                    ),
1763                );
1764                let ev = if precise_branch {
1765                    with_guest_rip(guest, ev).await
1766                } else {
1767                    ev
1768                };
1769
1770                if delta_rcbs > 0 {
1771                    // We don't fill the end_rip here, because the current rip is NOT precisely the
1772                    // end of this block of branch events.  Other instructions may have occured
1773                    // since the last branch.
1774                    vec.push(ev)
1775                } else {
1776                    trace!(
1777                        "[detcore, dtid {}] Refusing to record zero-braches event: {:?}",
1778                        &ev.dettid, ev
1779                    );
1780                }
1781                if !precise_branch {
1782                    // This will ALWAYS record, even if the branches above are zero.
1783                    let ev2 = with_guest_time(
1784                        guest,
1785                        SchedEvent {
1786                            dettid,
1787                            op: Op::OtherInstructions,
1788                            count: 1,
1789                            start_rip: None,
1790                            end_rip: None,
1791                            end_time: None,
1792                        },
1793                    );
1794                    // Fill in end_rip because current rip represents the end of this event.
1795                    let ev2 = with_guest_rip(guest, ev2).await;
1796                    vec.push(ev2);
1797                }
1798                Some(vec)
1799            } else {
1800                None
1801            }
1802        } else {
1803            None
1804        }
1805    }
1806
1807    /// A common hook called at the start of *every* handler, just after we receive
1808    /// control from the guest.
1809    /// The guest's registers as Detcore's hooks report them: the completed
1810    /// projection while a SIGALRM delivery's signal pipeline runs at a
1811    /// syscall's completion, otherwise the backend's.
1812    async fn observed_regs<G: Guest<Self>>(&self, guest: &mut G) -> libc::user_regs_struct {
1813        match guest.thread_state().signal_register_projection {
1814            Some(projection) => projection,
1815            None => guest.regs().await,
1816        }
1817    }
1818
1819    /// What a syscall's prehook event adds to the access trace: what the
1820    /// registers say. `None` unless the run records one.
1821    fn access_trace_entry<G: Guest<Self>>(
1822        &self,
1823        guest: &G,
1824        call: &Syscall,
1825    ) -> Option<access_trace::AccessPayload> {
1826        self.cfg.access_trace.as_ref()?;
1827        let (sysno, args) = call.into_parts();
1828        let thread = guest.thread_state();
1829        let pid = thread.detpid.expect("detpid unset");
1830        Some(access_trace::AccessPayload::SyscallEntry(
1831            access_trace::SyscallEntry {
1832                pid,
1833                // `count_syscall` counts this call right after the prehook.
1834                nth: thread.stats.syscall_count + 1,
1835                sysno,
1836                args: raw_syscall_args(args),
1837                accesses: access_trace::entry_accesses(sysno, pid, thread.dettid),
1838            },
1839        ))
1840    }
1841
1842    /// Start noting the descriptor slots a traced syscall changes, from the
1843    /// descriptors it names as it reaches them.
1844    fn begin_traced_call<G: Guest<Self>>(&self, guest: &G, call: &Syscall) {
1845        if self.cfg.access_trace.is_none() {
1846            return;
1847        }
1848        let (sysno, args) = call.into_parts();
1849        let thread = guest.thread_state();
1850        let fds = thread.access_trace_fds(&access_trace::named_fds(sysno, raw_syscall_args(args)));
1851        thread.traced_call.begin(fds);
1852    }
1853
1854    /// What a syscall's posthook event adds to the access trace: the result
1855    /// and what [`access_trace::accesses`] finds. It reads guest memory the
1856    /// way `--detlog-io-buffers` reads a syscall's buffers, and nothing that
1857    /// a scheduling decision reads. `None` unless the call was traced.
1858    fn access_trace_exit<G: Guest<Self>>(
1859        &self,
1860        guest: &G,
1861        call: &Syscall,
1862        result: Option<i64>,
1863        rng_output: Option<&[io_buffers::BufferExtent]>,
1864    ) -> Option<access_trace::AccessPayload> {
1865        let state = guest.thread_state().traced_call.take()?;
1866        let result = result?;
1867        let (sysno, args) = call.into_parts();
1868        let args = raw_syscall_args(args);
1869        let memory = guest.memory();
1870        let written = access_trace::read_written(&memory, sysno, args, result);
1871        let moved = io_buffers::access_trace_buffers(&memory, call, result, rng_output);
1872        let thread = guest.thread_state();
1873        let futex_id = |address: u64, private: bool| thread.futex_id(address as usize, private);
1874        let accesses = access_trace::accesses(&access_trace::CallFacts {
1875            sysno,
1876            args,
1877            result,
1878            pid: thread.detpid.expect("detpid unset"),
1879            tid: thread.dettid,
1880            files: thread.files_id(),
1881            mm: thread.mm_id,
1882            fds: &state.fds,
1883            notes: &state.notes,
1884            buffers: &moved.buffers,
1885            written: &written,
1886            futex_id: &futex_id,
1887        });
1888        Some(access_trace::AccessPayload::SyscallExit(Box::new(
1889            access_trace::SyscallOutcome {
1890                result,
1891                accesses,
1892                returned: moved.returned,
1893                out_bytes: moved.out_bytes,
1894            },
1895        )))
1896    }
1897
1898    /// What a signal event adds to the access trace: its siginfo and the
1899    /// installed disposition observed before submitting the event. `None`
1900    /// unless the run records an access trace.
1901    fn access_trace_signal<G: Guest<Self>>(
1902        &self,
1903        guest: &G,
1904        signal: SigWrapper,
1905    ) -> Option<access_trace::AccessPayload> {
1906        self.cfg.access_trace.as_ref()?;
1907        let siginfo = guest.signal_info();
1908        Some(access_trace::AccessPayload::Signal(
1909            access_trace::signal_note(
1910                signal.raw(),
1911                siginfo.as_ref().map(|info| &info[..]),
1912                traced_disposition(guest, signal.raw()),
1913            ),
1914        ))
1915    }
1916
1917    /// Detcore's handling of an inbound signal, for both of Reverie's signal
1918    /// hooks (`Tool::handle_signal_event`, and `Tool::handle_raw_signal_event`
1919    /// for a realtime signal): a signal-delivery stop, the delivery of
1920    /// `signal` the backend reports once, after the kernel dequeued it (the
1921    /// ptrace backend's stop). The signal takes its turn in the schedule
1922    /// before it is delivered. Returns the signal to deliver, or `None` to
1923    /// suppress it. It runs
1924    /// the stop's three parts in order: what the dequeue settles
1925    /// ([`Self::inbound_dequeued`]), the delivery's turn
1926    /// ([`Self::inbound_turn`]), and what the granted delivery settles
1927    /// ([`Self::inbound_delivered`]). A backend that announces a delivery
1928    /// before the kernel dequeues it runs the turn first and the rest at its
1929    /// commit (`Tool::announce_signal_event`, `Tool::commit_signal_event`).
1930    async fn handle_inbound_signal<G: Guest<Self>>(
1931        &self,
1932        guest: &mut G,
1933        signal: SigWrapper,
1934    ) -> Result<Option<SigWrapper>, Errno> {
1935        let stop = self.inbound_stop(guest);
1936        let Some(timer_entry) = self.inbound_dequeued(guest, signal).await? else {
1937            return Ok(None);
1938        };
1939        let turn = self
1940            .inbound_turn(guest, signal, stop, TurnScope::Delivery)
1941            .await;
1942        self.inbound_delivered(guest, signal, turn, timer_entry)
1943            .await
1944    }
1945
1946    /// What a delivery knows before anything of it can hand the thread to
1947    /// the scheduler.
1948    fn inbound_stop<G: Guest<Self>>(&self, guest: &mut G) -> tool_local::InboundStop {
1949        // Whether this stop comes before any scheduling point of the thread
1950        // since its previous handler's delivery was granted, or right after a
1951        // reported `rt_sigreturn` where reports are read
1952        // (`ThreadState::returned_from_sigreturn`).
1953        let reported = std::mem::take(&mut guest.thread_state_mut().returned_from_sigreturn)
1954            && crate::syscalls::signal_shadow::observes_sigreturns(&self.cfg);
1955        // The thread's grants so far: a grant before this delivery's request
1956        // (a slice end, a schedule-replay trace, any other hand-off) means
1957        // other threads may have run, so the delivery continues nothing.
1958        let grants_at_stop = guest.thread_state().scheduler_grants;
1959        tool_local::InboundStop {
1960            reported,
1961            grants_at_stop,
1962        }
1963    }
1964
1965    #[cfg(test)]
1966    fn note_test_phase(phase: &'static str) {
1967        signal_announcement_tests::note_phase(phase);
1968    }
1969
1970    /// What the kernel's dequeue of `signal` settles before the delivery's
1971    /// turn: an operator's SIGINT, the `ITIMER_REAL` re-arm, a duplicate
1972    /// child-exit `SIGCHLD`, a stale POSIX timer entry. `None` when the
1973    /// delivery is dropped; otherwise whether it is a POSIX timer's entry.
1974    async fn inbound_dequeued<G: Guest<Self>>(
1975        &self,
1976        guest: &mut G,
1977        signal: SigWrapper,
1978    ) -> Result<Option<bool>, Errno> {
1979        if signal.signal() == Some(Signal::SIGINT) && self.cfg.sigint_instakill {
1980            warn!("Fatal: Exiting hermit container immediately upon SIGINT");
1981            // ⚠️ NOT A REFUSAL. The operator interrupted the run; hermit examined
1982            // nothing and decided nothing. Reporting `128 + SIGINT` is what every
1983            // other tool reports for this, and it keeps 122 meaning one thing.
1984            unrecoverable_shutdown(guest, detcore_model::HERMIT_SIGINT_DEATH_EXIT).await
1985        }
1986        if signal.signal() == Some(Signal::SIGALRM) {
1987            self.normalize_reported_itimer_real(guest).await;
1988        }
1989        if signal.signal() == Some(Signal::SIGCHLD)
1990            && self.cfg.backend.reports_kernel_child_exit_signals
1991            && !self.child_exit_sigchld_delivers(guest).await
1992        {
1993            // A claimed copy that is dropped has no delivery to place, but
1994            // the claim may have held this duplicate while the claimed copy
1995            // is still pending (`syscalls::signal::claim_still_pending`).
1996            let (pid, tid) = (guest.pid(), guest.tid());
1997            let claimed =
1998                crate::syscalls::claim_still_pending(guest.thread_state().claimed_sigchld, || {
1999                    crate::syscalls::read_thread_signal_state(pid, tid)
2000                });
2001            guest.thread_state_mut().claimed_sigchld = claimed;
2002            self.keep_timer_across_dropped_signal(guest);
2003            return Ok(None);
2004        }
2005        // The test-only claim fault drops the claimed copy here, as the
2006        // ledger drops a duplicate above (`syscalls::signal::take_claim_fault`).
2007        if signal.signal() == Some(Signal::SIGCHLD)
2008            && guest.thread_state().claimed_sigchld
2009            && crate::syscalls::take_claim_fault(crate::syscalls::ClaimFault::DropClaimed)
2010        {
2011            warn!(
2012                "[dtid {}] claim fault hook: dropped the claimed SIGCHLD",
2013                guest.thread_state().dettid
2014            );
2015            guest.thread_state_mut().claimed_sigchld = false;
2016            self.keep_timer_across_dropped_signal(guest);
2017            return Ok(None);
2018        }
2019        // A POSIX timer's entry: delivered with its own siginfo, or, if its
2020        // timer was set again since it was queued, skipped as Linux skips
2021        // it inside the dequeue, with no turn
2022        // (<https://github.com/rrnewton/hermit/issues/4262>).
2023        let mut timer_entry = false;
2024        let stop_time = guest.thread_state().thread_logical_time.as_nanos();
2025        if scheduler::discarded_at_send::records(&self.cfg) && (1..32).contains(&signal.raw()) {
2026            use scheduler::discarded_at_send::DiscardedSignalAnswer;
2027            use scheduler::discarded_at_send::DiscardedSignalControl;
2028            use scheduler::posix_timer_coalesce::DeliveryAnswer;
2029            let control = DiscardedSignalControl::Dequeue {
2030                signal: signal.raw(),
2031                at: stop_time,
2032            };
2033            let answer = tool_global::discarded_signal(guest, control).await;
2034            timer_entry = answer == DiscardedSignalAnswer::Delivery(DeliveryAnswer::TimerEntry);
2035            match answer {
2036                DiscardedSignalAnswer::Delivery(DeliveryAnswer::Suppress) => {
2037                    info!(
2038                        "[dtid {}] signal {} is a stale POSIX timer entry; skipped",
2039                        guest.thread_state().dettid,
2040                        signal
2041                    );
2042                    self.keep_timer_across_dropped_signal(guest);
2043                    return Ok(None);
2044                }
2045                DiscardedSignalAnswer::Delivery(DeliveryAnswer::Rewrite(info)) => {
2046                    if let Err(errno) = guest.set_signal_info(info.to_user()) {
2047                        warn!(
2048                            "[dtid {}] signal {} keeps its physical siginfo: {}",
2049                            guest.thread_state().dettid,
2050                            signal,
2051                            errno
2052                        );
2053                    }
2054                }
2055                _ => {}
2056            }
2057        }
2058        #[cfg(test)]
2059        Self::note_test_phase("dequeued");
2060        Ok(Some(timer_entry))
2061    }
2062
2063    /// The delivery's turn: its hooks, its count and trace, and its
2064    /// `InboundSignal` request. For an announcement
2065    /// ([`TurnScope::Announcement`]) it is the request and its hooks alone:
2066    /// what only a delivery justifies (ending the sender's hold, the count
2067    /// and trace, a claimed `SIGCHLD`'s placement, marking a handler as run)
2068    /// waits for the commit ([`Self::inbound_turn_settled`]).
2069    async fn inbound_turn<G: Guest<Self>>(
2070        &self,
2071        guest: &mut G,
2072        signal: SigWrapper,
2073        stop: tool_local::InboundStop,
2074        scope: TurnScope,
2075    ) -> tool_local::InboundTurn {
2076        let tool_local::InboundStop {
2077            reported,
2078            grants_at_stop,
2079        } = stop;
2080        let delivery = scope == TurnScope::Delivery;
2081        // The kernel dequeued `signal` before this stop: a hold for it ends
2082        // here, and this stop's hooks may end the slice
2083        // (`own_signal_turn_hold`).
2084        if delivery && guest.thread_state().own_signal_turn_hold == Some(signal.raw()) {
2085            guest.thread_state_mut().own_signal_turn_hold = None;
2086        }
2087        let timeslice = guest.thread_state().stats.timeslice_count;
2088        let mut boundary_token = self
2089            .pre_handler_hook_observed(
2090                guest,
2091                false,
2092                CheckIn::Event,
2093                Some(slice_boundary::callbacks::Callback::Signal {
2094                    signal,
2095                    announced: !delivery,
2096                }),
2097            )
2098            .await;
2099        // The delivery continues the thread's turn only while the slice
2100        // its previous delivery ran in is still open. A stop that ends the
2101        // slice is Linux's TIF_NEED_RESCHED case: other threads run
2102        // (`schedule()`) before this one resumes and delivers, so the
2103        // delivery waits its place behind them instead of being taken
2104        // right after the yield. And at most
2105        // `MAX_SIGRETURN_CONTINUATIONS` in a row
2106        // (`consecutive_continuations`): in every mode, that cap is what
2107        // stops a handler that raises its own signal from holding the
2108        // CPU for ever.
2109        let slice_open = guest.thread_state().stats.timeslice_count == timeslice;
2110
2111        // For `hermit run --verify`: a guest that faulted on an illegal
2112        // instruction may have run code that made syscalls Detcore never
2113        // saw, so the run reports `/`, the ancestor of every path, and
2114        // names no host input change. A false alarm only withholds a name.
2115        // The case this was written for no longer reaches here: on ptrace,
2116        // a syscall issued from the untraced stub of Reverie's private page
2117        // is followed by a `ud2`, and Reverie now ends the run at that
2118        // SIGILL (`UntracedStubSyscall`, a policy refusal) before any Tool
2119        // sees it. A stub syscall that never returns to the `ud2` (execve,
2120        // exit, rt_sigreturn) raises no SIGILL at all
2121        // (https://github.com/rrnewton/reverie/issues/1064). This stays as
2122        // the conservative answer for any other illegal instruction. A
2123        // guest that first rewrites or replaces the page's code is reported
2124        // when it does so (`record_untraced_code_change`).
2125        if signal.signal() == Some(Signal::SIGILL) && self.cfg.record_host_inputs {
2126            tool_global::record_host_mutation(guest, String::from("/")).await;
2127        }
2128
2129        let dettid = guest.thread_state().dettid;
2130        let mycount = guest.thread_state().stats.signal_count;
2131        if delivery {
2132            info!(
2133                "[dtid {}] handling inbound signal (#{}) {}",
2134                dettid, mycount, signal
2135            );
2136            guest.thread_state_mut().stats.count_signal();
2137            preemptions::callback_history::delivered_signal(&mut boundary_token, signal, mycount)
2138                .expect("actual signal delivery must link to its entry");
2139        } else {
2140            // Counted, and logged with its ordinal, at its commit
2141            // (`inbound_turn_settled`).
2142            info!("[dtid {}] announcing inbound signal {}", dettid, signal);
2143        }
2144        let time = &guest.thread_state().thread_logical_time;
2145        let nanos = time.as_nanos();
2146
2147        if delivery && self.cfg.sequentialize_threads && self.cfg.observes_schedevents() {
2148            let access = self.access_trace_signal(guest, signal);
2149            trace_schedevent_with_access(
2150                guest,
2151                SchedEvent {
2152                    dettid,
2153                    op: Op::SignalReceived(signal),
2154                    count: 1,
2155                    start_rip: None,
2156                    end_rip: None,
2157                    end_time: Some(nanos),
2158                },
2159                true,
2160                access,
2161            )
2162            .await;
2163        }
2164
2165        // The delivery of a `SIGCHLD` this thread claimed is placed, so its
2166        // turn is not deferred behind runnable work
2167        // (`ThreadState::claimed_sigchld`).
2168        if delivery
2169            && signal.signal() == Some(Signal::SIGCHLD)
2170            && std::mem::take(&mut guest.thread_state_mut().claimed_sigchld)
2171        {
2172            tool_global::child_exit_sigchld(
2173                guest,
2174                scheduler::child_exit_sigchld::ChildExitSigchldControl::ClaimedDelivery,
2175            )
2176            .await;
2177        }
2178        // Read-write marks this as the delivery itself, which the
2179        // scheduler's write-only wakes of a signaled sleeper are not
2180        // (`Scheduler::thread_timer_copies`).
2181        let mut request = guest
2182            .thread_state_mut()
2183            .mk_request(ResourceID::InboundSignal(signal), Permission::RW);
2184        // Decided here, after every await of this stop that can hand the
2185        // thread to the scheduler (the hooks above, the trace), from the
2186        // window as it stands now.
2187        // An announcement continues as the delivery before it at the same
2188        // return would let it on ptrace, when the announcements come in the
2189        // order the kernel delivers them: an outstanding announcement whose
2190        // delivery runs a handler stands for the `delivered_since_request` its
2191        // commit will set. When a signal arrives between them and changes that
2192        // order, or an announcement that consumed `returned_from_sigreturn` is
2193        // withdrawn, the turns differ from ptrace's: the named difference of a
2194        // signal arriving between a delivery's turn and its final read.
2195        let announced_handler = !delivery
2196            && guest
2197                .thread_state()
2198                .signal_announcements
2199                .iter()
2200                .any(|held| delivery_runs_handler(guest, held.signal));
2201        request.continues_sigreturn =
2202            (guest.thread_state().delivered_since_request || reported || announced_handler)
2203                && guest.thread_state().scheduler_grants == grants_at_stop
2204                && guest.thread_state().consecutive_continuations
2205                    < crate::tool_local::MAX_SIGRETURN_CONTINUATIONS
2206                && signal.signal() != Some(Signal::SIGCHLD)
2207                && slice_open;
2208        resource_request(guest, request).await;
2209        // The delivery is granted; if it runs a handler, a stop that
2210        // comes before the thread files another request continues it.
2211        if delivery && delivery_runs_handler(guest, signal.raw()) {
2212            guest.thread_state_mut().delivered_since_request = true;
2213        }
2214        tool_local::InboundTurn {
2215            dettid,
2216            mycount,
2217            boundary_token,
2218        }
2219    }
2220
2221    /// What an announced delivery's turn left for its commit
2222    /// ([`TurnScope::Announcement`]), in the order a whole delivery does it:
2223    /// the sender's hold ends, the delivery is counted and traced, a claimed
2224    /// `SIGCHLD` is placed, and a handler that runs marks the thread's next
2225    /// request as continuing it.
2226    /// Returns the delivery's ordinal, the thread's signal count before it.
2227    async fn inbound_turn_settled<G: Guest<Self>>(&self, guest: &mut G, signal: SigWrapper) -> u64 {
2228        if guest.thread_state().own_signal_turn_hold == Some(signal.raw()) {
2229            guest.thread_state_mut().own_signal_turn_hold = None;
2230        }
2231        let mycount = guest.thread_state().stats.signal_count;
2232        info!(
2233            "[dtid {}] handling inbound signal (#{}) {}",
2234            guest.thread_state().dettid,
2235            mycount,
2236            signal
2237        );
2238        guest.thread_state_mut().stats.count_signal();
2239        if self.cfg.sequentialize_threads && self.cfg.observes_schedevents() {
2240            let dettid = guest.thread_state().dettid;
2241            let nanos = guest.thread_state().thread_logical_time.as_nanos();
2242            let access = self.access_trace_signal(guest, signal);
2243            trace_schedevent_with_access(
2244                guest,
2245                SchedEvent {
2246                    dettid,
2247                    op: Op::SignalReceived(signal),
2248                    count: 1,
2249                    start_rip: None,
2250                    end_rip: None,
2251                    end_time: Some(nanos),
2252                },
2253                true,
2254                access,
2255            )
2256            .await;
2257        }
2258        if signal.signal() == Some(Signal::SIGCHLD)
2259            && std::mem::take(&mut guest.thread_state_mut().claimed_sigchld)
2260        {
2261            tool_global::child_exit_sigchld(
2262                guest,
2263                scheduler::child_exit_sigchld::ChildExitSigchldControl::ClaimedDelivery,
2264            )
2265            .await;
2266        }
2267        if delivery_runs_handler(guest, signal.raw()) {
2268            guest.thread_state_mut().delivered_since_request = true;
2269        }
2270        mycount
2271    }
2272
2273    /// What the granted delivery of `signal` settles: a fatal delivery's
2274    /// group exit, a timer entry's siginfo, robust-list wakes, the handler's
2275    /// restore mask, and the post-handler hook.
2276    async fn inbound_delivered<G: Guest<Self>>(
2277        &self,
2278        guest: &mut G,
2279        signal: SigWrapper,
2280        turn: tool_local::InboundTurn,
2281        timer_entry: bool,
2282    ) -> Result<Option<SigWrapper>, Errno> {
2283        let tool_local::InboundTurn {
2284            dettid,
2285            mycount,
2286            boundary_token,
2287        } = turn;
2288        // A delivery that ends the process is committed in this turn
2289        // (`Scheduler::commit_fatal_delivery`). Under `--pipe-wake-latency`
2290        // or `--virtual-timeout` the doom feeds the hand-off scan
2291        // (`scheduler::pipe_wake`) and the bound (`thread_is_doomed`). Where the
2292        // kernel's signal state is the guest's own (ptrace), the process's
2293        // other threads also leave the scheduler now, as Linux kills them at
2294        // this delivery, instead of at their host-timed exit reports. That
2295        // needs the kernel to carry out the default action when it resumes
2296        // this thread, as its signal state says now: the signal is unblocked
2297        // (`ptrace_signal` requeues a blocked one) and no gdbserver client
2298        // can change or cancel it at the resume.
2299        let may_retire_others = crate::syscalls::retires_others_at_fatal_delivery(&self.cfg);
2300        let asks_about_fatal_delivery = may_retire_others
2301            || self.cfg.pipe_wake_latency_in_effect().is_some()
2302            || self.cfg.virtual_timeout.is_some();
2303        let input_facility = crate::reserved_input::capability(guest).is_some();
2304        let fatal_observation = if input_facility {
2305            crate::syscalls::fatal_delivery_observation(guest, signal.raw(), may_retire_others)
2306        } else if asks_about_fatal_delivery {
2307            match crate::syscalls::fatal_default_delivery(guest, signal.raw(), may_retire_others) {
2308                Some(may_retire_others) => {
2309                    crate::syscalls::FatalDeliveryObservation::Fatal { may_retire_others }
2310                }
2311                None => crate::syscalls::FatalDeliveryObservation::Nonfatal,
2312            }
2313        } else {
2314            crate::syscalls::FatalDeliveryObservation::Unknown
2315        };
2316        if input_facility
2317            && fatal_observation != crate::syscalls::FatalDeliveryObservation::Nonfatal
2318        {
2319            crate::reserved_input::freeze_group(guest).unwrap_or_else(|error| {
2320                panic!("reserved-input pre-delivery ownership failure: {error}")
2321            });
2322        }
2323        let retired_the_others = asks_about_fatal_delivery
2324            && match fatal_observation {
2325                crate::syscalls::FatalDeliveryObservation::Fatal {
2326                    may_retire_others: retire_others,
2327                } => tool_global::commit_fatal_delivery(guest, retire_others).await,
2328                _ => false,
2329            };
2330        crate::reserved_input::after_fatal_commit(guest, fatal_observation, retired_the_others)
2331            .unwrap_or_else(|error| {
2332                panic!("reserved-input post-delivery ownership failure: {error}")
2333            });
2334        // A timer entry's siginfo, with its overrun counted now that the
2335        // delivery's turn is granted, where the handler's time begins.
2336        if timer_entry {
2337            use scheduler::discarded_at_send::DiscardedSignalAnswer;
2338            use scheduler::posix_timer_coalesce::DeliveryAnswer;
2339            let control = scheduler::discarded_at_send::DiscardedSignalControl::DeliveryInfo {
2340                signal: signal.raw(),
2341                at: guest.thread_state().thread_logical_time.as_nanos(),
2342            };
2343            if let DiscardedSignalAnswer::Delivery(DeliveryAnswer::Rewrite(info)) =
2344                tool_global::discarded_signal(guest, control).await
2345                && let Err(errno) = guest.set_signal_info(info.to_user())
2346            {
2347                warn!(
2348                    "[dtid {}] signal {} keeps its physical siginfo: {}",
2349                    guest.thread_state().dettid,
2350                    signal,
2351                    errno
2352                );
2353            }
2354        }
2355        if self
2356            .end_thread_group_before_fatal_delivery(guest, signal)
2357            .await
2358        {
2359            // As an exit_group does after its grant, file nothing more:
2360            // the group's turns are held until it has ended, which needs
2361            // this thread to die (Claude review of
2362            // https://github.com/rrnewton/hermit/pull/4387, finding 4).
2363            info!(
2364                "[dtid {}] finish delivering signal (#{}) {}",
2365                dettid, mycount, signal
2366            );
2367            return Ok(Some(signal));
2368        }
2369        // A delivered signal may be caught, ignored, or terminate the
2370        // thread group. Reading the lists is harmless; the collected
2371        // wakes remain inert unless the ptrace exit callback later reports
2372        // that this exact signal caused physical exit.
2373        self.stage_thread_group_robust_list_wakes(
2374            guest,
2375            tool_local::RobustListExit::Signal(signal.raw()),
2376        )
2377        .await;
2378        info!(
2379            "[dtid {}] finish delivering signal (#{}) {}",
2380            dettid, mycount, signal
2381        );
2382        if retired_the_others {
2383            // As an exit_group does after its grant, file nothing more: turn
2384            // selection waits for the retired threads' deaths
2385            // (`Scheduler::sigkill_fence_holds`), which only this delivery
2386            // causes, so a request filed here would never be granted.
2387            return Ok(Some(signal));
2388        }
2389        self.record_handler_restore_mask(guest, signal).await;
2390
2391        self.post_handler_hook_observed(guest, CheckIn::Event, boundary_token)
2392            .await;
2393        #[cfg(test)]
2394        Self::note_test_phase("delivered");
2395        #[cfg(test)]
2396        signal_announcement_tests::grant_during_commit_if_asked(guest.thread_state_mut());
2397        Ok(Some(signal))
2398    }
2399
2400    /// Take back the POSIX timer copies a successful exec flushes
2401    /// (`Scheduler::take_exec_timer_flush`), one zero-timeout
2402    /// `rt_sigtimedwait` each, before the new image runs. Linux discards a
2403    /// queued timer signal at exec (`flush_itimer_signals`) and keeps every
2404    /// other pending signal; Hermit sends a timer's expiration as an ordinary
2405    /// signal, which exec would keep
2406    /// (<https://github.com/rrnewton/hermit/issues/4184>). A copy that cannot
2407    /// be singled out refuses the run by name.
2408    async fn flush_exec_timer_copies<G: Guest<Self>>(&self, guest: &mut G) {
2409        let dettid = guest.thread_state().dettid;
2410        let refusal = match tool_global::exec_timer_flush(guest).await {
2411            Err(reason) => Some(reason),
2412            Ok(flush) => {
2413                let mut refusal = None;
2414                // The thread's own queue is dequeued first, as Linux does.
2415                for set in [flush.own, flush.shared] {
2416                    for signo in
2417                        (1..=64).filter(|signo| set & scheduler::kernel_signal_bit(*signo) != 0)
2418                    {
2419                        match syscalls::take_pending_signal(guest, signo).await {
2420                            Ok(taken) if taken == signo as i64 => info!(
2421                                "[detcore, dtid {}] exec flushed a POSIX timer's pending signal {}",
2422                                dettid, signo
2423                            ),
2424                            other => {
2425                                refusal = Some(format!(
2426                                    "taking back a POSIX timer's pending signal {signo} at exec answered {other:?}"
2427                                ));
2428                                break;
2429                            }
2430                        }
2431                    }
2432                }
2433                refusal
2434            }
2435        };
2436        if let Some(reason) = refusal {
2437            let message = format!(
2438                "hermit: thread {dettid} exec'd with a POSIX timer's signal possibly pending: {reason}; Linux discards a queued timer signal at exec, and Detcore cannot single this one out, so the run is refused (https://github.com/rrnewton/hermit/issues/4184)"
2439            );
2440            error!("{message}");
2441            let _ = writeln!(crate::util::RetryingStderr, "{message}");
2442            tool_global::unrecoverable_shutdown(guest, detcore_model::HERMIT_POLICY_REFUSAL_EXIT)
2443                .await;
2444        }
2445    }
2446
2447    /// After a dropped signal's delivery stop, which made no other hook run:
2448    /// request the thread's timer event again for the same point of its
2449    /// branch clock as the last handler armed it (`post_handler_hook`). The
2450    /// stop is a Tool callback, which cancels the event, and nothing else
2451    /// re-arms it, so a thread spinning in user code would run on unpreempted
2452    /// until its next system call: measured, a chaos schedule whose sibling
2453    /// took a dropped kernel `SIGCHLD` spun about 4e10 branches past its
2454    /// 2e7-branch slice (https://github.com/rrnewton/hermit/issues/4304). The
2455    /// point is fixed by the clock value of that handler, not by when the
2456    /// host-timed stop came, and no logical time is charged here, so the stop
2457    /// stays invisible to the schedule.
2458    fn keep_timer_across_dropped_signal<G: Guest<Self>>(&self, guest: &mut G) {
2459        if !self.cfg.use_preemption_timer() {
2460            return;
2461        }
2462        if let Some(end) = guest.thread_state().lone_slice_end {
2463            let now = guest.read_clock().expect("Couldn't read clock");
2464            arm_lone_backstop(guest, end.saturating_sub(now));
2465            return;
2466        }
2467        let thread_state = guest.thread_state();
2468        let Some(armed) = thread_state.last_rcb_timer else {
2469            return;
2470        };
2471        let thread_state_committed = thread_state.committed_clock_value;
2472        // The stop cancelled the event: an unreadable clock leaves the thread
2473        // unpreempted, so it fails closed, as every handler's clock read does.
2474        let now = guest.read_clock().expect("Couldn't read clock");
2475        assert!(thread_state_committed <= now);
2476        let schedule = TimerSchedule::Rcbs(kept_timer_rcbs(thread_state_committed, armed, now));
2477        let armed = if guest.config().imprecise_timers {
2478            guest.set_timer(schedule)
2479        } else {
2480            guest.set_timer_precise(schedule)
2481        };
2482        armed.expect("Failed to set timer");
2483    }
2484
2485    /// Install Linux timer metadata only for the exact first kept expiry
2486    /// claimed before this callback advances logical time.
2487    async fn normalize_reported_itimer_real<G: Guest<Self>>(&self, guest: &mut G) {
2488        use scheduler::discarded_at_send::DiscardedSignalAnswer;
2489        use scheduler::discarded_at_send::DiscardedSignalControl;
2490        if !scheduler::discarded_at_send::records(&self.cfg) {
2491            return;
2492        }
2493        let Some(info) = guest.signal_info() else {
2494            return;
2495        };
2496        if info == scheduler::discarded_at_send::itimer_real_siginfo() {
2497            // A claimed copy may have requeued after a blocked resume. Its
2498            // canonical payload is already installed; do not claim a newer
2499            // publication that arrived while this copy was in flight.
2500            return;
2501        }
2502        if tool_global::discarded_signal(guest, DiscardedSignalControl::ClaimItimerReal).await
2503            != DiscardedSignalAnswer::ItimerReal(true)
2504        {
2505            return;
2506        }
2507        if let Err(errno) =
2508            guest.set_signal_info(scheduler::discarded_at_send::itimer_real_siginfo())
2509        {
2510            tool_global::report_determinism_loss(
2511                guest,
2512                format!("ITIMER_REAL_SIGINFO_UNAVAILABLE: owned timer delivery cannot install Linux siginfo: {errno}"),
2513            ).await;
2514            guest_log::flush_unsent();
2515            match guest
2516                .tail_inject(
2517                    reverie::syscalls::ExitGroup::new()
2518                        .with_status(detcore_model::HERMIT_POLICY_REFUSAL_EXIT),
2519                )
2520                .await {}
2521        }
2522    }
2523
2524    /// Whether a `SIGCHLD` that stopped this thread is delivered, on a backend
2525    /// that reports child-exit publication, where both the kernel and the
2526    /// scheduler send a child's exit `SIGCHLD` (`scheduler::child_exit_sigchld`).
2527    /// The first one delivered for a child's exit stands for it, and a copy
2528    /// of either kind that arrives after it is not delivered. The scheduler's
2529    /// copy (`SI_USER` from this tracer) is delivered with the siginfo Linux
2530    /// gives the parent. Any other `SIGCHLD` is delivered as it is.
2531    async fn child_exit_sigchld_delivers<G: Guest<Self>>(&self, guest: &mut G) -> bool {
2532        use scheduler::child_exit_sigchld::ChildExitSigchldAnswer;
2533        use scheduler::child_exit_sigchld::OwnCopy;
2534        let Some(info) = guest.signal_info() else {
2535            return true;
2536        };
2537        // A delivery stop: a kept copy's `InboundSignal` request follows.
2538        let control = scheduler::child_exit_sigchld::taken_control(&info, true);
2539        match tool_global::child_exit_sigchld(guest, control).await {
2540            ChildExitSigchldAnswer::Claimed(dropped) => !dropped,
2541            ChildExitSigchldAnswer::OwnCopy(OwnCopy::Duplicate) => false,
2542            ChildExitSigchldAnswer::OwnCopy(OwnCopy::Deliver(own))
2543            | ChildExitSigchldAnswer::ClaimedFor(own) => {
2544                if let Err(errno) = guest.set_signal_info(own.to_bytes()) {
2545                    warn!(
2546                        "[dtid {}] the SIGCHLD for child {} keeps its siginfo: {}",
2547                        guest.thread_state().dettid,
2548                        own.pid,
2549                        errno
2550                    );
2551                }
2552                true
2553            }
2554            _ => true,
2555        }
2556    }
2557
2558    #[cfg(test)]
2559    async fn pre_handler_hook<G: Guest<Self>>(
2560        &self,
2561        guest: &mut G,
2562        precise_branch: bool,
2563        boundary: CheckIn,
2564    ) {
2565        self.pre_handler_hook_observed(guest, precise_branch, boundary, None)
2566            .await;
2567    }
2568
2569    async fn pre_handler_hook_observed<G: Guest<Self>>(
2570        &self,
2571        guest: &mut G,
2572        precise_branch: bool,
2573        boundary: CheckIn,
2574        entry: Option<slice_boundary::callbacks::Callback>,
2575    ) -> Option<preemptions::callback_history::Token> {
2576        let mut token = preemptions::callback_history::begin(guest.thread_state_mut(), entry)
2577            .unwrap_or_else(|error| crate::scheduler::exit_on_scheduler_refusal(&error));
2578        // A handler that left early (an error return) may not have cleared
2579        // these; no request made by this new handler belongs to that syscall,
2580        // and a wake that syscall made no longer ends the slice.
2581        guest.thread_state_mut().in_uncharged_bootstrap_syscall = false;
2582        guest.thread_state_mut().yield_after_futex_wake = false;
2583        guest.thread_state_mut().returned_from_sigreturn = false;
2584        let dettid = guest.thread_state().dettid;
2585        let evs = self.update_logical_time_rcbs(guest, precise_branch).await;
2586
2587        if guest.thread_state().replaying_lone_slice_end.is_some() {
2588            // The guest is stopped later, not at this end: its registers are
2589            // not this end's.
2590            detlog_debug!(
2591                "(pre) registers [dtid {}][rcbs {}]. not read: slice end taken without a stop",
2592                dettid,
2593                guest.thread_state().thread_logical_time.rcbs(),
2594            );
2595        } else if guest.thread_state().guest_past_first_execve() {
2596            detlog_debug!(
2597                "(pre) registers [dtid {}][rcbs {}]. {}",
2598                dettid,
2599                guest.thread_state().thread_logical_time.rcbs(),
2600                self.observed_regs(guest).await.display()
2601            );
2602        }
2603        trace!(
2604            "prehook [dtid {}] Updating rcbs and checking time remaining.",
2605            dettid
2606        );
2607        if let Some(vec) = evs {
2608            for ev in vec {
2609                trace_schedevent(guest, ev, false).await;
2610            }
2611        }
2612
2613        // The semantic entry is observed after the original guest-RCB update,
2614        // before any existing path can reset the slice. The owned token stays
2615        // with this handler across nested callbacks and awaits.
2616        preemptions::callback_history::pre(guest.thread_state_mut(), &mut token)
2617            .unwrap_or_else(|error| crate::scheduler::exit_on_scheduler_refusal(&error));
2618        self.pipe_hand_off_if_due(guest).await;
2619        self.end_timeslice_if_needed(
2620            guest,
2621            boundary,
2622            &mut token,
2623            preemptions::callback_history::Site::Prehook,
2624        )
2625        .await;
2626        token
2627    }
2628
2629    // AUTONOMOUS-BOT-IMPLEMENTED
2630    /// Yield when accumulated logical time reaches the syscall-boundary target deadline.
2631    async fn end_timeslice_if_needed<G: Guest<Self>>(
2632        &self,
2633        guest: &mut G,
2634        boundary: CheckIn,
2635        token: &mut Option<preemptions::callback_history::Token>,
2636        site: preemptions::callback_history::Site,
2637    ) {
2638        if let Some(ends) = slice_boundary::replay_expiration(guest.thread_state())
2639            .unwrap_or_else(|error| crate::scheduler::exit_on_scheduler_refusal(&error))
2640        {
2641            if ends {
2642                self.end_timeslice(guest).await;
2643            }
2644            return;
2645        }
2646        let thread_state = guest.thread_state();
2647        // A thread that sent a signal to its own process keeps its turn until
2648        // it dequeues that signal (`own_signal_turn_hold`).
2649        if let Some(signal) = thread_state.own_signal_turn_hold {
2650            trace!(
2651                "[dtid {}] keeps its turn until it takes signal {}",
2652                thread_state.dettid, signal
2653            );
2654            return;
2655        }
2656        if self
2657            .replay_callback_checkpoint(guest, boundary, token, site)
2658            .await
2659        {
2660            return;
2661        }
2662        // An annotated priority-history row ends only at its recorded
2663        // callback endpoint, never at the first stop that reaches its RCBs.
2664        if self
2665            .priority_checkpoint_at(guest, boundary, token, site)
2666            .await
2667        {
2668            return;
2669        }
2670        let thread_state = guest.thread_state();
2671        let Some(slice_end) = thread_state.end_of_timeslice else {
2672            return;
2673        };
2674        let ends = match boundary {
2675            CheckIn::Event => thread_state.timeslice_expired(),
2676            #[cfg(target_arch = "x86_64")]
2677            CheckIn::InstructionTrap => thread_state.instruction_trap_ends_timeslice(&self.cfg),
2678        };
2679        if !ends {
2680            return;
2681        }
2682
2683        trace!(
2684            "[dtid {}] logical time {} reached timeslice target {}",
2685            thread_state.dettid,
2686            thread_state.thread_logical_time.as_nanos(),
2687            slice_end
2688        );
2689        let origin = self.record_callback_origin(guest, token, site);
2690        let checkin = PriorityCheckin {
2691            token: token.as_ref(),
2692            site,
2693        };
2694        self.end_timeslice_with_checkpoint(guest, false, origin, boundary, Some(checkin))
2695            .await;
2696    }
2697
2698    fn record_callback_origin<G: Guest<Self>>(
2699        &self,
2700        guest: &G,
2701        token: &Option<preemptions::callback_history::Token>,
2702        site: preemptions::callback_history::Site,
2703    ) -> Option<preemptions::callback_history::Origin> {
2704        if !preemptions::callback_history::recording_enabled(&self.cfg) {
2705            return None;
2706        }
2707        let thread = guest.thread_state();
2708        thread.callback_history.origin(thread, token, site)
2709    }
2710
2711    async fn replay_callback_checkpoint<G: Guest<Self>>(
2712        &self,
2713        guest: &mut G,
2714        boundary: CheckIn,
2715        token: &mut Option<preemptions::callback_history::Token>,
2716        site: preemptions::callback_history::Site,
2717    ) -> bool {
2718        if self.cfg.replay_schedule_from.is_none()
2719            || self.cfg.strict_schedule_boundaries
2720            || guest.thread_state().own_signal_turn_hold.is_some()
2721        {
2722            return false;
2723        }
2724        let thread = guest.thread_state();
2725        if let Some(token) = token {
2726            token.note_cut(thread.callback_history.progress(), site);
2727        }
2728        let Some(actual) = thread.callback_history.origin(thread, token, site) else {
2729            return false;
2730        };
2731        let Some(origin) = self
2732            .take_callback_checkpoint(guest, actual, token.as_ref())
2733            .await
2734        else {
2735            return false;
2736        };
2737        self.end_timeslice_with_checkpoint(guest, false, Some(origin), boundary, None)
2738            .await;
2739        true
2740    }
2741
2742    /// Priority-history replay of a callback-annotated record
2743    /// (`Config::annotated_priority_replay`): decide this actual check-in
2744    /// for the thread's active recorded row. True when it decided: the row
2745    /// ended at its recorded endpoint, or that endpoint is still ahead and the
2746    /// slice must not end here. False when the active row has no checkpoint,
2747    /// and the legacy expiry decides. A refusal ends the run.
2748    async fn priority_checkpoint_at<G: Guest<Self>>(
2749        &self,
2750        guest: &mut G,
2751        boundary: CheckIn,
2752        token: &mut Option<preemptions::callback_history::Token>,
2753        site: preemptions::callback_history::Site,
2754    ) -> bool {
2755        if !self.cfg.annotated_priority_replay
2756            || guest.thread_state().active_priority_row().is_none()
2757        {
2758            return false;
2759        }
2760        if let Some(token) = token.as_mut() {
2761            token.note_cut(guest.thread_state().callback_history.progress(), site);
2762        }
2763        match self.decide_priority_row(guest, token.as_ref(), Some(site)) {
2764            preemptions::callback_history::RowDecision::NoCheckpoint => false,
2765            preemptions::callback_history::RowDecision::Pending => true,
2766            preemptions::callback_history::RowDecision::Match(checkpoint) => {
2767                let checkin = PriorityCheckin {
2768                    token: token.as_ref(),
2769                    site,
2770                };
2771                self.end_timeslice_with_checkpoint(
2772                    guest,
2773                    false,
2774                    Some(checkpoint.origin),
2775                    boundary,
2776                    Some(checkin),
2777                )
2778                .await;
2779                true
2780            }
2781            preemptions::callback_history::RowDecision::Refuse {
2782                refusal,
2783                checkpoint,
2784                actual,
2785            } => {
2786                self.refuse_priority_checkpoint(guest, refusal, checkpoint, actual)
2787                    .await;
2788                unreachable!("a priority-history refusal ends the run")
2789            }
2790        }
2791    }
2792
2793    /// The decision for the thread's active recorded row at a check-in of
2794    /// `token`'s callback at `site` (`None`: no callback identity), with
2795    /// progress, time and RCBs observed now.
2796    fn decide_priority_row<G: Guest<Self>>(
2797        &self,
2798        guest: &mut G,
2799        token: Option<&preemptions::callback_history::Token>,
2800        site: Option<preemptions::callback_history::Site>,
2801    ) -> preemptions::callback_history::RowDecision {
2802        let thread = guest.thread_state();
2803        let Some(active) = thread.active_priority_row() else {
2804            return preemptions::callback_history::RowDecision::NoCheckpoint;
2805        };
2806        let actual = site.and_then(|site| thread.callback_history.observed(thread, token, site));
2807        let rcbs = thread.committed_clock_value;
2808        guest
2809            .thread_state_mut()
2810            .callback_history
2811            .decide_row(active, actual, token, rcbs)
2812    }
2813
2814    /// Refuse a recorded callback checkpoint of priority-history replay by
2815    /// name. With an actual origin, the scheduler first pops its own copy of
2816    /// the same checkpoint and records the DESYNC line, so both custodies
2817    /// consume it; a check-in without a callback identity has only the local
2818    /// one, and the refusal says so. Never returns.
2819    async fn refuse_priority_checkpoint<G: Guest<Self>>(
2820        &self,
2821        guest: &mut G,
2822        refusal: preemptions::callback_history::Refusal,
2823        checkpoint: preemptions::callback_history::Checkpoint,
2824        actual: Option<preemptions::callback_history::Origin>,
2825    ) {
2826        let dettid = guest.thread_state().dettid;
2827        let observed = match &actual {
2828            Some(actual) => format!("observed {actual:?}"),
2829            None => "observed a check-in with no callback identity (scheduler custody not \
2830                     reported)"
2831                .to_owned(),
2832        };
2833        let message = format!(
2834            "{}: dtid {} priority-history row {}: expected {:?}, {observed}",
2835            refusal.name(),
2836            dettid,
2837            checkpoint.history_index,
2838            checkpoint.origin,
2839        );
2840        if let Some(actual) = actual {
2841            let (time, response) = tool_global::send_and_update_time(
2842                guest,
2843                tool_global::GlobalRequest::CallbackCheckpointMismatch {
2844                    checkpoint,
2845                    hard: true,
2846                    actual,
2847                },
2848            )
2849            .await;
2850            assert_eq!(
2851                (time, response),
2852                (
2853                    None,
2854                    tool_global::GlobalResponse::CallbackCheckpointMismatch(())
2855                )
2856            );
2857        }
2858        crate::scheduler::exit_on_replay_refusal(&message)
2859    }
2860
2861    fn callback_checkpoint_expiry_due<G: Guest<Self>>(
2862        &self,
2863        guest: &G,
2864        boundary: CheckIn,
2865        site: preemptions::callback_history::Site,
2866    ) -> bool {
2867        let thread = guest.thread_state();
2868        if thread.own_signal_turn_hold.is_some() {
2869            return false;
2870        }
2871        if site == preemptions::callback_history::Site::PosthookPmuEpsilon {
2872            let epsilon = thread.pmu_epsilon(guest.config());
2873            return thread.replay_rcb_end.is_none()
2874                && thread.max_timeslice_end.is_some_and(|maximum| {
2875                    thread.thread_logical_time.as_nanos() + epsilon > maximum
2876                });
2877        }
2878        thread.end_of_timeslice.is_some()
2879            && match boundary {
2880                CheckIn::Event => thread.timeslice_expired(),
2881                #[cfg(target_arch = "x86_64")]
2882                CheckIn::InstructionTrap => thread.instruction_trap_ends_timeslice(&self.cfg),
2883            }
2884    }
2885
2886    async fn take_callback_checkpoint<G: Guest<Self>>(
2887        &self,
2888        guest: &mut G,
2889        actual: preemptions::callback_history::Origin,
2890        token: Option<&preemptions::callback_history::Token>,
2891    ) -> Option<preemptions::callback_history::Origin> {
2892        loop {
2893            match guest
2894                .thread_state_mut()
2895                .callback_history
2896                .check_at(actual.clone(), token)
2897            {
2898                preemptions::callback_history::Decision::Wait => return None,
2899                preemptions::callback_history::Decision::Act(origin) => return Some(origin),
2900                preemptions::callback_history::Decision::Mismatch {
2901                    checkpoint,
2902                    hard,
2903                    actual,
2904                } => {
2905                    let (time, response) = tool_global::send_and_update_time(
2906                        guest,
2907                        tool_global::GlobalRequest::CallbackCheckpointMismatch {
2908                            checkpoint,
2909                            hard,
2910                            actual,
2911                        },
2912                    )
2913                    .await;
2914                    assert_eq!(
2915                        (time, response),
2916                        (
2917                            None,
2918                            tool_global::GlobalResponse::CallbackCheckpointMismatch(())
2919                        )
2920                    );
2921                }
2922            }
2923        }
2924    }
2925
2926    /// A common hook called at the end of *every* handler, just before returning control
2927    /// to the guest. This enforces the logical target and resets the PMU maximum timer.
2928    ///
2929    /// However, note that the thread's timeslice (turn) may have expired DURING this handler.
2930    /// Therefore the timeslice can end in the posthook as well as in the prehook.
2931    #[cfg(test)]
2932    async fn post_handler_hook<G: Guest<Self>>(&self, guest: &mut G, boundary: CheckIn) {
2933        self.post_handler_hook_observed(guest, boundary, None).await;
2934    }
2935
2936    async fn post_handler_hook_observed<G: Guest<Self>>(
2937        &self,
2938        guest: &mut G,
2939        boundary: CheckIn,
2940        mut token: Option<preemptions::callback_history::Token>,
2941    ) {
2942        preemptions::callback_history::post(guest.thread_state_mut(), &token)
2943            .unwrap_or_else(|error| crate::scheduler::exit_on_scheduler_refusal(&error));
2944        // Only `kill` and `rt_sigqueueinfo` set the hold, and they wake no
2945        // futex waiter (https://github.com/rrnewton/hermit/issues/4034).
2946        assert!(
2947            guest.thread_state().own_signal_turn_hold.is_none()
2948                || !guest.thread_state().yield_after_futex_wake,
2949            "a futex wake must not end the turn a signal to the sender's own process holds"
2950        );
2951        if std::mem::take(&mut guest.thread_state_mut().yield_after_futex_wake) {
2952            // A turn of its own, outside the timeslice bookkeeping: the slice
2953            // keeps its deadlines, draws nothing from the thread's chaos PRNG
2954            // and consumes no recorded preemption point, so a replay that
2955            // makes the same decision stays aligned with its record. (Under
2956            // a random `--sched-heuristic` the turn's selection draws from the
2957            // run queue's generator, as every turn does, identically in record
2958            // and replay.)
2959            trace!(
2960                "posthook [dtid {}] FUTEX_WAKE woke a waiter; yielding to it",
2961                guest.thread_state().dettid
2962            );
2963            let request = Self::yield_request(guest);
2964            resource_request(guest, request).await;
2965        }
2966        self.pipe_hand_off_if_due(guest).await;
2967        self.end_timeslice_if_needed(
2968            guest,
2969            boundary,
2970            &mut token,
2971            preemptions::callback_history::Site::Posthook,
2972        )
2973        .await;
2974        self.replay_callback_checkpoint(
2975            guest,
2976            boundary,
2977            &mut token,
2978            preemptions::callback_history::Site::PosthookPmuEpsilon,
2979        )
2980        .await;
2981        // A row recorded as ending within one RCB of the PMU maximum ends at
2982        // the same posthook site here; the recorded site is the authority,
2983        // not a re-derived epsilon test. The signal hold stays authoritative.
2984        if guest.thread_state().own_signal_turn_hold.is_none() {
2985            self.priority_checkpoint_at(
2986                guest,
2987                boundary,
2988                &mut token,
2989                preemptions::callback_history::Site::PosthookPmuEpsilon,
2990            )
2991            .await;
2992        }
2993
2994        let dettid = guest.thread_state().dettid;
2995        let mut current_time = guest.thread_state().thread_logical_time.as_nanos();
2996
2997        if let Some(mut max_timeslice_end) = guest.thread_state().max_timeslice_end {
2998            assert!(guest.config().max_timeslice.is_some());
2999            let mut replay_rcb_end = guest.thread_state().replay_rcb_end;
3000            // TODO: get rid of fractional NANOS_PER_RCB so it's clear that this does not lose precision:
3001            // AUTONOMOUS-BOT-IMPLEMENTED
3002            // TODO-HUMAN-REVIEW(PR-1151)
3003            let mut clock_multiplier = guest.thread_state().clock_multiplier(guest.config());
3004            let mut epsilon = guest.thread_state().pmu_epsilon(guest.config());
3005
3006            if replay_rcb_end.is_none() && current_time + epsilon > max_timeslice_end {
3007                if guest.thread_state().own_signal_turn_hold.is_some() {
3008                    // No guest instruction runs before the delivery stop that
3009                    // ends the hold, and that stop's post-hook ends this slice
3010                    // and sets the next timer (`own_signal_turn_hold`).
3011                    trace!(
3012                        "posthook [dtid {}] past PMU maximum {} while it keeps its turn",
3013                        dettid, max_timeslice_end
3014                    );
3015                    return;
3016                }
3017                trace!(
3018                    "posthook [dtid {}] less than one RCB remains before PMU maximum {}; ending slice",
3019                    dettid, max_timeslice_end
3020                );
3021                let origin = self.record_callback_origin(
3022                    guest,
3023                    &token,
3024                    preemptions::callback_history::Site::PosthookPmuEpsilon,
3025                );
3026                let checkin = PriorityCheckin {
3027                    token: token.as_ref(),
3028                    site: preemptions::callback_history::Site::PosthookPmuEpsilon,
3029                };
3030                self.end_timeslice_with_checkpoint(guest, false, origin, boundary, Some(checkin))
3031                    .await;
3032                max_timeslice_end = guest
3033                    .thread_state()
3034                    .max_timeslice_end
3035                    .expect("ending a PMU-backed timeslice must install a new maximum");
3036                current_time = guest.thread_state().thread_logical_time.as_nanos();
3037                replay_rcb_end = guest.thread_state().replay_rcb_end;
3038                if guest.config().replay_edited_preemptions {
3039                    // The slice ending path chose the new slice with the
3040                    // margin at the chaos slowdown it installed, which can
3041                    // differ from the one before; check against that one.
3042                    // A faithful replay keeps the earlier values for this
3043                    // check and the timer below, as its recording did, so
3044                    // that existing records replay unchanged.
3045                    clock_multiplier = guest.thread_state().clock_multiplier(guest.config());
3046                    epsilon = guest.thread_state().pmu_epsilon(guest.config());
3047                }
3048            }
3049            if replay_rcb_end.is_none() && current_time + epsilon > max_timeslice_end {
3050                panic!(
3051                    "Ended time slice, but current time {} is still beyond PMU maximum {}",
3052                    current_time, max_timeslice_end
3053                );
3054            }
3055
3056            let current_rcbs = guest.thread_state().thread_logical_time.rcbs();
3057            let current_pmu_rcbs = guest.thread_state().committed_clock_value;
3058            // A callback-annotated priority history (`annotated_priority_replay`)
3059            // holds a row whose recorded endpoint is still ahead: its RCBs,
3060            // and whether the recorded endpoint is the PMU timer's own stop.
3061            let pending_checkpoint = if self.cfg.annotated_priority_replay {
3062                guest.thread_state().pending_checkpoint().map(|checkpoint| {
3063                    (
3064                        checkpoint.origin.rcbs,
3065                        checkpoint.origin.callback == slice_boundary::callbacks::Callback::PmuTimer,
3066                    )
3067                })
3068            } else {
3069                None
3070            };
3071            let pending_checkpoint_rcbs = pending_checkpoint.map(|(rcbs, _)| rcbs);
3072            if let Some(replay_rcb_end) = replay_rcb_end
3073                && self.cfg.annotated_priority_replay
3074                && guest.thread_state().own_signal_turn_hold.is_some()
3075                && current_pmu_rcbs >= pending_checkpoint_rcbs.unwrap_or(replay_rcb_end)
3076            {
3077                // As at the PMU maximum above: no guest instruction runs
3078                // before the delivery stop that ends the hold, and that stop's
3079                // hooks decide the row and set the next timer. The hold, the
3080                // request and the pending checkpoint are left as they are.
3081                trace!(
3082                    "posthook [dtid {}] at its recorded RCB deadline {} while it keeps its turn",
3083                    dettid, replay_rcb_end
3084                );
3085                return;
3086            }
3087            // The timer aims one RCB past an annotated row's endpoint unless
3088            // the recorded endpoint is the timer's own stop, and always once
3089            // the thread waits at the endpoint's RCBs. The recorded path
3090            // reaches a software endpoint with no stop at those RCBs, and
3091            // with no further conditional branch, so the timer fires only
3092            // for a replay that branches past the endpoint, whose next
3093            // check-in refuses it; the recording saw no stop there, and
3094            // neither does a faithful replay. This is a physical arm target
3095            // only; the recorded endpoint is unchanged.
3096            let replay_rcb_end = match (replay_rcb_end, pending_checkpoint) {
3097                (Some(end), Some((endpoint, timer_endpoint)))
3098                    if !timer_endpoint || endpoint == current_pmu_rcbs =>
3099                {
3100                    assert_eq!(end, endpoint, "a checkpoint's RCBs are its row's");
3101                    Some(endpoint.checked_add(1).unwrap_or_else(|| {
3102                        crate::scheduler::exit_on_scheduler_refusal(&format!(
3103                            "{}: dtid {} has no RCB beyond its recorded endpoint {} to aim at",
3104                            preemptions::callback_history::Refusal::Overshot.name(),
3105                            dettid,
3106                            endpoint
3107                        ))
3108                    }))
3109                }
3110                (other, _) => other,
3111            };
3112            // With an annotated row pending, a target that is not ahead of the
3113            // committed RCBs means the thread is already past the recorded
3114            // endpoint: refuse that by name. The assertion below still guards
3115            // every other path.
3116            if let (Some(target), Some((endpoint, _))) = (replay_rcb_end, pending_checkpoint)
3117                && target <= current_pmu_rcbs
3118            {
3119                crate::scheduler::exit_on_scheduler_refusal(&format!(
3120                    "{}: dtid {} is at RCB {}, past its recorded endpoint {}",
3121                    preemptions::callback_history::Refusal::Overshot.name(),
3122                    dettid,
3123                    current_pmu_rcbs,
3124                    endpoint
3125                ));
3126            }
3127            let (ns_remaining, max_rcbs_remaining) = if let Some(replay_rcb_end) = replay_rcb_end {
3128                assert!(
3129                    replay_rcb_end > current_pmu_rcbs,
3130                    "recorded PMU RCB deadline {} is not ahead of current {}",
3131                    replay_rcb_end,
3132                    current_pmu_rcbs
3133                );
3134                let logical_remaining = if max_timeslice_end > current_time {
3135                    max_timeslice_end - current_time
3136                } else {
3137                    LogicalTime::ZERO
3138                };
3139                (logical_remaining, replay_rcb_end - current_pmu_rcbs)
3140            } else {
3141                let logical_remaining = max_timeslice_end - current_time;
3142                (
3143                    logical_remaining,
3144                    logical_remaining.into_rcbs_with_multiplier(clock_multiplier),
3145                )
3146            };
3147            let next_interrupt = self
3148                .cfg
3149                .use_rcb_time()
3150                .then(|| {
3151                    guest
3152                        .thread_state()
3153                        .interrupt_at
3154                        .range((current_rcbs + 1)..)
3155                        .next()
3156                        .copied()
3157                })
3158                .flatten();
3159            let (rcbs_remaining, timer_is_max) =
3160                choose_rcb_timer(max_rcbs_remaining, current_rcbs, next_interrupt);
3161            if let Some(next_interrupt) = next_interrupt {
3162                debug!(
3163                    "[dtid: {}] current rcbs: {}, next interrupt_at: {}",
3164                    dettid, current_rcbs, next_interrupt
3165                )
3166            }
3167
3168            trace!(
3169                "posthook [dtid {}] {} remaining before PMU maximum ({} rcbs).",
3170                dettid, ns_remaining, rcbs_remaining,
3171            );
3172
3173            if replay_rcb_end.is_none() && ns_remaining.is_zero() {
3174                panic!(
3175                    "Timer invariant broken: we should not exit a handler with 0 timeslice remaining."
3176                );
3177            }
3178            assert!(rcbs_remaining > 0);
3179            // The maximum also governs logical expiry and instruction traps.
3180            // Delivery capability gates only the physical arm and its record.
3181            if guest.config().use_preemption_timer() {
3182                trace!(
3183                    "posthook [dtid {}] Resetting timer to {:?} RCBs in the future (current {})",
3184                    dettid,
3185                    rcbs_remaining,
3186                    guest.thread_state().thread_logical_time.rcbs()
3187                );
3188                let replaying = guest.thread_state().replaying_lone_slice_end.is_some();
3189                if replaying || self.skips_lone_slice_end(guest, timer_is_max) {
3190                    // No stop at this slice end: the thread's next entry into
3191                    // Detcore processes it at this branch count. While a
3192                    // skipped end is processed, the next one is only noted;
3193                    // the counter has already passed it or the caller arms.
3194                    let thread_state = guest.thread_state_mut();
3195                    thread_state.lone_slice_end = Some(current_pmu_rcbs + rcbs_remaining);
3196                    thread_state.last_rcb_timer = None;
3197                    thread_state.last_rcb_timer_is_max = false;
3198                    if !replaying {
3199                        arm_lone_backstop(guest, rcbs_remaining);
3200                    }
3201                } else {
3202                    {
3203                        let thread_state = guest.thread_state_mut();
3204                        thread_state.lone_slice_end = None;
3205                        thread_state.last_rcb_timer = Some(rcbs_remaining);
3206                        thread_state.last_rcb_timer_is_max = timer_is_max;
3207                    }
3208
3209                    if guest.config().imprecise_timers {
3210                        guest
3211                            .set_timer(TimerSchedule::Rcbs(rcbs_remaining))
3212                            .expect("Failed to set timer");
3213                    } else {
3214                        guest
3215                            .set_timer_precise(TimerSchedule::Rcbs(rcbs_remaining))
3216                            .expect("Failed to set timer");
3217                    }
3218                }
3219            } else {
3220                guest.thread_state_mut().last_rcb_timer = None;
3221                guest.thread_state_mut().last_rcb_timer_is_max = false;
3222            }
3223        } else {
3224            assert!(guest.config().max_timeslice.is_none());
3225            guest.thread_state_mut().last_rcb_timer = None;
3226            guest.thread_state_mut().last_rcb_timer_is_max = false;
3227        }
3228
3229        if guest.thread_state().replaying_lone_slice_end.is_some() {
3230            detlog_debug!(
3231                "(post) registers [dtid {}][rcbs {}]. not read: slice end taken without a stop",
3232                guest.thread_state().dettid,
3233                guest.thread_state().thread_logical_time.rcbs(),
3234            );
3235        } else if guest.thread_state().guest_past_first_execve() {
3236            detlog_debug!(
3237                "(post) registers [dtid {}][rcbs {}]. {}",
3238                dettid,
3239                guest.thread_state().thread_logical_time.rcbs(),
3240                self.observed_regs(guest).await.display(),
3241            );
3242        }
3243    }
3244
3245    /// End this logical timeslice and talk to the scheduler before continuing.
3246    ///
3247    /// Effects
3248    ///  - ends timeslice (mutating thread stats and both deadlines)
3249    ///  - priority change / yield RPC
3250    async fn end_timeslice<G: Guest<Self>>(&self, guest: &mut G) {
3251        self.end_timeslice_with_sched_yield(guest, false).await;
3252    }
3253
3254    async fn end_timeslice_for_sched_yield<G: Guest<Self>>(&self, guest: &mut G) {
3255        self.end_timeslice_with_sched_yield(guest, true).await;
3256    }
3257
3258    async fn end_timeslice_with_sched_yield<G: Guest<Self>>(
3259        &self,
3260        guest: &mut G,
3261        explicit_sched_yield: bool,
3262    ) {
3263        self.end_timeslice_with_checkpoint(guest, explicit_sched_yield, None, CheckIn::Event, None)
3264            .await;
3265    }
3266
3267    async fn end_timeslice_with_checkpoint<G: Guest<Self>>(
3268        &self,
3269        guest: &mut G,
3270        mut explicit_sched_yield: bool,
3271        mut checkpoint: Option<preemptions::callback_history::Origin>,
3272        check_in: CheckIn,
3273        checkin: Option<PriorityCheckin<'_>>,
3274    ) {
3275        let chaos = guest.config().chaos;
3276        loop {
3277            let thread_state = guest.thread_state();
3278            let dettid = thread_state.dettid;
3279            let end_time = thread_state.thread_logical_time.as_nanos();
3280            if let Some(origin) = &checkpoint {
3281                checkpoint = Some(thread_state.callback_history.refresh(thread_state, origin));
3282            }
3283            info!(
3284                "[detcore, dtid {}] ending timeslice T{}. {} syscalls and {} signals this timeslice.",
3285                dettid,
3286                thread_state.stats.timeslice_count,
3287                thread_state.stats.timeslice_syscall_count,
3288                thread_state.stats.timeslice_signal_count,
3289            );
3290            let replay_checkpoint = checkpoint.is_some() && self.cfg.replay_schedule_from.is_some();
3291            // Read the live cursor authority after checkpoint acquisition and
3292            // any replay handoff. The administrative turn subsumes an expiry
3293            // already due at this exact site, including its original reset.
3294            let expiry_due = replay_checkpoint
3295                && self.callback_checkpoint_expiry_due(
3296                    guest,
3297                    check_in,
3298                    checkpoint.as_ref().unwrap().site,
3299                );
3300            // The recorded row this slice end acknowledges, read before
3301            // `next_timeslice` installs the next one.
3302            let priority_row = if self.cfg.annotated_priority_replay {
3303                match guest.thread_state().active_priority_row() {
3304                    Some(preemptions::callback_history::ActiveRow::Row(row)) => {
3305                        Some(u64::try_from(row).expect("row index fits in u64"))
3306                    }
3307                    _ => None,
3308                }
3309            } else {
3310                None
3311            };
3312            let (maybe_prio, boundary) = {
3313                let thread = guest.thread_state_mut();
3314                if replay_checkpoint && !expiry_due {
3315                    // The event cursor may just have installed a future deadline,
3316                    // including through a TraceReplay resume. This administrative
3317                    // pass does not draw or reset a slice. Later real grants may
3318                    // update that authority normally; do not snapshot it here.
3319                    (None, None)
3320                } else if let Some(mut observed) = thread.slice_boundary.take() {
3321                    let result =
3322                        observed.close_slice(thread, &self.cfg, explicit_sched_yield, None);
3323                    thread.slice_boundary = Some(observed);
3324                    let closed = result.unwrap_or_else(|error| {
3325                        crate::scheduler::exit_on_scheduler_refusal(&error)
3326                    });
3327                    (closed.next_priority, Some(Box::new(closed.boundary)))
3328                } else {
3329                    (thread.next_timeslice(&self.cfg), None)
3330                }
3331            };
3332
3333            // A preemption replay's change point at or before the thread's
3334            // last one means the thread left its record (a slice closed with
3335            // no time in it). Filed, it was never granted and the replay
3336            // stalled until `--timeout`. Report it and run on without the
3337            // record's remaining ends for this thread: the run ends in the
3338            // replay-departure refusal instead. Publication is acknowledged before
3339            // abandonment, even if no other resource request follows.
3340            if !replay_checkpoint && maybe_prio.is_some() {
3341                let thread = guest.thread_state_mut();
3342                if let Err(last) = thread.file_replay_change_point(&self.cfg, end_time) {
3343                    tool_local::report_stale_replay_filing(
3344                        self.cfg.panic_on_rcb_overshoot,
3345                        dettid,
3346                        thread.stats.timeslice_count,
3347                        end_time,
3348                        last,
3349                    );
3350                    let slice = thread.stats.timeslice_count;
3351                    tool_global::replay_departure::publish(guest, last, end_time, slice).await;
3352                    guest.thread_state_mut().abandon_replayed_slices(&self.cfg);
3353                    break;
3354                }
3355            }
3356
3357            // Depending on chaos mode, a received timer event is either a preemption or a changepoint
3358            let mut req = if replay_checkpoint {
3359                // The selected scheduler operation reads its current replay
3360                // band. This placeholder never changes that band at filing.
3361                Self::priority_changepoint_request(
3362                    guest,
3363                    end_time,
3364                    scheduler::runqueue::DEFAULT_PRIORITY,
3365                )
3366            } else if let Some(prio) = maybe_prio {
3367                Self::priority_changepoint_request(guest, end_time, prio)
3368            } else if chaos {
3369                Self::random_priority_changepoint_request(guest, end_time)
3370            } else if explicit_sched_yield && self.cfg.replay_schedule_from.is_none() {
3371                Self::sched_yield_request(guest)
3372            } else {
3373                Self::yield_request(guest)
3374            };
3375            req.slice_boundary = boundary;
3376            req.callback_checkpoint = checkpoint.clone().map(Box::new);
3377            req.priority_row = priority_row;
3378            req.slice_expired = !explicit_sched_yield;
3379            resource_request(guest, req).await;
3380
3381            if self.cfg.replay_schedule_from.is_some()
3382                && let Some(origin) = &checkpoint
3383            {
3384                let thread = guest.thread_state();
3385                let actual = thread.callback_history.refresh(thread, origin);
3386                checkpoint = self.take_callback_checkpoint(guest, actual, None).await;
3387                if checkpoint.is_some() {
3388                    continue;
3389                }
3390                break;
3391            }
3392
3393            // AUTONOMOUS-BOT-IMPLEMENTED
3394            // TODO-HUMAN-REVIEW(PR-1151)
3395            // Multiple scheduler commits can occur without an intervening
3396            // conditional branch. Exact-RCB replay represents those as
3397            // adjacent zero-RCB slices, which must be consumed before the
3398            // guest resumes.
3399            let ends = match slice_boundary::replay_expiration(guest.thread_state())
3400                .unwrap_or_else(|error| crate::scheduler::exit_on_scheduler_refusal(&error))
3401            {
3402                Some(ends) => ends,
3403                // A callback-annotated priority history decides each row
3404                // this check-in installs from the same actual callback and
3405                // site, with refreshed progress. Only an unannotated row
3406                // takes the legacy expiry, and it inherits no origin.
3407                None if self.cfg.annotated_priority_replay => {
3408                    let (token, site) = match checkin {
3409                        Some(checkin) => (checkin.token, Some(checkin.site)),
3410                        None => (None, None),
3411                    };
3412                    match self.decide_priority_row(guest, token, site) {
3413                        preemptions::callback_history::RowDecision::NoCheckpoint => {
3414                            checkpoint = None;
3415                            guest.thread_state().timeslice_expired()
3416                        }
3417                        preemptions::callback_history::RowDecision::Pending => false,
3418                        preemptions::callback_history::RowDecision::Match(next) => {
3419                            checkpoint = Some(next.origin);
3420                            true
3421                        }
3422                        preemptions::callback_history::RowDecision::Refuse {
3423                            refusal,
3424                            checkpoint,
3425                            actual,
3426                        } => {
3427                            self.refuse_priority_checkpoint(guest, refusal, checkpoint, actual)
3428                                .await;
3429                            unreachable!("a priority-history refusal ends the run")
3430                        }
3431                    }
3432                }
3433                None => guest.thread_state().timeslice_expired(),
3434            };
3435            if !ends {
3436                break;
3437            }
3438            explicit_sched_yield = false;
3439        }
3440    }
3441
3442    /// Hash the guest REGISTER FILE and log it.
3443    ///
3444    /// # The sampling boundary: GUEST-LOGICAL CONTROL, never handler interior
3445    ///
3446    /// This is called from exactly one place -- immediately after a syscall has finished and its
3447    /// result has been written back, before the guest resumes. At that instant the guest
3448    /// LOGICALLY HAS CONTROL: the architectural state is what the guest itself would observe at
3449    /// its own RIP, and it is the same instant at which stack and heap are already hashed.
3450    ///
3451    /// It is deliberately NOT sampled anywhere inside a tool handler. A backend that runs its
3452    /// handler IN-GUEST (sabre, liteinst, e9patch) executes instructions the ptrace reference
3453    /// never executes, using guest registers as scratch while it does. Registers there
3454    /// legitimately differ across backends, so comparing them would report correct behaviour as a
3455    /// divergence and burn the prefix-depth ratchet on artifacts. Handler-interior state is out of
3456    /// the domain, not excluded from it by a filter -- the same "define the domain" rule the heap
3457    /// definition follows.
3458    ///
3459    /// # What is in the hash, and what is deliberately not
3460    ///
3461    /// Included: the general-purpose registers the guest can observe, `rip`, `rsp`, `rflags`,
3462    /// `orig_rax`, and the TLS bases `fs_base`/`gs_base`.
3463    ///
3464    /// EXCLUDED, with reasons rather than by convenience:
3465    /// * `rcx` and `r11` -- architecturally clobbered by the `SYSCALL` instruction, which stores
3466    ///   the return RIP and RFLAGS in them. They carry no information beyond `rip`/`eflags`, which
3467    ///   ARE hashed, and a patching backend that reaches the kernel by some route other than a
3468    ///   bare `SYSCALL` will leave different values there for a reason that is not a determinism
3469    ///   defect.
3470    /// * The segment selectors `cs`/`ss`/`ds`/`es`/`fs`/`gs` -- constant for a 64-bit userspace
3471    ///   guest, so they add no signal; the TLS BASES are what a guest actually observes and those
3472    ///   are hashed.
3473    ///
3474    /// That is the x86_64 list. On aarch64 the hash covers `x0`-`x30` except `x7`, then `sp`,
3475    /// `pc` and `pstate`; `x7` holds the kernel's ptrace entry/exit marker at a syscall stop, not
3476    /// guest state (`arch::detlog_register_words`).
3477    fn detlog_registers<G: Guest<Self>>(
3478        &self,
3479        guest: &mut G,
3480        regs: &libc::user_regs_struct,
3481        seq: u64,
3482    ) {
3483        if !self.cfg.detlog_regs {
3484            return;
3485        }
3486        // COST TIER: cadence 1 == full (every control point); N > 1 == spot-check every Nth.
3487        // The cadence index is a PER-THREAD counter, NOT a shared one: a global atomic would be
3488        // incremented in whatever order threads happen to reach it, so the cadence -- and
3489        // therefore which points got sampled -- would itself be nondeterministic. A determinism
3490        // instrument must not have a nondeterministic sampling schedule.
3491        //
3492        // It is `stats.syscall_count`, which starts at ZERO, rather than the syscall ORDINAL used
3493        // in the log (which starts at 2). With the ordinal, a guest whose control points never
3494        // land on a multiple of the cadence emitted NOTHING and the run still reported PASS -- a
3495        // spot-tier green backed by zero samples. Indexing from zero makes the first control point
3496        // of every thread always sampled, so a spot-tier run can never be silently empty.
3497        let cadence = self.cfg.detlog_regs_cadence.max(1);
3498        let index = {
3499            let stats = &mut guest.thread_state_mut().stats;
3500            let i = stats.regs_sample_index;
3501            stats.regs_sample_index = i.saturating_add(1);
3502            i
3503        };
3504        let _ = seq;
3505        if !index.is_multiple_of(cadence) {
3506            return;
3507        }
3508        let tier = if cadence == 1 {
3509            "full".to_string()
3510        } else {
3511            format!("spot-1/{cadence}")
3512        };
3513        let words = arch::detlog_register_words(regs);
3514        let mut bytes = Vec::with_capacity(words.len() * 8);
3515        for v in words {
3516            bytes.extend_from_slice(&v.to_le_bytes());
3517        }
3518        detlog!(
3519            "[registers][dtid {}] control_point=syscall-exit tier={} {}",
3520            guest.thread_state().dettid,
3521            tier,
3522            Digest::new(&bytes)
3523        );
3524    }
3525
3526    fn detlog_memory_maps<G: Guest<Self>>(&self, guest: &mut G) -> Result<(), reverie::Error> {
3527        if !(self.cfg.detlog_stack || self.cfg.detlog_heap || self.cfg.detlog_anon.is_some()) {
3528            // Don't incur the *significant* performance penalty for reading
3529            // /proc/maps unless one of these flags is enabled.
3530            return Ok(());
3531        }
3532        // ...and don't incur it when nothing would observe the record either.
3533        //
3534        // The hash below is an argument to `detlog!`, so `tracing` already skips
3535        // it when INFO is disabled. Enumerating the maps is NOT: it happens
3536        // before the macro, on every syscall, whether or not a record is ever
3537        // written. Measured on a QEMU/Linux boot with `RUST_LOG` unset, where
3538        // each run emitted 123 bytes of log in total: no flag 43.71s,
3539        // `--detlog-stack` 190.37s (4.36x), `--detlog-heap` 207.90s (4.76x).
3540        // `--detlog-regs` was already inert because everything it does before
3541        // its own `detlog!` is trivial; this restores the same property here.
3542        //
3543        // Skipping is invisible to the guest: enumerating maps and hashing guest
3544        // memory are host-side observations of the tracee that neither issue a
3545        // guest syscall nor advance virtual time, so a run that skips them
3546        // executes the same guest instruction stream as one that does not.
3547        if !detlog_observed!() {
3548            return Ok(());
3549        }
3550        // Out-of-process backends (e.g. KVM) report their guest memory regions
3551        // directly, because `guest.pid()` is the host VMM process there and its
3552        // `/proc/<pid>/maps` describes the VMM, not the guest address space.
3553        // Reading those host addresses through `guest.memory()` (guest-address
3554        // space) would fault and abort the syscall. When the backend supplies
3555        // regions, hash those guest ranges; otherwise fall back to the ptrace
3556        // path of parsing `/proc/<pid>/maps`.
3557        if let Some(regions) = guest.detlog_memory_regions() {
3558            for region in regions {
3559                let want = match region.kind {
3560                    reverie::DetlogRegionKind::Stack => self.cfg.detlog_stack,
3561                    reverie::DetlogRegionKind::Heap => self.cfg.detlog_heap,
3562                };
3563                if !want {
3564                    continue;
3565                }
3566                let dettid = guest.thread_state().dettid;
3567                detlog!(
3568                    "[memory][dtid {}] {:?} {:#x}-{:#x}->{}",
3569                    dettid,
3570                    region.kind,
3571                    region.start,
3572                    region.end,
3573                    procmaps::compute_hash_range(guest, region.start, region.end)?
3574                )
3575            }
3576            return Ok(());
3577        }
3578        // Where Detcore answers brk from its model (a launcher run), the
3579        // kernel's `[heap]` is the launcher's, not the guest's.
3580        let modeled_brk = guest
3581            .thread_state()
3582            .memory_metadata
3583            .lock()
3584            .expect("memory metadata mutex poisoned")
3585            .brk_model
3586            .is_some();
3587        let mut labelled_heap = false;
3588        let anon = self.cfg.detlog_anon;
3589        for mmap in procmaps::from_pid(guest.pid(), |map| match map.pathname {
3590            procmaps::MMapPath::Stack if self.cfg.detlog_stack => true,
3591            procmaps::MMapPath::Heap if self.cfg.detlog_heap && !modeled_brk => true,
3592            _ => anon.is_some() && procmaps::is_hashed_anonymous(map),
3593        })? {
3594            labelled_heap |= matches!(mmap.pathname, procmaps::MMapPath::Heap);
3595            let dettid = guest.thread_state().dettid;
3596            if !procmaps::is_hashed_anonymous(&mmap) {
3597                detlog!(
3598                    "[memory][dtid {}] {}->{}",
3599                    dettid,
3600                    procmaps::display(&mmap),
3601                    procmaps::compute_hash(guest, &mmap)?
3602                );
3603                continue;
3604            }
3605            // `--detlog-anon`: a mapping another thread of the process unmaps
3606            // between the listing and the read (threads not sequentialized)
3607            // is recorded as unreadable rather than failing the guest's call.
3608            let pages = anon == Some(detcore_model::config::DetlogAnon::Pages);
3609            match procmaps::compute_hash_and_pages(guest, mmap.address.0, mmap.address.1, pages) {
3610                Ok(procmaps::AnonHash::Hashed(digest, Some(pages))) => detlog!(
3611                    "[memory][dtid {}] {}->{} pages={}",
3612                    dettid,
3613                    procmaps::display(&mmap),
3614                    digest,
3615                    pages
3616                ),
3617                Ok(procmaps::AnonHash::Hashed(digest, None)) => detlog!(
3618                    "[memory][dtid {}] {}->{}",
3619                    dettid,
3620                    procmaps::display(&mmap),
3621                    digest
3622                ),
3623                Ok(procmaps::AnonHash::TooLarge(size)) => detlog!(
3624                    "[memory][dtid {}] {}->too large ({size} bytes)",
3625                    dettid,
3626                    procmaps::display(&mmap)
3627                ),
3628                Err(error) => detlog!(
3629                    "[memory][dtid {}] {}->unreadable ({error})",
3630                    dettid,
3631                    procmaps::display(&mmap)
3632                ),
3633            }
3634        }
3635
3636        // The kernel labels `[heap]` only for `[mm->start_brk, mm->brk)`. Under a
3637        // backend that loads the guest itself (DynamoRIO) that break belongs to
3638        // the loader, so the guest's heap is an unlabelled anonymous mapping and
3639        // the filter above matches nothing. Emitting no record there is worse
3640        // than a wrong one: downstream a zero-record heap comparison reads as
3641        // "compared and matched" rather than "never measured". Fall back to the
3642        // brk range Detcore observed, which identifies the heap on every backend.
3643        // Where Detcore models the break, the observed range is the model's.
3644        if self.cfg.detlog_heap && !labelled_heap {
3645            self.detlog_brk_heap(guest)?;
3646        }
3647        Ok(())
3648    }
3649
3650    /// Emit the `[heap]` record from the observed program break, for backends
3651    /// where the kernel does not label the guest's heap.
3652    ///
3653    /// Reports `[start_brk, brk)` -- the range Detcore observed -- taking the
3654    /// non-address columns from the enclosing anonymous mapping, so the record
3655    /// is textually comparable with the labelled record another backend
3656    /// produces for the same guest.
3657    ///
3658    /// ⚠️ THE EXTENT IS THE OBSERVED BREAK, NOT THE MAPPING THAT CONTAINS IT,
3659    /// and the two are not the same region. The selector below admits any
3660    /// anonymous mapping with `address.0 <= start && end <= address.1`, which
3661    /// is a SUPERSET by construction. Reporting that mapping instead would make
3662    /// the comparability claim above true only when the arena happens to
3663    /// coincide with the break, and would fold non-heap bytes into the digest.
3664    /// On the backend this path exists for, the premise is that the LOADER owns
3665    /// the break, so those extra bytes are loader arena -- the least
3666    /// reproducible region in the process. A silently empty record would become
3667    /// a loudly divergent one, for a reason that is not the guest's heap.
3668    ///
3669    /// The labelled path above hashes its mapping directly, which is correct
3670    /// there because the kernel defines `[heap]` as exactly `[start_brk, brk)`.
3671    /// Both paths therefore report the same quantity.
3672    fn detlog_brk_heap<G: Guest<Self>>(&self, guest: &mut G) -> Result<(), reverie::Error> {
3673        let Some((start, end)) = guest
3674            .thread_state()
3675            .memory_metadata
3676            .lock()
3677            .expect("memory metadata mutex poisoned")
3678            .brk_heap_range()
3679        else {
3680            return Ok(());
3681        };
3682        let modeled = guest
3683            .thread_state()
3684            .memory_metadata
3685            .lock()
3686            .expect("memory metadata mutex poisoned")
3687            .brk_model
3688            .is_some();
3689        if modeled {
3690            return self.detlog_modeled_brk_heap(guest, start, end);
3691        }
3692        let enclosing = procmaps::from_pid(guest.pid(), |map| {
3693            matches!(map.pathname, procmaps::MMapPath::Anonymous)
3694                && map.address.0 <= start
3695                && end <= map.address.1
3696        })?;
3697        let Some(mmap) = enclosing.into_iter().next() else {
3698            return Ok(());
3699        };
3700        let dettid = guest.thread_state().dettid;
3701        detlog!(
3702            "[memory][dtid {}] {}->{}",
3703            dettid,
3704            procmaps::display_range_as(&mmap, start, end, "[heap]"),
3705            procmaps::compute_hash_range(guest, start, end)?
3706        );
3707        Ok(())
3708    }
3709
3710    /// The heap records where Detcore models the break (a launcher run): one
3711    /// per anonymous mapping that holds part of the modeled `[start, end)`,
3712    /// clipped to it, as Linux's `[heap]` is one mapping per piece once an
3713    /// mprotect splits it, and up to the break's page-aligned end, where the
3714    /// kernel's `[heap]` mapping ends. So the records are the ones the
3715    /// kernel's `[heap]` mappings give where the kernel's break is the
3716    /// guest's, and a heap whose pages the guest unmapped gives none, as
3717    /// Linux's label goes with its last page.
3718    fn detlog_modeled_brk_heap<G: Guest<Self>>(
3719        &self,
3720        guest: &mut G,
3721        start: u64,
3722        end: u64,
3723    ) -> Result<(), reverie::Error> {
3724        // The page brk(2) rounds the break up to.
3725        let page_mask = crate::page::page_size() - 1;
3726        let end = end
3727            .checked_add(page_mask)
3728            .map_or(end, |end| end & !page_mask);
3729        let pieces = procmaps::from_pid(guest.pid(), |map| {
3730            matches!(map.pathname, procmaps::MMapPath::Anonymous)
3731                && map.address.0 < end
3732                && start < map.address.1
3733        })?;
3734        let dettid = guest.thread_state().dettid;
3735        for piece in pieces {
3736            let (from, to) = (piece.address.0.max(start), piece.address.1.min(end));
3737            detlog!(
3738                "[memory][dtid {}] {}->{}",
3739                dettid,
3740                procmaps::display_range_as(&piece, from, to, "[heap]"),
3741                procmaps::compute_hash_range(guest, from, to)?
3742            );
3743        }
3744        Ok(())
3745    }
3746}
3747
3748/// A syscall's six argument registers as the access trace records them.
3749fn raw_syscall_args(args: reverie::syscalls::SyscallArgs) -> [u64; 6] {
3750    [
3751        args.arg0 as u64,
3752        args.arg1 as u64,
3753        args.arg2 as u64,
3754        args.arg3 as u64,
3755        args.arg4 as u64,
3756        args.arg5 as u64,
3757    ]
3758}
3759
3760/// Render a finished syscall for the `finish syscall` DETLOG line.
3761///
3762/// Output pointers are dereferenced only when the kernel can have written
3763/// them. Linux leaves the output buffer of the syscalls listed in
3764/// [`failure_leaves_outputs_unwritten`] untouched when they fail with any errno
3765/// other than EFAULT, so rendering it would publish whatever the guest
3766/// happened to have there -- typically uninitialized stack -- as if it were a
3767/// result, and two otherwise identical runs would diverge on it
3768/// (<https://github.com/rrnewton/hermit/issues/3153>). For those syscalls such
3769/// a failure renders the pointer arguments without their pointees; the errno
3770/// is printed next to this rendering from the result itself.
3771///
3772/// EFAULT keeps the outputs rendered, because it is also the error of a copy
3773/// that faulted part way: `copy_to_user()` may already have stored a prefix of
3774/// the struct, which the guest can read and DETLOG must show. A tool error is
3775/// not a guest errno and proves nothing about the buffer, so it keeps them
3776/// rendered too.
3777///
3778/// Every other syscall keeps rendering its outputs on failure, because some
3779/// Linux syscalls do write an output on an error return (for example
3780/// `gettimeofday` stores `tv` before it faults on a bad `tz`). Hiding such an
3781/// output would remove real evidence from DETLOG.
3782fn display_syscall_finished<'a, M: MemoryAccess>(
3783    syscall: &'a Syscall,
3784    memory: &'a M,
3785    result: &Result<i64, Error>,
3786) -> reverie::syscalls::Display<'a, M, Syscall> {
3787    match syscall {
3788        // Address only: the whole `fstat` structure is hashed and every field
3789        // decoded in the call's `[iobuf]` record (`io_buffers::stat_fields`),
3790        // which is where it is compared, so it is not printed twice. That
3791        // record retires T136880615 ("fstat structure isn't fully deterministic
3792        // yet"): strict verify of `fstat` on stdio, a pipe, a socketpair, an
3793        // eventfd, a memfd and an unlinked file passes on ptrace (review of
3794        // https://github.com/rrnewton/hermit/pull/4559), and a procfs root
3795        // reports one link on every backend (`determinize_stat`).
3796        Syscall::Fstat(_) => syscall.display(memory),
3797        _ if failure_proves_outputs_unwritten(result)
3798            && failure_leaves_outputs_unwritten(syscall) =>
3799        {
3800            syscall.display(memory)
3801        }
3802        _ => syscall.display_with_outputs(memory),
3803    }
3804}
3805
3806/// Syscalls whose output buffer Linux does not write when the syscall fails
3807/// with an errno other than EFAULT, restricted to those whose output the pinned Reverie formatter dereferences.
3808///
3809/// This is deliberately a list of syscalls PROVEN not to write on failure,
3810/// rather than a list of exceptions that do: a syscall missing from it keeps
3811/// its outputs rendered, which can at worst show stale guest memory, whereas a
3812/// syscall wrongly missing from an exception list would silently hide a value
3813/// the kernel really wrote.
3814///
3815/// Kernel behaviour (Linux `fs/stat.c`, `kernel/time/posix-timers.c`):
3816///
3817/// - `stat`, `lstat`, `fstat`, `newfstatat`: `vfs_stat()`, `vfs_lstat()`,
3818///   `vfs_fstat()` and `vfs_fstatat()` return their error before
3819///   `cp_new_stat()` is called, so the `struct stat` is never copied out.
3820///   Hermit's `handle_stat_family` likewise returns the injected syscall's
3821///   error before it rewrites the buffer.
3822/// - `statx`: `do_statx()` returns the `vfs_statx()` error before
3823///   `cp_statx()` copies the `struct statx` out.
3824/// - `clock_gettime`: `put_timespec64()` runs only when
3825///   `kc->clock_get_timespec()` succeeded (`if (!error && put_timespec64(..))`),
3826///   and an unknown clock returns `-EINVAL` before that. Hermit's
3827///   `handle_clock_gettime` writes `tp` only on its success path.
3828///
3829/// In every case the one failure that follows a copy attempt is the `-EFAULT`
3830/// from the copy itself, where `copy_to_user()` may have stored a prefix of
3831/// the struct before faulting. [`failure_proves_outputs_unwritten`] therefore
3832/// keeps rendering EFAULT failures, so that partial prefix stays visible.
3833///
3834/// `gettimeofday` is intentionally absent: `SYSCALL_DEFINE2(gettimeofday)` in
3835/// `kernel/time/time.c` stores `tv` and then returns `-EFAULT` if copying
3836/// `tz` faults, so its rendered output can be kernel-written on failure.
3837fn failure_leaves_outputs_unwritten(syscall: &Syscall) -> bool {
3838    match syscall {
3839        Syscall::Fstat(_) | Syscall::Statx(_) | Syscall::ClockGettime(_) => true,
3840        #[cfg(target_arch = "x86_64")]
3841        Syscall::Stat(_) | Syscall::Lstat(_) | Syscall::Newfstatat(_) => true,
3842        #[cfg(target_arch = "aarch64")]
3843        Syscall::Fstatat(_) => true,
3844        _ => false,
3845    }
3846}
3847
3848/// Whether `result` is a failure that, for a syscall listed in
3849/// [`failure_leaves_outputs_unwritten`], proves Linux stored no output: a
3850/// guest errno other than EFAULT. EFAULT can follow a partial copy, and a
3851/// tool error is not the syscall's result.
3852fn failure_proves_outputs_unwritten(result: &Result<i64, Error>) -> bool {
3853    matches!(result, Err(Error::Errno(errno)) if *errno != Errno::EFAULT)
3854}
3855
3856/// Whether `pkey_mprotect(start, len, prot, key)`, which returned `result`,
3857/// may have applied a protection key to some pages
3858/// (`MemoryMetadata::protection_keys`). It cannot have when:
3859/// - `key` is -1: that is plain `mprotect` (`mprotect` is
3860///   `do_mprotect_pkey(start, len, prot, -1)`, mm/mprotect.c), which applies
3861///   no key a writable page can carry (on x86 it can give a mapping that is
3862///   only `PROT_EXEC` the kernel's execute-only key, a page Detcore's
3863///   write-back refuses anyway, and Linux resets that key when the mapping
3864///   leaves execute-only protection, arch/x86/mm/pkeys.c);
3865/// - it returned ENOSYS: it never ran (a backend that refuses the
3866///   protection-key calls answers every pkey_mprotect so);
3867/// - `len` is 0: Linux returns 0 or EINVAL having changed nothing;
3868/// - it failed, and its arguments are ones `do_mprotect_pkey` refuses before
3869///   it takes the mmap lock, so before it changes any mapping: both
3870///   `PROT_GROWSDOWN` and `PROT_GROWSUP`, a start that is not page-aligned
3871///   (untagging an address changes only its high bits), or a `prot` bit other
3872///   than `PROT_READ`, `PROT_WRITE`, `PROT_EXEC`, `PROT_SEM` and those two
3873///   (x86-64 has the generic `arch_validate_prot`).
3874///
3875/// Any other call may have, failures included: one that fails part way
3876/// (ENOMEM, EACCES, or EINVAL from a check of a later VMA) can have applied
3877/// the key to the VMAs before, and Linux does not say which; nor can an
3878/// EINVAL for a key never allocated, which Linux returns before it changes
3879/// anything, be told from those by its result.
3880fn pkey_mprotect_may_apply_a_key(
3881    start: usize,
3882    len: usize,
3883    prot: usize,
3884    key: usize,
3885    result: &Result<i64, Error>,
3886) -> bool {
3887    /// `PROT_SEM` (include/uapi/asm-generic/mman-common.h).
3888    const PROT_SEM: usize = 0x8;
3889    /// x86-64's page size.
3890    const PAGE: usize = 4096;
3891    let grows = (libc::PROT_GROWSDOWN | libc::PROT_GROWSUP) as usize;
3892    let known = (libc::PROT_READ | libc::PROT_WRITE | libc::PROT_EXEC) as usize | PROT_SEM | grows;
3893    let refused_before_any_change =
3894        (prot & grows) == grows || !start.is_multiple_of(PAGE) || (prot & !known) != 0;
3895    let never_applied = key as i32 == -1
3896        || len == 0
3897        || matches!(result, Err(Error::Errno(Errno::ENOSYS)))
3898        || (result.is_err() && refused_before_any_change);
3899    !never_applied
3900}
3901
3902/// The happens-before request a syscall entry makes once its result is
3903/// decided, for the posthook anchors it reached at its prehook (`anchors`), or
3904/// `None` (<https://github.com/rrnewton/hermit/issues/3929>):
3905/// - a value or a guest errno other than an internal restart code is a
3906///   completion: Linux returns it unchanged, so the anchors fire
3907///   ([`ResourceID::HappensBeforeSyscallCheckpoint`]);
3908/// - an internal restart code (`ERESTARTSYS`, `ERESTARTNOINTR`,
3909///   `ERESTARTNOHAND`, `ERESTART_RESTARTBLOCK`) is refused
3910///   ([`ResourceID::HappensBeforePosthookInterrupted`]): the kernel decides
3911///   only at signal delivery, after this point, whether the guest sees `EINTR`
3912///   or the entry is restarted and never returns, so neither firing nor not
3913///   firing would keep the anchor's promise;
3914/// - a tool or backend failure is not a result the guest receives; nothing
3915///   fires, and the failure ends the run on its own path.
3916fn posthook_checkpoint(
3917    count: u64,
3918    anchors: Vec<String>,
3919    result: &Result<i64, Error>,
3920) -> Option<ResourceID> {
3921    if anchors.is_empty() {
3922        return None;
3923    }
3924    match result {
3925        Err(Error::Errno(
3926            errno @ (Errno::ERESTARTSYS
3927            | Errno::ERESTARTNOINTR
3928            | Errno::ERESTARTNOHAND
3929            | Errno::ERESTART_RESTARTBLOCK),
3930        )) => Some(ResourceID::HappensBeforePosthookInterrupted {
3931            count,
3932            anchors,
3933            code: errno
3934                .name()
3935                .map_or_else(|| errno.to_string(), str::to_owned),
3936        }),
3937        Ok(_) | Err(Error::Errno(_)) => {
3938            Some(ResourceID::HappensBeforeSyscallCheckpoint { count, anchors })
3939        }
3940        Err(_) => None,
3941    }
3942}
3943
3944#[reverie::tool]
3945impl<T: RecordOrReplay> Tool for Detcore<T> {
3946    type GlobalState = GlobalState;
3947    type ThreadState = ThreadState<T::ThreadState>;
3948
3949    fn stream_role() -> reverie::ToolStreamRole {
3950        T::stream_role()
3951    }
3952
3953    fn observes_native_syscalls(config: &Config) -> bool {
3954        matches!(
3955            config.guest_ids,
3956            detcore_model::pid::GuestIds::Chosen { .. }
3957        )
3958    }
3959
3960    fn observe_native_syscall(
3961        &self,
3962        _state: &Self::ThreadState,
3963        facts: &dyn reverie::native_syscall::NativeSyscallFacts,
3964        call: &reverie::native_syscall::NativeSyscallCall,
3965    ) -> Result<Option<std::sync::Arc<dyn reverie::native_syscall::NativeSyscallObserver>>, Error>
3966    {
3967        crate::reserved_input::observe_native_syscall(facts, call)
3968    }
3969
3970    fn facility_admission(config: &Config) -> Result<(), reverie::Error> {
3971        // This is an operational pre-start hook. subscriptions and the
3972        // capability queries remain pure, including when called by another
3973        // Tool to decide whether its own guarantees hold.
3974        if let Err(refusal) =
3975            crate::record_or_replay::require_chosen_input_stream(config, T::stream_role())
3976        {
3977            crate::scheduler::exit_on_scheduler_refusal(&refusal);
3978        }
3979        T::facility_admission(config)
3980    }
3981
3982    fn reserved_input_requirement(
3983        config: &Config,
3984    ) -> Option<reverie::reserved_input::ReservedInputRequirement> {
3985        matches!(
3986            config.guest_ids,
3987            detcore_model::pid::GuestIds::Chosen { .. }
3988        )
3989        .then(reverie::reserved_input::ReservedInputRequirement::sealed_page)
3990    }
3991
3992    fn native_mm_ownership(config: &Config) -> reverie::NativeMmOwnership {
3993        // Guest-local initialization and callback polling use the backend's
3994        // managed runtime. Scheduler/controller workers execute in another MM;
3995        // the native fatal-core worker is called only by the ptrace tracer.
3996        // io_uring remains intercepted and refused, including passthru_opt.
3997        // This guarantee must also be supplied by the inner Tool: recording,
3998        // replay, and arbitrary extensions do not inherit it implicitly.
3999        T::native_mm_ownership(config)
4000    }
4001
4002    /// Only a sequentialized run on a backend with process signal control
4003    /// can install that control. Whether this run observes dequeues is
4004    /// decided after installation by [`GlobalState`]'s
4005    /// `GlobalTool::observe_signal_dequeues`.
4006    fn may_observe_signal_dequeues(config: &Config) -> bool {
4007        config.backend.provides_process_signal_control && config.sequentialize_threads
4008    }
4009
4010    /// The same runs require the control: real-timer signals on such a run
4011    /// travel only through acknowledged shared dequeues. Reverie refuses it
4012    /// before any guest code, with ENOSYS on a runner that offers no control,
4013    /// as Detcore's own installation did before this check moved into
4014    /// Reverie, and with EINVAL on a runner whose offer was not taken. So
4015    /// Detcore's scheduler needs no refusal of its own for a capable backend
4016    /// without a control.
4017    fn requires_signal_control(config: &Config) -> bool {
4018        config.backend.provides_process_signal_control && config.sequentialize_threads
4019    }
4020
4021    /// A sequentialized run lets one guest thread run guest code at a time,
4022    /// and Detcore refuses every way to install a seccomp filter: `seccomp(2)`
4023    /// (`handle_seccomp`, EOPNOTSUPP or an earlier argument error) and
4024    /// `prctl(PR_SET_SECCOMP)` (not a supported option, ENOSYS). Both
4025    /// refusals need the call intercepted, so the declaration also requires
4026    /// both in the subscriptions. The ptrace backend then keeps the guest's
4027    /// SIGTRAP action and mask across its own single-steps
4028    /// (<https://github.com/rrnewton/hermit/issues/4409>).
4029    ///
4030    /// One exception to "one at a time": a call the scheduler runs in the
4031    /// background (`BlockingExternalIO`, `BlockingRtSigsuspend`) is injected
4032    /// with a returning `inject`, so its thread runs only that syscall, in the
4033    /// kernel, and stops before any guest instruction. It can install no
4034    /// filter. Its injection is single-stepped, though, and that step's trap,
4035    /// at a host-timed moment, can reset the process's shared SIGTRAP action
4036    /// while another thread's injection is kept. An ignored, or caught and
4037    /// blocked, SIGTRAP can then end at `SIG_DFL`, where an undeclared run
4038    /// always leaves it; the blocked mask is per thread and is not affected.
4039    fn enforces_serialized_execution(config: &Config) -> bool {
4040        let subscriptions = Self::subscriptions(config);
4041        config.sequentialize_threads
4042            && subscriptions
4043                .iter_syscalls()
4044                .any(|sysno| sysno == Sysno::seccomp)
4045            && subscriptions
4046                .iter_syscalls()
4047                .any(|sysno| sysno == Sysno::prctl)
4048    }
4049
4050    async fn handle_signal_dequeue<G: Guest<Self>>(
4051        &self,
4052        guest: &mut G,
4053        dequeue: reverie::SignalDequeue,
4054    ) -> Result<(), Errno> {
4055        tool_global::signal_dequeued(guest, dequeue).await
4056    }
4057
4058    /// Constructor for Detcore process-local state.
4059    fn new(pid: Pid, cfg: &Config) -> Self {
4060        let detpid = DetPid::from_raw(pid.into()); // TODO(T78538674): virtualize pid.
4061        cfg.validate_invariants();
4062        if let Some(policy) = &cfg.in_guest_detlog_forward_policy {
4063            // This image's forwarder was set up before the Tool (SaBRe's
4064            // connect, the LiteInst constructor); without one, or with another
4065            // policy, refuse.
4066            detlog::require_forwarding(policy);
4067        }
4068        if cfg.in_guest_site_patching_off {
4069            // Record the host's request; the preload constructor compares it
4070            // with the settings the runtime captured once installation
4071            // returns (see in_guest_site_patching).
4072            in_guest_site_patching::require_site_patching_off();
4073        }
4074        // Read before any guest syscall, so a readlink of another process's
4075        // pipe or socket never depends on the tracer's descriptors then. The
4076        // outcome is kept; a failure is reported only if a guest needs it.
4077        let _ = syscalls::anonymous_object_devices();
4078        Self {
4079            detpid,
4080            cfg: cfg.clone(),
4081            record_or_replay: T::new(pid, cfg),
4082        }
4083    }
4084
4085    /// The signal-state copy follows handler returns from the backend's
4086    /// reports where it has them (`syscalls::signal_shadow`).
4087    fn observes_sigreturns(config: &Config) -> bool {
4088        crate::syscalls::signal_shadow::observes_sigreturns(config)
4089    }
4090
4091    /// The in-guest delivery gate exactly as the host prepared it
4092    /// (`Config::in_guest_signal_delivery`): Hermit sets it only where the
4093    /// whole protocol holds, and clears it in every other run.
4094    fn in_guest_signal_delivery(config: &Config) -> bool {
4095        config.in_guest_signal_delivery
4096    }
4097
4098    fn handle_sigreturn(
4099        &self,
4100        thread_state: &mut Self::ThreadState,
4101        report: reverie::SigreturnReport,
4102    ) {
4103        crate::syscalls::signal_shadow::record_sigreturn_report(thread_state, report);
4104        thread_state.returned_from_sigreturn = true;
4105    }
4106
4107    /// Counts each signal the backend passes on without reporting it, which
4108    /// could reach a guest handler, so the delivery counters show what no
4109    /// delivery check saw (A5 step 3.2b).
4110    fn note_unreported_signal(
4111        &self,
4112        thread_state: &mut Self::ThreadState,
4113        signal: &reverie::UnreportedSignal,
4114        kept: bool,
4115    ) {
4116        if kept && self.copy_misses_unreported(signal.number()) {
4117            let dettid = thread_state.dettid;
4118            thread_state
4119                .delivery_counters
4120                .count_unreported(dettid, signal.reason());
4121        }
4122    }
4123
4124    /// NOTE: these subscriptions are used ONLY for hermit run mode.  Hermit record has its own
4125    /// subscriptions specified in recorder/mod.rs.
4126    fn subscriptions(config: &Config) -> Subscription {
4127        let do_sched =
4128            config.sched_heuristic != SchedHeuristic::None || config.sequentialize_threads;
4129
4130        if !config.passthru_opt {
4131            // Fail closed by default in every build profile. Besides allowing syscall-specific
4132            // handlers to run, interception is what charges generic syscall logical time.
4133            //
4134            // `cpuid` is the one exception. With CPUID virtualization off the handler only
4135            // re-executes the host instruction, so trapping does not change what the guest
4136            // sees, but the subscription makes Reverie probe and enable CPUID faulting and log
4137            // an ERROR for each exec on hosts that lack it
4138            // (https://github.com/rrnewton/hermit/issues/3460). No backend installs a CPUID
4139            // table of its own, so an untrapped guest CPUID is the host's on every backend.
4140            let mut subscription = Subscription::all_syscalls();
4141            subscription.rdtsc();
4142            if config.virtualize_cpuid {
4143                subscription.cpuid();
4144            }
4145            subscription
4146        } else {
4147            // Explicit performance opt-in: unlisted syscalls bypass Detcore entirely. Keep this
4148            // path separate so its allow-list can be tightened without weakening the default.
4149            let mut subscription = Subscription::none();
4150            subscription.syscalls([
4151                Sysno::write,
4152                // AUTONOMOUS-BOT-IMPLEMENTED
4153                // TODO-HUMAN-REVIEW(#547)
4154                Sysno::writev,
4155                // Timer-slack procfs virtualization must see every scalar and
4156                // vectored read/write form that Linux accepts for the file.
4157                Sysno::readv,
4158                Sysno::preadv,
4159                Sysno::preadv2,
4160                Sysno::pwritev,
4161                Sysno::pwritev2,
4162                // AUTONOMOUS-BOT-IMPLEMENTED
4163                // TODO-HUMAN-REVIEW(#683)
4164                Sysno::pwrite64,
4165                Sysno::openat,
4166                // Detcore answers access and extended-attribute requests on the
4167                // procfs files it renders
4168                // (https://github.com/rrnewton/hermit/issues/4213); a backend
4169                // keeps no copy of them, so these must not bypass Detcore.
4170                Sysno::faccessat,
4171                Sysno::faccessat2,
4172                Sysno::getxattr,
4173                Sysno::lgetxattr,
4174                Sysno::setxattr,
4175                Sysno::lsetxattr,
4176                Sysno::removexattr,
4177                Sysno::lremovexattr,
4178                Sysno::listxattr,
4179                Sysno::llistxattr,
4180                Sysno::close,
4181                Sysno::read,
4182                Sysno::pread64,
4183                Sysno::lseek,
4184                Sysno::fadvise64,
4185                Sysno::mmap,
4186                Sysno::madvise,
4187                Sysno::munmap,
4188                Sysno::mremap,
4189                Sysno::fcntl,
4190                // AUTONOMOUS-BOT-IMPLEMENTED
4191                // TODO-HUMAN-REVIEW(PR-2150): Timer-slack prctl state is
4192                // virtual and must not bypass Detcore under passthru_opt.
4193                Sysno::prctl,
4194                Sysno::ioctl,
4195                Sysno::futex,
4196                Sysno::clone,
4197                Sysno::clone3,
4198                Sysno::wait4,
4199                Sysno::waitid,
4200                Sysno::setsid,
4201                Sysno::uname,
4202                Sysno::exit_group,
4203                Sysno::exit,
4204                // AUTONOMOUS-BOT-IMPLEMENTED
4205                // The scheduler must observe changes to the address used for
4206                // its modeled CHILD_CLEARTID wake, including record/replay's
4207                // passthrough optimization.
4208                Sysno::set_tid_address,
4209                // AUTONOMOUS-BOT-IMPLEMENTED
4210                // Rare (once per thread) but load-bearing: without it the exit
4211                // hook cannot replay `exit_robust_list()` and robust-mutex
4212                // waiters are never woken.
4213                Sysno::set_robust_list,
4214                Sysno::dup,
4215                Sysno::dup3,
4216                Sysno::pipe2,
4217                Sysno::getrandom,
4218                Sysno::utimensat,
4219                Sysno::socket,
4220                Sysno::socketpair,
4221                Sysno::eventfd2,
4222                Sysno::sched_getaffinity,
4223                Sysno::sched_setaffinity,
4224                Sysno::signalfd4,
4225                Sysno::timerfd_create,
4226                Sysno::timerfd_settime,
4227                Sysno::timerfd_gettime,
4228                Sysno::inotify_init1,
4229                Sysno::inotify_add_watch,
4230                Sysno::inotify_rm_watch,
4231                // Inotify fault recovery leaves a faulted read's bytes alone
4232                // once the address space has used protection keys, which it
4233                // learns only from the first two calls
4234                // (`MemoryMetadata::protection_keys`). All three still pass
4235                // through; a backend's refusal of any of them is named
4236                // (`Detcore::report_backend_refusal`).
4237                Sysno::pkey_alloc,
4238                Sysno::pkey_mprotect,
4239                Sysno::pkey_free,
4240                Sysno::memfd_create,
4241                // AUTONOMOUS-BOT-IMPLEMENTED
4242                // TODO-HUMAN-REVIEW(PR-862): Keep modeled pidfd creation intercepted.
4243                Sysno::pidfd_open,
4244                // AUTONOMOUS-BOT-IMPLEMENTED
4245                // TODO-HUMAN-REVIEW(PR-1175): Keep pidfd signal/get-fd
4246                // determinization from being bypassed under the passthru opt-in.
4247                Sysno::pidfd_send_signal,
4248                Sysno::pidfd_getfd,
4249                Sysno::userfaultfd,
4250                Sysno::io_uring_setup,
4251                Sysno::io_uring_enter,
4252                Sysno::io_uring_register,
4253                Sysno::accept,
4254                Sysno::accept4,
4255                Sysno::nanosleep,
4256                Sysno::clock_nanosleep,
4257                Sysno::sched_yield,
4258                Sysno::ppoll,
4259                Sysno::prlimit64,
4260                Sysno::epoll_create1,
4261                Sysno::epoll_ctl,
4262                Sysno::epoll_pwait,
4263                Sysno::recvfrom,
4264                Sysno::rt_sigsuspend,
4265                Sysno::rt_sigtimedwait,
4266                Sysno::execve,
4267                Sysno::execveat,
4268                Sysno::rseq,
4269                Sysno::getpid,
4270                Sysno::gettid,
4271                Sysno::getcpu,
4272                Sysno::rt_sigprocmask,
4273                Sysno::rt_sigaction,
4274                Sysno::getrusage,
4275                Sysno::sysinfo,
4276                // AUTONOMOUS-BOT-IMPLEMENTED
4277                // TODO-HUMAN-REVIEW(#686): Review scratch fd sets and scheduler polling.
4278                Sysno::pselect6,
4279                // `select` is included by the Determinized sweep below;
4280                // T137258824 tracks improving its implementation.
4281            ]);
4282            // The same calls under their x86_64-only legacy numbers.
4283            #[cfg(target_arch = "x86_64")]
4284            subscription.syscalls([
4285                Sysno::open,
4286                Sysno::creat,
4287                Sysno::access,
4288                Sysno::arch_prctl,
4289                Sysno::fork,
4290                Sysno::vfork,
4291                Sysno::dup2,
4292                Sysno::pipe,
4293                Sysno::utime,
4294                Sysno::utimes,
4295                Sysno::futimesat,
4296                Sysno::eventfd,
4297                Sysno::signalfd,
4298                Sysno::inotify_init,
4299                Sysno::poll,
4300                Sysno::epoll_create,
4301                Sysno::epoll_wait,
4302                Sysno::epoll_wait_old,
4303                Sysno::epoll_ctl_old,
4304            ]);
4305
4306            // TODO: some of the above could probably move to this bucket.
4307            // aarch64 has neither `alarm` nor `pause`; its libc builds them
4308            // from `setitimer` and `ppoll`, which are listed above.
4309            #[cfg(target_arch = "x86_64")]
4310            if do_sched {
4311                subscription.syscalls([Sysno::alarm, Sysno::pause]);
4312            }
4313
4314            if config.virtualize_metadata {
4315                subscription.syscalls([Sysno::getdents64, Sysno::fstat, Sysno::statx]);
4316                #[cfg(target_arch = "x86_64")]
4317                subscription.syscalls([
4318                    Sysno::getdents,
4319                    Sysno::stat,
4320                    Sysno::lstat,
4321                    Sysno::newfstatat,
4322                ]);
4323                #[cfg(target_arch = "aarch64")]
4324                subscription.syscall(Sysno::fstatat);
4325            }
4326
4327            if true
4328            // TODO: could introduce a flag for this:
4329            /* config.virtualize_keys */
4330            {
4331                subscription.syscalls([Sysno::add_key, Sysno::request_key, Sysno::keyctl]);
4332            }
4333
4334            if do_sched {
4335                subscription.syscall(Sysno::connect);
4336            }
4337            if do_sched || config.warn_non_zero_binds {
4338                subscription.syscall(Sysno::bind);
4339            }
4340
4341            if config.warn_non_zero_binds {
4342                subscription.syscall(Sysno::bind);
4343            }
4344
4345            if config.virtualize_time {
4346                subscription.rdtsc();
4347                subscription.syscalls([
4348                    Sysno::gettimeofday,
4349                    Sysno::clock_gettime,
4350                    Sysno::clock_getres,
4351                ]);
4352                #[cfg(target_arch = "x86_64")]
4353                subscription.syscall(Sysno::time);
4354            }
4355
4356            if config.virtualize_cpuid {
4357                subscription.cpuid();
4358            }
4359
4360            // AUTONOMOUS-BOT-IMPLEMENTED
4361            // TODO-HUMAN-REVIEW(PR-978): Keep the passthru-opt allow-list in sync
4362            // with the complete Determinized classification AND with the
4363            // Unsupported set. Even under the performance opt-in, Detcore must
4364            // see every syscall that its audited policy says it models or
4365            // deterministically refuses. Otherwise record/replay can bypass a
4366            // Detcore handler and execute the syscall natively against the host.
4367            //
4368            // `Determinized` and `Unsupported` are disjoint classifications, so
4369            // the determinized filter alone leaves the Unsupported set
4370            // unsubscribed: record/replay would silently execute an unsupported
4371            // syscall on the live host instead of invalidating its determinism
4372            // claim. Both halves are required; neither implies the other.
4373            // NOTE: `all_pinned_syscalls()`, not `Sysno::iter()`. The latter
4374            // stops one short of the end of the table, which silently dropped
4375            // `lsm_list_modules` out of this sweep.
4376            subscription.syscalls(crate::all_pinned_syscalls().filter(|sysno| {
4377                crate::is_determinized_syscall(*sysno) || crate::is_unsupported_syscall(*sysno)
4378            }));
4379
4380            // Make sure we also intercept everything that the record-or-replay tool
4381            // wants.
4382            subscription | T::subscriptions(config)
4383        }
4384    }
4385
4386    #[cfg(target_arch = "x86_64")]
4387    async fn handle_cpuid_event<G: Guest<Self>>(
4388        &self,
4389        guest: &mut G,
4390        eax: u32,
4391        ecx: u32,
4392    ) -> Result<CpuIdResult, Errno> {
4393        self.catch_up_lone_slice_ends(guest).await;
4394        trace!("handle_cpuid_event: eax: {}, ecx: {}", eax, ecx);
4395        // A trapped instruction stops the guest in user code (A5 step 3.2).
4396        guest.thread_state_mut().delivery_interruption = Some(reverie::Interruption::NotInSyscall);
4397        if self.cfg.backend.virtualizes_guest_sigalrm && guest.thread_state().sigalrm_handled {
4398            self.sync_sigalrm_eligibility(guest).await;
4399        }
4400        let boundary_entry = slice_boundary::callbacks::Callback::Cpuid;
4401        let boundary_token = self
4402            .pre_handler_hook_observed(guest, false, CheckIn::InstructionTrap, Some(boundary_entry))
4403            .await;
4404        let res = if self.cfg.virtualize_cpuid {
4405            let dettid = guest.thread_state().dettid;
4406            let time = &mut guest.thread_state_mut().thread_logical_time;
4407            let intercepted = self.virtual_cpuid();
4408            time.add_cpuid();
4409            let nanos = time.as_nanos();
4410            trace!(
4411                "[dtid {}] inbound cpuid, new logical time: {:?}",
4412                dettid, time
4413            );
4414            if self.cfg.observes_schedevents() {
4415                trace_schedevent(
4416                    guest,
4417                    SchedEvent {
4418                        dettid,
4419                        op: Op::Cpuid,
4420                        count: 1,
4421                        start_rip: None,
4422                        end_rip: None,
4423                        end_time: Some(nanos),
4424                    },
4425                    true,
4426                )
4427                .await;
4428            }
4429            match intercepted.cpuid(eax, ecx) {
4430                // Under `--virtual-cpus` each thread executes on its own
4431                // virtual CPU, whose APIC IDs the answer names.
4432                Some(result)
4433                    if let Some(cpus) = self.cfg.virtual_cpus
4434                        && cpuid::names_executing_cpu(eax) =>
4435                {
4436                    let cpu = self.virtual_cpu(guest, cpus.get()).await;
4437                    cpuid::on_virtual_cpu(eax, result, cpu)
4438                }
4439                Some(result) => result,
4440                None => {
4441                    warn!(
4442                        "[dtid {}] cpuid leaf 0x{:x} subleaf 0x{:x} not in deterministic table; returning zero result",
4443                        dettid, eax, ecx
4444                    );
4445                    CpuIdResult {
4446                        eax: 0,
4447                        ebx: 0,
4448                        ecx: 0,
4449                        edx: 0,
4450                    }
4451                }
4452            }
4453        } else {
4454            cpuid!(eax, ecx)
4455        };
4456        self.post_handler_hook_observed(guest, CheckIn::InstructionTrap, boundary_token)
4457            .await;
4458        Ok(res)
4459    }
4460
4461    #[cfg(target_arch = "x86_64")]
4462    async fn handle_rdtsc_event<G: Guest<Self>>(
4463        &self,
4464        guest: &mut G,
4465        request: Rdtsc,
4466    ) -> Result<RdtscResult, Errno> {
4467        self.catch_up_lone_slice_ends(guest).await;
4468        trace!("handle_rdtsc_event: {:?}", request);
4469        // A trapped instruction stops the guest in user code (A5 step 3.2).
4470        guest.thread_state_mut().delivery_interruption = Some(reverie::Interruption::NotInSyscall);
4471        if self.cfg.backend.virtualizes_guest_sigalrm && guest.thread_state().sigalrm_handled {
4472            self.sync_sigalrm_eligibility(guest).await;
4473        }
4474        let boundary_entry = slice_boundary::callbacks::Callback::Rdtsc;
4475        let boundary_token = self
4476            .pre_handler_hook_observed(guest, false, CheckIn::InstructionTrap, Some(boundary_entry))
4477            .await;
4478        let result = if guest.config().virtualize_time {
4479            let dettid = guest.thread_state().dettid;
4480            guest.thread_state_mut().thread_logical_time.add_rdtsc();
4481            crate::ordered_info!(
4482                "[dtid {}] inbound rdtsc, new logical time: {:?}",
4483                dettid,
4484                guest.thread_state().thread_logical_time
4485            );
4486            // The guest TSC must name the same instant as `clock_gettime`. Both
4487            // now read the coordinator's clock through the shared per-process
4488            // floor, so a guest comparing the two -- a clocksource watchdog, a
4489            // delay loop calibrated against a device timer, a second vCPU
4490            // reading the TSC -- sees one time base instead of two.
4491            //
4492            // The `add_rdtsc()` charge above is folded into global time by the
4493            // same RPC that reads it back (`send_and_update_time` updates the
4494            // coordinator before dispatching the request), so consecutive reads
4495            // from one thread still advance.
4496            //
4497            // Read it before the event is traced. Under `--replay-schedule-from`
4498            // tracing the event is where the replayer hands the CPU to the next
4499            // recorded thread when this read was the last event of a slice, so
4500            // reading after it would see the time those threads ran. The
4501            // recording read the clock here, before its slice ended in the post
4502            // hook, and so does the replay now.
4503            let tsc = guest_clock_time(guest).await;
4504            if self.cfg.observes_schedevents() {
4505                let ev = with_guest_time(
4506                    guest,
4507                    SchedEvent {
4508                        dettid,
4509                        op: Op::Rdtsc,
4510                        count: 1,
4511                        start_rip: None,
4512                        end_rip: None,
4513                        end_time: None,
4514                    },
4515                );
4516                trace_schedevent(guest, ev, true).await;
4517            }
4518            Ok(RdtscResult {
4519                // We treat virtual cycles as equivalent to virtual nanoseconds.
4520                tsc: tsc.as_nanos(),
4521                aux: None,
4522            })
4523        } else {
4524            self.record_or_replay
4525                .handle_rdtsc_event(&mut guest.into_guest(), request)
4526                .await
4527        };
4528        self.post_handler_hook_observed(guest, CheckIn::InstructionTrap, boundary_token)
4529            .await;
4530        result
4531    }
4532
4533    // Note: we will not see SIGSTKFLT used for timers.
4534    async fn handle_signal_event<G: Guest<Self>>(
4535        &self,
4536        guest: &mut G,
4537        signal: Signal,
4538    ) -> Result<Option<Signal>, Errno> {
4539        self.catch_up_lone_slice_ends(guest).await;
4540        let delivered = self
4541            .handle_inbound_signal(guest, SigWrapper::from(signal))
4542            .await?;
4543        if scheduler::discarded_at_send::records(&self.cfg)
4544            && let Some(control) = consumed_signal_control(
4545                guest.pid(),
4546                guest.tid(),
4547                signal as i32,
4548                delivered.is_some(),
4549            )
4550        {
4551            tool_global::discarded_signal(guest, control).await;
4552        }
4553        if delivered.is_some() {
4554            crate::syscalls::signal_shadow::check_and_record_delivery(guest, signal as i32).await;
4555        }
4556        Ok(delivered.map(|_| signal))
4557    }
4558
4559    /// A realtime signal (32 to 64), which `Signal` cannot name: handled as
4560    /// any other (`handle_inbound_signal`), so it takes its turn in the
4561    /// schedule (<https://github.com/rrnewton/reverie/issues/207>).
4562    async fn handle_raw_signal_event<G: Guest<Self>>(
4563        &self,
4564        guest: &mut G,
4565        signal: i32,
4566    ) -> Result<Option<i32>, Errno> {
4567        self.catch_up_lone_slice_ends(guest).await;
4568        let delivered = self
4569            .handle_inbound_signal(guest, SigWrapper(signal))
4570            .await?;
4571        if delivered.is_some() {
4572            crate::syscalls::signal_shadow::check_and_record_delivery(guest, signal).await;
4573        }
4574        Ok(delivered.map(|signal| signal.raw()))
4575    }
4576
4577    /// The first half of a delivery a backend announces before the kernel
4578    /// dequeues it (an in-guest backend's prediction at a return to guest
4579    /// code): the delivery's turn alone (`inbound_turn` with
4580    /// `TurnScope::Announcement`), kept under the announcement's token
4581    /// (`ThreadState::signal_announcements`). Detcore never suppresses here:
4582    /// its drop decisions (a duplicate child-exit `SIGCHLD`, a stale POSIX
4583    /// timer entry) exist only in configurations it refuses. A signal outside
4584    /// 1 to 64 is `EINVAL`. A configuration in which the commit could not
4585    /// keep the protocol, or a thread holding a claimed `SIGCHLD`, is refused
4586    /// here, before any turn, with `EPROTONOSUPPORT` (`two_phase_refusal`):
4587    /// Detcore has opted in, and `EOPNOTSUPP` is left to mean that a Tool has
4588    /// not (Reverie's contract, step 4).
4589    async fn announce_signal_event<G: Guest<Self>>(
4590        &self,
4591        guest: &mut G,
4592        signal: i32,
4593    ) -> Result<Option<reverie::SignalAnnouncement>, Errno> {
4594        self.catch_up_lone_slice_ends(guest).await;
4595        if !(1..=64).contains(&signal) {
4596            return Err(Errno::EINVAL);
4597        }
4598        let refusal = two_phase_refusal(&self.cfg).or_else(|| {
4599            guest
4600                .thread_state()
4601                .claimed_sigchld
4602                .then_some("a claimed SIGCHLD must be placed before its delivery's turn")
4603        });
4604        if let Some(reason) = refusal {
4605            warn!(
4606                "[dtid {}] refusing a two-phase delivery of signal {}: {}",
4607                guest.thread_state().dettid,
4608                signal,
4609                reason
4610            );
4611            return Err(Errno::EPROTONOSUPPORT);
4612        }
4613        // A5 step 3c.1e A, IR-15: under the in-guest delivery gate, no
4614        // default-action delivery is predicted for a process that is the init
4615        // of a PID namespace it created (`SIGNAL_UNKILLABLE`: such a signal is
4616        // ignored at send unless blocked, kernel/signal.c:92-94 via :104-120,
4617        // and a blocked one is discarded at dequeue, :2843-2845 at v6.12;
4618        // SIGKILL and SIGSTOP too, unless the send is forced, from an
4619        // ancestor PID namespace or the kernel, `force && sig_kernel_only`,
4620        // :93 and :1217-1224, which the hook cannot tell); a caught signal is
4621        // announced as for any process
4622        // (`pid_namespace_init_refused`). Refused before any turn, by name;
4623        // nothing is asked with the gate off.
4624        if self.cfg.in_guest_signal_delivery
4625            && delivery_takes_default_action(guest, signal)
4626            && self
4627                .pid_namespace_init_refused(guest, "announce_signal_event", signal)
4628                .await
4629        {
4630            return Err(Errno::EPROTONOSUPPORT);
4631        }
4632        let stop = self.inbound_stop(guest);
4633        let turn = self
4634            .inbound_turn(guest, SigWrapper(signal), stop, TurnScope::Announcement)
4635            .await;
4636        let state = guest.thread_state_mut();
4637        let token = state.next_signal_announcement;
4638        state.next_signal_announcement = token.wrapping_add(1);
4639        state
4640            .signal_announcements
4641            .push(tool_local::SignalAnnouncementState {
4642                token,
4643                signal,
4644                turn,
4645            });
4646        Ok(Some(reverie::SignalAnnouncement::new(signal, token)))
4647    }
4648
4649    /// The second half of an announced delivery, once the backend's final
4650    /// read confirmed it, in ptrace's order: what the dequeue settles
4651    /// (`inbound_dequeued`), what the announcement's turn left
4652    /// (`inbound_turn_settled`, which also gives the delivery its ordinal),
4653    /// what the granted delivery settles (`inbound_delivered`), the delivery
4654    /// check (`check_delivery_to_record`, whose `Guest::deliver_signal` is
4655    /// the backend's check of its prediction and whose siginfo read gets
4656    /// none), and, if the check approves, the record
4657    /// (`record_checked_delivery`). An announcement this thread does not
4658    /// hold, signal and token both, is `EINVAL`; a drop decision, which only
4659    /// refused configurations make, is `EPROTO`.
4660    ///
4661    /// It must not wait for a turn. The announcement refused every
4662    /// configuration in which one of its parts files a scheduler request; if
4663    /// the thread was granted a turn all the same, the backend's final read
4664    /// no longer holds, so the commit fails closed with `EDEADLK`, before the
4665    /// delivery is recorded. It checks twice: after `inbound_delivered`'s
4666    /// effects, before the delivery check, and after the delivery check
4667    /// (whose `Guest::deliver_signal` must not wait for a turn), before the
4668    /// record. Any `Err` ends the run.
4669    async fn commit_signal_event<G: Guest<Self>>(
4670        &self,
4671        guest: &mut G,
4672        announcement: reverie::SignalAnnouncement,
4673    ) -> Result<(), Errno> {
4674        self.catch_up_lone_slice_ends(guest).await;
4675        let Some(index) = guest
4676            .thread_state()
4677            .signal_announcements
4678            .iter()
4679            .position(|held| {
4680                held.token == announcement.token() && held.signal == announcement.signal()
4681            })
4682        else {
4683            return Err(Errno::EINVAL);
4684        };
4685        let held = guest.thread_state_mut().signal_announcements.remove(index);
4686        let grants = guest.thread_state().scheduler_grants;
4687        let signal = SigWrapper(held.signal);
4688        let Some(timer_entry) = self.inbound_dequeued(guest, signal).await? else {
4689            warn!(
4690                "[dtid {}] announced signal {} was dropped at its commit",
4691                guest.thread_state().dettid,
4692                held.signal
4693            );
4694            return Err(Errno::EPROTO);
4695        };
4696        let mycount = self.inbound_turn_settled(guest, signal).await;
4697        let mut boundary_token = held.turn.boundary_token;
4698        preemptions::callback_history::delivered_signal(&mut boundary_token, signal, mycount)
4699            .expect("committed announcement must link to its actual delivery ordinal");
4700        let turn = tool_local::InboundTurn {
4701            dettid: held.turn.dettid,
4702            mycount,
4703            boundary_token,
4704        };
4705        self.inbound_delivered(guest, signal, turn, timer_entry)
4706            .await?;
4707        if guest.thread_state().scheduler_grants != grants {
4708            warn!(
4709                "[dtid {}] the commit of announced signal {} was granted a turn; \
4710                 the backend's final read no longer holds",
4711                guest.thread_state().dettid,
4712                held.signal
4713            );
4714            return Err(Errno::EDEADLK);
4715        }
4716        let record =
4717            crate::syscalls::signal_shadow::check_delivery_to_record(guest, held.signal).await;
4718        if guest.thread_state().scheduler_grants != grants {
4719            warn!(
4720                "[dtid {}] checking announced signal {} was granted a turn",
4721                guest.thread_state().dettid,
4722                held.signal
4723            );
4724            return Err(Errno::EDEADLK);
4725        }
4726        if record {
4727            crate::syscalls::signal_shadow::record_checked_delivery(guest, held.signal);
4728        }
4729        Ok(())
4730    }
4731
4732    /// An announced delivery that does not happen at the backend's return:
4733    /// its turn stays taken (one more turn than ptrace, which never reports a
4734    /// delivery that does not happen), and nothing else was done for it, so
4735    /// the signal, still pending, is announced again at a later return.
4736    fn withdraw_signal_event(
4737        &self,
4738        thread_state: &mut Self::ThreadState,
4739        announcement: reverie::SignalAnnouncement,
4740    ) {
4741        thread_state.signal_announcements.retain(|held| {
4742            held.token != announcement.token() || held.signal != announcement.signal()
4743        });
4744    }
4745
4746    /// A death the backend's final read predicted, after its commits (A5
4747    /// design 16.30; Reverie's contract, step 3): the thread keeps its turn
4748    /// for its group's exit (`Scheduler::hold_turn_for_group_exit`), so no
4749    /// other thread takes a turn between the decision and the grant, then
4750    /// files the group `Exit`, which that hold grants at the very next turn
4751    /// and which starts the physical-exit hold. This is what DBT's fatal
4752    /// default action does (`end_thread_group_before_fatal_delivery`), here
4753    /// at the in-guest runtime's return, where the kernel then ends the
4754    /// group.
4755    ///
4756    /// Robust-futex owner-death wakes are not claimed: the group's robust
4757    /// lists are not walked here, which stays the named gap of
4758    /// <https://github.com/rrnewton/hermit/issues/4268> (D24, IR-11).
4759    ///
4760    /// Refused with `EPROTONOSUPPORT` before anything is filed when the run
4761    /// does not take in-guest delivery (`Config::in_guest_signal_delivery`,
4762    /// the gate this protocol needs), and when the thread does not hold the
4763    /// turn, which leaves the decision unprotected: Detcore has opted in, so
4764    /// `EOPNOTSUPP` stays Reverie's "not opted in". A status that is not a
4765    /// death by a signal is `EINVAL`.
4766    async fn exit_for_predicted_death<G: Guest<Self>>(
4767        &self,
4768        guest: &mut G,
4769        status: ExitStatus,
4770    ) -> Result<(), Errno> {
4771        self.catch_up_lone_slice_ends(guest).await;
4772        if !matches!(
4773            status,
4774            ExitStatus::Signaled(..) | ExitStatus::SignaledRaw(..)
4775        ) {
4776            return Err(Errno::EINVAL);
4777        }
4778        let dettid = guest.thread_state().dettid;
4779        if !self.cfg.in_guest_signal_delivery {
4780            warn!(
4781                "[dtid {dettid}] refusing a predicted death ({status:?}): this run does not \
4782                 take in-guest signal delivery"
4783            );
4784            return Err(Errno::EPROTONOSUPPORT);
4785        }
4786        // A5 step 3c.1e A, IR-15: a predicted death is a default action, and
4787        // a PID namespace's init is not killed by one (`SIGNAL_UNKILLABLE`:
4788        // ignored at send unless blocked, kernel/signal.c:92-94 via :104-120,
4789        // a blocked one discarded at dequeue, :2843-2845 at v6.12), so no
4790        // death is predicted for it: refused before the turn is held.
4791        let signal = match status {
4792            ExitStatus::Signaled(signal, _) => signal as i32,
4793            ExitStatus::SignaledRaw(signal, _) => signal,
4794            ExitStatus::Exited(_) => unreachable!("checked above"),
4795        };
4796        if self
4797            .pid_namespace_init_refused(guest, "exit_for_predicted_death", signal)
4798            .await
4799        {
4800            return Err(Errno::EPROTONOSUPPORT);
4801        }
4802        if !tool_global::hold_turn_for_group_exit(guest, dettid).await {
4803            warn!(
4804                "[dtid {dettid}] refusing a predicted death ({status:?}): the thread does not \
4805                 hold the turn its group's exit needs"
4806            );
4807            return Err(Errno::EPROTONOSUPPORT);
4808        }
4809        debug!("[dtid {dettid}] predicted death ({status:?}) ends its thread group");
4810        let state = guest.thread_state();
4811        let request = state.mk_request(
4812            ResourceID::Exit {
4813                group: true,
4814                process: state.detpid.expect("detpid unset"),
4815                mm: state.mm_id,
4816            },
4817            Permission::RW,
4818        );
4819        tool_global::resource_request(guest, request).await;
4820        Ok(())
4821    }
4822
4823    /// A `SIGCHLD` the backend passes on without reporting it is dropped when
4824    /// it is the kernel's copy of a child-exit notification the scheduler
4825    /// sends itself (`scheduler::child_exit_sigchld`). A copy the scheduler
4826    /// sent is kept, with the siginfo the tracer gave it: this hook cannot
4827    /// replace it.
4828    ///
4829    /// Either way, the scheduler's record of copies Linux discarded when they
4830    /// were sent learns what became of the copy (`consumed_signal_control`,
4831    /// <https://github.com/rrnewton/hermit/issues/4293>).
4832    async fn filter_unreported_signal<R: GlobalRPC<Self::GlobalState>>(
4833        &self,
4834        rpc: &R,
4835        unreported: reverie::UnreportedSignal,
4836    ) -> bool {
4837        if unreported.signal() == Some(Signal::SIGALRM)
4838            && scheduler::discarded_at_send::records(&self.cfg)
4839        {
4840            let tid = DetTid::from_raw(unreported.tid().as_raw());
4841            rpc.send_rpc((
4842                DetTime::default(),
4843                MmId::initial(tid),
4844                tool_global::GlobalRequest::DiscardedSignal(
4845                    scheduler::discarded_at_send::DiscardedSignalControl::UnreportedAlarm,
4846                ),
4847            ))
4848            .await;
4849        }
4850        // A POSIX timer's stale entry is skipped, as Linux skips it inside the
4851        // dequeue (<https://github.com/rrnewton/hermit/issues/4262>).
4852        let stale_timer_entry = scheduler::discarded_at_send::records(&self.cfg)
4853            && (1..32).contains(&unreported.number())
4854            && {
4855                let tid = DetTid::from_raw(unreported.tid().as_raw());
4856                let control = scheduler::discarded_at_send::DiscardedSignalControl::Unreported {
4857                    signal: unreported.number(),
4858                };
4859                // Carries no logical time; answered before any clock accounting.
4860                let (_, response) = rpc
4861                    .send_rpc((
4862                        DetTime::default(),
4863                        MmId::initial(tid),
4864                        tool_global::GlobalRequest::DiscardedSignal(control),
4865                    ))
4866                    .await;
4867                matches!(
4868                    response,
4869                    tool_global::GlobalResponse::DiscardedSignal(
4870                        scheduler::discarded_at_send::DiscardedSignalAnswer::Delivery(
4871                            scheduler::posix_timer_coalesce::DeliveryAnswer::Suppress
4872                        )
4873                    )
4874                )
4875            };
4876        let kept = !stale_timer_entry && async {
4877            // The test-only claim fault drops the claimed copy here too, where a
4878            // relative `nanosleep`'s restart code makes the backend pass it on
4879            // unreported (`syscalls::signal::take_claim_fault`).
4880            if unreported.signal() == Some(Signal::SIGCHLD)
4881                && crate::syscalls::take_claim_fault(crate::syscalls::ClaimFault::DropClaimed)
4882            {
4883                warn!(
4884                    "[tid {}] claim fault hook: dropped the claimed SIGCHLD",
4885                    unreported.tid()
4886                );
4887                return false;
4888            }
4889            if unreported.signal() != Some(Signal::SIGCHLD)
4890                || !self.cfg.backend.reports_kernel_child_exit_signals
4891            {
4892                return true;
4893            }
4894            let Some(info) = unreported.siginfo() else {
4895                return true;
4896            };
4897            let tid = DetTid::from_raw(unreported.tid().as_raw());
4898            // Passed on unreported, a kept copy makes no `InboundSignal` request.
4899            match scheduler::child_exit_sigchld::control_for(&info, std::process::id(), false) {
4900                None => true,
4901                Some(control) => {
4902                    use scheduler::child_exit_sigchld::ChildExitSigchldAnswer;
4903                    use scheduler::child_exit_sigchld::OwnCopy;
4904                    // The control message carries no logical time; the scheduler answers
4905                    // it before any clock accounting.
4906                    let (_, response) = rpc
4907                        .send_rpc((
4908                            DetTime::default(),
4909                            MmId::initial(tid),
4910                            tool_global::GlobalRequest::ChildExitSigchld(control),
4911                        ))
4912                        .await;
4913                    match response {
4914                        tool_global::GlobalResponse::ChildExitSigchld(
4915                            ChildExitSigchldAnswer::Claimed(dropped),
4916                        ) => !dropped,
4917                        tool_global::GlobalResponse::ChildExitSigchld(
4918                            ChildExitSigchldAnswer::OwnCopy(OwnCopy::Duplicate),
4919                        ) => false,
4920                        tool_global::GlobalResponse::ChildExitSigchld(
4921                            ChildExitSigchldAnswer::OwnCopy(OwnCopy::Deliver(own))
4922                            | ChildExitSigchldAnswer::ClaimedFor(own),
4923                        ) => {
4924                            warn!(
4925                                "[dtid {}] the SIGCHLD for child {} passed on unreported keeps its delivered siginfo",
4926                                tid, own.pid
4927                            );
4928                            true
4929                        }
4930                        _ => true,
4931                    }
4932                }
4933            }
4934
4935        }
4936        .await;
4937        if scheduler::discarded_at_send::records(&self.cfg)
4938            && let Some(control) = consumed_signal_control(
4939                unreported.pid(),
4940                unreported.tid(),
4941                unreported.number(),
4942                kept,
4943            )
4944        {
4945            let tid = DetTid::from_raw(unreported.tid().as_raw());
4946            // Carries no logical time; answered before any clock accounting.
4947            rpc.send_rpc((
4948                DetTime::default(),
4949                MmId::initial(tid),
4950                tool_global::GlobalRequest::DiscardedSignal(control),
4951            ))
4952            .await;
4953        }
4954        // The signal-state copy lives in this process wherever it is kept
4955        // (`syscalls::signal_shadow`); a signal that reaches a handler
4956        // unreported is one it never saw.
4957        let signal = unreported.number();
4958        if kept && self.copy_misses_unreported(signal) {
4959            crate::syscalls::signal_shadow::note_unreported(
4960                unreported.pid().as_raw(),
4961                unreported.tid().as_raw(),
4962                signal,
4963            );
4964        }
4965        kept
4966    }
4967
4968    fn init_thread_state(
4969        &self,
4970        tid: Tid,
4971        parent: Option<(Tid, &Self::ThreadState)>,
4972    ) -> Self::ThreadState {
4973        trace!("[tid {}] detcore init new thread state", tid);
4974
4975        let record_or_replay = self
4976            .record_or_replay
4977            .init_thread_state(tid, parent.map(|(ptid, ts)| (ptid, ts.as_ref())));
4978
4979        // TODO(T78538674): virtualize tid, extend tid<=>dettid mapping here.
4980        match parent {
4981            None => ThreadState::new(DetPid::from_raw(tid.into()), &self.cfg, record_or_replay),
4982            Some(pts) => {
4983                let clone_flags = pts
4984                    .1
4985                    .clone_flags
4986                    .expect("clone_flags must be set by parent");
4987                let dettid = DetPid::from_raw(tid.into());
4988
4989                // If we had mutable access to the parent state, we could update it here, but
4990                // instead we leave that to the clone/fork handling.
4991                let (_next_parent_pedigree, child_pedigree) = pts.1.pedigree.fork();
4992                let child_logical_time = pts.1.thread_logical_time.clone_for_child();
4993                let last_accounted_user_time = child_logical_time.user_cpu_time();
4994                let last_accounted_system_time = child_logical_time.system_cpu_time();
4995                if !clone_flags.contains(CloneFlags::CLONE_THREAD) {
4996                    pts.1.prepare_child_process_cpu_time(dettid);
4997                }
4998                let guest_clock = Arc::clone(&pts.1.guest_clock);
4999                let unix_connections = Arc::clone(&pts.1.unix_connections);
5000                // A pending unreported signal may already have changed the
5001                // parent's actions unseen: the child gets no copy then.
5002                let child_signal_shadow = pts
5003                    .1
5004                    .signal_shadow
5005                    .as_ref()
5006                    .filter(|_| !crate::syscalls::signal_shadow::unreported_pending())
5007                    .and_then(|shadow| shadow.for_clone(clone_flags.bits()));
5008                // A child sharing the actions without a copy could change them
5009                // unrecorded: they become unknown to every sharer.
5010                if child_signal_shadow.is_none()
5011                    && clone_flags.contains(CloneFlags::CLONE_SIGHAND)
5012                    && let Some(shadow) = pts.1.signal_shadow.as_ref()
5013                {
5014                    shadow.forget_dispositions();
5015                }
5016                if child_signal_shadow.is_some() {
5017                    trace!(
5018                        "[dtid {}] signal-state copy: child {} gets a copy",
5019                        pts.1.dettid, dettid
5020                    );
5021                } else if pts.1.signal_shadow.is_some() {
5022                    debug!(
5023                        "[dtid {}] signal-state copy: child {} gets none (created inside a \
5024                         handler, an unreported signal pending, or the actions unknown)",
5025                        pts.1.dettid, dettid
5026                    );
5027                } else if pts.1.signal_shadow_lost {
5028                    debug!(
5029                        "[dtid {}] signal-state copy: child {} gets none (its creator's was lost)",
5030                        pts.1.dettid, dettid
5031                    );
5032                }
5033                debug!(
5034                    "[init_thread-state, parent dtid = {}] child thread {}, clone_flags = {:x?}",
5035                    pts.0, tid, clone_flags
5036                );
5037                // The table's holders count the child, of its creator's
5038                // process if it is a thread and of its own otherwise, here,
5039                // as its creator's `clone` makes it
5040                // (`tool_local::FileMetadata::holders`).
5041                let holder = if clone_flags.contains(CloneFlags::CLONE_THREAD) {
5042                    pts.1.detpid.unwrap_or(pts.1.dettid)
5043                } else {
5044                    dettid
5045                };
5046                let file_metadata = if clone_flags.contains(CloneFlags::CLONE_FILES) {
5047                    pts.1.file_metadata.lock().unwrap().join_holder(holder);
5048                    pts.1.file_metadata.clone()
5049                } else {
5050                    let mut copy = pts.1.file_metadata.lock().unwrap().fork_for(dettid);
5051                    copy.hold_alone(holder);
5052                    Arc::new(Mutex::new(copy))
5053                };
5054                // A thread is of its creator's process. A new process leads a
5055                // thread group of its own, whose table is the one it starts
5056                // with: its creator's under CLONE_FILES, a copy otherwise.
5057                let process_files = if clone_flags.contains(CloneFlags::CLONE_THREAD) {
5058                    pts.1.process_files
5059                } else {
5060                    Some(file_metadata.lock().unwrap().files_id)
5061                };
5062
5063                ThreadState {
5064                    dettid,
5065                    // Boundary production is not enabled yet; a child must
5066                    // never inherit its parent's occurrence/hint ownership.
5067                    slice_boundary: None,
5068                    callback_history: Default::default(),
5069                    schedule_replay_over: pts.1.schedule_replay_over,
5070                    detpid: None, // Initialized later.
5071                    thread_start_entered: false,
5072                    vfork_origin: if clone_flags.contains(CloneFlags::CLONE_VFORK) {
5073                        pts.1.pending_vfork.as_ref().map(|pending| pending.op)
5074                    } else {
5075                        None
5076                    },
5077                    physical_tid: None,
5078                    signal_task_identity: None,
5079                    open_file_creator: None,
5080                    receive_geometry: None,
5081                    entry_import: None,
5082                    send_inspection: None,
5083                    inotify_queue: None,
5084                    call_parts: None,
5085                    traced_call: Default::default(),
5086                    mm_id: MmId::for_clone(
5087                        pts.1.mm_id,
5088                        dettid,
5089                        clone_flags.contains(CloneFlags::CLONE_VM),
5090                    ),
5091                    memory_metadata: if clone_flags.contains(CloneFlags::CLONE_VM) {
5092                        Arc::clone(&pts.1.memory_metadata)
5093                    } else {
5094                        Arc::new(Mutex::new(
5095                            pts.1
5096                                .memory_metadata
5097                                .lock()
5098                                .expect("memory metadata mutex poisoned")
5099                                .clone(),
5100                        ))
5101                    },
5102                    pedigree: child_pedigree.clone(),
5103                    initialized_random_auxv: None,
5104                    stats: ThreadStats::new(),
5105                    file_metadata,
5106                    process_files,
5107                    tool_shares_guest_descriptor_table: pts.1.tool_shares_guest_descriptor_table,
5108                    // Linux copies the creating thread's current timer slack
5109                    // into both fields of every new task (thread or process).
5110                    timer_slack_ns: pts.1.timer_slack_ns,
5111                    default_timer_slack_ns: pts.1.timer_slack_ns,
5112                    // A new process created with CLONE_NEWPID is the init of
5113                    // a namespace below its creator's; everything else lives
5114                    // in its creator's.
5115                    nested_pid_namespace: pts.1.nested_pid_namespace
5116                        || (clone_flags.contains(CloneFlags::CLONE_NEWPID)
5117                            && !clone_flags.contains(CloneFlags::CLONE_THREAD)),
5118                    // A new task starts in its creator's root, and with
5119                    // CLONE_FS keeps sharing it.
5120                    changed_root: if clone_flags.contains(CloneFlags::CLONE_FS) {
5121                        Arc::clone(&pts.1.changed_root)
5122                    } else {
5123                        Arc::new(std::sync::atomic::AtomicBool::new(
5124                            pts.1.changed_root.load(std::sync::atomic::Ordering::SeqCst),
5125                        ))
5126                    },
5127                    // A thread shares its leader's filesystem information
5128                    // where it and every thread before it in its line were
5129                    // created with CLONE_FS; a new process leads its own.
5130                    fs_apart_from_leader: clone_flags.contains(CloneFlags::CLONE_THREAD)
5131                        && (pts.1.fs_apart_from_leader
5132                            || !clone_flags.contains(CloneFlags::CLONE_FS)),
5133                    // A new task runs its creator's image.
5134                    exe: pts.1.exe.clone(),
5135                    // POSIX timers are shared among threads of a process but are
5136                    // NOT inherited across fork(2). Share the table for a new
5137                    // thread (CLONE_THREAD); give a new process a fresh, empty
5138                    // one.
5139                    posix_timers: if clone_flags.contains(CloneFlags::CLONE_THREAD) {
5140                        Arc::clone(&pts.1.posix_timers)
5141                    } else {
5142                        Arc::new(Mutex::new(PosixTimers::default()))
5143                    },
5144                    // Resource limits are process state: threads share them,
5145                    // while a forked process inherits a snapshot.
5146                    resource_limits: if clone_flags.contains(CloneFlags::CLONE_THREAD) {
5147                        Arc::clone(&pts.1.resource_limits)
5148                    } else {
5149                        Arc::new(Mutex::new(
5150                            pts.1
5151                                .resource_limits
5152                                .lock()
5153                                .expect("resource limits mutex poisoned")
5154                                .clone(),
5155                        ))
5156                    },
5157                    process_cpu_time: if clone_flags.contains(CloneFlags::CLONE_THREAD) {
5158                        Arc::clone(&pts.1.process_cpu_time)
5159                    } else {
5160                        Arc::new(Mutex::new(ProcessCpuTime::default()))
5161                    },
5162                    // Wall time belongs to the traced process tree, not to an
5163                    // individual process. Forked processes and cloned threads
5164                    // therefore retain one monotonic view of raw logical time.
5165                    guest_clock,
5166                    // So do the connections waiting for an accept.
5167                    unix_connections,
5168                    parent_process_cpu_time: if clone_flags.contains(CloneFlags::CLONE_THREAD) {
5169                        pts.1.parent_process_cpu_time.clone()
5170                    } else {
5171                        Some(Arc::clone(&pts.1.process_cpu_time))
5172                    },
5173                    parent_cpu_publication: if clone_flags.contains(CloneFlags::CLONE_THREAD) {
5174                        pts.1.parent_cpu_publication.clone()
5175                    } else if self.cfg.backend.emulates_child_waits
5176                        && self.cfg.sequentialize_threads
5177                    {
5178                        Some(pts.1.prepare_child_cpu_publication(dettid))
5179                    } else {
5180                        None
5181                    },
5182                    // A new thread has made no syscall yet.
5183                    restart_block: None,
5184                    syscall_cwd: None,
5185                    // A fork child inherits its parent's signal dispositions.
5186                    sigalrm_handled: pts.1.sigalrm_handled,
5187                    signal_shadow: child_signal_shadow.clone(),
5188                    // A parent whose shared actions were unknown gives its
5189                    // child no copy, and that child is never seeded either.
5190                    // Only a divergence marks a lineage as never to be seeded
5191                    // again; a child denied a copy for a quiet reason is
5192                    // seeded at its exec.
5193                    signal_shadow_lost: pts.1.signal_shadow_lost,
5194                    // A new thread's first stop is at the exit of the call
5195                    // that made it, which Detcore did not finish for it.
5196                    delivery_interruption: None,
5197                    // A new thread has no wait in progress.
5198                    wait_signal_end: None,
5199                    delivery_frame_handler: 0,
5200                    delivery_frame_signal: 0,
5201                    delivery_frame_orig_ax: 0,
5202                    delivery_counters: Default::default(),
5203                    // The threads of a process share the record of what the
5204                    // scheduler holds for it; a new process has nothing
5205                    // published.
5206                    // The scheduler holds no mask for a new thread.
5207                    published_mask: Some(None),
5208                    published_dispositions: if clone_flags.contains(CloneFlags::CLONE_THREAD) {
5209                        Arc::clone(&pts.1.published_dispositions)
5210                    } else {
5211                        Arc::default()
5212                    },
5213                    sigalrm_blocked_published: pts.1.sigalrm_blocked_published,
5214                    handler_restore_rt_reported: pts.1.handler_restore_rt_reported,
5215                    signal_register_projection: None,
5216                    last_accounted_user_time,
5217                    last_accounted_system_time,
5218                    thread_cpu_start_user_time: last_accounted_user_time,
5219                    thread_cpu_start_system_time: last_accounted_system_time,
5220                    clone_flags: None,
5221                    pending_vfork: pts.1.pending_vfork.clone(),
5222                    restarted_clone_id: None,
5223                    procfs_sequence_taken: None,
5224
5225                    // Child RNG identity follows the deterministic creation
5226                    // pedigree, never the backend/host Tid. Guest-visible IDs
5227                    // and scheduler targeting continue to use `dettid`.
5228                    prng: tool_local::thread_rng_from_parent_pedigree(
5229                        "USER RAND",
5230                        &pts.1.prng,
5231                        &child_pedigree,
5232                        tool_local::ChildRngStream::User,
5233                    ),
5234                    chaos_prng: tool_local::thread_rng_from_parent_pedigree(
5235                        "CHAOSRAND",
5236                        &pts.1.chaos_prng,
5237                        &child_pedigree,
5238                        tool_local::ChildRngStream::Chaos,
5239                    ),
5240
5241                    // For comparing progress to other threads, it is important that our
5242                    // child thread start at a sensible place, rather than starting back
5243                    // at zero:
5244                    thread_logical_time: child_logical_time,
5245                    // A new thread gets a new clock, so we've committed 0 ticks
5246                    committed_clock_value: 0,
5247                    // A new thread or process is never the backend runtime's
5248                    // bootstrapping thread, so it starts outside any window.
5249                    uncharged_bootstrap_syscalls: 0,
5250                    in_uncharged_bootstrap_syscall: false,
5251                    // The parent reports its own export with its next request.
5252                    passed_descriptors_out_unreported: false,
5253                    passed_descriptors_out_reported: false,
5254                    nonblocking_result_normalized: false,
5255                    yield_after_futex_wake: false,
5256                    pipe_handoff_due: None,
5257                    own_signal_turn_hold: None,
5258                    delivered_since_request: false,
5259                    returned_from_sigreturn: false,
5260                    // A child starts with no announcement: its parent
5261                    // settles its own before it returns from the clone.
5262                    signal_announcements: Vec::new(),
5263                    next_signal_announcement: 0,
5264                    consecutive_continuations: 0,
5265                    scheduler_grants: 0,
5266
5267                    end_of_timeslice: None,
5268                    replay_rcb_end: None,
5269                    last_replay_change_point: None,
5270                    // AUTONOMOUS-BOT-IMPLEMENTED
5271                    // TODO-HUMAN-REVIEW(PR-1151)
5272                    chaos_epoch: tool_local::chaos_epoch_sentinel(),
5273                    chaos_slowdown_factor: RcbTimeMultiplier::ONE,
5274                    chaos_slowdown_active: false,
5275                    claimed_sigchld: false,
5276                    pending_chaos_epochs: Vec::new(),
5277                    max_timeslice_end: None,
5278                    last_grant_deadline: None,
5279                    published_runtime_private_memory: None,
5280                    last_rcb_timer: None,
5281                    last_rcb_timer_is_max: false,
5282                    lone_grant: false,
5283                    lone_slice_end: None,
5284                    replaying_lone_slice_end: None,
5285                    lone_replay_unsound: false,
5286
5287                    record_or_replay,
5288                    preemption_points: None,
5289
5290                    // We only get to the point of creating child threads if we're past the first execve.
5291                    past_global_first_execve: true,
5292                    interrupt_at: self.cfg.interrupts_for_thread(dettid),
5293
5294                    // `copy_process()` sets `p->robust_list = NULL` for every
5295                    // new task, thread or process alike. The child re-registers
5296                    // its own head before it can own a robust futex.
5297                    robust_list_head: None,
5298                    robust_list_process: if clone_flags.contains(CloneFlags::CLONE_THREAD) {
5299                        Arc::clone(&pts.1.robust_list_process)
5300                    } else {
5301                        Arc::new(Mutex::new(tool_local::RobustListProcessState::default()))
5302                    },
5303                }
5304            }
5305        }
5306    }
5307
5308    fn on_initial_image_attached(
5309        &self,
5310        tid: Tid,
5311        state: &mut Self::ThreadState,
5312    ) -> Result<(), Error> {
5313        state.bind_initial_current_image(DetTid::from_raw(tid.as_raw()))?;
5314        self.record_or_replay
5315            .on_initial_image_attached(tid, state.as_mut())
5316    }
5317
5318    async fn handle_thread_start<G: Guest<Self>>(&self, guest: &mut G) -> Result<(), Error> {
5319        guest.thread_state_mut().thread_start_entered = true;
5320        self.decide_cpuinfo_placeholders(guest)?;
5321        let new_dettid = DetTid::from_raw(guest.tid().into()); // TODO(T78538674): virtualize pid/tid:
5322        assert_eq!(new_dettid, guest.thread_state().dettid);
5323        let detpid = select_thread_start_detpid(guest.thread_state().detpid, guest.pid());
5324        let needs_registration = thread_start_needs_registration(
5325            guest.is_root_process(),
5326            guest.thread_state().past_global_first_execve,
5327            new_dettid,
5328            detpid,
5329        );
5330        trace!(
5331            "[tid {}] detcore handle_thread_start, pid={}",
5332            guest.tid(),
5333            detpid
5334        );
5335
5336        // Delayed initialization of thread_state for this new thread:
5337        let thread_state = guest.thread_state_mut();
5338        thread_state.detpid = Some(detpid);
5339        if thread_state.recover_process_mm_id(detpid) {
5340            debug!(
5341                "[detcore, dtid {}] recovered process memory identity {} for unparented thread state",
5342                new_dettid, detpid
5343            );
5344        }
5345
5346        if let Some(vfork) = guest.thread_state_mut().pending_vfork.take() {
5347            // The vfork that made this process made a user namespace whose
5348            // creator's effective ID may differ from its real one: noted for
5349            // that namespace now that the clone has made it, in the turn of
5350            // this process's own registration (`scheduler::user_namespaces`).
5351            let note_owner = vfork.user_namespace_owner_may_differ
5352                && vfork.flags.contains(CloneFlags::CLONE_NEWUSER);
5353            create_vfork_child_thread(guest, new_dettid, vfork).await;
5354            if note_owner {
5355                tool_global::note_user_namespace_owner_may_differ(guest, new_dettid).await;
5356            }
5357        } else if needs_registration {
5358            // A root has no parent registration. A fresh runtime after exec
5359            // also needs this handshake, even when the Guest has an in-tree
5360            // parent: the global owner consumes its prepared exec and returns
5361            // the replacement MmId before any ordinary image request. State
5362            // inherited from a parent is already past the first exec and does
5363            // not register a second birth here.
5364            debug!(
5365                "[detcore, dtid {}] thread start registration, scheduling.. full config:\n {:?}",
5366                &new_dettid,
5367                guest.config()
5368            );
5369            let physical_ids = if guest
5370                .config()
5371                .backend
5372                .requires_thread_directed_process_signals
5373            {
5374                Some((
5375                    guest.pid().as_raw(),
5376                    guest
5377                        .thread_state()
5378                        .physical_tid
5379                        .expect("backend requires a host thread ID before registration"),
5380                ))
5381            } else {
5382                None
5383            };
5384            if let Some(post_exec_mm) =
5385                create_child_thread(guest, new_dettid, 0, None, libc::SIGCHLD, physical_ids).await
5386            {
5387                guest.thread_state_mut().mm_id = post_exec_mm;
5388            }
5389        }
5390        if new_dettid.as_raw() == detpid.as_raw() {
5391            // A process's first thread, with its final address space: a forked
5392            // child's copy of its parent's unsent warning count is not its own.
5393            guest_log::starts_new_process(guest.thread_state().mm_id);
5394        }
5395
5396        // The table this thread holds, for the terminal receipt of its
5397        // process's death by a fatal signal to let go of
5398        // (`Detcore::name_table_for_death`), before it runs. Boxed, so that its
5399        // request does not grow this future.
5400        Box::pin(self.name_table_for_death(guest)).await;
5401
5402        // Except for the root task, let's block until it's our turn to go:
5403        let th = tool_global::thread_start_request(&self.cfg, guest, detpid).await;
5404
5405        tool_global::finish_thread_start(&self.cfg, guest, th);
5406
5407        // The prehook is a noop for a thread just starting.  Can't end the timeslice.  There's no
5408        // RCB progress to record.  However, we call it for consistency with all the other handlers.
5409        let boundary_entry = slice_boundary::callbacks::Callback::ThreadStart;
5410        let boundary_token = self
5411            .pre_handler_hook_observed(guest, true, CheckIn::Event, Some(boundary_entry))
5412            .await;
5413        // ^ precise_branch=true: There should have been ZERO prior instructions before this,
5414        // because the thread hasn't done anything yet.
5415
5416        self.record_or_replay
5417            .handle_thread_start(&mut guest.into_guest())
5418            .await?;
5419
5420        self.post_handler_hook_observed(guest, CheckIn::Event, boundary_token)
5421            .await;
5422        Ok(())
5423    }
5424
5425    async fn handle_post_exec<G: Guest<Self>>(&self, guest: &mut G) -> Result<(), Errno> {
5426        // The exec's own entry processed the ends the old image ran past; the
5427        // new image's first slice is installed fresh, by the exec continuation.
5428        guest.thread_state_mut().lone_slice_end = None;
5429        // The new image's first stop is at the exit of the exec Detcore did not
5430        // finish (A5 step 3.2). The old image's delivery counts are logged
5431        // under the identity they were taken under, and the new image counts
5432        // afresh.
5433        guest.thread_state_mut().delivery_interruption = None;
5434        let dettid = guest.thread_state().dettid;
5435        let counters = std::mem::take(&mut guest.thread_state_mut().delivery_counters);
5436        counters.log(dettid);
5437        // The new image's table may hold placeholders it rediscovered before
5438        // this, the image's first guest callback.
5439        self.decide_cpuinfo_placeholders(guest)?;
5440        // Exec resets caught signals to their defaults, on a private copy of
5441        // the actions; a thread without a copy (the first image, or one given
5442        // up quietly) is seeded instead. Before any request: a nonleader
5443        // exec's reconnect asks for a turn, and every request first publishes
5444        // the process's dispositions, which must be the new image's.
5445        crate::syscalls::signal_shadow::at_exec(guest);
5446        // A nonleader exec preserves the survivor's state and PMU counter, but
5447        // Linux changes its TID. Bind that state before any image callback RPC.
5448        match tool_global::reconnect_exec(guest).await {
5449            Err(Errno::EOPNOTSUPP) => {
5450                let message = "unsupported: preemption recording and replay across nonleader exec";
5451                error!("{message}");
5452                // Report even when logging is disabled or redirected. The
5453                // ordinary refusal helper preserves the policy exit class;
5454                // reconnect already bound identity, so its diagnostic RPC is
5455                // authenticated and cannot silently retire an unbound owner.
5456                let _ = writeln!(crate::util::RetryingStderr, "{message}");
5457                tool_global::unrecoverable_shutdown(
5458                    guest,
5459                    detcore_model::HERMIT_POLICY_REFUSAL_EXIT,
5460                )
5461                .await;
5462            }
5463            result => result?,
5464        }
5465        // Every non-default profile requires Detcore's CPUID answers. Ptrace
5466        // verifies faulting before each new image; this backend-independent
5467        // guard also refuses a missing interception capability before the
5468        // image executes. Native CPUID must never leak hardware randomness
5469        // under host-no-rdrand (https://github.com/rrnewton/hermit/issues/4534).
5470        if !self.cfg.cpu_profile.is_default() && !guest.has_cpuid_interception() {
5471            let message = format!(
5472                "--cpu-profile {} refused: CPUID faulting is not active for this image, \
5473                 so the guest would see the host's CPUID; use a host and kernel with CPUID faulting",
5474                self.cfg.cpu_profile
5475            );
5476            error!("{message}");
5477            let _ = writeln!(crate::util::RetryingStderr, "{message}");
5478            tool_global::unrecoverable_shutdown(guest, detcore_model::HERMIT_POLICY_REFUSAL_EXIT)
5479                .await;
5480        }
5481        // Refuse an invalid profile configuration before the guest's first
5482        // instruction, instead of reaching its CPUID provider.
5483        if let Some(why) = cpu_profile_admission_refusal(&self.cfg) {
5484            let message = format!("--cpu-profile {} refused: {why}", self.cfg.cpu_profile);
5485            error!("{message}");
5486            let _ = writeln!(crate::util::RetryingStderr, "{message}");
5487            tool_global::unrecoverable_shutdown(guest, detcore_model::HERMIT_POLICY_REFUSAL_EXIT)
5488                .await;
5489        }
5490        // Exec gives the task filesystem information of its own
5491        // (`unshare_fs_struct`): a root it shared through CLONE_FS is now its
5492        // own, as it stands.
5493        let changed_root = guest
5494            .thread_state()
5495            .changed_root
5496            .load(std::sync::atomic::Ordering::SeqCst);
5497        guest.thread_state_mut().changed_root =
5498            Arc::new(std::sync::atomic::AtomicBool::new(changed_root));
5499        // The exec'ing thread leads its process now, so its filesystem
5500        // information is the leader's.
5501        guest.thread_state_mut().fs_apart_from_leader = false;
5502        // The exec'ing thread is its process's only thread now, and leads
5503        // it, so the table it holds is the process's. (Where Detcore handles
5504        // the execve, `handle_execveat` already installed it; a backend that
5505        // runs execve natively, as DBT does, reaches only this callback.)
5506        guest.thread_state_mut().adopt_process_files();
5507        // The guest's first image is entered by an execve. A backend that traces
5508        // the guest from before it (ptrace) intercepts that execve and counts it
5509        // as the guest's first syscall. A backend that starts the guest already
5510        // executing its image (DBT under DynamoRIO, SaBRe) reaches this callback
5511        // with nothing counted, so under DBT, which then sees every later
5512        // syscall, numbers and virtual time ran exactly one syscall behind
5513        // ptrace's. Count that execve here, at its own cost. (SaBRe also misses
5514        // the dynamic loader's syscalls before its plugin starts; this closes
5515        // only the execve's share of that gap.)
5516        let thread = guest.thread_state_mut();
5517        if !thread.past_global_first_execve && thread.stats.syscall_count == 0 {
5518            thread.stats.count_syscall(Sysno::execve);
5519            thread
5520                .thread_logical_time
5521                .add_syscall_with_cost(syscall_time::cost_ns(Sysno::execve));
5522        }
5523        guest.thread_state_mut().past_global_first_execve = true;
5524        // exec resets a handled signal to its default action.
5525        guest.thread_state_mut().sigalrm_handled = false;
5526        guest.thread_state_mut().sigalrm_blocked_published = false;
5527        // Only a successful exec reaches this callback. Delete the old image's
5528        // POSIX timer IDs while exec still owns its scheduler turn; the global
5529        // notification below cancels their deadlines before the pre-handler
5530        // can yield. alarm()/ITIMER_REAL and the continuous clock survive exec.
5531        guest
5532            .thread_state()
5533            .posix_timers
5534            .lock()
5535            .unwrap()
5536            .clear_for_exec();
5537        // A successful exec clears the kernel's clear_child_tid registration.
5538        // Mirror that reset before any replacement-image syscall can run.
5539        if guest.config().sequentialize_threads {
5540            tool_global::set_child_tid_address(guest, 0).await;
5541        }
5542        // The exec walked the old thread group's robust lists; wake one waiter
5543        // per owner word it marked FUTEX_OWNER_DIED.
5544        let robust_wakes = guest.thread_state().take_robust_list_wakes_for_exec();
5545        let identities: Vec<_> = robust_wakes
5546            .iter()
5547            .map(|(owner, wake)| (*owner, wake.futex))
5548            .collect();
5549        let counts = robust_list_wakes_after_exec(guest, robust_wakes).await;
5550        for ((owner, futex), count) in identities.into_iter().zip(counts) {
5551            info!(
5552                "[detcore, dtid {}] robust-list owner death woke {} waiter(s) on futex {:?} at exec",
5553                owner, count, futex,
5554            );
5555        }
5556
5557        // In a run that declares a launcher, an exec Detcore did not prepare
5558        // leaves the new image without a record: resolve it here from the
5559        // path the exec was given.
5560        let unprepared_exe = if self.declares_launcher() {
5561            self.exec_image_from_execfn(guest)
5562        } else {
5563            None
5564        };
5565        tool_global::mark_past_first_execve(guest, unprepared_exe).await;
5566        // The files the kernel opened for this exec, the image and its
5567        // program interpreter, join the run's host inputs (`host_inputs`).
5568        if guest.config().record_host_inputs {
5569            self.record_exec_host_inputs(guest).await;
5570        }
5571        // The task's name for its rendered `/proc/<pid>/stat` where the tool
5572        // cannot read the kernel's (`procfs::task_stat`): the one the new
5573        // image's `AT_EXECFN` (`bprm->filename`) gives, under the leader's
5574        // tid, which the exec'ing thread holds now. Where it cannot be read
5575        // the previous name stands.
5576        let detpid = guest.thread_state().detpid.expect("detpid unset");
5577        if let Some(comm) = self.execfn_comm(guest) {
5578            tool_global::set_task_comm(guest, DetTid::from_raw(detpid.as_raw()), comm).await;
5579        }
5580        // That request took the process out of its old descriptor table in
5581        // `procfs::locks::LockLedger`, whose descriptions that hold a
5582        // placeholder `flock` its new table references until now; name the
5583        // descriptions whose locks Detcore follows that survived the exec (its
5584        // close-on-exec slots are gone from this table): also none, where the
5585        // exec closed one it follows, so that the ledger releases a
5586        // placeholder lock the exec closed. A process that followed none has
5587        // nothing to name. A failed exec reaches neither.
5588        let surviving = guest.thread_state().lock_tracked_slots();
5589        if !surviving.is_empty() || guest.thread_state().exec_dropped_lock_tracked() {
5590            tool_global::note_table_exec(guest, surviving).await;
5591        }
5592        // The descriptors of the table the exec replaced go now, in the
5593        // exec's turn, the close-on-exec ones as the exec dropped them, with
5594        // what the queues of their sockets held
5595        // (`Detcore::release_exec_dropped_descriptors`): a copy in flight to a
5596        // receiving socket the exec closed is released, as Linux releases it
5597        // at the exec, and the lock ledger stops counting its locks. A failed
5598        // exec never reaches this callback, and its kept table went with it.
5599        self.release_exec_dropped_descriptors(guest).await;
5600        // The table the exec gave this thread, in place of the one it named
5601        // at its start (`Detcore::name_table_for_death`). Boxed, as below.
5602        Box::pin(self.name_table_for_death(guest)).await;
5603        // The new image's auxiliary vector, as it finds it on its initial
5604        // stack, for its `/proc/<pid>/auxv` (package K slice K2 of
5605        // https://github.com/rrnewton/hermit/issues/4213). Not under record
5606        // and replay, where Detcore renders no procfs file.
5607        // Boxed, so its injected calls do not grow the post-exec future,
5608        // which a backend may poll on a small stack.
5609        if !self.cfg.recordreplay_modes {
5610            Box::pin(self.record_process_auxv(guest)).await;
5611        }
5612        // The new image's program-break model, checked against the kernel's
5613        // once its exec record is in place.
5614        self.check_brk_model(guest).await;
5615        // The new image's exclusive pairs (store-exclusive retry loops).
5616        // Boxed, so its scan does not grow the post-exec future either.
5617        #[cfg(target_arch = "aarch64")]
5618        Box::pin(store_exclusive::report_after_exec(guest)).await;
5619        // Linux discards every queued POSIX timer signal at exec
5620        // (`flush_itimer_signals`), and Hermit's are ordinary signals it sent:
5621        // take them back before the new image runs
5622        // (https://github.com/rrnewton/hermit/issues/4184).
5623        if scheduler::timer_copies_recorded() || scheduler::real_time_expiration_kept_pending() {
5624            self.flush_exec_timer_copies(guest).await;
5625        }
5626        let boundary_entry = slice_boundary::callbacks::Callback::Exec;
5627        let boundary_token = self
5628            .pre_handler_hook_observed(guest, false, CheckIn::Event, Some(boundary_entry))
5629            .await;
5630
5631        let auxv = guest.auxv();
5632        let initialized = guest
5633            .thread_state_mut()
5634            .complete_initial_random_auxv(auxv.at_random().map(|p| p.as_raw()))
5635            .expect("authenticated initial random handoff no longer matches this image");
5636        // A backend that wrote AT_RANDOM before this callback (the loader
5637        // handoff) emitted no record: emit it here from the bytes it wrote, with
5638        // no second draw or write, so the record sits where every backend puts
5639        // it, after the root thread's seeding records. The loader's getrandom
5640        // fills (glibc's early initialization makes one) and resource-limit
5641        // reads (glibc's startup reads RLIMIT_STACK) emitted none either: their
5642        // records follow, in order, as when this thread's own handlers serve
5643        // them after this callback.
5644        if let Some((bytes, early_requests)) = initialized {
5645            let dettid = guest.thread_state().dettid;
5646            random::record_initial_auxv(dettid, &bytes);
5647            for request in early_requests {
5648                match request {
5649                    random::EarlyRequest::Getrandom(fill) => {
5650                        random::record_early_getrandom(dettid, fill)
5651                    }
5652                    random::EarlyRequest::LimitRead {
5653                        call,
5654                        resource,
5655                        current,
5656                        maximum,
5657                    } => {
5658                        syscalls::sysinfo::record_early_limit_read(call, resource, current, maximum)
5659                    }
5660                }
5661            }
5662        } else if let Some(ptr) = auxv.at_random() {
5663            // It is safe to mutate this address since libc has not yet had a
5664            // chance to modify or copy the auxv table.
5665            let memory = guest.memory();
5666            let dettid = guest.thread_state().dettid;
5667            let ptr = unsafe { ptr.into_mut() };
5668            random::initialize_auxv(
5669                guest.thread_state_mut().thread_prng(),
5670                memory,
5671                ptr.cast(),
5672                dettid,
5673            )?;
5674        }
5675
5676        // Successful exec never returns through handle_syscall_event, so the
5677        // nested recorder/replayer needs this callback to commit or retire its
5678        // pending exec state before the replacement image issues another exec.
5679        self.record_or_replay
5680            .handle_post_exec(&mut guest.into_guest())
5681            .await?;
5682
5683        self.post_handler_hook_observed(guest, CheckIn::Event, boundary_token)
5684            .await;
5685        Ok(())
5686    }
5687
5688    /// A timer fires to preempt the guest and give other threads a turn.
5689    async fn handle_timer_event<G: Guest<Self>>(&self, guest: &mut G) {
5690        if guest.thread_state().lone_slice_end.is_some() {
5691            // While a thread runs alone the only timer armed is the backstop
5692            // (`arm_lone_backstop`): no slice ends here. Process the ends it
5693            // ran past, and keep running alone or arm the next end. The DEBUG
5694            // line lets a test see the backstop fire on any backend; where it
5695            // lands changes no INFO record.
5696            tracing::debug!(
5697                target: "detcore::lone_backstop",
5698                "[detcore, dtid {}] lone backstop timer event",
5699                guest.thread_state().dettid
5700            );
5701            self.catch_up_lone_slice_ends(guest).await;
5702            if let Some(end) = guest.thread_state().lone_slice_end {
5703                let now = guest.read_clock().expect("Couldn't read clock");
5704                arm_lone_backstop(guest, end.saturating_sub(now));
5705            }
5706            return;
5707        }
5708        self.take_timer_event(guest).await;
5709    }
5710
5711    async fn handle_syscall_event<G: Guest<Self>>(
5712        &self,
5713        guest: &mut G,
5714        call: Syscall,
5715    ) -> Result<i64, Error> {
5716        // Before anything this callback changes: the slice ends a lone thread
5717        // ran past came first (`catch_up_lone_slice_ends`).
5718        self.catch_up_lone_slice_ends(guest).await;
5719        #[cfg(feature = "dispatch-observer")]
5720        if let Some(observer) = dispatch_observer::installed() {
5721            // This is the genuine incoming event. An entry record does not
5722            // establish that subsequent signal gates or handling completed.
5723            let registers = guest.regs().await;
5724            let (number, arguments) = call.into_parts();
5725            observer(dispatch_observer::Entry {
5726                pid: guest.pid().as_raw(),
5727                tid: guest.tid().as_raw(),
5728                dettid: guest.thread_state().dettid.as_raw(),
5729                number: number as u64,
5730                instruction_pointer: registers.rip,
5731                arguments: [
5732                    arguments.arg0 as u64,
5733                    arguments.arg1 as u64,
5734                    arguments.arg2 as u64,
5735                    arguments.arg3 as u64,
5736                    arguments.arg4 as u64,
5737                    arguments.arg5 as u64,
5738                ],
5739            });
5740        }
5741        // A FIFO open's path page that a killed task of this address space
5742        // left mapped goes before this call runs (and, below, before a call
5743        // that was waiting returns). The flag is the load the cleanup tests
5744        // first; read here, it also spares building the cleanup's future
5745        // while no page is left.
5746        if crate::fifo_peer::any_orphaned_page() {
5747            Self::unmap_orphaned_staging_pages(guest).await;
5748        }
5749        crate::syscalls::signal_shadow::at_syscall_entry(guest);
5750        // Until Detcore finishes it, the kernel may run it (A5 step 3.2).
5751        guest.thread_state_mut().delivery_interruption = Some(reverie::Interruption::KernelRun {
5752            syscall: call.number() as i64,
5753        });
5754        // Signal controls an in-guest runtime refused since this process's
5755        // last report, which Detcore did not see as calls.
5756        crate::refused_signal_controls::report(guest).await;
5757        // Signal phase 1: before this syscall's first scheduler request. A
5758        // delivery prepared here runs the guest's handler before the syscall,
5759        // which then starts again; what this returns is discarded.
5760        if self.cfg.backend.virtualizes_guest_sigalrm
5761            && guest.thread_state().sigalrm_handled
5762            && self.sigalrm_at_syscall_entry(guest).await
5763        {
5764            return Ok(0);
5765        }
5766
5767        // A thread that keeps its turn for a signal it sent its own process
5768        // takes that signal before it can make another syscall.
5769        if let Some(signal) = guest.thread_state_mut().own_signal_turn_hold.take() {
5770            tracing::error!(
5771                "[detcore, dtid {}] reached a syscall without taking signal {} it sent to its own \
5772                 process (https://github.com/rrnewton/hermit/issues/4034)",
5773                guest.thread_state().dettid,
5774                signal
5775            );
5776        }
5777        // A claimed `SIGCHLD` stays claimed only while it is still pending for
5778        // this thread (`syscalls::signal::claim_still_pending`).
5779        let (pid, tid) = (guest.pid(), guest.tid());
5780        let claimed =
5781            crate::syscalls::claim_still_pending(guest.thread_state().claimed_sigchld, || {
5782                crate::syscalls::read_thread_signal_state(pid, tid)
5783            });
5784        guest.thread_state_mut().claimed_sigchld = claimed;
5785        let boundary_entry = slice_boundary::callbacks::Callback::Syscall(call.number());
5786        let boundary_token = self
5787            .pre_handler_hook_observed(guest, false, CheckIn::Event, Some(boundary_entry))
5788            .await;
5789
5790        // Bind this exact callback after its original grant. Nested backend
5791        // signal callbacks receive the backend Guest, never this operation.
5792        // Namespace runs use the forwarding None arm without capability checks.
5793        let input_operation = crate::reserved_input::NativeCallOperation::begin(guest, call)?;
5794        let mut native_guest = reverie::NativeOperationGuest::<_, Self>::optional(
5795            guest,
5796            input_operation
5797                .as_ref()
5798                .map(|operation| operation.binding()),
5799        )?;
5800        let guest = &mut native_guest;
5801        let result = Box::pin(async {
5802            // Linux's restart of an interrupted wait is the thread's very next syscall,
5803            // so the record a wait kept for it lives only until then
5804            // (`handle_restart_syscall`).
5805            let restart_block = guest.thread_state_mut().restart_block.take();
5806            // A working directory read for the previous syscall may be stale by
5807            // now (`ThreadState::syscall_cwd`).
5808            guest.thread_state_mut().syscall_cwd = None;
5809
5810            let dettid = guest.thread_state().dettid;
5811
5812            if guest.thread_state().guest_past_first_execve() {
5813                detlog!(
5814                    event = crate::detlog::DetLogEvent::Syscall;
5815                    "[syscall][detcore, dtid {}] inbound syscall: {} = ?",
5816                    dettid,
5817                    call.display(&guest.memory())
5818                );
5819            }
5820
5821            // The hot syscall path only reads a few Copy flags from the config, so copy
5822            // just those out instead of cloning the entire Config on every intercepted
5823            // syscall (previously flagged inline as an unnecessary copy). guest.config()
5824            // borrows guest immutably; bind the flags in a tight scope so the borrow ends
5825            // before the later thread_state_mut()/&mut guest borrows below.
5826            let (sequentialize_threads, virtualize_time, panic_on_unsupported_syscalls) = {
5827                let config = guest.config();
5828                (
5829                    config.sequentialize_threads,
5830                    config.virtualize_time,
5831                    config.panic_on_unsupported_syscalls,
5832                )
5833            };
5834
5835            if sequentialize_threads && self.cfg.observes_schedevents() {
5836                let access = self.access_trace_entry(guest, &call);
5837                trace_schedevent_with_access(
5838                    guest,
5839                    with_guest_time(
5840                        guest,
5841                        SchedEvent::syscall(dettid, call.number(), SyscallPhase::Prehook),
5842                    ),
5843                    true,
5844                    access,
5845                )
5846                .await;
5847                // Its descriptors as the call reaches them: after this thread's
5848                // turn at its prehook event.
5849                self.begin_traced_call(guest, &call);
5850            }
5851
5852            let syscall_cost_ns = syscall_time::cost_ns(call.number());
5853            // The backend-runtime bootstrap window. A backend-resident runtime (the
5854            // LiteInst preload constructor) issues hundreds to thousands of syscalls
5855            // on one thread (315 for a small C program, 6,970 for emacs; see
5856            // `syscall_time::MAX_UNCHARGED_BOOTSTRAP_SYSCALLS`) between its validated
5857            // begin trap and the trap that ends its bootstrap: the ready report, or
5858            // the report that preparation failed.
5859            // Reverie reports that window only for the bootstrapping thread; every
5860            // other thread and every forked process sees no window.
5861            //
5862            // What is withheld: the per-syscall cost below, for the bootstrapping
5863            // thread only, for each syscall it makes inside the window that does not
5864            // observe virtual time, up to
5865            // `syscall_time::MAX_UNCHARGED_BOOTSTRAP_SYSCALLS` per window. That
5866            // includes syscalls made by guest code the runtime calls into, such as
5867            // an interposed malloc or open64. Charging the runtime's own syscalls
5868            // would make guest-visible virtual time (uptime, CLOCK_MONOTONIC, CPU
5869            // time) depend on which backend ran the program. Every withheld syscall
5870            // is still counted and fully handled below, so Detcore keeps tracking
5871            // the fds, mappings and inodes it creates.
5872            //
5873            // What is always charged, inside the window too: syscalls that observe
5874            // virtual time (`syscall_time::observes_virtual_time`), so two clock
5875            // reads on the bootstrapping thread differ by at least one syscall cost
5876            // exactly as outside the window; and every syscall past the cap, so a
5877            // loop inside the window, or a runtime that never reports ready, again
5878            // advances the clock at each syscall.
5879            //
5880            // When it matters: with a syscall-driven clock (no PMU, or
5881            // --max-timeslice=disabled, which Hermit selects when perf counters are
5882            // unavailable, or --no-rcb-time) these charges are the thread's only
5883            // per-syscall progress, and with sequentialized threads the 500x no-RCB
5884            // multiplier makes each one large. With RCB time on a PMU host, retired
5885            // branches still advance the thread's clock inside the window and only
5886            // the syscall costs are withheld.
5887            //
5888            // The decision depends only on the window the backend reports (opened
5889            // and closed by trap instructions the runtime executes), the syscall
5890            // number and the count of earlier uncharged syscalls in the same
5891            // window: all functions of the guest's own execution. Record/replay and
5892            // both --verify runs therefore make the same decisions. The clock is
5893            // never reset: the first syscall after the window continues from the
5894            // value the window left. See https://github.com/rrnewton/hermit/issues/3338
5895            // and https://github.com/rrnewton/hermit/pull/3430#issuecomment-5928691696.
5896            //
5897            // The scheduler turns such a syscall needs are withheld the same way.
5898            // Handling an uncharged syscall can still commit scheduler turns (for
5899            // example the file resources of a read of /proc/self/maps), and each
5900            // committed turn normally advances global time by the per-turn
5901            // scheduler cost. While the syscall is being handled the thread is
5902            // marked `in_uncharged_bootstrap_syscall`, `tool_global::resource_request`
5903            // copies that mark into the request, and the scheduler does not
5904            // advance global time for a marked turn unless it is an IO-polling
5905            // retry, whose time enforces timeouts. Without this, a LiteInst run of
5906            // the clock-trajectory fixture reached its first clock read 11.5 ms of
5907            // virtual time later than with it, and each exec added 12.0 ms more
5908            // (measured locally), which was enough for its sysinfo uptime to
5909            // differ from the ptrace backend's on the hosted runner
5910            // (https://github.com/rrnewton/hermit/issues/3517). A charged syscall
5911            // (one that observes virtual time, or any past the cap) is not marked,
5912            // so its turns advance the clock as everywhere else.
5913            let in_backend_runtime_bootstrap = guest.is_backend_runtime_bootstrap();
5914            let new_count = {
5915                // which results from not being able to borrow guest twice.
5916                let thread_state = guest.thread_state_mut();
5917                thread_state.stats.count_syscall(call.number());
5918
5919                // Every intercepted syscall advances logical time, including configurations that do
5920                // not serialize threads. This keeps virtual clocks productive during syscall loops.
5921                // The one exception is the bootstrap window described above: on the bootstrapping
5922                // thread, up to MAX_UNCHARGED_BOOTSTRAP_SYSCALLS syscalls per window that do not
5923                // observe virtual time are left uncharged.
5924                let charged =
5925                    thread_state.charge_syscall_time(in_backend_runtime_bootstrap, call.number());
5926                if charged {
5927                    thread_state
5928                        .thread_logical_time
5929                        .add_syscall_with_cost(syscall_cost_ns);
5930                }
5931                thread_state.in_uncharged_bootstrap_syscall = !charged;
5932                // This only folds the thread's new user and system time into the process total.
5933                // An uncharged syscall added none, so it needs no guard.
5934                thread_state.account_process_cpu_time();
5935                thread_state.stats.syscall_count
5936            };
5937            // The backend's injection count at entry: the histogram's outcome
5938            // compares it with the count at the result (`syscall_histogram`).
5939            let injected_at_entry = guest.injected_syscalls();
5940
5941            // Happens-before enforcement checkpoint. When the run carries a
5942            // happens-before program and an anchor at exactly
5943            // `Position::SyscallCount(new_count)` names this thread (by dettid, or by
5944            // a spawn ordinal only the scheduler can resolve), this syscall checks in
5945            // with the scheduler at its prehook, carrying this thread's running
5946            // syscall count.
5947            // The scheduler fires any anchor at that count on this thread and parks
5948            // the thread (out of the run queue) when that anchor is the AFTER
5949            // endpoint of a Hard edge whose BEFORE endpoint has not fired yet. For
5950            // any other (thread, count) the scheduler would find no anchor and do
5951            // nothing, so the request is skipped: one scheduler round trip per anchor
5952            // the thread reaches rather than per syscall
5953            // (https://github.com/rrnewton/hermit/issues/3877), and threads the spec
5954            // does not name keep their schedule.
5955            // This is the gate that makes an authored partial order reproduce a known
5956            // race deterministically (see detcore-model `happens_before`). It
5957            // requires sequentialized threads (enforced by the CLI) so the scheduler
5958            // owns ordering.
5959            //
5960            // A syscall-occurrence anchor ("the thread's Nth writev to fd 9") is
5961            // counted here, in this thread's own counters, once per syscall entry;
5962            // the thread checks in only when a call is some such anchor's nth
5963            // occurrence, naming the anchors it reached.
5964            let hb_checkpoint = if guest.config().happens_before.is_some() {
5965                let dettid = guest.thread_state().dettid;
5966                let mut counters =
5967                    std::mem::take(&mut guest.thread_state_mut().stats.hb_occurrences);
5968                let program = guest
5969                    .config()
5970                    .happens_before
5971                    .as_ref()
5972                    .expect("checked above");
5973                // A posthook anchor this entry reaches checks in when the call
5974                // completes, below, not here
5975                // (https://github.com/rrnewton/hermit/issues/3929).
5976                let mut reached = if program.has_syscall_occurrence_anchors() {
5977                    let (sysno, args) = call.into_parts();
5978                    program.count_syscall_occurrences(
5979                        &mut counters,
5980                        dettid,
5981                        sysno,
5982                        [args.arg0, args.arg1],
5983                        new_count,
5984                    )
5985                } else {
5986                    Vec::new()
5987                };
5988                let posthook: Vec<String> = reached
5989                    .extract_if(.., |name| {
5990                        program
5991                            .anchors
5992                            .get(name)
5993                            .is_some_and(|a| a.fires_at_posthook())
5994                    })
5995                    .collect();
5996                let checkpoint = if !reached.is_empty() {
5997                    Some(ResourceID::HappensBeforeSyscallCheckpoint {
5998                        count: new_count,
5999                        anchors: reached,
6000                    })
6001                } else if program.may_have_syscall_count_anchor_at(dettid, new_count) {
6002                    Some(ResourceID::HappensBeforeCheckpoint(new_count))
6003                } else {
6004                    None
6005                };
6006                guest.thread_state_mut().stats.hb_occurrences = counters;
6007                guest.thread_state_mut().stats.hb_pending_posthook = posthook;
6008                checkpoint
6009            } else {
6010                None
6011            };
6012            if let Some(checkpoint) = hb_checkpoint {
6013                let request = guest.thread_state().mk_request(checkpoint, Permission::R);
6014                resource_request(guest, request).await;
6015            }
6016
6017            // For `hermit run --verify`: a name the guest itself rebinds is not a
6018            // host input change (see `detcore_model::host_input`). Reported before
6019            // the call runs, whatever its outcome, so that a change the kernel
6020            // commits is never missing from the record, even if the run ends
6021            // before this handler returns.
6022            if self.cfg.record_host_inputs {
6023                self.record_host_namespace_change(guest, &call).await;
6024                self.record_untraced_code_change(guest, &call).await;
6025            }
6026
6027            // The file and descriptor checks below act only on a call that may
6028            // name a file or a descriptor; for any other (getpid, futex,
6029            // clock_gettime, ...) each answers its trivial value with no effect,
6030            // so they are skipped, futures and all. The test reads the syscall
6031            // number only (`may_name_file_or_descriptor`); no scheduler request,
6032            // logical-time update or turn moves.
6033            let may_name_files = crate::syscalls::may_name_file_or_descriptor(call.number());
6034            // The inodes whose last name this call removes, stat'ed before it runs
6035            // and retired once it succeeds, so that a host reusing one cannot hand
6036            // a new file the old file's number
6037            // (https://github.com/rrnewton/hermit/issues/3840).
6038            let removed_inodes = if may_name_files {
6039                self.last_names_removed_by(guest, &call).await
6040            } else {
6041                Vec::new()
6042            };
6043            // The guest-made FIFOs whose name this call removes, forgotten once it
6044            // succeeds (https://github.com/rrnewton/hermit/issues/2203).
6045            let removed_guest_fifos = if may_name_files {
6046                self.guest_made_fifos_removed_by(guest, &call).await
6047            } else {
6048                Vec::new()
6049            };
6050
6051            // Only an emulated RNG readv supplies authoritative imported geometry.
6052            // A generic pre-dispatch snapshot would become stale across pipe waits.
6053            let mut rng_readv_output = None;
6054            // A vectored call's memory as it began, if its handler keeps it
6055            // (`ThreadState::entry_import`): never an earlier call's.
6056            guest.thread_state_mut().entry_import = None;
6057            // Signal phase 1: a process that handles SIGALRM runs only the calls
6058            // its table places (`sigalrm_phase1`).
6059            let sigalrm_refusal = if guest.thread_state().sigalrm_handled {
6060                self.sigalrm_phase1_gate(guest, call)
6061            } else {
6062                None
6063            };
6064            // A `/proc` name outside the run's `--proc` level does not exist. A
6065            // procfs file Detcore renders answers, as Linux does, the calls that
6066            // would reach the placeholder standing in for it.
6067            // A rendered procfs descriptor that reached this table over
6068            // SCM_RIGHTS is adopted as the file it is before anything answers a
6069            // call through it.
6070            let adoption = if sigalrm_refusal.is_none() && may_name_files {
6071                self.adopt_received_placeholders(guest, &call).await
6072            } else {
6073                Ok(())
6074            };
6075            let rendered_procfs_answer = if let Err(error) = adoption {
6076                Some(Err(error))
6077            } else if sigalrm_refusal.is_none()
6078                && Self::looks_up_below_placeholders(&call)
6079                && let Some(refusal) =
6080                    Box::pin(self.refuse_lookup_below_placeholder(guest, &call)).await
6081            {
6082                // An exec or an `AF_UNIX` address below a `/proc` directory
6083                // Detcore serves on a placeholder (`descriptor_directory`).
6084                Some(refusal)
6085            } else if sigalrm_refusal.is_none() && !may_name_files {
6086                // The `/proc` device is still taken where the level check would
6087                // have taken it first.
6088                crate::syscalls::take_proc_device();
6089                None
6090            } else if sigalrm_refusal.is_none()
6091                && let Some(answer) = Box::pin(self.answer_placeholder_call(guest, &call)).await
6092            {
6093                Some(answer)
6094            } else if sigalrm_refusal.is_none() {
6095                match self
6096                    .refuse_procfs_name_outside_level(guest, &call)
6097                    .await
6098                    .transpose()
6099                {
6100                    Some(refusal) => {
6101                        syscall_histogram::mark_refused::<T, _>(guest);
6102                        Some(refusal)
6103                    }
6104                    // Another guest process's `cmdline`, descriptor link or
6105                    // `fdinfo`, where the backend's `/proc/<pid>` is not the
6106                    // guest's process: decided here, never by a host lookup
6107                    // (`other_process_procfs`).
6108                    None => match self.other_process_procfs_answer(guest, &call).await {
6109                        Some(answer) => Some(answer),
6110                        None => match self.answer_rendered_procfs_operation(guest, &call).await {
6111                            Some(answer) => Some(answer),
6112                            None => match self.answer_rendered_procfs_mapping(guest, &call).await {
6113                                Some(answer) => Some(answer),
6114                                None => self
6115                                    .answer_rendered_procfs_path_call(guest, &call)
6116                                    .await
6117                                    .transpose(),
6118                            },
6119                        },
6120                    },
6121                }
6122            } else {
6123                None
6124            };
6125            // This handler's future is as large as the largest future it awaits,
6126            // and every syscall moves the whole of it once, into the box the Tool
6127            // trait returns it in, whichever arm runs. `handle_openat`'s future is
6128            // the outlier (about 7.5 KiB, against about 6.2 KiB for the next,
6129            // `handle_write`), so the open family runs in its own box, allocated
6130            // only when one of those calls is made, which already does far more
6131            // work than one allocation. The hot read and write arms stay inline:
6132            // boxing them too would cost those calls an allocation each.
6133            // Box::pin(f).await polls `f` exactly as `f.await` does: no await
6134            // moves and no arm changes behaviour. `syscall_hot_path_tests` bounds
6135            // the result.
6136            let res = match classify_syscall(call.number()) {
6137                _ if sigalrm_refusal.is_some() => {
6138                    syscall_histogram::mark_refused::<T, _>(guest);
6139                    Err(sigalrm_refusal.expect("checked").into())
6140                }
6141                _ if rendered_procfs_answer.is_some() => rendered_procfs_answer.expect("checked"),
6142                SyscallClassification::PassThrough if call.number() == Sysno::chroot => {
6143                    let result = self.passthrough(guest, call).await;
6144                    if result.is_ok() {
6145                        guest
6146                            .thread_state()
6147                            .changed_root
6148                            .store(true, std::sync::atomic::Ordering::SeqCst);
6149                        // Another process's view of this one's mount table
6150                        // follows its root (`Detcore::rendered_procfs_at`).
6151                        crate::tool_global::mark_changed_root(guest).await;
6152                    }
6153                    result
6154                }
6155                // Outbound TCP channels under a network trace mode; never with no mode.
6156                _ if self.network_trace_owns(guest, &call) => {
6157                    self.handle_network_trace_syscall(guest, call).await
6158                }
6159                // Rseq is not type-safe in the pinned Reverie revision. Dispatch by Sysno so a
6160                // future typed representation preserves this explicit policy.
6161                SyscallClassification::Determinized if call.number() == Sysno::rseq => {
6162                    if panic_on_unsupported_syscalls {
6163                        Err(Error::Errno(Errno::ENOSYS))
6164                    } else {
6165                        let (_, args) = call.into_parts();
6166                        // RSEQ_FLAG_UNREGISTER is the only known non-registering
6167                        // operation. Unknown flags are not disjointness evidence.
6168                        if args.arg2 != 1 {
6169                            crate::reserved_input::expose_local_overlap(
6170                                guest,
6171                                args.arg0 as u64,
6172                                args.arg1 as u64,
6173                                "rseq-reserved-reference",
6174                            )?;
6175                            crate::reserved_input::expose_local(
6176                                guest,
6177                                "native-rseq-reference-unverified",
6178                            )?;
6179                        }
6180                        self.passthrough(guest, call).await
6181                    }
6182                }
6183                // AUTONOMOUS-BOT-IMPLEMENTED
6184                // TODO-HUMAN-REVIEW(#663)
6185                // The pinned Reverie revision exposes process_madvise only as a raw call.
6186                SyscallClassification::Determinized if call.number() == Sysno::process_madvise => {
6187                    match call {
6188                        Syscall::Other(_, args) => {
6189                            Self::handle_process_madvise(args.arg0, args.arg4)
6190                        }
6191                        _ => unreachable!("process_madvise unexpectedly gained a typed variant"),
6192                    }
6193                }
6194                // AUTONOMOUS-BOT-IMPLEMENTED
6195                // TODO-HUMAN-REVIEW(PR-1175): The pinned Reverie revision exposes
6196                // pidfd_send_signal/pidfd_getfd only as raw calls, so dispatch on the
6197                // Sysno. See the handlers in syscalls/files.rs for the determinism
6198                // argument.
6199                SyscallClassification::Determinized
6200                    if call.number() == Sysno::pidfd_send_signal =>
6201                {
6202                    match call {
6203                        Syscall::Other(_, args) => {
6204                            let result = self
6205                                .handle_pidfd_send_signal(
6206                                    guest,
6207                                    call,
6208                                    args.arg0 as RawFd,
6209                                    args.arg1 as i32,
6210                                    args.arg3 as u32,
6211                                )
6212                                .await;
6213                            if args.arg1 as i32 == libc::SIGALRM && result.is_ok() {
6214                                self.note_guest_sigalrm(guest, GuestSigalrmTarget::Unclassified)
6215                                    .await;
6216                            }
6217                            result
6218                        }
6219                        _ => unreachable!("pidfd_send_signal unexpectedly gained a typed variant"),
6220                    }
6221                }
6222                // AUTONOMOUS-BOT-IMPLEMENTED
6223                // TODO-HUMAN-REVIEW(PR-1175): pidfd_getfd, likewise untyped.
6224                SyscallClassification::Determinized if call.number() == Sysno::pidfd_getfd => {
6225                    match call {
6226                        Syscall::Other(_, args) => {
6227                            self.handle_pidfd_getfd(
6228                                guest,
6229                                call,
6230                                args.arg0 as RawFd,
6231                                args.arg1 as RawFd,
6232                                args.arg2 as u32,
6233                            )
6234                            .await
6235                        }
6236                        _ => unreachable!("pidfd_getfd unexpectedly gained a typed variant"),
6237                    }
6238                }
6239                // AUTONOMOUS-BOT-IMPLEMENTED
6240                // TODO-HUMAN-REVIEW(#715): Deterministic ENOSYS for syscalls the pinned
6241                // x86_64 kernel leaves unimplemented (sys_ni_syscall). A fixed -ENOSYS is
6242                // deterministic by construction and identical to the modern kernel's own
6243                // return, so no guest-visible behavior changes versus the legacy
6244                // pass-through. These are untyped (Syscall::Other) in the pinned Reverie,
6245                // so dispatch on the Sysno before the typed match below.
6246                SyscallClassification::Determinized
6247                    if is_unimplemented_enosys_syscall(call.number()) =>
6248                {
6249                    Err(Error::Errno(Errno::ENOSYS))
6250                }
6251                // AUTONOMOUS-BOT-IMPLEMENTED
6252                // TODO-HUMAN-REVIEW(PR-852): Review the futex2 fallback contract.
6253                // Detcore models legacy futex but not the newer vector/sized futex2
6254                // ABI. Match a kernel without futex2 so runtimes take their
6255                // established legacy-futex fallback without consulting the host.
6256                SyscallClassification::Determinized if is_futex2_enosys_syscall(call.number()) => {
6257                    Err(Error::Errno(Errno::ENOSYS))
6258                }
6259                // AUTONOMOUS-BOT-IMPLEMENTED
6260                // TODO-HUMAN-REVIEW(PR-836): Host filesystem and mount
6261                // introspection are outside the deterministic model. Return the
6262                // portable feature-absence errno so callers use /proc fallbacks.
6263                // AUTONOMOUS-BOT-IMPLEMENTED
6264                // TODO-HUMAN-REVIEW(PR-859): Extend this boundary to obsolete ustat
6265                // host-filesystem capacity counters.
6266                SyscallClassification::Determinized
6267                    if is_mount_introspection_enosys_syscall(call.number()) =>
6268                {
6269                    Err(Error::Errno(Errno::ENOSYS))
6270                }
6271                // AUTONOMOUS-BOT-IMPLEMENTED
6272                // TODO-HUMAN-REVIEW(PR-848): Hide unmodeled shared keyrings and
6273                // request-key upcalls behind the portable CONFIG_KEYS-absent errno.
6274                // TODO-HUMAN-REVIEW(PR-916): Fail closed whenever the
6275                // panic-on-unsupported policy is active; ordinary runs select that
6276                // policy by default. The explicit compatibility opt-out keeps the
6277                // pre-848 host pass-through so the guest observes a real working
6278                // keyring, restoring the enabled rr `keyctl` compatibility test.
6279                // Under fail-closed execution the deterministic ENOSYS boundary is
6280                // preserved.
6281                SyscallClassification::Determinized if is_kernel_keyring_syscall(call.number()) => {
6282                    if panic_on_unsupported_syscalls {
6283                        Err(Error::Errno(Errno::ENOSYS))
6284                    } else {
6285                        self.passthrough(guest, call).await
6286                    }
6287                }
6288                // AUTONOMOUS-BOT-IMPLEMENTED
6289                // TODO-HUMAN-REVIEW(PR-855): Fail-closed runs cannot expose
6290                // unmodeled pipe-buffer ownership or vmsplice page pinning, but
6291                // preserve host pass-through under the explicit compatibility
6292                // opt-out, `hermit run --allow-unsupported-syscalls`. splice and tee
6293                // refuse with EINVAL, the errno Linux returns when a descriptor
6294                // pair cannot be spliced and the one callers fall back to
6295                // read/write on: GNU grep 3.12 drains a pipe into /dev/null with
6296                // splice and falls back on EINVAL, but on ENOSYS it reports a read
6297                // error and exits 2. vmsplice keeps ENOSYS.
6298                SyscallClassification::Determinized if is_zero_copy_pipe_syscall(call.number()) => {
6299                    if panic_on_unsupported_syscalls {
6300                        syscall_histogram::mark_refused::<T, _>(guest);
6301                        if call.number() == Sysno::vmsplice {
6302                            Err(Error::Errno(Errno::ENOSYS))
6303                        } else {
6304                            Err(Error::Errno(Errno::EINVAL))
6305                        }
6306                    } else {
6307                        let result = self.passthrough(guest, call).await;
6308                        // A forwarded splice from a socket may take a message
6309                        // whose descriptors Linux releases unseen, and one into a
6310                        // socket sends messages Detcore does not count.
6311                        if let Syscall::Splice(splice) = call {
6312                            let read = crate::syscalls::scm_rights::ReceiveWithoutControl {
6313                                peek: false,
6314                                request: crate::syscalls::scm_rights::Request::Spliced(
6315                                    splice.len(),
6316                                ),
6317                                returns_at_once_when_empty: true,
6318                                address_out: false,
6319                                takes: crate::syscalls::scm_rights::Takes::One,
6320                                // Forwarded in the caller's turn; its result
6321                                // depends on no guest memory either way.
6322                                in_turn: true,
6323                                noted_by_recordings: true,
6324                            };
6325                            self.note_receive_without_control(guest, splice.fd_in(), &result, read);
6326                            self.note_spliced_send(guest, splice.fd_out(), &result);
6327                        }
6328                        result
6329                    }
6330                }
6331                // AUTONOMOUS-BOT-IMPLEMENTED
6332                // TODO-HUMAN-REVIEW(PR-860): Host LSM attributes are outside
6333                // Detcore's model. Present a stable feature-absence boundary
6334                // instead of forwarding probes.
6335                SyscallClassification::Determinized
6336                    if is_host_security_identity_probe_syscall(call.number()) =>
6337                {
6338                    Err(Error::Errno(Errno::ENOSYS))
6339                }
6340                // AUTONOMOUS-BOT-IMPLEMENTED
6341                // TODO-HUMAN-REVIEW(#722): Deterministic EPERM for privileged
6342                // system-administration syscalls (module load/unload, kexec, reboot,
6343                // swap, raw I/O ports, root-mount pivot, host/domain name, tty
6344                // hangup, disk quotas). The deterministic guest does not hold the
6345                // capabilities these require against the host kernel, so a fixed
6346                // -EPERM matches the unprivileged errno, never perturbs global host
6347                // state, and is identical across --verify and record/replay. These
6348                // are untyped (Syscall::Other) in the pinned Reverie, so dispatch on
6349                // the Sysno before the typed match below.
6350                SyscallClassification::Determinized
6351                    if is_privileged_admin_refused_syscall(call.number()) =>
6352                {
6353                    syscall_histogram::mark_refused::<T, _>(guest);
6354                    Err(Error::Errno(Errno::EPERM))
6355                }
6356                // AUTONOMOUS-BOT-IMPLEMENTED
6357                // TODO-HUMAN-REVIEW(PR-844): Enforce a deterministic boundary
6358                // around host-global process accounting and cross-process memory.
6359                SyscallClassification::Determinized
6360                    if is_process_isolation_refused_syscall(call.number()) =>
6361                {
6362                    syscall_histogram::mark_refused::<T, _>(guest);
6363                    Err(Error::Errno(Errno::EPERM))
6364                }
6365                // AUTONOMOUS-BOT-IMPLEMENTED
6366                // TODO-HUMAN-REVIEW(PR-876): Guest performance events
6367                // expose host PMU availability, policy, and asynchronous counter
6368                // state that Detcore does not model. A fixed ENOSYS preserves the
6369                // portable feature-probe fallback without creating an untracked fd.
6370                SyscallClassification::Determinized
6371                    if is_perf_event_enosys_syscall(call.number()) =>
6372                {
6373                    Err(Error::Errno(Errno::ENOSYS))
6374                }
6375                // AUTONOMOUS-BOT-IMPLEMENTED
6376                // TODO-HUMAN-REVIEW(PR-853): Refuse nested tracing, host-object
6377                // comparison at the deterministic boundary.
6378                SyscallClassification::Determinized
6379                    if is_privileged_observation_refused_syscall(call.number()) =>
6380                {
6381                    syscall_histogram::mark_refused::<T, _>(guest);
6382                    Err(Error::Errno(Errno::EPERM))
6383                }
6384                // AUTONOMOUS-BOT-IMPLEMENTED
6385                // TODO-HUMAN-REVIEW(#720): set_mempolicy_home_node is untyped in the
6386                // pinned Reverie revision. Hermit exposes a single virtual NUMA node,
6387                // so setting a memory range's home node has no observable effect: a
6388                // deterministic no-op.
6389                SyscallClassification::Determinized
6390                    if call.number() == Sysno::set_mempolicy_home_node =>
6391                {
6392                    Ok(0)
6393                }
6394                // AUTONOMOUS-BOT-IMPLEMENTED
6395                // TODO-HUMAN-REVIEW(#724): Deterministic EPERM for privileged mount
6396                // and namespace administration syscalls (mount/umount2/mount_setattr/
6397                // move_mount/open_tree/fsopen/fsmount/fsconfig/fspick, unshare, setns,
6398                // open_by_handle_at, fanotify_init/fanotify_mark, settimeofday). A
6399                // deterministic container pins the guest's namespaces, mount
6400                // hierarchy, and virtual clock for the whole run, so these are
6401                // refused with a fixed -EPERM: the unprivileged errno for the
6402                // capability-gated operations and a deliberate deterministic refusal
6403                // otherwise. Never forwarded to the host; identical across --verify
6404                // and record/replay. Untyped (Syscall::Other) in the pinned Reverie,
6405                // so dispatch on the Sysno before the typed match below.
6406                SyscallClassification::Determinized
6407                    if is_mount_ns_admin_refused_syscall(call.number()) =>
6408                {
6409                    syscall_histogram::mark_refused::<T, _>(guest);
6410                    Err(Error::Errno(Errno::EPERM))
6411                }
6412                // AUTONOMOUS-BOT-IMPLEMENTED
6413                // TODO-HUMAN-REVIEW(#731): Deterministic ENOSYS for the
6414                // asynchronous and message-passing I/O and IPC interfaces Detcore
6415                // does not model: Linux native AIO (io_setup/io_destroy/io_submit/
6416                // io_cancel/io_getevents/io_pgetevents), POSIX message queues
6417                // (mq_*), and System V message queues (msg*). AIO completion is
6418                // kernel-driven and lives outside logical time; the message-queue
6419                // families operate on global, key/name-addressed kernel objects
6420                // shared with the whole host. A fixed -ENOSYS is the errno a kernel
6421                // built without AIO/CONFIG_POSIX_MQUEUE/CONFIG_SYSVIPC returns, is
6422                // never forwarded to the host, and is identical across --verify and
6423                // record/replay (mirrors the io_uring refusal). Untyped
6424                // (Syscall::Other) in the pinned Reverie, so dispatch on the Sysno
6425                // before the typed match below.
6426                // AUTONOMOUS-BOT-IMPLEMENTED
6427                // TODO-HUMAN-REVIEW(PR-859): Include System V semaphore and shared-
6428                // memory objects in the existing CONFIG_SYSVIPC refusal boundary.
6429                SyscallClassification::Determinized
6430                    if is_unsupported_async_ipc_syscall(call.number()) =>
6431                {
6432                    Err(Error::Errno(Errno::ENOSYS))
6433                }
6434                // AUTONOMOUS-BOT-IMPLEMENTED
6435                // TODO-HUMAN-REVIEW(PR-882): Legacy nonlinear page
6436                // remapping has host-dependent kernel support and VMA behavior that
6437                // Detcore does not model. Preserve the documented mmap fallback.
6438                SyscallClassification::Determinized
6439                    if is_remap_file_pages_enosys_syscall(call.number()) =>
6440                {
6441                    Err(Error::Errno(Errno::ENOSYS))
6442                }
6443                // AUTONOMOUS-BOT-IMPLEMENTED
6444                // TODO-HUMAN-REVIEW(#787): BATCH 38. openat2 is untyped (Syscall::Other)
6445                // in the pinned Reverie revision. It is a superset of openat whose
6446                // callers must fall back to openat when it returns ENOSYS (kernels
6447                // before 5.6 lack openat2), so a fixed -ENOSYS routes them onto the
6448                // already-determinized openat path with no host dependency and behavior
6449                // identical across --verify and record/replay.
6450                SyscallClassification::Determinized if call.number() == Sysno::openat2 => {
6451                    Err(Error::Errno(Errno::ENOSYS))
6452                }
6453                // AUTONOMOUS-BOT-IMPLEMENTED
6454                // TODO-HUMAN-REVIEW(#787): BATCH 38. The credential-setting family
6455                // (setuid/setgid and their re-/res-/fs- variants, and setgroups) is
6456                // untyped (Syscall::Other) in the pinned Reverie. Detcore presents a
6457                // fixed virtual-root identity (getuid/geteuid/getgid/getegid are
6458                // virtualized to 0) and never tracks a credential change, so these
6459                // succeed as deterministic no-ops returning 0 -- the value a real root
6460                // process gets for a permitted credential change (and the previous
6461                // fs-id, virtual 0, for setfsuid/setfsgid). That lets privilege-
6462                // dropping programs proceed instead of fail-closing and is identical
6463                // across --verify and record/replay.
6464                SyscallClassification::Determinized
6465                    if is_credential_identity_noop_syscall(call.number()) =>
6466                {
6467                    Ok(0)
6468                }
6469                // AUTONOMOUS-BOT-IMPLEMENTED
6470                // TODO-HUMAN-REVIEW(#1851): The file-ownership mutation family
6471                // (chown/fchown/fchownat/lchown) completes the fixed virtual-root
6472                // identity that the credential query (#1549) and credential set
6473                // (#787) families already implement. A real root process's chown
6474                // succeeds for any uid, so 0 is the value the virtual identity must
6475                // observe; forwarding instead returned the errno of whatever host
6476                // identity the backend happened to run under (EPERM with no user
6477                // namespace, EINVAL for an unmapped uid inside a one-uid map, and
6478                // backend-dependent for in-process backends).
6479                //
6480                // The emulation covers the IDENTITY half only. Root privilege
6481                // waives the ownership permission check; it does not waive pathname,
6482                // descriptor, or flag errors, so handle_ownership_change_noop
6483                // translates the target arguments into a side-effect-free metadata
6484                // lookup and returns 0 only if that validation succeeds. ENOENT,
6485                // EBADF, EFAULT, ENOTDIR and the fchownat flag EINVAL therefore
6486                // still reach the guest; the host-identity-dependent EPERM/EINVAL
6487                // cannot be produced at all. No setattr is attempted, so host
6488                // ownership, mode bits, and timestamps are never modified, and
6489                // Detcore does not model per-file ownership, so the success is not
6490                // observable through a later stat -- see
6491                // is_ownership_change_noop_syscall and handle_ownership_change_noop
6492                // for the full boundary, and
6493                // hermit-cli/tests/chown_virtual_root_identity.rs for the bracket
6494                // that fails if this arm's RESULT regresses.
6495                SyscallClassification::Determinized
6496                    if is_ownership_change_noop_syscall(call.number()) =>
6497                {
6498                    self.handle_ownership_change_noop(guest, call).await
6499                }
6500                // AUTONOMOUS-BOT-IMPLEMENTED
6501                // TODO-HUMAN-REVIEW(#827): Deterministic ENOSYS for the Landlock
6502                // unprivileged-sandbox syscalls (landlock_create_ruleset,
6503                // landlock_add_rule, landlock_restrict_self). Landlock availability
6504                // and ABI version depend on the host kernel build
6505                // (CONFIG_SECURITY_LANDLOCK) and runtime LSM stacking, so forwarding
6506                // them (the legacy pass-through) is host-dependent and, because a
6507                // ruleset restricts the whole thread tree, a global-state isolation
6508                // hole. A fixed -ENOSYS is the errno a kernel built without Landlock
6509                // returns, so the guest sees a consistent "sandbox unavailable"
6510                // answer regardless of host; never forwarded to the host and
6511                // bitwise-identical across --verify and record/replay. Untyped
6512                // (Syscall::Other) in the pinned Reverie, so dispatch on the Sysno
6513                // before the typed match below.
6514                SyscallClassification::Determinized
6515                    if is_landlock_sandbox_syscall(call.number()) =>
6516                {
6517                    Err(Error::Errno(Errno::ENOSYS))
6518                }
6519                // AUTONOMOUS-BOT-IMPLEMENTED
6520                // TODO-HUMAN-REVIEW(PR-847): Refuse unmodeled host-kernel probes
6521                // with a fixed ENOSYS so guest behavior does not depend on BPF/LSM
6522                // configuration or mutable page-cache state.
6523                SyscallClassification::Determinized
6524                    if is_host_kernel_probe_syscall(call.number()) =>
6525                {
6526                    Err(Error::Errno(Errno::ENOSYS))
6527                }
6528                // AUTONOMOUS-BOT-IMPLEMENTED
6529                // TODO-HUMAN-REVIEW(PR-838): Review close_range descriptor-table
6530                // synchronization. The pinned Reverie exposes close_range as a raw
6531                // call, so dispatch by Sysno before the typed match.
6532                SyscallClassification::Determinized if call.number() == Sysno::close_range => {
6533                    self.handle_close_range(guest, call).await
6534                }
6535                // AUTONOMOUS-BOT-IMPLEMENTED
6536                // TODO-HUMAN-REVIEW(PR-839): Optional modern memory APIs vary with
6537                // host kernel configuration, CET support, and pidfd lifecycle.
6538                // Present the portable feature-absence result instead.
6539                SyscallClassification::Determinized
6540                    if is_optional_memory_feature_syscall(call.number()) =>
6541                {
6542                    Err(Error::Errno(Errno::ENOSYS))
6543                }
6544                // AUTONOMOUS-BOT-IMPLEMENTED
6545                // TODO-HUMAN-REVIEW(#773): epoll_pwait2 is untyped (Syscall::Other)
6546                // in the pinned Reverie revision. It is epoll_pwait with a
6547                // `struct timespec *` timeout; recent glibc routes epoll_wait/
6548                // epoll_pwait through it. Handled identically to epoll_pwait
6549                // (scheduler yield + record/replay forwarding).
6550                SyscallClassification::Determinized if call.number() == Sysno::epoll_pwait2 => {
6551                    self.handle_epoll_pwait2(guest, call).await
6552                }
6553                SyscallClassification::Determinized => match call {
6554                    Syscall::Getpid(_) | Syscall::Gettid(_) => {
6555                        // Keep the inner Tool's syscall observation/injection and
6556                        // all errors, then project a successful own ID from shared
6557                        // state. Do not return early past the normal posthooks.
6558                        self.passthrough(guest, call)
6559                            .await
6560                            .map(|result| self.project_own_getter_result(guest, call, result))
6561                    }
6562                    Syscall::Write(w) => {
6563                        let send = self.send_without_control(
6564                            guest,
6565                            w.fd(),
6566                            |guest| self.completes_in_turn(guest, w),
6567                            false,
6568                            false,
6569                        );
6570                        let result = self.handle_write(guest, w).await;
6571                        self.note_send_without_control(guest, send, &result);
6572                        result
6573                    }
6574                    // AUTONOMOUS-BOT-IMPLEMENTED
6575                    // TODO-HUMAN-REVIEW(#547)
6576                    // The vectored handlers keep the call's entry import
6577                    // across their retries, so they are boxed like the open
6578                    // family (`hot_path_futures_stay_small`).
6579                    Syscall::Writev(w) => {
6580                        let send = self.send_without_control(
6581                            guest,
6582                            w.fd(),
6583                            |guest| self.completes_in_turn(guest, w),
6584                            false,
6585                            false,
6586                        );
6587                        let result = Box::pin(self.handle_writev(guest, w)).await;
6588                        self.note_send_without_control(guest, send, &result);
6589                        result
6590                    }
6591                    Syscall::Openat(o) => Box::pin(self.handle_openat(guest, o)).await,
6592                    #[cfg(target_arch = "x86_64")]
6593                    Syscall::Open(o) => Box::pin(self.handle_openat(guest, o.into())).await,
6594                    #[cfg(target_arch = "x86_64")]
6595                    Syscall::Creat(o) => Box::pin(self.handle_openat(guest, o.into())).await,
6596                    Syscall::Close(s) => self.handle_close(guest, s).await,
6597                    Syscall::Read(s) if self.sock_diag_reply_fd(guest, s.fd()) => {
6598                        self.handle_sock_diag_read(guest, s).await
6599                    }
6600                    Syscall::Read(s) => {
6601                        let in_turn = self.receives_in_turn(guest, s.fd(), s);
6602                        let result = self.handle_read(guest, s).await;
6603                        let read = crate::syscalls::scm_rights::ReceiveWithoutControl {
6604                            peek: false,
6605                            request: crate::syscalls::scm_rights::Request::Bytes(
6606                                s.buf().map_or(0, |buf| buf.as_raw()),
6607                                s.len(),
6608                            ),
6609                            returns_at_once_when_empty: true,
6610                            address_out: false,
6611                            takes: crate::syscalls::scm_rights::Takes::Counted,
6612                            in_turn,
6613                            noted_by_recordings: true,
6614                        };
6615                        self.note_receive_without_control(guest, s.fd(), &result, read);
6616                        result
6617                    }
6618                    Syscall::Pread64(s) => self.handle_pread64(guest, s).await,
6619                    Syscall::Lseek(s) => self.handle_lseek(guest, s).await,
6620                    // AUTONOMOUS-BOT-IMPLEMENTED
6621                    // TODO-HUMAN-REVIEW(PR-838): Review regular-file sendfile mediation.
6622                    Syscall::Sendfile(s) => self.handle_sendfile(guest, s).await,
6623                    // AUTONOMOUS-BOT-IMPLEMENTED
6624                    // TODO-HUMAN-REVIEW(PR-887): Present a stable pre-4.5-kernel
6625                    // boundary so callers use determinized read/write copying.
6626                    Syscall::CopyFileRange(_) => Err(Error::Errno(Errno::ENOSYS)),
6627                    // TODO-HUMAN-REVIEW(#794): vectored scatter/gather I/O, mirroring
6628                    // read/pread64/pwrite64/writev.
6629                    Syscall::Readv(s) if self.sock_diag_reply_fd(guest, s.fd()) => {
6630                        self.handle_sock_diag_readv(guest, s).await
6631                    }
6632                    Syscall::Readv(s) => {
6633                        let in_turn = self.receives_in_turn(guest, s.fd(), s);
6634                        let result = self
6635                            .handle_readv_with_output(guest, s, &mut rng_readv_output)
6636                            .await;
6637                        let read = crate::syscalls::scm_rights::ReceiveWithoutControl {
6638                            peek: false,
6639                            request: crate::syscalls::scm_rights::Request::Iovecs(
6640                                s.iov().map_or(0, |iov| iov.as_raw()),
6641                                s.len(),
6642                            ),
6643                            returns_at_once_when_empty: true,
6644                            address_out: false,
6645                            takes: crate::syscalls::scm_rights::Takes::Counted,
6646                            in_turn,
6647                            noted_by_recordings: true,
6648                        };
6649                        self.note_receive_without_control(guest, s.fd(), &result, read);
6650                        result
6651                    }
6652                    Syscall::Preadv(s) => {
6653                        self.handle_preadv_with_output(guest, s, &mut rng_readv_output)
6654                            .await
6655                    }
6656                    Syscall::Preadv2(s) => {
6657                        let current = crate::syscalls::vectored_offset(s.pos_l(), s.pos_h()) == -1;
6658                        let in_turn = current && self.receives_in_turn(guest, s.fd(), s);
6659                        let result = self
6660                            .handle_preadv2_with_output(guest, s, &mut rng_readv_output)
6661                            .await;
6662                        // At the current offset it reads a socket as `readv` does.
6663                        if current {
6664                            let read = crate::syscalls::scm_rights::ReceiveWithoutControl {
6665                                peek: false,
6666                                request: crate::syscalls::scm_rights::Request::Iovecs(
6667                                    s.iov().map_or(0, |iov| iov.as_raw()),
6668                                    s.iov_len() as usize,
6669                                ),
6670                                returns_at_once_when_empty: true,
6671                                address_out: false,
6672                                takes: crate::syscalls::scm_rights::Takes::Counted,
6673                                in_turn,
6674                                // Recordings made before saw `preadv2` take
6675                                // nothing from a socket's queue.
6676                                noted_by_recordings: false,
6677                            };
6678                            self.note_receive_without_control(guest, s.fd(), &result, read);
6679                        }
6680                        result
6681                    }
6682                    Syscall::Pwritev(s) => Box::pin(self.handle_pwritev(guest, s)).await,
6683                    Syscall::Pwritev2(s) => {
6684                        // At the current offset it writes a socket as `writev`
6685                        // does.
6686                        let send =
6687                            (crate::syscalls::vectored_offset(s.pos_l(), s.pos_h()) == -1)
6688                                .then(|| {
6689                                    self.send_without_control(
6690                                        guest,
6691                                        s.fd(),
6692                                        |guest| {
6693                                            matches!(
6694                                        crate::syscalls::helpers::ioaction_based_on_fd(
6695                                            guest,
6696                                            s.fd()
6697                                        ),
6698                                        Ok(crate::syscalls::helpers::IOAction::NonblockizeRetry
6699                                            | crate::syscalls::helpers::IOAction::PassThru)
6700                                    )
6701                                        },
6702                                        false,
6703                                        false,
6704                                    )
6705                                })
6706                                .flatten();
6707                        let result = Box::pin(self.handle_pwritev2(guest, s)).await;
6708                        self.note_send_without_control(guest, send, &result);
6709                        result
6710                    }
6711                    // AUTONOMOUS-BOT-IMPLEMENTED
6712                    // TODO-HUMAN-REVIEW(#683)
6713                    Syscall::Pwrite64(s) => self.handle_pwrite64(guest, s).await,
6714                    // This syscall is advisory; fixed success preserves its API contract.
6715                    Syscall::Fadvise64(_) => Ok(0),
6716                    Syscall::Mmap(s) => self.handle_mmap(guest, s).await,
6717                    Syscall::Madvise(s) => self.handle_madvise(guest, s).await,
6718                    // AUTONOMOUS-BOT-IMPLEMENTED
6719                    // TODO-HUMAN-REVIEW(#775)
6720                    Syscall::Mincore(s) => self.handle_mincore(guest, s).await,
6721                    Syscall::Munmap(s) => self.handle_munmap(guest, s).await,
6722                    Syscall::Mremap(s) => self.handle_mremap(guest, s).await,
6723                    #[cfg(target_arch = "x86_64")]
6724                    Syscall::Stat(s) => self.handle_stat_family(guest, s.into()).await,
6725                    #[cfg(target_arch = "x86_64")]
6726                    Syscall::Lstat(s) => self.handle_stat_family(guest, s.into()).await,
6727                    Syscall::Fstat(s) => self.handle_stat_family(guest, s.into()).await,
6728                    #[cfg(target_arch = "x86_64")]
6729                    Syscall::Newfstatat(s) => self.handle_stat_family(guest, s.into()).await,
6730                    #[cfg(target_arch = "aarch64")]
6731                    Syscall::Fstatat(s) => self.handle_stat_family(guest, s.into()).await,
6732                    Syscall::Statx(s) => self.handle_statx(guest, s).await,
6733                    // AUTONOMOUS-BOT-IMPLEMENTED
6734                    // TODO-HUMAN-REVIEW(#877)
6735                    #[cfg(target_arch = "x86_64")]
6736                    Syscall::Readlink(s) => self.handle_readlink(guest, s).await,
6737                    // AUTONOMOUS-BOT-IMPLEMENTED
6738                    Syscall::Readlinkat(s) => self.handle_readlinkat(guest, s).await,
6739                    Syscall::Fcntl(s) => self.handle_fcntl(guest, s).await,
6740                    // AUTONOMOUS-BOT-IMPLEMENTED
6741                    // TODO-HUMAN-REVIEW(PR-912)
6742                    Syscall::Ioctl(s)
6743                        if syscalls::socket_timestamp_ioctl::is_socket_timestamp_ioctl(s) =>
6744                    {
6745                        self.handle_socket_timestamp_ioctl(guest, s).await
6746                    }
6747                    Syscall::Ioctl(s) => self.handle_ioctl(guest, s).await,
6748                    Syscall::Futex(s) => self.handle_futex(guest, s).await,
6749
6750                    Syscall::Clone(s) => self.handle_clone_family(guest, s.into()).await,
6751                    Syscall::Clone3(s) => self.handle_clone_family(guest, s.into()).await,
6752                    #[cfg(target_arch = "x86_64")]
6753                    Syscall::Fork(s) => self.handle_clone_family(guest, s.into()).await,
6754
6755                    // Forward vfork as vfork (rather than rewriting to fork) so the
6756                    // kernel enforces the CLONE_VFORK parent-blocking contract while the
6757                    // child registers itself and runs to exec/exit.
6758                    #[cfg(target_arch = "x86_64")]
6759                    Syscall::Vfork(s) => self.handle_clone_family(guest, s.into()).await,
6760                    Syscall::Wait4(s) => self.handle_wait4(guest, s).await,
6761                    Syscall::Waitid(s) => self.handle_waitid(guest, s).await,
6762
6763                    Syscall::Setpgid(s) => self.handle_setpgid(guest, s).await,
6764                    Syscall::Setsid(s) => self.handle_setsid(guest, s).await,
6765                    // AUTONOMOUS-BOT-IMPLEMENTED
6766                    #[cfg(target_arch = "x86_64")]
6767                    Syscall::Getpgrp(_) => self.handle_group_or_session_getter(guest, call).await,
6768                    Syscall::Getpgid(_) | Syscall::Getsid(_) => {
6769                        self.handle_group_or_session_getter(guest, call).await
6770                    }
6771                    // Without virtual time the guest reads the host clock. Each of these
6772                    // must have a record/replay handler: a recording captures the value
6773                    // the guest observed, and replay returns it without reading the host.
6774                    Syscall::Gettimeofday(s) => {
6775                        if virtualize_time {
6776                            self.handle_gettimeofday(guest, s).await
6777                        } else {
6778                            self.passthrough(guest, call).await
6779                        }
6780                    }
6781                    #[cfg(target_arch = "x86_64")]
6782                    Syscall::Time(s) => {
6783                        if virtualize_time {
6784                            self.handle_time(guest, s).await
6785                        } else {
6786                            self.passthrough(guest, call).await
6787                        }
6788                    }
6789                    Syscall::ClockGettime(s) => {
6790                        if virtualize_time {
6791                            self.handle_clock_gettime(guest, s).await
6792                        } else {
6793                            self.passthrough(guest, call).await
6794                        }
6795                    }
6796                    Syscall::ClockGetres(s) => {
6797                        if virtualize_time {
6798                            self.handle_clock_getres(guest, s).await
6799                        } else {
6800                            self.passthrough(guest, call).await
6801                        }
6802                    }
6803                    // AUTONOMOUS-BOT-IMPLEMENTED
6804                    // TODO-HUMAN-REVIEW(#663)
6805                    Syscall::ClockSettime(_) => Err(Error::Errno(Errno::EPERM)),
6806                    // AUTONOMOUS-BOT-IMPLEMENTED
6807                    // TODO-HUMAN-REVIEW(PR-892)
6808                    Syscall::Getitimer(s) => self.handle_getitimer(guest, s).await,
6809                    // AUTONOMOUS-BOT-IMPLEMENTED
6810                    // TODO-HUMAN-REVIEW(#663)
6811                    Syscall::Setitimer(s) => self.handle_setitimer(guest, s).await,
6812                    // AUTONOMOUS-BOT-IMPLEMENTED
6813                    // TODO-HUMAN-REVIEW(PR-857): Virtual NTP query and fixed mutation refusal.
6814                    Syscall::Adjtimex(s) => {
6815                        if virtualize_time {
6816                            self.handle_adjtimex(guest, s).await
6817                        } else {
6818                            self.handle_unsupported_syscall(
6819                                guest,
6820                                call,
6821                                dettid,
6822                                panic_on_unsupported_syscalls,
6823                            )
6824                            .await
6825                        }
6826                    }
6827                    // AUTONOMOUS-BOT-IMPLEMENTED
6828                    // TODO-HUMAN-REVIEW(PR-857): Clock-id form of virtual NTP query.
6829                    Syscall::ClockAdjtime(s) => {
6830                        if virtualize_time {
6831                            self.handle_clock_adjtime(guest, s).await
6832                        } else {
6833                            self.handle_unsupported_syscall(
6834                                guest,
6835                                call,
6836                                dettid,
6837                                panic_on_unsupported_syscalls,
6838                            )
6839                            .await
6840                        }
6841                    }
6842                    // AUTONOMOUS-BOT-IMPLEMENTED
6843                    // TODO-HUMAN-REVIEW(PR-857): Empty virtual kernel ring buffer.
6844                    Syscall::Syslog(s) => self.handle_syslog(guest, s).await,
6845                    #[cfg(target_arch = "x86_64")]
6846                    Syscall::ArchPrctl(s) => self.handle_arch_prctl(guest, s).await,
6847                    // AUTONOMOUS-BOT-IMPLEMENTED
6848                    Syscall::Seccomp(s) => self.handle_seccomp(guest, s).await,
6849                    // AUTONOMOUS-BOT-IMPLEMENTED
6850                    // TODO-HUMAN-REVIEW(#663)
6851                    Syscall::Prctl(s) => self.handle_prctl(guest, s).await,
6852                    // AUTONOMOUS-BOT-IMPLEMENTED
6853                    // TODO-HUMAN-REVIEW(#663)
6854                    Syscall::Getpriority(s) => self.handle_getpriority(guest, s).await,
6855                    // AUTONOMOUS-BOT-IMPLEMENTED
6856                    // TODO-HUMAN-REVIEW(#663)
6857                    Syscall::Setpriority(s) => self.handle_setpriority(guest, s).await,
6858                    Syscall::Uname(s) => self.handle_uname(guest, s).await,
6859                    Syscall::ExitGroup(s) => self.handle_exit_group(guest, s).await,
6860                    Syscall::Exit(s) => self.handle_exit(guest, s).await,
6861
6862                    Syscall::Dup(w) => self.handle_dup(guest, w).await.map_err(Into::into),
6863                    #[cfg(target_arch = "x86_64")]
6864                    Syscall::Dup2(w) => self.handle_dup2(guest, w).await,
6865                    Syscall::Dup3(w) => self.handle_dup3(guest, w).await,
6866                    #[cfg(target_arch = "x86_64")]
6867                    Syscall::Pipe(w) => self.handle_pipe2(guest, w.into()).await,
6868                    Syscall::Pipe2(w) => self.handle_pipe2(guest, w).await,
6869                    Syscall::Getrandom(s) => self.handle_getrandom(guest, s).await,
6870                    #[cfg(target_arch = "x86_64")]
6871                    Syscall::Utime(s) => self.handle_utime(guest, s).await.map_err(Into::into),
6872                    #[cfg(target_arch = "x86_64")]
6873                    Syscall::Utimes(s) => self.handle_utimes(guest, s).await.map_err(Into::into),
6874                    // NB: lutimes is a libc function not a syscall
6875                    Syscall::Utimensat(s) => {
6876                        self.handle_utimensat(guest, s).await.map_err(Into::into)
6877                    }
6878                    // NB: futimes/futimens are libc functions not a syscall,
6879                    // futimesat is obsolete, return -ENOSYS for simplicity.
6880                    #[cfg(target_arch = "x86_64")]
6881                    Syscall::Futimesat(_s) => Err(Error::Errno(Errno::ENOSYS)),
6882                    // io_uring completion and memory-sharing semantics are not deterministic.
6883                    Syscall::IoUringSetup(_)
6884                    | Syscall::IoUringEnter(_)
6885                    | Syscall::IoUringRegister(_) => Err(Error::Errno(Errno::ENOSYS)),
6886                    Syscall::Socket(s) => self.handle_socket(guest, s).await,
6887                    Syscall::Socketpair(s) => self.handle_socketpair(guest, s).await,
6888                    Syscall::Connect(s) => self.handle_connect(guest, s).await,
6889                    Syscall::Bind(s) => self.handle_bind(guest, s).await,
6890                    // AUTONOMOUS-BOT-IMPLEMENTED
6891                    // TODO-HUMAN-REVIEW(#663)
6892                    Syscall::Setsockopt(s) => {
6893                        let result = self.handle_setsockopt(guest, s).await;
6894                        // Credentials passing names the socket at its next send.
6895                        if result.is_ok()
6896                            && s.level() == libc::SOL_SOCKET
6897                            && matches!(s.optname(), libc::SO_PASSCRED | libc::SO_PASSPIDFD)
6898                        {
6899                            self.note_unix_socket_reachable(guest, s.fd());
6900                        }
6901                        result
6902                    }
6903                    // AUTONOMOUS-BOT-IMPLEMENTED
6904                    // TODO-HUMAN-REVIEW(#663)
6905                    Syscall::Listen(s) => self.handle_listen(guest, s).await,
6906                    // AUTONOMOUS-BOT-IMPLEMENTED
6907                    // TODO-HUMAN-REVIEW(#663)
6908                    Syscall::Getsockname(s) => self.handle_getsockname(guest, s).await,
6909                    // AUTONOMOUS-BOT-IMPLEMENTED
6910                    // TODO-HUMAN-REVIEW(#663)
6911                    Syscall::Getpeername(s) => self.handle_getpeername(guest, s).await,
6912                    // AUTONOMOUS-BOT-IMPLEMENTED
6913                    // TODO-HUMAN-REVIEW(#663)
6914                    Syscall::Getsockopt(s) => self.handle_getsockopt(guest, s).await,
6915                    // AUTONOMOUS-BOT-IMPLEMENTED
6916                    // TODO-HUMAN-REVIEW(#818): shutdown is the lone remaining
6917                    // socket-family syscall; half-closes a tracked socket and
6918                    // forwards via record_or_replay (KVM ratchet round 12).
6919                    Syscall::Shutdown(s) => self.handle_shutdown(guest, s).await,
6920                    #[cfg(target_arch = "x86_64")]
6921                    Syscall::Eventfd(s) => self.handle_eventfd2(guest, s.into()).await,
6922                    Syscall::Eventfd2(s) => self.handle_eventfd2(guest, s).await,
6923                    #[cfg(target_arch = "x86_64")]
6924                    Syscall::Signalfd(s) => self.handle_signalfd4(guest, s.into()).await,
6925                    Syscall::Signalfd4(s) => self.handle_signalfd4(guest, s).await,
6926                    Syscall::TimerfdCreate(s) => self.handle_timerfd_create(guest, s).await,
6927                    Syscall::TimerfdSettime(s) => self.handle_timerfd_settime(guest, s).await,
6928                    Syscall::TimerfdGettime(s) => self.handle_timerfd_gettime(guest, s).await,
6929                    #[cfg(target_arch = "x86_64")]
6930                    Syscall::InotifyInit(s) => {
6931                        self.handle_inotify_init1(guest, InotifyInit1::from(s))
6932                            .await
6933                    }
6934                    Syscall::InotifyInit1(s) => self.handle_inotify_init1(guest, s).await,
6935                    Syscall::InotifyAddWatch(s) => self.handle_inotify_add_watch(guest, s).await,
6936                    Syscall::InotifyRmWatch(s) => self.handle_inotify_rm_watch(guest, s).await,
6937                    Syscall::MemfdCreate(s) => self.handle_memfd_create(guest, s).await,
6938                    // AUTONOMOUS-BOT-IMPLEMENTED
6939                    // TODO-HUMAN-REVIEW(PR-862): Record/replay and register pidfds.
6940                    Syscall::PidfdOpen(s) => self.handle_pidfd_open(guest, s).await,
6941                    // AUTONOMOUS-BOT-IMPLEMENTED
6942                    // TODO-HUMAN-REVIEW(PR-899): Host object handles and mount IDs
6943                    // are outside Detcore's filesystem identity model.
6944                    Syscall::NameToHandleAt(_) => Err(Error::Errno(Errno::EOPNOTSUPP)),
6945                    Syscall::Userfaultfd(s) => self.handle_userfaultfd(guest, s).await,
6946                    Syscall::Accept(s) => self.handle_accept4(guest, s.into()).await,
6947                    Syscall::Accept4(s) => self.handle_accept4(guest, s).await,
6948
6949                    Syscall::Nanosleep(s) => self.handle_nanosleep_family(guest, s.into()).await,
6950                    Syscall::ClockNanosleep(s) => {
6951                        self.handle_nanosleep_family(guest, s.into()).await
6952                    }
6953                    Syscall::SchedYield(s) => self.handle_sched_yield(guest, s).await,
6954
6955                    // NB: getdents is not recommended, (g)libc should call getdents64 only
6956                    // see: sysdeps/unix/sysv/linux/getdents.c.
6957                    #[cfg(target_arch = "x86_64")]
6958                    Syscall::Getdents(s) => self.handle_getdents(guest, s).await,
6959                    Syscall::Getdents64(s) => self.handle_getdents64(guest, s).await,
6960
6961                    #[cfg(target_arch = "x86_64")]
6962                    Syscall::Poll(s) => self.handle_poll(guest, s).await,
6963                    // AUTONOMOUS-BOT-IMPLEMENTED
6964                    // TODO-HUMAN-REVIEW(#686): Review scratch fd sets and scheduler polling.
6965                    Syscall::Pselect6(s) => self.handle_pselect6(guest, s).await,
6966                    // AUTONOMOUS-BOT-IMPLEMENTED
6967                    // TODO-HUMAN-REVIEW(#800): select is the timeval sibling of pselect6.
6968                    #[cfg(target_arch = "x86_64")]
6969                    Syscall::Select(s) => self.handle_select(guest, s).await,
6970                    // AUTONOMOUS-BOT-IMPLEMENTED
6971                    Syscall::Ppoll(s) => self.handle_ppoll(guest, s).await,
6972                    #[cfg(target_arch = "x86_64")]
6973                    Syscall::EpollCreate(s) => {
6974                        self.handle_epoll_create1(guest, EpollCreate1::from(s))
6975                            .await
6976                    }
6977                    Syscall::EpollCreate1(s) => self.handle_epoll_create1(guest, s).await,
6978                    Syscall::EpollCtl(s) => self.handle_epoll_ctl(guest, s).await,
6979                    Syscall::EpollPwait(s) => self.handle_epoll_pwait(guest, s).await,
6980                    #[cfg(target_arch = "x86_64")]
6981                    Syscall::EpollWait(s) => self.handle_epoll_wait(guest, s).await,
6982                    #[cfg(target_arch = "x86_64")]
6983                    Syscall::EpollWaitOld(s) => panic!(
6984                        "Not handling deprecated syscall: {}",
6985                        s.display(&guest.memory())
6986                    ),
6987                    // AUTONOMOUS-BOT-IMPLEMENTED
6988                    // TODO-HUMAN-REVIEW(#549)
6989                    // The obsolete x86_64 entry point is absent from modern Linux kernels.
6990                    #[cfg(target_arch = "x86_64")]
6991                    Syscall::EpollCtlOld(_) => Err(Error::Errno(Errno::ENOSYS)),
6992
6993                    Syscall::SchedGetaffinity(s) => self.handle_sched_getaffinity(guest, s).await,
6994                    Syscall::SchedSetaffinity(s) => self.handle_sched_setaffinity(guest, s).await,
6995
6996                    // ===== BATCH 3: NUMA memory-placement and Linux CPU-scheduling
6997                    // policy. Hermit exposes a single virtual NUMA node and replaces
6998                    // the Linux scheduler with Detcore, so these are inoperative and
6999                    // are virtualized to fixed, host-independent results (see the
7000                    // determinism argument in syscall_classification.rs). Setters and
7001                    // count-returning calls are no-ops; getters emulate a default
7002                    // single-node / SCHED_OTHER answer.
7003                    // AUTONOMOUS-BOT-IMPLEMENTED
7004                    // TODO-HUMAN-REVIEW(#720)
7005                    Syscall::Mbind(_) => Ok(0),
7006                    Syscall::SetMempolicy(_) => Ok(0),
7007                    Syscall::GetMempolicy(s) => self.handle_get_mempolicy(guest, s).await,
7008                    Syscall::MigratePages(_) => Ok(0),
7009                    Syscall::MovePages(s) => self.handle_move_pages(guest, s).await,
7010                    Syscall::SchedSetscheduler(_) => Ok(0),
7011                    Syscall::SchedSetparam(_) => Ok(0),
7012                    // Report the fixed default policy SCHED_OTHER (0).
7013                    Syscall::SchedGetscheduler(_) => Ok(0),
7014                    Syscall::SchedGetparam(s) => self.handle_sched_getparam(guest, s).await,
7015                    Syscall::SchedRrGetInterval(s) => {
7016                        self.handle_sched_rr_get_interval(guest, s).await
7017                    }
7018
7019                    // ===== BATCH 51: fail-closed utility syscalls, re-enabling chrt,
7020                    // ionice, and flock under --strict. Detcore replaces the Linux
7021                    // scheduler, exposes a single virtual CPU, and serializes guest
7022                    // threads, so a thread's Linux scheduling attributes (sched_getattr)
7023                    // and I/O priority (ioprio_set) are inert: those two have no
7024                    // deterministic effect and are emulated to fixed, host-independent
7025                    // results (see syscall_classification.rs).
7026                    //
7027                    // flock is NOT in that inert group and is not emulated. This comment
7028                    // used to claim it "is never contended inside the serialized
7029                    // container"; that was measured false -- two open file descriptions
7030                    // in ONE process both held the same LOCK_EX under the old no-op,
7031                    // where native Linux excluded the second -- and the no-op was
7032                    // removed. Serializing THREADS does not make a whole-file lock
7033                    // uncontended, because flock conflicts are between OPEN FILE
7034                    // DESCRIPTIONS. flock is now forwarded to the kernel, like fcntl's
7035                    // POSIX record locks; handle_flock carries the determinism argument
7036                    // and the one case Detcore refuses (a contended BLOCKING request,
7037                    // which it cannot park a thread on deterministically).
7038                    // AUTONOMOUS-BOT-IMPLEMENTED
7039                    // TODO-HUMAN-REVIEW(#791)
7040                    Syscall::SchedGetattr(s) => self.handle_sched_getattr(guest, s).await,
7041                    // AUTONOMOUS-BOT-IMPLEMENTED
7042                    // TODO-HUMAN-REVIEW(PR-841): Review virtual sched_setattr no-op policy.
7043                    Syscall::SchedSetattr(s) => self.handle_sched_setattr(guest, s).await,
7044                    // AUTONOMOUS-BOT-IMPLEMENTED
7045                    // TODO-HUMAN-REVIEW(#791)
7046                    Syscall::IoprioSet(s) => self.handle_ioprio_set(guest, s).await,
7047                    // AUTONOMOUS-BOT-IMPLEMENTED
7048                    // TODO-HUMAN-REVIEW(PR-881): Review virtual ioprio_get defaults.
7049                    Syscall::IoprioGet(s) => self.handle_ioprio_get(guest, s).await,
7050                    // AUTONOMOUS-BOT-IMPLEMENTED
7051                    // TODO-HUMAN-REVIEW(#2373)
7052                    Syscall::Flock(s) => self.handle_flock(guest, s).await,
7053
7054                    // TODO-HUMAN-REVIEW(PR-1064): recvfrom/read/readv/recvmmsg reach
7055                    // a NETLINK_SOCK_DIAG dump exactly as recvmsg does. Until they
7056                    // were routed through the same sanitizer, four of the five
7057                    // usable receive syscalls returned raw host socket inode
7058                    // numbers, which made the determinization optional from the
7059                    // guest's point of view: `socket.recv()` alone was enough to
7060                    // skip it. Non-socket-diag descriptors take the same path as
7061                    // before; the predicate is checked inside.
7062                    Syscall::Recvfrom(s) if self.sock_diag_reply_fd(guest, s.fd()) => {
7063                        self.handle_sock_diag_recvfrom(guest, s).await
7064                    }
7065                    Syscall::Recvfrom(s) => {
7066                        let in_turn = self.receives_in_turn(guest, s.fd(), s);
7067                        let result = self.handle_socket_receive(guest, s, s.fd(), true).await;
7068                        let read = crate::syscalls::scm_rights::ReceiveWithoutControl {
7069                            peek: s.flags() & libc::MSG_PEEK != 0,
7070                            request: crate::syscalls::scm_rights::Request::Bytes(
7071                                s.buf().map_or(0, |buf| buf.as_raw()),
7072                                s.len(),
7073                            ),
7074                            returns_at_once_when_empty: false,
7075                            address_out: s.addr().is_some(),
7076                            takes: crate::syscalls::scm_rights::Takes::Counted,
7077                            in_turn,
7078                            noted_by_recordings: true,
7079                        };
7080                        self.note_receive_without_control(guest, s.fd(), &result, read);
7081                        result
7082                    }
7083                    // AUTONOMOUS-BOT-IMPLEMENTED
7084                    // TODO-HUMAN-REVIEW(PR-901)
7085                    Syscall::Recvmsg(s) => Box::pin(self.handle_recvmsg(guest, s)).await,
7086                    Syscall::Sendto(s) => {
7087                        let send = self.send_without_control(
7088                            guest,
7089                            s.fd(),
7090                            |guest| self.completes_in_turn(guest, s),
7091                            s.addr().is_some() && s.addr_len() != 0,
7092                            s.flags() & libc::MSG_OOB as u32 != 0,
7093                        );
7094                        let result = self.handle_sendrecv(guest, s).await;
7095                        self.note_send_without_control(guest, send, &result);
7096                        result
7097                    }
7098                    Syscall::Sendmsg(s) => Box::pin(self.handle_sendmsg(guest, s)).await,
7099                    Syscall::Sendmmsg(s) => Box::pin(self.handle_sendmmsg(guest, s)).await,
7100
7101                    // AUTONOMOUS-BOT-IMPLEMENTED
7102                    // TODO-HUMAN-REVIEW(#788): recvmmsg is the multi-message form of
7103                    // recvmsg and shares its NonblockableSyscall impl. The fd is made
7104                    // temporarily nonblocking, the kernel fills the mmsghdr array
7105                    // atomically, and the Detcore scheduler owns any blocking, so the
7106                    // timeout argument (deliberately ignored, see helpers.rs) does not
7107                    // introduce nondeterminism.
7108                    // TODO-HUMAN-REVIEW(PR-901): Review batched ancillary timestamp rewriting.
7109                    Syscall::Recvmmsg(s) if self.sock_diag_reply_fd(guest, s.fd()) => {
7110                        self.handle_sock_diag_recvmmsg(guest, s).await
7111                    }
7112                    Syscall::Recvmmsg(s) => Box::pin(self.handle_recvmmsg(guest, s)).await,
7113                    Syscall::RtSigtimedwait(s) => self.handle_rt_sigtimedwait(guest, s).await,
7114                    Syscall::RtSigsuspend(s) => self.handle_rt_sigsuspend(guest, s).await,
7115                    // AUTONOMOUS-BOT-IMPLEMENTED
7116                    // TODO-HUMAN-REVIEW(#663)
7117                    Syscall::RtSigpending(s) => self.handle_rt_sigpending(guest, s).await,
7118                    // AUTONOMOUS-BOT-IMPLEMENTED
7119                    // TODO-HUMAN-REVIEW(#663)
7120                    Syscall::Kill(s) => {
7121                        let result = self.handle_kill(guest, s).await;
7122                        let result = self
7123                            .report_backend_signal_refusal(guest, Sysno::kill, result)
7124                            .await;
7125                        if s.sig() == libc::SIGALRM && result.is_ok() {
7126                            let target = if guest.thread_state().nested_pid_namespace {
7127                                Some(GuestSigalrmTarget::Unclassified)
7128                            } else {
7129                                match s.pid() {
7130                                    pid if pid > 0 => {
7131                                        Some(GuestSigalrmTarget::Process(DetPid::from_raw(pid)))
7132                                    }
7133                                    0 => Some(GuestSigalrmTarget::Group(None)),
7134                                    -1 => Some(GuestSigalrmTarget::All),
7135                                    libc::pid_t::MIN => None,
7136                                    pid => Some(GuestSigalrmTarget::Group(Some(DetPid::from_raw(
7137                                        -pid,
7138                                    )))),
7139                                }
7140                            };
7141                            if let Some(target) = target {
7142                                self.note_guest_sigalrm(guest, target).await;
7143                            }
7144                        }
7145                        result
7146                    }
7147                    // AUTONOMOUS-BOT-IMPLEMENTED
7148                    // TODO-HUMAN-REVIEW(#663)
7149                    Syscall::Tgkill(s) => {
7150                        let result = self.handle_tgkill(guest, s).await;
7151                        self.report_backend_signal_refusal(guest, Sysno::tgkill, result)
7152                            .await
7153                    }
7154                    // AUTONOMOUS-BOT-IMPLEMENTED
7155                    // TODO-HUMAN-REVIEW(#812)
7156                    Syscall::Tkill(s) => {
7157                        let result = self.handle_tkill(guest, s).await;
7158                        self.report_backend_signal_refusal(guest, Sysno::tkill, result)
7159                            .await
7160                    }
7161                    // AUTONOMOUS-BOT-IMPLEMENTED
7162                    // TODO-HUMAN-REVIEW(#812)
7163                    Syscall::RtSigqueueinfo(s) => {
7164                        let result = self.handle_rt_sigqueueinfo(guest, s).await;
7165                        let result = self
7166                            .report_backend_signal_refusal(guest, Sysno::rt_sigqueueinfo, result)
7167                            .await;
7168                        if s.sig() == libc::SIGALRM && result.is_ok() {
7169                            let target = if guest.thread_state().nested_pid_namespace {
7170                                GuestSigalrmTarget::Unclassified
7171                            } else {
7172                                GuestSigalrmTarget::Process(DetPid::from_raw(s.tgid()))
7173                            };
7174                            self.note_guest_sigalrm(guest, target).await;
7175                        }
7176                        result
7177                    }
7178                    // AUTONOMOUS-BOT-IMPLEMENTED
7179                    // TODO-HUMAN-REVIEW(#812)
7180                    Syscall::RtTgsigqueueinfo(s) => {
7181                        let result = self.handle_rt_tgsigqueueinfo(guest, s).await;
7182                        self.report_backend_signal_refusal(guest, Sysno::rt_tgsigqueueinfo, result)
7183                            .await
7184                    }
7185
7186                    Syscall::Execve(s) => self.handle_execveat(guest, s.into()).await,
7187                    Syscall::Execveat(s) => self.handle_execveat(guest, s).await,
7188
7189                    Syscall::Getcpu(s) => self.handle_getcpu(guest, s).await,
7190
7191                    // AUTONOMOUS-BOT-IMPLEMENTED
7192                    // TODO-HUMAN-REVIEW(#1549): Credential-query
7193                    // family emulated to the fixed virtual-root identity (0). See
7194                    // syscall_classification.rs for the determinism rationale.
7195                    // getuid/geteuid/getgid/getegid return the constant directly;
7196                    // getresuid/getresgid write the constant to each provided result
7197                    // pointer. Never forwarded to the host, so the answer no longer
7198                    // depends on whether the backend runs the guest in a
7199                    // CLONE_NEWUSER namespace.
7200                    Syscall::Getuid(_)
7201                    | Syscall::Geteuid(_)
7202                    | Syscall::Getgid(_)
7203                    | Syscall::Getegid(_) => Ok(0),
7204                    Syscall::Getresuid(s) => self.handle_getresuid(guest, s).await,
7205                    Syscall::Getresgid(s) => self.handle_getresgid(guest, s).await,
7206                    Syscall::RtSigprocmask(s) => self.handle_rt_sigprocmask(guest, s).await,
7207                    Syscall::RtSigaction(s) => {
7208                        let result = self.handle_rt_sigaction(guest, s).await;
7209                        // Rule 5, decided from the installed disposition: an
7210                        // old-action fault after the install still installed it
7211                        // (https://github.com/rrnewton/hermit/issues/4139).
7212                        if s.signum() == libc::SIGALRM
7213                            && s.action().is_some()
7214                            && guest.config().sequentialize_threads
7215                        {
7216                            crate::tool_global::note_sigalrm_event(
7217                                guest,
7218                                crate::tool_global::GlobalRequest::NoteSigalrmDisposition,
7219                            )
7220                            .await;
7221                        }
7222                        result
7223                    }
7224                    #[cfg(target_arch = "x86_64")]
7225                    Syscall::Alarm(s) => self.handle_alarm(guest, s).await,
7226                    #[cfg(target_arch = "x86_64")]
7227                    Syscall::Pause(s) => self.handle_pause(guest, s).await,
7228
7229                    Syscall::Getrusage(s) => self.handle_getrusage(guest, s).await,
7230                    Syscall::Sysinfo(s) => self.handle_sysinfo(guest, s).await,
7231                    // AUTONOMOUS-BOT-IMPLEMENTED
7232                    Syscall::Times(s) => self.handle_times(guest, s).await,
7233                    Syscall::Prlimit64(s) => self.handle_prlimit64(guest, s).await,
7234                    // AUTONOMOUS-BOT-IMPLEMENTED
7235                    // TODO-HUMAN-REVIEW(#663)
7236                    Syscall::Getrlimit(s) => self.handle_getrlimit(guest, s).await,
7237                    // AUTONOMOUS-BOT-IMPLEMENTED
7238                    // TODO-HUMAN-REVIEW(#663)
7239                    Syscall::Setrlimit(s) => self.handle_setrlimit(guest, s).await,
7240
7241                    // POSIX per-process timers use the virtual clock and scheduler
7242                    // for deterministic arming and supported signal delivery.
7243                    Syscall::TimerCreate(s) => self.handle_timer_create(guest, s).await,
7244                    Syscall::TimerSettime(s) => self.handle_timer_settime(guest, s).await,
7245                    Syscall::TimerGettime(s) => self.handle_timer_gettime(guest, s).await,
7246                    Syscall::TimerGetoverrun(s) => self.handle_timer_getoverrun(guest, s).await,
7247                    Syscall::TimerDelete(s) => self.handle_timer_delete(guest, s).await,
7248
7249                    // Serialized threads share a total memory order, so process-wide
7250                    // memory barriers are trivially satisfied and can be no-ops.
7251                    Syscall::Membarrier(s) => self.handle_membarrier(guest, s).await,
7252
7253                    // Filesystem statistics: passthrough is record/replay-aware so the
7254                    // (otherwise host-dependent) result is captured and reproduced.
7255                    // statfs/fstatfs run the real syscall, then canonicalize the
7256                    // host-varying fields (free blocks/inodes, fsid) so the result is
7257                    // deterministic under --verify (a bare passthrough diverged, e.g.
7258                    // for tar).
7259                    Syscall::Statfs(s) => self.handle_statfs(guest, s).await,
7260                    Syscall::Fstatfs(s) => self.handle_fstatfs(guest, s).await,
7261
7262                    unexpected => {
7263                        self.handle_unsupported_syscall(
7264                            guest,
7265                            unexpected,
7266                            dettid,
7267                            panic_on_unsupported_syscalls,
7268                        )
7269                        .await
7270                    }
7271                },
7272                // Where the run declares a launcher, the kernel's program break
7273                // is the launcher's: Detcore answers brk from its model of the
7274                // guest's (`Detcore::handle_brk`).
7275                SyscallClassification::PassThrough
7276                    if call.number() == Sysno::brk && self.declares_launcher() =>
7277                {
7278                    self.handle_brk(guest, call).await
7279                }
7280                // AUTONOMOUS-BOT-IMPLEMENTED
7281                // TODO-HUMAN-REVIEW(PR-2985): Review scheduler tracking of set_tid_address.
7282                // Linux and the backend own the pass-through call. Detcore also
7283                // mirrors its successful registration into the scheduler because
7284                // the scheduler supplies the logical CHILD_CLEARTID wake.
7285                SyscallClassification::PassThrough if call.number() == Sysno::set_tid_address => {
7286                    match call {
7287                        Syscall::SetTidAddress(s) => self.handle_set_tid_address(guest, s).await,
7288                        _ => unreachable!("set_tid_address unexpectedly lost its typed variant"),
7289                    }
7290                }
7291                // AUTONOMOUS-BOT-IMPLEMENTED
7292                // TODO-HUMAN-REVIEW(PR-2223): Review observing
7293                // the robust-list registration without changing its pass-through
7294                // classification. This is still a pass-through — Linux owns the
7295                // registration and supplies its result — but Detcore remembers the
7296                // head address so thread exit can replay `exit_robust_list()`
7297                // against its own futex waiter pool.
7298                SyscallClassification::PassThrough if call.number() == Sysno::set_robust_list => {
7299                    match call {
7300                        Syscall::SetRobustList(s) => self.handle_set_robust_list(guest, s).await,
7301                        _ => self.passthrough(guest, call).await,
7302                    }
7303                }
7304                // faccessat2 and fchmodat2 are untyped in the pinned Reverie revision; the
7305                // reviewed classification table routes them, and every other reviewed
7306                // PassThrough syscall, through the blanket arm below.
7307                // AUTONOMOUS-BOT-IMPLEMENTED
7308                // TODO-HUMAN-REVIEW(PR-644): Keep dispatch aligned with the reviewed classification.
7309                // A pass-through whose restored mask may deliver a pending real-time
7310                // timer expiration (https://github.com/rrnewton/hermit/issues/4089).
7311                SyscallClassification::PassThrough if call.number() == Sysno::rt_sigreturn => {
7312                    crate::syscalls::signal_shadow::record_rt_sigreturn(guest).await;
7313                    self.observe_sigreturn_mask(guest).await;
7314                    self.passthrough(guest, call).await
7315                }
7316                // The protection-key calls, boxed: their state is not carried by
7317                // every other call's future (`syscall_hot_path_tests`).
7318                SyscallClassification::PassThrough
7319                    if matches!(
7320                        call.number(),
7321                        Sysno::pkey_alloc | Sysno::pkey_mprotect | Sysno::pkey_free
7322                    ) =>
7323                {
7324                    Box::pin(self.handle_protection_key_call(guest, call)).await
7325                }
7326                SyscallClassification::PassThrough => self.passthrough(guest, call).await,
7327                // The kernel's restart of a wait Detcore emulates, or ran in the
7328                // kernel as the guest made it, resumes it; any other
7329                // `restart_syscall` keeps the unsupported-syscall policy below.
7330                // AUTONOMOUS-BOT-IMPLEMENTED
7331                SyscallClassification::Unsupported if call.number() == Sysno::restart_syscall => {
7332                    self.handle_restart_syscall(
7333                        guest,
7334                        call,
7335                        restart_block,
7336                        dettid,
7337                        panic_on_unsupported_syscalls,
7338                    )
7339                    .await
7340                }
7341                SyscallClassification::Unsupported => {
7342                    self.handle_unsupported_syscall(
7343                        guest,
7344                        call,
7345                        dettid,
7346                        panic_on_unsupported_syscalls,
7347                    )
7348                    .await
7349                }
7350            };
7351            // A Tool error/cancellation deliberately retains an unresolved import.
7352            // The restart checkpoint (A5 step 3c.1c, design D40, placed by IR-2):
7353            // under the in-guest delivery gate, a wait a signal ended gets the
7354            // result the guest sees here, before anything below observes `res`.
7355            // With the gate off it changes nothing.
7356            let (res, interrupted) = self.wait_end_checkpoint(guest, &call, res).await;
7357
7358            // A call Detcore injected that the backend left to the Tool
7359            // (`UnsupportedOperation::ToolDecided`), such as the getpgrp a clone3
7360            // race passes through (https://github.com/rrnewton/hermit/issues/4531),
7361            // ends the run as an internal failure before its ENOSYS can reach the
7362            // guest, when it is the event's last injection. The check reads only
7363            // the handler's latest refusal, and only if no consumer took it
7364            // first; it is not a general guard (see
7365            // `fail_on_tool_decided_injection`).
7366            crate::syscalls::helpers::fail_on_tool_decided_injection::<T, _>(guest, call.number());
7367            syscall_histogram::finish::<T, _>(guest, call.number(), &res, injected_at_entry);
7368
7369            // A copy may already have changed guest memory before reporting a
7370            // terminal backend error. Do not perform even the syscall-result
7371            // display's memory reads, or later observers/posthooks/timer effects.
7372            // Random-device reads have completed their release RPC before returning;
7373            // getrandom acquired no file resource. Physical cleanup belongs to the
7374            // backend failure owner, not to this observer fence.
7375            if res.as_ref().is_err_and(crate::random::is_copy_failure) {
7376                return res;
7377            }
7378
7379            // A `write`, `writev` or `sendto` through a datagram socket whose
7380            // connected peer has gone purged the socket's receive queue
7381            // (`Detcore::note_send_refused`; `sendmsg` and `sendmmsg` note it
7382            // themselves).
7383            if crate::syscalls::scm_rights::found_its_peer_gone(&res) {
7384                self.note_send_refused(guest, &call);
7385            }
7386            // What the call made Linux release from `SCM_RIGHTS` queues (the
7387            // close of a receiving socket's last descriptor, a receive, a purge)
7388            // is released now, in the call's turn, before any observer, posthook
7389            // or turn hand-off (`Detcore::settle_released_rights`).
7390            if guest.thread_state().holds_released_rights() {
7391                Box::pin(self.settle_released_rights(guest)).await;
7392            }
7393
7394            if res.is_ok() && !removed_inodes.is_empty() {
7395                self.retire_removed_inodes(guest, removed_inodes).await;
7396            }
7397            if res.is_ok() {
7398                for fifo in removed_guest_fifos {
7399                    crate::fifo_peer::forget_guest_made(fifo);
7400                }
7401                self.note_guest_made_fifo(guest, &call).await;
7402            }
7403            // A read or write that moved bytes through a pipe or FIFO readies the
7404            // threads polling its other direction (`--pipe-wake-latency`).
7405            if let Ok(moved) = res
7406                && moved > 0
7407                && let Some((fd, wants)) = crate::pipe_wake::progress_of(&call)
7408            {
7409                self.note_fd_readied(guest, fd, wants).await;
7410            }
7411            if crate::fifo_peer::any_orphaned_page() {
7412                // Boxed: a rare cleanup, kept out of every syscall's future
7413                // (`hot_path_futures_stay_small`).
7414                Box::pin(Self::unmap_orphaned_staging_pages(guest)).await;
7415            }
7416
7417            // With asynchronous exit completion, a descriptor that would make this
7418            // guest serve another guest's exit ends the run before anything
7419            // observes the result (crate::exit_dependencies).
7420            // It reads a received message's descriptors from the call's header,
7421            // so it reads it as the kernel's view holds it (`EntryImport`).
7422            // Boxed: it keeps the view across an await, which would otherwise
7423            // grow every syscall's future (`hot_path_futures_stay_small`).
7424            if let Ok(ret) = res
7425                && let Err(error) =
7426                    Box::pin(self.refuse_exit_dependencies_in_view(guest, &call, ret)).await
7427            {
7428                return Box::pin(self.complete_after_posthook(
7429                    guest,
7430                    new_count,
7431                    Err(error),
7432                    interrupted,
7433                ))
7434                .await;
7435            }
7436            // A new executable file mapping's exclusive pairs (store-exclusive
7437            // retry loops). Boxed, so this rare path does not grow every
7438            // syscall's future.
7439            #[cfg(target_arch = "aarch64")]
7440            if let Ok(ret) = res
7441                && let Some(range) = store_exclusive::range_to_scan(guest, &call, ret)
7442            {
7443                Box::pin(store_exclusive::report_new_mapping(guest, range)).await;
7444            }
7445
7446            // The guest resumes with this result in its return register (A5 step
7447            // 3.2). An error that is not an errno names no register value.
7448            let finished = match &res {
7449                Ok(value) => Some(*value),
7450                Err(Error::Errno(errno)) => Some(-(errno.into_raw() as i64)),
7451                Err(_) => None,
7452            };
7453            if let Some(result) = finished {
7454                guest.thread_state_mut().delivery_interruption =
7455                    Some(reverie::Interruption::Return {
7456                        syscall: call.number() as i64,
7457                        result,
7458                    });
7459            }
7460            detlog!(
7461                event = crate::detlog::DetLogEvent::SyscallResult {
7462                    finished_syscall_number: new_count,
7463                };
7464                "[syscall][detcore, dtid {}] finish syscall #{}: {} = {:?}",
7465                dettid,
7466                new_count,
7467                display_syscall_finished(&call, &guest.memory(), &res),
7468                res
7469            );
7470
7471            // Same guest-logical-control point that already anchors the stack/heap hashes: the
7472            // syscall is complete and its result written back, so the guest logically has control.
7473            // Reading registers is itself backend work, so keep the disabled path inert.  In
7474            // particular, a run that does not request register evidence must not be perturbed by
7475            // collecting data that will immediately be discarded.
7476            if self.cfg.detlog_regs {
7477                let control_point_regs = guest.regs().await;
7478                let regs_seq = guest.thread_state().stats.syscall_count;
7479                self.detlog_registers(guest, &control_point_regs, regs_seq);
7480            }
7481
7482            // brk is PassThrough, so nothing else records where the guest's heap is.
7483            // Both brk(NULL) and brk(addr) return the break in effect afterwards.
7484            if let Syscall::Brk(_) = &call
7485                && let Ok(brk) = res
7486                && brk > 0
7487            {
7488                guest
7489                    .thread_state()
7490                    .memory_metadata
7491                    .lock()
7492                    .expect("memory metadata mutex poisoned")
7493                    .observe_brk(brk as u64);
7494            }
7495
7496            if let Err(error) = self.detlog_memory_maps(guest) {
7497                return Box::pin(self.complete_after_posthook(
7498                    guest,
7499                    new_count,
7500                    Err(error),
7501                    interrupted,
7502                ))
7503                .await;
7504            }
7505            // Same control point again, for the bytes this syscall moved through a guest buffer.
7506            // Unlike the two mapping hashes above, the extent comes from the syscall's OWN
7507            // arguments, so it does not matter whether the buffer lives on the stack, in the brk
7508            // heap, in BSS or in an anonymous mmap -- the last two of which neither mapping hash
7509            // can see. Only successful calls moved anything.
7510            // Incoming readv/recvmsg observation uses complete geometry captured
7511            // for the successful attempt, when available, but hashes the actual
7512            // returned guest bytes without reinstating metadata over the payload.
7513            // Outbound observation retains its entry-metadata overlay: the call
7514            // may have sent those reinstated bytes. Exit-dependency readers keep
7515            // their separate existing overlay contract.
7516            if let Ok(ret) = &res
7517                && self.cfg.detlog_io_buffers
7518            {
7519                // Boxed: the entry view it holds would otherwise grow every
7520                // syscall's future (`hot_path_futures_stay_small`).
7521                if let Some(done) = Box::pin(self.detlog_io_buffers_in_entry_view(
7522                    guest,
7523                    &call,
7524                    *ret,
7525                    dettid,
7526                    rng_readv_output.as_deref(),
7527                    new_count,
7528                    interrupted,
7529                ))
7530                .await
7531                {
7532                    return done;
7533                }
7534            }
7535
7536            if sequentialize_threads && self.cfg.observes_schedevents() {
7537                let access =
7538                    self.access_trace_exit(guest, &call, finished, rng_readv_output.as_deref());
7539                trace_schedevent_with_access(
7540                    guest,
7541                    with_guest_time(
7542                        guest,
7543                        SchedEvent::syscall(dettid, call.number(), SyscallPhase::Posthook),
7544                    ),
7545                    true,
7546                    access,
7547                )
7548                .await;
7549            }
7550
7551            // Posthook happens-before anchors this call reached fire now that its
7552            // result is decided and before the guest resumes
7553            // (https://github.com/rrnewton/hermit/issues/3929).
7554            self.publish_posthook_checkpoint(guest, new_count, &res, interrupted)
7555                .await;
7556
7557            // The syscall is finished; a turn the post-hook takes (a timeslice
7558            // end) is the thread's own and advances global time as usual.
7559            guest.thread_state_mut().in_uncharged_bootstrap_syscall = false;
7560            self.post_handler_hook_observed(guest, CheckIn::Event, boundary_token)
7561                .await;
7562
7563            // Signal phase 1, step I4: a due SIGALRM entry is delivered here.
7564            self.complete_syscall(guest, res).await
7565        })
7566        .await;
7567        // Every return in the body above has finished decoder/copyout and its
7568        // existing posthook. No later physical call may use this operation.
7569        if let Some(operation) = &input_operation {
7570            operation.close();
7571            if result.is_ok() || matches!(&result, Err(Error::Errno(_))) {
7572                operation.readmitted()?;
7573            }
7574        }
7575        // Tool failure/cancellation retains unresolved access and history.
7576        result
7577    }
7578
7579    async fn on_exit_thread<G: GlobalRPC<Self::GlobalState>>(
7580        &self,
7581        tid: Tid,
7582        global_state: &G,
7583        mut thread_state: Self::ThreadState,
7584        exit_status: ExitStatus,
7585    ) -> Result<(), Error> {
7586        let dettid = thread_state.dettid;
7587        thread_state.delivery_counters.log(dettid);
7588        // Its kept /proc status file reads only ESRCH from now on.
7589        crate::syscalls::forget_thread_status(Pid::from_raw(tid.as_raw()));
7590        // Cancellation can consume the backend's transferred state before the
7591        // successful-exec callback has rebound its logical identity.
7592        let current = DetTid::from_raw(tid.as_raw());
7593        let transferred_exec = current != dettid;
7594        debug!(
7595            "[detcore, dtid {}] thread exit hook, deregistering from scheduler.",
7596            dettid
7597        );
7598        // Close the final in-progress timeslice so this thread contributes its
7599        // last (partial) slice to the run report, even if it never exhausted a
7600        // full slice.
7601        let now = thread_state.thread_logical_time.as_nanos();
7602        thread_state.stats.close_final_timeslice(now);
7603        // Ptrace invokes this hook after consuming the final kernel exit status,
7604        // when the kernel has closed the thread's descriptors. KVM runs this
7605        // owner hook after worker joins and child waitability publication.
7606        // Publish final CPU in this synchronous prefix: serial KVM waitid waits
7607        // for that owned notification before consuming, not for the hook tail.
7608        // DETERMINISTIC RECOVERY (TODO-HUMAN-REVIEW(PR-1147)). `ThreadState::detpid`
7609        // is `Option` and starts as `None` ("Initialized later" at the clone site),
7610        // so a thread that reaches the exit hook before its per-thread identity is
7611        // populated used to `.expect()` here. That panic fires inside a Reverie
7612        // teardown callback, while the backend still owns the exit event, which is
7613        // the worst place to abort: it can wedge the supervisor rather than fail one
7614        // thread. Fall back to the PROCESS-level `self.detpid`, which is
7615        // non-optional and preserves the identity source selected for this
7616        // backend: virtual for the current DBT ABI, physical for ABI v1. Warn so
7617        // the exceptional window is observable instead of silently papered over.
7618        //
7619        // CORRECTED BY #2348, and stated rather than quietly dropped: the normal
7620        // current-ABI DBT path pre-populates `thread_state.detpid` with the
7621        // client-published virtual process identity, so thread start and exit
7622        // agree on that value. This recovery is only for a thread that exits
7623        // before that initialization. In that exceptional window thread start
7624        // would fall back to `guest.pid()` (the physical host pid for `DbtGuest`),
7625        // while this exit path uses `self.detpid`. ABI v1 also intentionally keeps
7626        // physical callback identities. The warning makes either compatibility
7627        // or recovery path observable; this comment does not claim those fallback
7628        // identities are virtual or independently deterministic.
7629        let (detpid, used_process_detpid) =
7630            select_thread_exit_detpid(thread_state.detpid, self.detpid);
7631        if used_process_detpid {
7632            tracing::warn!(
7633                "[detcore, dtid {}] thread exited before its per-thread detpid was \
7634                 initialized; falling back to the process detpid {}",
7635                dettid,
7636                self.detpid
7637            );
7638        }
7639        // The transferred survivor is the final leader even if cancellation
7640        // precedes local rebinding. Its snapshot includes the worker's tail;
7641        // the earlier displaced-leader snapshot is only a prefix.
7642        if current == detpid {
7643            if !thread_state.record_exited_child_process_cpu_time(detpid) {
7644                return Err(Error::Tool(anyhow::anyhow!(
7645                    "child CPU publication ownership changed before final exit"
7646                )));
7647            }
7648        } else {
7649            thread_state.account_process_cpu_time();
7650        }
7651        let mm_id = thread_state.mm_id;
7652        let exit_signal = match &exit_status {
7653            ExitStatus::Signaled(signal, _) => Some(*signal as i32),
7654            ExitStatus::SignaledRaw(signal, _) => Some(*signal),
7655            ExitStatus::Exited(_) => None,
7656        };
7657        // Publish each owner's final clock under its own RPC identity before
7658        // counting that owner in the complete physical-exit barrier. Otherwise
7659        // the group's maximum could be charged to whichever callback finishes
7660        // last, followed by a backwards update from its real deregistration.
7661        // Exec preparation already cleared the old image's robust list. An
7662        // unbound transfer must authenticate its consuming cleanup before any
7663        // ordinary RPC can publish the survivor's clock under the leader TID.
7664        let exit_time_accounted = !transferred_exec
7665            && (!thread_state.has_matching_robust_list_exit(exit_signal)
7666                || acknowledge_robust_list_exit_time(
7667                    thread_state.thread_logical_time.clone(),
7668                    global_state,
7669                    mm_id,
7670                )
7671                .await);
7672        if !exit_time_accounted {
7673            // Preserve benign cleanup for a retired incarnation without
7674            // acknowledging an unaccounted owner or releasing its staged wakes.
7675            thread_state.drop_robust_list_head_at_exit();
7676        }
7677        if exit_time_accounted
7678            && let Some((_group_time, ready)) = thread_state.take_robust_list_wakes_after_exit(
7679                exit_signal,
7680                thread_state.thread_logical_time.clone(),
7681            )
7682        {
7683            let identities: Vec<_> = ready
7684                .iter()
7685                .map(|(owner, wake)| (*owner, wake.futex))
7686                .collect();
7687            let counts = robust_list_wakes_after_exit(
7688                thread_state.thread_logical_time.clone(),
7689                global_state,
7690                mm_id,
7691                ready,
7692            )
7693            .await;
7694            for ((owner, futex), count) in identities.into_iter().zip(counts) {
7695                info!(
7696                    "[detcore, dtid {}] robust-list owner death woke {} waiter(s) on futex {:?} after physical exit",
7697                    owner, count, futex,
7698                );
7699            }
7700        }
7701        // A SIGKILL sent by a guest thread ended this one without a stop where
7702        // its robust list could be read: its sender recorded the list.
7703        if !transferred_exec {
7704            self.sigkill_robust_list_wakes(global_state, &thread_state, exit_signal)
7705                .await;
7706        }
7707        // Warnings this address space could not send, this hook's own among
7708        // them, go out while the hook's connection still serves it
7709        // (`crate::guest_log`).
7710        guest_log::flush_unsent_through(global_state).await;
7711        let pending_chaos_epochs = thread_state.take_pending_chaos_epochs();
7712        let deregistration = ThreadDeregistration {
7713            dettid,
7714            detpid,
7715            mm: mm_id,
7716            thread_start_entered: thread_state.thread_start_entered,
7717            vfork_origin: thread_state.vfork_origin,
7718            timeslice_stats: thread_state.stats.timeslice_stats,
7719            syscall_count: thread_state.stats.syscall_count,
7720            syscall_histogram: (&thread_state.stats.syscall_histogram).into(),
7721            chaos_epochs: pending_chaos_epochs,
7722            killed_by_group_signal: signal_ends_thread_group(&exit_status),
7723            passed_descriptors_out: thread_state.passed_descriptors_out_unreported,
7724        };
7725        if transferred_exec {
7726            tool_global::retire_exec(
7727                thread_state.thread_logical_time.clone(),
7728                global_state,
7729                deregistration,
7730                exit_status.signal().is_some(),
7731            )
7732            .await?;
7733        } else {
7734            deregister_thread(
7735                thread_state.thread_logical_time.clone(),
7736                &self.cfg,
7737                global_state,
7738                deregistration,
7739            )
7740            .await;
7741        }
7742        // Only once the thread has left the scheduler, which then no longer
7743        // finds it in its process, does its id stop naming a CPU clock; until
7744        // then its clock reads its final time. A leader stays as long as its
7745        // process does (`ProcessCpuTime::threads`).
7746        if current != detpid {
7747            thread_state.forget_thread_cpu_time();
7748        }
7749
7750        self.record_or_replay
7751            .on_exit_thread(
7752                tid,
7753                global_state,
7754                thread_state.record_or_replay,
7755                exit_status,
7756            )
7757            .await?;
7758
7759        // The thread's state is released when this returns, and with it a
7760        // descriptor table it may be the last to hold, where Detcore did not
7761        // let that table go at the exit (`Detcore::release_table_at_exit`,
7762        // docs/RACE_PROBES.md).
7763        #[cfg(debug_assertions)]
7764        crate::race_probe::delay_on_any_backend(
7765            crate::race_probe::EXIT_HOOK_TAIL,
7766            self.cfg.backend.tool_shares_guest_descriptor_table,
7767        )
7768        .await;
7769        Ok(())
7770    }
7771}
7772
7773/// Whether a thread that ended with `status` took its whole thread group with
7774/// it. A thread reported killed by a signal died in a group exit: Linux ends
7775/// every thread of the group when a signal's default action is fatal
7776/// (`get_signal` calls `do_group_exit`, which queues SIGKILL to the others and
7777/// gives each the same status). The one exception is SIGSYS, which seccomp's
7778/// `SECCOMP_RET_KILL_THREAD` uses to end a single thread (`__seccomp_filter`
7779/// calls `do_exit(SIGSYS)`), so a SIGSYS death never makes the scheduler wait
7780/// for the rest of the group (`Scheduler::note_signal_killed_group`). A
7781/// death by a signal nix cannot name (`SignaledRaw`, a real-time signal) is a
7782/// group exit too: the test is on the signal's number, whichever form the
7783/// status takes.
7784fn signal_ends_thread_group(status: &ExitStatus) -> bool {
7785    status
7786        .signal()
7787        .is_some_and(|signal| signal != Signal::SIGSYS as i32)
7788}
7789
7790#[cfg(test)]
7791mod group_signal_tests {
7792    use super::*;
7793
7794    #[test]
7795    fn a_fatal_signal_ends_the_group_except_seccomps_sigsys() {
7796        for signal in [
7797            Signal::SIGKILL,
7798            Signal::SIGSEGV,
7799            Signal::SIGABRT,
7800            Signal::SIGTERM,
7801        ] {
7802            assert!(
7803                signal_ends_thread_group(&ExitStatus::Signaled(signal, true)),
7804                "{signal:?}"
7805            );
7806        }
7807        assert!(!signal_ends_thread_group(&ExitStatus::Signaled(
7808            Signal::SIGSYS,
7809            true
7810        )));
7811        // A real-time signal's default action (terminate) ends the group too.
7812        assert!(signal_ends_thread_group(&ExitStatus::SignaledRaw(
7813            libc::SIGRTMIN() + 1,
7814            false
7815        )));
7816        assert!(!signal_ends_thread_group(&ExitStatus::Exited(0)));
7817        assert!(!signal_ends_thread_group(&ExitStatus::Exited(1)));
7818    }
7819}
7820
7821/// The memory `call` copied when it began, if its handler kept it because
7822/// another thread has rewritten it since (`ThreadState::entry_import`), for a
7823/// reader of the call's arguments after it returns.
7824fn entry_view<T: crate::record_or_replay::RecordOrReplay, G: Guest<Detcore<T>>>(
7825    guest: &G,
7826    call: &Syscall,
7827) -> Option<Box<crate::syscalls::entry_import::EntryImport>> {
7828    let entry = guest.thread_state().entry_import.clone()?;
7829    if entry.call() != *call {
7830        tracing::debug!(
7831            "dropping the entry view of {:?}, which is not {:?}",
7832            entry.call(),
7833            call
7834        );
7835        return None;
7836    }
7837    Some(entry)
7838}
7839
7840/// Restore another thread's rewrite of a call's memory after a reader that
7841/// saw the kernel's view of it (`EntryImport::reinstate_returned`). A failure
7842/// leaves the guest's memory as the call began, so it is logged and ends the
7843/// call.
7844fn restore_entry_view<G: Guest<T>, T: Tool>(
7845    guest: &mut G,
7846    view: Option<(
7847        &crate::syscalls::entry_import::EntryImport,
7848        crate::syscalls::entry_import::Displaced,
7849    )>,
7850) -> Result<(), Error> {
7851    let Some((entry, displaced)) = view else {
7852        return Ok(());
7853    };
7854    entry
7855        .put_back(&mut guest.memory(), displaced, false)
7856        .map_err(|errno| {
7857            tracing::error!(
7858                "Detcore could not restore the guest's rewrite of {:?} after reading the call: {errno}",
7859                entry.call()
7860            );
7861            Error::from(errno)
7862        })
7863}
7864
7865#[cfg(test)]
7866mod subscription_tests {
7867    use super::*;
7868
7869    fn strict_config(passthru_opt: bool) -> Config {
7870        Config {
7871            sequentialize_threads: true,
7872            deterministic_io: true,
7873            passthru_opt,
7874            ..Default::default()
7875        }
7876    }
7877
7878    /// The last row of the pinned table is the one a `Sysno::iter()` sweep
7879    /// drops. `lsm_list_modules` is Determinized AND deterministically refused,
7880    /// so before this was fixed it executed natively against the host under
7881    /// `--passthru-opt` — which is the default for `hermit record` and
7882    /// `hermit replay` — instead of receiving its fixed refusal.
7883    #[test]
7884    fn passthru_opt_covers_the_final_row_of_the_pinned_table() {
7885        let last = Sysno::last();
7886        // Guard the premise: if the table endpoint moves, this test must be
7887        // re-derived rather than silently passing on a different syscall.
7888        assert_eq!(last, Sysno::lsm_list_modules);
7889        assert!(crate::is_determinized_syscall(last));
7890        assert!(crate::is_deterministically_refused_syscall(last));
7891        // The bug this pins: the final row is absent from `Sysno::iter()`.
7892        assert!(!Sysno::iter().any(|sysno| sysno == last));
7893
7894        let subscriptions = <Detcore as Tool>::subscriptions(&strict_config(true));
7895        assert!(
7896            subscriptions.iter_syscalls().any(|sysno| sysno == last),
7897            "{last} must be intercepted under passthru_opt; it is deterministically refused"
7898        );
7899    }
7900
7901    #[test]
7902    fn passthru_opt_intercepts_every_unsupported_syscall() {
7903        let subscriptions = <Detcore as Tool>::subscriptions(&strict_config(true));
7904        let unsupported: Vec<Sysno> = crate::all_pinned_syscalls()
7905            .filter(|sysno| crate::is_unsupported_syscall(*sysno))
7906            .collect();
7907
7908        assert_eq!(unsupported, [Sysno::restart_syscall]);
7909        for syscall in unsupported {
7910            assert!(
7911                subscriptions
7912                    .iter_syscalls()
7913                    .any(|subscribed| subscribed == syscall),
7914                "passthru_opt allowed unsupported {syscall} to bypass Detcore"
7915            );
7916        }
7917    }
7918
7919    /// `passthru_opt` is not a niche flag: `record_or_replay_config` turns it on
7920    /// for every `hermit record` / `hermit replay`, so this covers the record
7921    /// and replay subscription too.
7922    #[test]
7923    fn passthru_opt_subscribes_every_determinized_syscall() {
7924        let determinized: Vec<Sysno> = crate::all_pinned_syscalls()
7925            .filter(|sysno| crate::is_determinized_syscall(*sysno))
7926            .collect();
7927        let subscriptions = <Detcore as Tool>::subscriptions(&strict_config(true));
7928        let delivered: Vec<Sysno> = subscriptions.iter_syscalls().collect();
7929        let missing = determinized
7930            .iter()
7931            .filter(|sysno| !delivered.contains(sysno))
7932            .copied()
7933            .collect::<Vec<_>>();
7934
7935        assert!(
7936            missing.is_empty(),
7937            "passthru_opt let Determinized syscalls bypass Detcore: {}",
7938            missing
7939                .iter()
7940                .map(|sysno| sysno.to_string())
7941                .collect::<Vec<_>>()
7942                .join(" ")
7943        );
7944        assert!(
7945            delivered.contains(&Sysno::syslog),
7946            "syslog must reach its deterministic Detcore handler"
7947        );
7948        for sysno in [
7949            Sysno::access,
7950            Sysno::faccessat,
7951            Sysno::faccessat2,
7952            Sysno::getxattr,
7953            Sysno::lgetxattr,
7954            Sysno::setxattr,
7955            Sysno::lsetxattr,
7956            Sysno::removexattr,
7957            Sysno::lremovexattr,
7958            Sysno::listxattr,
7959            Sysno::llistxattr,
7960        ] {
7961            assert!(
7962                delivered.contains(&sysno),
7963                "{sysno} must reach Detcore, which answers it for the procfs files it renders"
7964            );
7965        }
7966    }
7967
7968    #[test]
7969    fn passthru_opt_intercepts_thread_exit_registrations() {
7970        let subscriptions = <Detcore as Tool>::subscriptions(&strict_config(true));
7971        for syscall in [Sysno::set_tid_address, Sysno::set_robust_list] {
7972            assert!(
7973                subscriptions
7974                    .iter_syscalls()
7975                    .any(|subscribed| subscribed == syscall),
7976                "passthru_opt allowed {syscall} to bypass Detcore's thread-exit state"
7977            );
7978        }
7979    }
7980
7981    #[test]
7982    fn passthru_opt_leaves_unlisted_passthrough_syscalls_unsubscribed() {
7983        assert_eq!(
7984            syscall_classification::classify_syscall(Sysno::chdir),
7985            syscall_classification::SyscallClassification::PassThrough
7986        );
7987
7988        let subscriptions = <Detcore as Tool>::subscriptions(&strict_config(true));
7989        assert!(
7990            !subscriptions
7991                .iter_syscalls()
7992                .any(|sysno| sysno == Sysno::chdir),
7993            "chdir is PassThrough and must remain outside the partial subscription"
7994        );
7995    }
7996
7997    /// Inotify fault recovery decides from pkey_alloc and pkey_mprotect
7998    /// whether the address space has used protection keys
7999    /// (`MemoryMetadata::protection_keys`). Under `--passthru-opt` a
8000    /// PassThrough call is otherwise never seen, and recovery would rewrite
8001    /// bytes a protection key kept Linux from copying. pkey_free is seen as
8002    /// well, so that a backend's refusal of any of the three is named there
8003    /// too (`Detcore::report_backend_refusal`).
8004    #[test]
8005    fn passthru_opt_intercepts_the_protection_key_calls() {
8006        let subscriptions = <Detcore as Tool>::subscriptions(&strict_config(true));
8007        for syscall in [Sysno::pkey_alloc, Sysno::pkey_mprotect, Sysno::pkey_free] {
8008            assert_eq!(
8009                syscall_classification::classify_syscall(syscall),
8010                syscall_classification::SyscallClassification::PassThrough
8011            );
8012            assert!(
8013                subscriptions.iter_syscalls().any(|sysno| sysno == syscall),
8014                "passthru_opt let {syscall} bypass Detcore's protection-key handling"
8015            );
8016        }
8017    }
8018
8019    /// A pkey_mprotect is recorded unless it cannot have applied a key: key
8020    /// -1, which is plain mprotect, however the argument register holds it;
8021    /// ENOSYS, which a backend without protection keys answers without
8022    /// running the call; no length; and a failure whose arguments Linux
8023    /// refuses before it changes any mapping (the ineffective controls).
8024    /// Success, and every failure that can follow a partial change, are
8025    /// recorded (the partially effective controls), as is a success reported
8026    /// for arguments Linux refuses.
8027    #[test]
8028    fn only_a_pkey_mprotect_that_may_apply_a_key_is_recorded() {
8029        let may_apply = pkey_mprotect_may_apply_a_key;
8030        let start = 0x7000_0000_usize;
8031        let rw = (libc::PROT_READ | libc::PROT_WRITE) as usize;
8032        let enosys = Err(Error::Errno(Errno::ENOSYS));
8033        let einval = Err(Error::Errno(Errno::EINVAL));
8034        for minus_one in [u32::MAX as usize, usize::MAX] {
8035            assert!(!may_apply(start, 4096, rw, minus_one, &Ok(0)));
8036            assert!(!may_apply(start, 4096, rw, minus_one, &enosys));
8037        }
8038        assert!(!may_apply(start, 4096, rw, 5, &enosys));
8039        assert!(!may_apply(start, 0, rw, 5, &Ok(0)), "no length");
8040        assert!(
8041            !may_apply(start + 1, 4096, rw, 5, &einval),
8042            "an unaligned start"
8043        );
8044        let both_growths = (libc::PROT_GROWSDOWN | libc::PROT_GROWSUP) as usize;
8045        assert!(!may_apply(start, 4096, rw | both_growths, 5, &einval));
8046        assert!(
8047            !may_apply(start, 4096, rw | 0x10, 5, &einval),
8048            "an unknown prot bit"
8049        );
8050
8051        assert!(may_apply(start, 4096, rw, 5, &Ok(0)));
8052        assert!(
8053            may_apply(start, 4096, rw | 0x8, 5, &Ok(0)),
8054            "PROT_SEM is valid"
8055        );
8056        for errno in [Errno::EINVAL, Errno::ENOMEM, Errno::EACCES] {
8057            assert!(may_apply(start, 2 * 4096, rw, 5, &Err(Error::Errno(errno))));
8058        }
8059        assert!(
8060            may_apply(start + 1, 4096, rw, 5, &Ok(0)),
8061            "a success is believed"
8062        );
8063    }
8064
8065    /// Io-buffer hashing can only hash a syscall Detcore is subscribed to.
8066    /// Under `--passthru-opt` the subscription narrows, so the check's coverage
8067    /// narrows with it -- silently, because a syscall that never reaches
8068    /// Detcore produces no record and no record is indistinguishable from a
8069    /// syscall that moved no bytes.
8070    ///
8071    /// Measured 2026-08-21 on `f05bf04e4f`, one probe calling `getcwd`,
8072    /// `recvmsg`, `readv` and `readlink`:
8073    ///
8074    /// ```text
8075    /// hermit --log=info run                --detlog-io-buffers -- ./probe   12 [iobuf] records
8076    /// hermit --log=info run --passthru-opt --detlog-io-buffers -- ./probe   11 [iobuf] records
8077    /// ```
8078    ///
8079    /// The single lost syscall is `getcwd`, and the reason is structural: 9 of
8080    /// the 20 are in the literal allow-list and 10 more arrive via the
8081    /// unconditional Determinized sweep at the end of `subscriptions`, but
8082    /// `getcwd` is classified `PassThrough`, so neither path picks it up.
8083    ///
8084    /// WHY THIS IS A TEST AND NOT A RUNTIME WARNING. The set is stable, so a
8085    /// warning would print the same sentence on every `--passthru-opt` run
8086    /// forever and be tuned out within a week. The exposure is not today's
8087    /// one-syscall gap; it is that 19 of 20 holds only because two
8088    /// INDEPENDENTLY MAINTAINED lists happen to agree -- the classification
8089    /// table in `syscall_classification.rs` and the match arms in
8090    /// `io_buffers.rs`. Reclassifying any one of those ten from `Determinized`
8091    /// to `PassThrough` would drop it out of the sweep and out of io-buffers'
8092    /// reach with nothing failing. This asserts the relationship so that edit
8093    /// cannot land quietly.
8094    ///
8095    /// It is deliberately an EQUALITY, not a subset check, so it fails in both
8096    /// directions: a nineteenth syscall going missing, and `getcwd` becoming
8097    /// covered while this expectation still claims it is not.
8098    #[test]
8099    fn passthru_opt_leaves_io_buffer_hashing_blind_only_for_getcwd() {
8100        let subscribed: Vec<Sysno> = <Detcore as Tool>::subscriptions(&strict_config(true))
8101            .iter_syscalls()
8102            .collect();
8103        let unreachable: Vec<Sysno> = crate::io_buffers::HASHED_SYSCALLS
8104            .iter()
8105            .copied()
8106            .filter(|sysno| !subscribed.contains(sysno))
8107            .collect();
8108
8109        assert_eq!(
8110            unreachable,
8111            vec![Sysno::getcwd],
8112            "--passthru-opt changes which syscalls io-buffer hashing can reach, and the set \
8113             moved. {} of {} reachable. If a syscall was RECLASSIFIED out of Determinized, that \
8114             silently shrank an enabled determinism check -- re-derive rather than editing this \
8115             expectation to match.",
8116            crate::io_buffers::HASHED_SYSCALLS.len() - unreachable.len(),
8117            crate::io_buffers::HASHED_SYSCALLS.len()
8118        );
8119    }
8120
8121    /// The other direction, and the reason the check above is worth having:
8122    /// with the default subscription every buffer-carrying syscall is
8123    /// reachable, so there is nothing to report on an ordinary run.
8124    #[test]
8125    fn the_default_subscription_reaches_every_io_buffer_syscall() {
8126        let subscriptions = <Detcore as Tool>::subscriptions(&strict_config(false));
8127        let unreachable: Vec<Sysno> = crate::io_buffers::HASHED_SYSCALLS
8128            .iter()
8129            .copied()
8130            .filter(|sysno| !subscriptions.iter_syscalls().any(|s| s == *sysno))
8131            .collect();
8132
8133        assert!(
8134            unreachable.is_empty(),
8135            "without --passthru-opt every io-buffer syscall must be reachable; missing {unreachable:?}"
8136        );
8137    }
8138
8139    #[test]
8140    fn strict_subscriptions_intercept_every_event_by_default() {
8141        let subscriptions = <Detcore as Tool>::subscriptions(&strict_config(false));
8142
8143        assert_eq!(subscriptions, Subscription::all());
8144        assert!(
8145            subscriptions
8146                .iter_syscalls()
8147                .any(|sysno| sysno == Sysno::ppoll)
8148        );
8149
8150        // On a backend without its own CPUID table (ptrace), `--no-virtualize-cpuid`
8151        // must not subscribe to `cpuid` in either mode. The
8152        // subscription is what makes Reverie probe and enable CPUID faulting, and
8153        // a host without faulting then logs an ERROR for each traced exec
8154        // (https://github.com/rrnewton/hermit/issues/3460). Everything else stays
8155        // as intercepted as it is with CPUID virtualization on.
8156        for passthru_opt in [false, true] {
8157            let virtualized = <Detcore as Tool>::subscriptions(&strict_config(passthru_opt));
8158            let config = Config {
8159                virtualize_cpuid: false,
8160                ..strict_config(passthru_opt)
8161            };
8162            let subscriptions = <Detcore as Tool>::subscriptions(&config);
8163
8164            assert!(virtualized.has_cpuid(), "passthru_opt={passthru_opt}");
8165            assert!(!subscriptions.has_cpuid(), "passthru_opt={passthru_opt}");
8166            assert!(subscriptions.has_rdtsc(), "passthru_opt={passthru_opt}");
8167            assert!(
8168                subscriptions
8169                    .iter_syscalls()
8170                    .eq(virtualized.iter_syscalls()),
8171                "passthru_opt={passthru_opt}: the syscall set must not depend on CPUID virtualization"
8172            );
8173        }
8174    }
8175
8176    /// Detcore declares the serialized-execution contract only where it holds:
8177    /// threads sequentialized, and both seccomp-filter routes intercepted and
8178    /// refused.
8179    #[test]
8180    fn serialized_execution_is_declared_only_where_detcore_enforces_it() {
8181        let enforces = <Detcore as Tool>::enforces_serialized_execution;
8182        for passthru_opt in [false, true] {
8183            let config = strict_config(passthru_opt);
8184            let subscriptions = <Detcore as Tool>::subscriptions(&config);
8185            for sysno in [Sysno::seccomp, Sysno::prctl] {
8186                assert!(
8187                    subscriptions.iter_syscalls().any(|s| s == sysno),
8188                    "passthru_opt={passthru_opt}: {sysno} must reach its refusal"
8189                );
8190            }
8191            assert!(enforces(&config), "passthru_opt={passthru_opt}");
8192        }
8193        assert!(!enforces(&Config {
8194            sequentialize_threads: false,
8195            ..strict_config(false)
8196        }));
8197        // The refusals themselves are pinned in `syscalls::misc`'s tests.
8198    }
8199
8200    #[test]
8201    fn passthru_opt_uses_the_partial_subscription_set() {
8202        let subscriptions = <Detcore as Tool>::subscriptions(&strict_config(true));
8203
8204        assert_ne!(subscriptions, Subscription::all());
8205        assert!(
8206            subscriptions
8207                .iter_syscalls()
8208                .any(|sysno| sysno == Sysno::clock_gettime)
8209        );
8210        assert!(
8211            subscriptions
8212                .iter_syscalls()
8213                .any(|sysno| sysno == Sysno::rt_sigsuspend)
8214        );
8215        assert!(
8216            subscriptions
8217                .iter_syscalls()
8218                .any(|sysno| sysno == Sysno::ppoll)
8219        );
8220        assert!(
8221            subscriptions
8222                .iter_syscalls()
8223                .any(|sysno| sysno == Sysno::madvise)
8224        );
8225        assert!(
8226            subscriptions
8227                .iter_syscalls()
8228                .any(|sysno| sysno == Sysno::arch_prctl)
8229        );
8230        assert!(
8231            subscriptions
8232                .iter_syscalls()
8233                .any(|sysno| sysno == Sysno::writev)
8234        );
8235        for sysno in [
8236            Sysno::read,
8237            Sysno::write,
8238            Sysno::pread64,
8239            Sysno::pwrite64,
8240            Sysno::readv,
8241            Sysno::writev,
8242            Sysno::preadv,
8243            Sysno::preadv2,
8244            Sysno::pwritev,
8245            Sysno::pwritev2,
8246            Sysno::prctl,
8247        ] {
8248            assert!(
8249                subscriptions
8250                    .iter_syscalls()
8251                    .any(|subscribed| subscribed == sysno),
8252                "timer-slack mediation requires {sysno:?}"
8253            );
8254        }
8255        assert!(
8256            subscriptions
8257                .iter_syscalls()
8258                .any(|sysno| sysno == Sysno::pwrite64)
8259        );
8260        assert!(
8261            subscriptions
8262                .iter_syscalls()
8263                .any(|sysno| sysno == Sysno::pidfd_open)
8264        );
8265    }
8266}
8267
8268#[cfg(test)]
8269mod rcb_overshoot_tests {
8270    use std::fmt::Write;
8271    use std::sync::Arc;
8272    use std::sync::Mutex;
8273
8274    use tracing::Event;
8275    use tracing::Id;
8276    use tracing::Level;
8277    use tracing::Metadata;
8278    use tracing::Subscriber;
8279    use tracing::field::Field;
8280    use tracing::field::Visit;
8281    use tracing::span::Attributes;
8282    use tracing::span::Record;
8283    use tracing::subscriber::with_default;
8284
8285    use super::rcb_timer_overshot;
8286    use super::report_rcb_overshoot;
8287
8288    struct ErrorSubscriber(Arc<Mutex<Option<String>>>);
8289
8290    struct EventVisitor(String);
8291
8292    impl Visit for EventVisitor {
8293        fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
8294            let _ = write!(self.0, "{}={:?}", field.name(), value);
8295        }
8296    }
8297
8298    impl Subscriber for ErrorSubscriber {
8299        fn enabled(&self, metadata: &Metadata<'_>) -> bool {
8300            *metadata.level() == Level::ERROR
8301        }
8302
8303        fn new_span(&self, _span: &Attributes<'_>) -> Id {
8304            Id::from_u64(1)
8305        }
8306
8307        fn record(&self, _span: &Id, _values: &Record<'_>) {}
8308
8309        fn record_follows_from(&self, _span: &Id, _follows: &Id) {}
8310
8311        fn event(&self, event: &Event<'_>) {
8312            if *event.metadata().level() == Level::ERROR {
8313                let mut visitor = EventVisitor(String::new());
8314                event.record(&mut visitor);
8315                *self.0.lock().unwrap() = Some(visitor.0);
8316            }
8317        }
8318
8319        fn enter(&self, _span: &Id) {}
8320
8321        fn exit(&self, _span: &Id) {}
8322    }
8323
8324    #[test]
8325    fn default_overshoot_policy_emits_error_and_returns() {
8326        let _ = reverie::take_skid_overshoot_count();
8327        let error = Arc::new(Mutex::new(None));
8328        with_default(ErrorSubscriber(error.clone()), || {
8329            report_rcb_overshoot(false, 16_249, 139, 100);
8330        });
8331
8332        let error = error.lock().unwrap().take().expect("missing ERROR event");
8333        assert!(error.contains(reverie::SKID_OVERSHOOT_MARKER), "{error}");
8334        assert!(error.contains("PMU RCB overshoot"), "{error}");
8335        assert!(error.contains("16249"), "{error}");
8336        assert!(error.contains("139"), "{error}");
8337        assert!(error.contains("100"), "{error}");
8338        assert_eq!(
8339            reverie::take_skid_overshoot_count(),
8340            1,
8341            "the log-and-continue path must feed the supervisor's structural count"
8342        );
8343    }
8344
8345    #[test]
8346    fn exact_rcb_timer_hit_is_not_an_overshoot() {
8347        assert!(!rcb_timer_overshot(100, 100));
8348        assert!(!rcb_timer_overshot(99, 100));
8349        assert!(rcb_timer_overshot(101, 100));
8350    }
8351
8352    #[test]
8353    #[should_panic(expected = "PMU RCB overshoot")]
8354    fn opt_in_overshoot_policy_panics() {
8355        report_rcb_overshoot(true, 16_249, 139, 100);
8356    }
8357}
8358
8359#[cfg(test)]
8360mod timeslice_timer_tests {
8361    use super::*;
8362
8363    /// A dropped signal's stop re-requests the timer event for the point the
8364    /// last handler armed, whenever the host-timed stop came: before it, the
8365    /// remaining branches; at or past it, at once
8366    /// (https://github.com/rrnewton/hermit/issues/4304).
8367    #[test]
8368    fn a_kept_timer_aims_at_the_armed_point() {
8369        assert_eq!(kept_timer_rcbs(1_000, 500, 1_000), 500);
8370        assert_eq!(kept_timer_rcbs(1_000, 500, 1_200), 300);
8371        assert_eq!(kept_timer_rcbs(1_000, 500, 1_499), 1);
8372        assert_eq!(kept_timer_rcbs(1_000, 500, 1_500), 1);
8373        assert_eq!(kept_timer_rcbs(1_000, 500, 40_000_000_000), 1);
8374        assert_eq!(kept_timer_rcbs(u64::MAX - 10, 500, u64::MAX - 5), 5);
8375    }
8376
8377    #[test]
8378    fn manual_interrupts_can_shorten_but_not_extend_maximum() {
8379        assert_eq!(choose_rcb_timer(100, 100, Some(150)), (50, false));
8380        assert_eq!(choose_rcb_timer(100, 100, Some(250)), (100, true));
8381        assert_eq!(choose_rcb_timer(100, 100, None), (100, true));
8382    }
8383
8384    #[test]
8385    fn pmu_duration_conversion_applies_clock_multiplier() {
8386        let duration = crate::types::LogicalTime::from_nanos(100);
8387        assert_eq!(duration.into_rcbs_with_multiplier(2.0), 5);
8388        assert_eq!(duration.into_rcbs_with_multiplier(0.5), 20);
8389        assert_eq!(
8390            crate::types::LogicalTime::from_nanos(101).into_rcbs_with_multiplier(2.0),
8391            5
8392        );
8393    }
8394
8395    #[test]
8396    #[should_panic(expected = "max_timeslice must be at least one RCB")]
8397    fn detcore_constructor_validates_programmatic_config() {
8398        let config = Config {
8399            max_timeslice: std::num::NonZeroU64::new(1),
8400            ..Default::default()
8401        };
8402
8403        let _ = <Detcore as Tool>::new(Pid::from_raw(1), &config);
8404    }
8405}
8406
8407#[cfg(test)]
8408mod process_tree_guest_clock_tests {
8409    use super::*;
8410
8411    #[test]
8412    fn forked_process_shares_guest_clock_domain() {
8413        let config = Config::default();
8414        let tool = <Detcore as Tool>::new(Pid::from_raw(1), &config);
8415        let mut parent = ThreadState::new(DetPid::from_raw(1), &config, ());
8416        parent.clone_flags = Some(CloneFlags::empty());
8417
8418        let child = <Detcore as Tool>::init_thread_state(
8419            &tool,
8420            Tid::from_raw(2),
8421            Some((Tid::from_raw(1), &parent)),
8422        );
8423
8424        assert!(Arc::ptr_eq(&parent.guest_clock, &child.guest_clock));
8425    }
8426}
8427
8428#[cfg(test)]
8429mod process_files_tests {
8430    use super::*;
8431
8432    fn child(
8433        tool: &Detcore,
8434        parent: &mut ThreadState<()>,
8435        tid: i32,
8436        clone_flags: CloneFlags,
8437    ) -> ThreadState<()> {
8438        parent.clone_flags = Some(clone_flags);
8439        let mut child = <Detcore as Tool>::init_thread_state(
8440            tool,
8441            Tid::from_raw(tid),
8442            Some((Tid::from_raw(parent.dettid.as_raw()), parent)),
8443        );
8444        child.detpid = Some(if clone_flags.contains(CloneFlags::CLONE_THREAD) {
8445            parent.detpid.unwrap()
8446        } else {
8447            DetPid::from_raw(tid)
8448        });
8449        child
8450    }
8451
8452    /// The table a thread group's leader holds, which procfs reads for the
8453    /// process's descriptor links, is known to every thread of the group,
8454    /// through every way a table is made: the root's, a thread's shared or
8455    /// own, a forked process's, a process made with `CLONE_FILES` (whose
8456    /// table was made for another process) and its threads, and the table an
8457    /// exec gives a thread that did not lead its process.
8458    #[test]
8459    fn every_thread_knows_the_table_its_process_holds() {
8460        let config = Config::default();
8461        let tool = <Detcore as Tool>::new(Pid::from_raw(1), &config);
8462        let thread = CloneFlags::CLONE_VM
8463            | CloneFlags::CLONE_FS
8464            | CloneFlags::CLONE_FILES
8465            | CloneFlags::CLONE_SIGHAND
8466            | CloneFlags::CLONE_THREAD;
8467        let mut root = ThreadState::new(DetPid::from_raw(3), &config, ());
8468        root.detpid = Some(DetPid::from_raw(3));
8469        assert!(root.holds_process_files());
8470
8471        // A thread that shares the leader's table holds it; one cloned
8472        // without CLONE_FILES holds its own.
8473        let shared = child(&tool, &mut root, 4, thread);
8474        assert_eq!(shared.process_files, root.process_files);
8475        assert!(shared.holds_process_files());
8476        let split = child(&tool, &mut root, 5, thread - CloneFlags::CLONE_FILES);
8477        assert_eq!(split.process_files, root.process_files);
8478        assert!(!split.holds_process_files());
8479
8480        // A forked process holds its copy; its creator's group is unchanged.
8481        let mut forked = child(&tool, &mut root, 6, CloneFlags::empty());
8482        assert_ne!(forked.process_files, root.process_files);
8483        assert!(forked.holds_process_files());
8484        assert_eq!(root.process_files, Some(root.files_id()));
8485
8486        // A process made with CLONE_FILES shares its creator's table, made
8487        // for that creator, and leads a group of its own: its thread holds
8488        // the process's table although the table's creator is neither the
8489        // thread nor its leader.
8490        let mut shares_files = child(&tool, &mut forked, 7, CloneFlags::CLONE_FILES);
8491        assert_eq!(shares_files.files_id(), forked.files_id());
8492        assert_eq!(shares_files.process_files, Some(forked.files_id()));
8493        let its_thread = child(&tool, &mut shares_files, 8, thread);
8494        assert_ne!(its_thread.files_id().creator(), DetPid::from_raw(7));
8495        assert!(its_thread.holds_process_files());
8496
8497        // A thread that did not lead its process execs, as handle_execveat
8498        // gives it its new table: the new table is the process's, and a
8499        // thread the new image creates holds it. A failed exec restores
8500        // both.
8501        let mut worker = child(&tool, &mut root, 9, thread);
8502        let old_metadata = Arc::clone(&worker.file_metadata);
8503        let old_process_files = worker.process_files;
8504        let new_metadata = worker.file_metadata.lock().unwrap().for_exec(worker.dettid);
8505        worker.file_metadata = Arc::new(Mutex::new(new_metadata));
8506        worker.adopt_process_files();
8507        assert_eq!(worker.files_id().creator(), DetPid::from_raw(9));
8508        assert!(worker.holds_process_files());
8509        assert_ne!(worker.process_files, root.process_files);
8510        assert_eq!(root.process_files, Some(root.files_id()));
8511        let after_exec = child(&tool, &mut worker, 10, thread);
8512        assert!(after_exec.holds_process_files());
8513        worker.file_metadata = old_metadata;
8514        worker.process_files = old_process_files;
8515        assert_eq!(worker.process_files, root.process_files);
8516        assert!(worker.holds_process_files());
8517    }
8518}
8519
8520#[cfg(test)]
8521mod child_rng_identity_tests {
8522    use super::*;
8523
8524    fn child_state(
8525        tool: &Detcore,
8526        parent: &mut ThreadState<()>,
8527        host_tid: i32,
8528        clone_flags: CloneFlags,
8529    ) -> ThreadState<()> {
8530        parent.clone_flags = Some(clone_flags);
8531        <Detcore as Tool>::init_thread_state(
8532            tool,
8533            Tid::from_raw(host_tid),
8534            Some((Tid::from_raw(parent.dettid.as_raw()), parent)),
8535        )
8536    }
8537
8538    fn rng_sample(child: &ThreadState<()>) -> [u64; 4] {
8539        let mut rng = child.prng.clone();
8540        std::array::from_fn(|_| rng.random())
8541    }
8542
8543    fn chaos_rng_sample(child: &ThreadState<()>) -> [u64; 4] {
8544        let mut rng = child.chaos_prng.clone();
8545        std::array::from_fn(|_| rng.random())
8546    }
8547
8548    #[test]
8549    fn common_child_rng_identity_ignores_backend_tid_for_every_clone_shape() {
8550        let config = Config::default();
8551        let tool = <Detcore as Tool>::new(Pid::from_raw(1), &config);
8552        for (label, clone_flags) in [
8553            ("fork", CloneFlags::empty()),
8554            ("vfork", CloneFlags::CLONE_VM | CloneFlags::CLONE_VFORK),
8555            ("clone-process", CloneFlags::CLONE_VM),
8556            (
8557                "clone-thread",
8558                CloneFlags::CLONE_VM | CloneFlags::CLONE_THREAD,
8559            ),
8560        ] {
8561            let mut low_tid_parent = ThreadState::new(DetPid::from_raw(1), &config, ());
8562            let mut high_tid_parent = ThreadState::new(DetPid::from_raw(1), &config, ());
8563            let low_tid_child = child_state(&tool, &mut low_tid_parent, 2, clone_flags);
8564            let high_tid_child = child_state(&tool, &mut high_tid_parent, 42_002, clone_flags);
8565
8566            assert_eq!(format!("{}", low_tid_child.pedigree), "C", "{label}");
8567            assert_eq!(low_tid_child.dettid.as_raw(), 2, "{label}");
8568            assert_eq!(high_tid_child.dettid.as_raw(), 42_002, "{label}");
8569            assert_eq!(
8570                rng_sample(&low_tid_child),
8571                rng_sample(&high_tid_child),
8572                "{label} child RNG depended on the backend Tid"
8573            );
8574            assert_eq!(
8575                chaos_rng_sample(&low_tid_child),
8576                chaos_rng_sample(&high_tid_child),
8577                "{label} child chaos RNG depended on the backend Tid"
8578            );
8579            assert_ne!(
8580                rng_sample(&low_tid_child),
8581                chaos_rng_sample(&low_tid_child),
8582                "{label} child guest and chaos RNG streams were coupled"
8583            );
8584        }
8585    }
8586
8587    #[test]
8588    fn serialized_siblings_receive_distinct_child_rng_streams() {
8589        let config = Config::default();
8590        let tool = <Detcore as Tool>::new(Pid::from_raw(1), &config);
8591        let mut parent = ThreadState::new(DetPid::from_raw(1), &config, ());
8592
8593        let first = child_state(&tool, &mut parent, 2, CloneFlags::empty());
8594        let first_pedigree = parent.pedigree.fork_mut();
8595        assert_eq!(format!("{}", first.pedigree), format!("{first_pedigree}"));
8596        let second = child_state(&tool, &mut parent, 3, CloneFlags::empty());
8597
8598        assert_eq!(format!("{}", second.pedigree), "PC");
8599        assert_ne!(rng_sample(&first), rng_sample(&second));
8600    }
8601}
8602
8603#[cfg(test)]
8604mod thread_exit_identity_tests {
8605    use super::*;
8606
8607    #[test]
8608    fn initialized_thread_exit_identity_is_preserved() {
8609        let thread_detpid = DetPid::from_raw(41);
8610        let process_detpid = DetPid::from_raw(7);
8611        assert_eq!(
8612            select_thread_exit_detpid(Some(thread_detpid), process_detpid),
8613            (thread_detpid, false)
8614        );
8615    }
8616
8617    #[test]
8618    fn missing_thread_exit_identity_uses_process_identity() {
8619        let process_detpid = DetPid::from_raw(7);
8620        assert_eq!(
8621            select_thread_exit_detpid(None, process_detpid),
8622            (process_detpid, true)
8623        );
8624    }
8625}
8626
8627#[cfg(test)]
8628mod thread_start_identity_tests {
8629    use super::*;
8630
8631    #[test]
8632    fn backend_process_identity_is_preserved() {
8633        let backend_detpid = DetPid::from_raw(3);
8634        assert_eq!(
8635            select_thread_start_detpid(Some(backend_detpid), Pid::from_raw(42_001)),
8636            backend_detpid
8637        );
8638        assert_eq!(
8639            select_thread_start_detpid(None, Pid::from_raw(42_001)),
8640            DetPid::from_raw(42_001)
8641        );
8642    }
8643
8644    #[test]
8645    fn root_thread_uses_deterministic_process_identity() {
8646        let detpid = DetPid::from_raw(3);
8647        for past_first_execve in [false, true] {
8648            assert!(thread_start_needs_registration(
8649                true,
8650                past_first_execve,
8651                DetTid::from_raw(3),
8652                detpid
8653            ));
8654            for is_root_process in [false, true] {
8655                assert!(!thread_start_needs_registration(
8656                    is_root_process,
8657                    past_first_execve,
8658                    DetTid::from_raw(4),
8659                    detpid
8660                ));
8661            }
8662        }
8663        assert!(thread_start_needs_registration(
8664            false,
8665            false,
8666            DetTid::from_raw(3),
8667            detpid
8668        ));
8669        assert!(!thread_start_needs_registration(
8670            false,
8671            true,
8672            DetTid::from_raw(3),
8673            detpid
8674        ));
8675    }
8676}
8677
8678#[cfg(test)]
8679mod thread_cpu_time_tests {
8680    use super::*;
8681
8682    #[test]
8683    fn cloned_threads_and_processes_keep_absolute_time_without_inheriting_work() {
8684        for clone_flags in [
8685            CloneFlags::CLONE_THREAD,
8686            CloneFlags::empty(),
8687            CloneFlags::CLONE_VFORK | CloneFlags::CLONE_VM,
8688        ] {
8689            let config = Config::default();
8690            let tool = <Detcore as Tool>::new(Pid::from_raw(1), &config);
8691            let mut parent = ThreadState::new(DetPid::from_raw(1), &config, ());
8692            parent.thread_logical_time.add_rcbs(7);
8693            parent.thread_logical_time.add_syscall_with_cost(101);
8694            parent.clone_flags = Some(clone_flags);
8695            let child = <Detcore as Tool>::init_thread_state(
8696                &tool,
8697                Tid::from_raw(2),
8698                Some((Tid::from_raw(1), &parent)),
8699            );
8700            assert_eq!(
8701                child.thread_logical_time.as_nanos(),
8702                parent.thread_logical_time.as_nanos()
8703            );
8704            assert_eq!(
8705                child.thread_logical_time.inherited_nanos(),
8706                LogicalTime::from_nanos(171)
8707            );
8708            let mut global = GlobalTime::new(&config);
8709            let epoch = global.as_nanos();
8710            for thread in [&parent, &child] {
8711                global.update_global_time(
8712                    thread.dettid,
8713                    thread.thread_logical_time.as_nanos(),
8714                    thread.thread_logical_time.inherited_nanos(),
8715                );
8716            }
8717            assert_eq!(global.as_nanos(), epoch + LogicalTime::from_nanos(171));
8718        }
8719    }
8720
8721    #[test]
8722    fn cloned_thread_and_fork_child_start_with_zero_thread_cpu() {
8723        for clone_flags in [CloneFlags::CLONE_THREAD, CloneFlags::empty()] {
8724            let config = Config::default();
8725            let tool = <Detcore as Tool>::new(Pid::from_raw(1), &config);
8726            let mut parent = ThreadState::new(DetPid::from_raw(1), &config, ());
8727
8728            // Give the parent nonzero CPU before the child exists. The child's
8729            // absolute logical clock inherits this position for scheduler
8730            // ordering, but Linux per-thread CPU accounting must not.
8731            parent.thread_logical_time.add_rcbs(200);
8732            parent.thread_logical_time.add_syscall();
8733            parent.clone_flags = Some(clone_flags);
8734
8735            let mut child = <Detcore as Tool>::init_thread_state(
8736                &tool,
8737                Tid::from_raw(2),
8738                Some((Tid::from_raw(1), &parent)),
8739            );
8740            assert_eq!(
8741                child.thread_cpu_time(),
8742                (LogicalTime::ZERO, LogicalTime::ZERO),
8743                "clone flags {clone_flags:?} inherited pre-creation CPU"
8744            );
8745
8746            child.thread_logical_time.add_rcbs(4);
8747            child.thread_logical_time.add_syscall();
8748            let (user, system) = child.thread_cpu_time();
8749            assert!(user > LogicalTime::ZERO);
8750            assert!(system > LogicalTime::ZERO);
8751            assert!(user < parent.thread_logical_time.user_cpu_time());
8752            assert!(system <= parent.thread_logical_time.system_cpu_time());
8753        }
8754    }
8755
8756    /// Another thread's CPU clock reads its process's per-thread table: a
8757    /// thread's own time as of its last accounting, zero before it has run,
8758    /// and nothing once it has exited, while its time stays in the process
8759    /// total. A fork child keeps a table of its own.
8760    #[test]
8761    fn the_per_thread_table_follows_each_threads_accounting() {
8762        let config = Config::default();
8763        let tool = <Detcore as Tool>::new(Pid::from_raw(1), &config);
8764        let mut parent = ThreadState::new(DetPid::from_raw(1), &config, ());
8765        parent.thread_logical_time.add_rcbs(30);
8766        parent.thread_logical_time.add_syscall_with_cost(700);
8767        let zero = (LogicalTime::ZERO, LogicalTime::ZERO);
8768        let new_child = |parent: &mut ThreadState<()>, tid: i32, flags: CloneFlags| {
8769            parent.clone_flags = Some(flags);
8770            let child = <Detcore as Tool>::init_thread_state(
8771                &tool,
8772                Tid::from_raw(tid),
8773                Some((Tid::from_raw(1), &*parent)),
8774            );
8775            parent.clone_flags = None;
8776            child
8777        };
8778        let mut thread = new_child(&mut parent, 2, CloneFlags::CLONE_THREAD);
8779        let mut fork = new_child(&mut parent, 3, CloneFlags::empty());
8780        let thread_id = DetPid::from_raw(2);
8781        let fork_id = DetPid::from_raw(3);
8782        assert_eq!(parent.sibling_cpu_time(thread_id), zero);
8783
8784        thread.thread_logical_time.add_rcbs(5);
8785        thread.thread_logical_time.add_syscall_with_cost(90);
8786        thread.account_process_cpu_time();
8787        let accounted = thread.thread_cpu_time();
8788        assert!(accounted.0 > LogicalTime::ZERO && accounted.1 > LogicalTime::ZERO);
8789        assert_eq!(parent.sibling_cpu_time(thread_id), accounted);
8790        assert_eq!(thread.sibling_cpu_time(DetPid::from_raw(1)), zero);
8791
8792        // Work not yet accounted is not seen.
8793        thread.thread_logical_time.add_rcbs(1_000);
8794        assert_eq!(parent.sibling_cpu_time(thread_id), accounted);
8795
8796        fork.thread_logical_time.add_syscall_with_cost(4_000);
8797        fork.account_process_cpu_time();
8798        assert_eq!(parent.sibling_cpu_time(fork_id), zero);
8799
8800        let own = parent.thread_cpu_time();
8801        let total = parent.process_cpu_time();
8802        assert_eq!(total.user, own.0 + accounted.0);
8803        assert_eq!(total.system, own.1 + accounted.1);
8804        assert_eq!(parent.sibling_cpu_time(DetPid::from_raw(1)), own);
8805
8806        thread.account_process_cpu_time();
8807        thread.forget_thread_cpu_time();
8808        let exited = thread.thread_cpu_time();
8809        assert_eq!(parent.sibling_cpu_time(thread_id), zero);
8810        let total = parent.process_cpu_time();
8811        assert_eq!(total.user, own.0 + exited.0);
8812        assert_eq!(total.system, own.1 + exited.1);
8813    }
8814}
8815
8816/// Regression tests for <https://github.com/rrnewton/hermit/issues/3153>: a
8817/// failed syscall must not render an output buffer the kernel never wrote.
8818///
8819/// Each buffer is pre-filled with sentinel values standing in for the
8820/// uninitialized guest stack the issue observed, and the rendering is checked
8821/// through the same `finish syscall` format the DETLOG line uses.
8822#[cfg(test)]
8823mod finished_syscall_display_tests {
8824    use std::ffi::CString;
8825
8826    use reverie::syscalls::AddrMut;
8827    use reverie::syscalls::AtFlags;
8828    use reverie::syscalls::ClockGettime;
8829    use reverie::syscalls::ClockId;
8830    use reverie::syscalls::Gettimeofday;
8831    use reverie::syscalls::LocalMemory;
8832    use reverie::syscalls::Newfstatat;
8833    use reverie::syscalls::PathPtr;
8834    use reverie::syscalls::StatPtr;
8835    use reverie::syscalls::Statx;
8836    use reverie::syscalls::StatxMask;
8837    use reverie::syscalls::StatxPtr;
8838    use reverie::syscalls::Timespec;
8839    use reverie::syscalls::TimespecMutPtr;
8840    use reverie::syscalls::Timeval;
8841    use reverie::syscalls::TimevalMutPtr;
8842
8843    use super::*;
8844
8845    /// A value no real `st_size` or timestamp in these tests can take.
8846    const SENTINEL: i64 = 0x5EED_0BAD_F00D;
8847
8848    /// Render exactly what the `finish syscall` DETLOG line renders.
8849    fn finish_line(syscall: &Syscall, result: Result<i64, Error>) -> String {
8850        let memory = LocalMemory::new();
8851        format!(
8852            "{} = {:?}",
8853            display_syscall_finished(syscall, &memory, &result),
8854            result
8855        )
8856    }
8857
8858    fn sentinel_stat() -> libc::stat {
8859        // SAFETY: all-zero is a valid `struct stat`.
8860        let mut stat: libc::stat = unsafe { std::mem::zeroed() };
8861        stat.st_mode = libc::S_IFREG | 0o644;
8862        stat.st_size = SENTINEL;
8863        stat
8864    }
8865
8866    fn sentinel_statx() -> reverie::syscalls::linux_abi::Statx {
8867        // SAFETY: all-zero is a valid `struct statx`.
8868        let mut statx: reverie::syscalls::linux_abi::Statx = unsafe { std::mem::zeroed() };
8869        statx.stx_mode = (libc::S_IFREG | 0o644) as u16;
8870        statx.stx_size = SENTINEL as u64;
8871        statx
8872    }
8873
8874    fn newfstatat(path: &CString, stat: &libc::stat) -> Syscall {
8875        Syscall::Newfstatat(
8876            Newfstatat::new()
8877                .with_dirfd(libc::AT_FDCWD)
8878                .with_path(PathPtr::from_ptr(path.as_ptr()))
8879                .with_stat(StatPtr::from_ptr(stat as *const libc::stat))
8880                .with_flags(AtFlags::empty()),
8881        )
8882    }
8883
8884    fn statx(path: &CString, statx: &reverie::syscalls::linux_abi::Statx) -> Syscall {
8885        Syscall::Statx(
8886            Statx::new()
8887                .with_dirfd(libc::AT_FDCWD)
8888                .with_path(PathPtr::from_ptr(path.as_ptr()))
8889                .with_flags(AtFlags::empty())
8890                .with_mask(StatxMask::STATX_BASIC_STATS)
8891                .with_statx(StatxPtr::from_ptr(
8892                    statx as *const reverie::syscalls::linux_abi::Statx,
8893                )),
8894        )
8895    }
8896
8897    fn assert_no_struct_rendered(line: &str) {
8898        assert!(!line.contains("st_mode"), "rendered st_mode: {line}");
8899        assert!(!line.contains("st_size"), "rendered st_size: {line}");
8900        assert!(
8901            !line.contains(&SENTINEL.to_string()),
8902            "rendered the unwritten sentinel: {line}"
8903        );
8904    }
8905
8906    #[test]
8907    fn failed_newfstatat_renders_pointer_and_errno_but_not_the_buffer() {
8908        let path = CString::new("/nonexistent/issue-3153").unwrap();
8909        let stat = sentinel_stat();
8910        let line = finish_line(&newfstatat(&path, &stat), Err(Errno::ENOENT.into()));
8911
8912        assert_no_struct_rendered(&line);
8913        assert!(
8914            line.contains(&format!("{:p}", &stat as *const libc::stat)),
8915            "stat pointer missing: {line}"
8916        );
8917        assert!(
8918            line.contains("\"/nonexistent/issue-3153\""),
8919            "path missing: {line}"
8920        );
8921        assert!(line.contains("ENOENT"), "errno missing: {line}");
8922    }
8923
8924    #[test]
8925    fn successful_newfstatat_still_renders_the_buffer() {
8926        let path = CString::new("/dev/null").unwrap();
8927        let stat = sentinel_stat();
8928        let line = finish_line(&newfstatat(&path, &stat), Ok(0));
8929
8930        assert!(
8931            line.contains(&format!(
8932                "{:p} -> {{st_mode=SFlag(S_IFREG) | 0644, st_size={}, ...}}",
8933                &stat as *const libc::stat, SENTINEL
8934            )),
8935            "stat struct missing: {line}"
8936        );
8937    }
8938
8939    #[test]
8940    fn failed_statx_renders_pointer_and_errno_but_not_the_buffer() {
8941        let path = CString::new("/nonexistent/issue-3153").unwrap();
8942        let buf = sentinel_statx();
8943        let line = finish_line(&statx(&path, &buf), Err(Errno::ENOENT.into()));
8944
8945        assert_no_struct_rendered(&line);
8946        assert!(
8947            line.contains(&format!(
8948                "{:p}",
8949                &buf as *const reverie::syscalls::linux_abi::Statx
8950            )),
8951            "statx pointer missing: {line}"
8952        );
8953        assert!(line.contains("ENOENT"), "errno missing: {line}");
8954    }
8955
8956    #[test]
8957    fn successful_statx_still_renders_the_buffer() {
8958        let path = CString::new("/dev/null").unwrap();
8959        let buf = sentinel_statx();
8960        let line = finish_line(&statx(&path, &buf), Ok(0));
8961
8962        assert!(
8963            line.contains(&format!(
8964                "{:p} -> {{st_mode=SFlag(S_IFREG) | 0644, st_size={}, ...}}",
8965                &buf as *const reverie::syscalls::linux_abi::Statx, SENTINEL
8966            )),
8967            "statx struct missing: {line}"
8968        );
8969    }
8970
8971    #[test]
8972    fn failed_clock_gettime_does_not_render_the_timespec() {
8973        let tp = Timespec {
8974            tv_sec: SENTINEL,
8975            tv_nsec: 0,
8976        };
8977        let call = Syscall::ClockGettime(
8978            ClockGettime::new()
8979                .with_clockid(ClockId::CLOCK_MONOTONIC)
8980                .with_tp(Some(TimespecMutPtr(
8981                    AddrMut::from_ptr(&tp as *const Timespec).unwrap(),
8982                ))),
8983        );
8984
8985        let failed = finish_line(&call, Err(Errno::EINVAL.into()));
8986        assert!(!failed.contains("tv_sec"), "rendered tv_sec: {failed}");
8987        assert!(
8988            failed.contains(&format!("{:p}", &tp as *const Timespec)),
8989            "tp pointer missing: {failed}"
8990        );
8991        assert!(failed.contains("EINVAL"), "errno missing: {failed}");
8992
8993        let succeeded = finish_line(&call, Ok(0));
8994        assert!(
8995            succeeded.contains(&format!("tv_sec: {SENTINEL}")),
8996            "timespec missing on success: {succeeded}"
8997        );
8998    }
8999
9000    /// EFAULT is also the error of a copy-out that faulted part way, after
9001    /// `copy_to_user()` stored a prefix the guest can read, so it must keep
9002    /// the buffer rendered for every allowlisted syscall.
9003    #[test]
9004    fn efault_failures_still_render_a_possible_partial_copy() {
9005        let path = CString::new("/dev/null").unwrap();
9006        let stat = sentinel_stat();
9007        let line = finish_line(&newfstatat(&path, &stat), Err(Errno::EFAULT.into()));
9008        assert!(
9009            line.contains(&format!(
9010                "{:p} -> {{st_mode=SFlag(S_IFREG) | 0644, st_size={}, ...}}",
9011                &stat as *const libc::stat, SENTINEL
9012            )),
9013            "partial stat copy hidden on EFAULT: {line}"
9014        );
9015        assert!(line.contains("EFAULT"), "errno missing: {line}");
9016
9017        let buf = sentinel_statx();
9018        let line = finish_line(&statx(&path, &buf), Err(Errno::EFAULT.into()));
9019        assert!(
9020            line.contains(&format!(
9021                "{:p} -> {{st_mode=SFlag(S_IFREG) | 0644, st_size={}, ...}}",
9022                &buf as *const reverie::syscalls::linux_abi::Statx, SENTINEL
9023            )),
9024            "partial statx copy hidden on EFAULT: {line}"
9025        );
9026
9027        let tp = Timespec {
9028            tv_sec: SENTINEL,
9029            tv_nsec: 0,
9030        };
9031        let call = Syscall::ClockGettime(
9032            ClockGettime::new()
9033                .with_clockid(ClockId::CLOCK_MONOTONIC)
9034                .with_tp(Some(TimespecMutPtr(
9035                    AddrMut::from_ptr(&tp as *const Timespec).unwrap(),
9036                ))),
9037        );
9038        let line = finish_line(&call, Err(Errno::EFAULT.into()));
9039        assert!(
9040            line.contains(&format!("tv_sec: {SENTINEL}")),
9041            "partial timespec copy hidden on EFAULT: {line}"
9042        );
9043    }
9044
9045    /// A posthook anchor fires on a value or a final guest errno, `EINTR`
9046    /// included; an internal restart code is refused; a tool error and an
9047    /// entry with no posthook anchor make no request
9048    /// (https://github.com/rrnewton/hermit/issues/3929).
9049    #[test]
9050    fn a_posthook_checkpoint_fires_only_on_a_final_result() {
9051        let anchors = || vec!["p".to_owned()];
9052        let fires = |result: Result<i64, Error>| {
9053            matches!(
9054                posthook_checkpoint(9, anchors(), &result),
9055                Some(ResourceID::HappensBeforeSyscallCheckpoint { count: 9, anchors })
9056                    if anchors == ["p"]
9057            )
9058        };
9059        assert!(fires(Ok(0)));
9060        assert!(fires(Ok(6)));
9061        for errno in [Errno::EINTR, Errno::EAGAIN, Errno::EBADF, Errno::EFAULT] {
9062            assert!(fires(Err(errno.into())), "{errno}");
9063        }
9064        for errno in [
9065            Errno::ERESTARTSYS,
9066            Errno::ERESTARTNOINTR,
9067            Errno::ERESTARTNOHAND,
9068            Errno::ERESTART_RESTARTBLOCK,
9069        ] {
9070            match posthook_checkpoint(9, anchors(), &Err(errno.into())) {
9071                Some(ResourceID::HappensBeforePosthookInterrupted {
9072                    count: 9,
9073                    anchors,
9074                    code,
9075                }) => {
9076                    assert_eq!(anchors, ["p"]);
9077                    assert_eq!(Some(code.as_str()), errno.name());
9078                }
9079                other => panic!("{errno}: {other:?}"),
9080            }
9081        }
9082        assert!(
9083            posthook_checkpoint(9, anchors(), &Err(Error::Tool(anyhow::anyhow!("tool")))).is_none()
9084        );
9085        assert!(posthook_checkpoint(9, Vec::new(), &Ok(0)).is_none());
9086        assert!(posthook_checkpoint(9, Vec::new(), &Err(Errno::ERESTARTSYS.into())).is_none());
9087    }
9088
9089    /// A tool error is not the syscall's result and proves nothing about the
9090    /// buffer, so only a guest errno other than EFAULT hides it.
9091    #[test]
9092    fn only_non_efault_guest_errnos_prove_the_buffer_unwritten() {
9093        for errno in [Errno::ENOENT, Errno::EINVAL, Errno::EBADF, Errno::ENOMEM] {
9094            assert!(failure_proves_outputs_unwritten(&Err(errno.into())));
9095        }
9096        assert!(!failure_proves_outputs_unwritten(
9097            &Err(Errno::EFAULT.into())
9098        ));
9099        assert!(!failure_proves_outputs_unwritten(&Ok(0)));
9100        assert!(!failure_proves_outputs_unwritten(&Err(Error::Tool(
9101            anyhow::anyhow!("tool failure")
9102        ))));
9103
9104        let path = CString::new("/dev/null").unwrap();
9105        let stat = sentinel_stat();
9106        let line = finish_line(
9107            &newfstatat(&path, &stat),
9108            Err(Error::Tool(anyhow::anyhow!("tool failure"))),
9109        );
9110        assert!(
9111            line.contains(&format!("st_size={SENTINEL}")),
9112            "buffer hidden on a tool error: {line}"
9113        );
9114    }
9115
9116    /// `gettimeofday` stores `tv` before it can fail with `EFAULT` on a bad
9117    /// `tz`, so a failed call can carry a kernel-written output. It must stay
9118    /// rendered: suppressing it would hide real evidence from DETLOG.
9119    #[test]
9120    fn failed_gettimeofday_still_renders_the_timeval() {
9121        let tv = Timeval {
9122            tv_sec: SENTINEL,
9123            tv_usec: 0,
9124        };
9125        let call = Syscall::Gettimeofday(
9126            Gettimeofday::new()
9127                .with_tv(Some(TimevalMutPtr(
9128                    AddrMut::from_ptr(&tv as *const Timeval).unwrap(),
9129                )))
9130                .with_tz(None),
9131        );
9132
9133        let line = finish_line(&call, Err(Errno::EFAULT.into()));
9134        assert!(
9135            line.contains(&format!("tv_sec: {SENTINEL}")),
9136            "kernel-written timeval hidden on failure: {line}"
9137        );
9138        assert!(line.contains("EFAULT"), "errno missing: {line}");
9139    }
9140
9141    /// `fstat` never renders its buffer (T136880615); the result must not
9142    /// change that in either direction.
9143    #[test]
9144    fn fstat_never_renders_the_buffer() {
9145        let stat = sentinel_stat();
9146        let call = Syscall::Fstat(
9147            reverie::syscalls::Fstat::new()
9148                .with_fd(3)
9149                .with_stat(StatPtr::from_ptr(&stat as *const libc::stat)),
9150        );
9151        for result in [Ok(0), Err(Errno::EBADF.into())] {
9152            assert_no_struct_rendered(&finish_line(&call, result));
9153        }
9154    }
9155}