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}