Skip to main content

detcore/
tool_global.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 tool global state, and centralized methods corresponding to the centralized portion of
10//! the Detcore tool.
11
12mod exec_identity;
13#[cfg(feature = "physical-mm-test-support")]
14pub mod initial_mm_probe;
15pub(crate) mod mapping_capture;
16pub mod native_exec;
17mod parked;
18pub(crate) mod replay_departure;
19use std::cmp::Ordering;
20use std::collections::BTreeMap;
21use std::collections::BTreeSet;
22use std::collections::HashMap;
23use std::collections::HashSet;
24use std::collections::btree_map::Entry;
25use std::fmt::Debug;
26use std::fs;
27use std::fs::File;
28use std::io::Write;
29use std::num::NonZeroUsize;
30use std::os::fd::FromRawFd;
31use std::path::PathBuf;
32use std::sync::Arc;
33use std::sync::Mutex;
34use std::sync::OnceLock;
35use std::sync::PoisonError;
36use std::sync::atomic::AtomicBool;
37use std::sync::atomic::AtomicI32;
38use std::sync::atomic::AtomicU16;
39use std::sync::atomic::AtomicU64;
40use std::sync::atomic::Ordering::SeqCst;
41use std::task::Poll;
42use std::time::Duration;
43use std::time::SystemTime;
44
45use anyhow::bail;
46use chrono::DateTime;
47use chrono::Utc;
48use detcore_model::host_input::HostFileIdentity;
49use detcore_model::host_input::HostInputRecord;
50use detcore_model::host_input::HostInputSet;
51use detcore_model::network_engine::NetworkEngine;
52use detcore_model::network_engine::NetworkEngineError;
53use detcore_model::network_engine::NetworkReply;
54use detcore_model::network_engine::NetworkRequest;
55use detcore_model::network_trace::NetworkTraceMode;
56use detcore_model::network_trace::NetworkTraceV2;
57use detcore_model::procfs::mount_ids_are_ordered_subset;
58use detcore_model::summary::PreemptionSummary;
59use detcore_model::summary::RunSummary;
60use detcore_model::summary::TimesliceStats;
61use detcore_model::syscall_histogram::SparseSyscallHistogram;
62pub use detcore_model::syscall_histogram::SyscallObservationGap;
63use detcore_model::syscall_histogram::SyscallObservationScope;
64pub(crate) use exec_identity::reconnect_exec;
65pub(crate) use exec_identity::retire_exec;
66use nix::sys::signal;
67use nix::sys::signal::Signal;
68use nix::unistd::Pid;
69pub(crate) use parked::child_wait_request;
70pub(crate) use parked::parked_wait_request;
71pub(crate) use parked::polled_read_request;
72pub(crate) use parked::signal_dequeued;
73use reverie::GlobalRPC;
74use reverie::GlobalTool;
75use reverie::Guest;
76use reverie::Tid;
77use reverie::syscalls::AddrMut;
78use reverie::syscalls::CloneFlags;
79use reverie::syscalls::MemoryAccess;
80use reverie::syscalls::Sysno;
81use serde::Deserialize;
82use serde::Serialize;
83use tracing::debug;
84use tracing::error;
85use tracing::info;
86use tracing::trace;
87use tracing::warn;
88
89use crate::config::Config;
90#[cfg(test)]
91use crate::consts::ROOT_DETPID;
92use crate::fd::SharedOpenFileError;
93use crate::ivar::Ivar;
94use crate::preemptions::ThreadHistory;
95use crate::preemptions::boundary_recording::HistoryReader;
96use crate::procfs_inode;
97use crate::procfs_inode::ProcfsName;
98use crate::record_or_replay::RecordOrReplay;
99use crate::resources::ChaosEpochTransition;
100use crate::resources::ExternalOpId;
101use crate::resources::Permission;
102use crate::resources::ResourceID;
103use crate::resources::Resources;
104use crate::scheduler::AdmitIntent;
105use crate::scheduler::AdmitSide;
106use crate::scheduler::ConsumeResult;
107use crate::scheduler::DEFAULT_PRIORITY;
108use crate::scheduler::ExecReconnect;
109use crate::scheduler::FutexSignalWatch;
110use crate::scheduler::HostTimedSignalScope;
111use crate::scheduler::MaybePrintStack;
112use crate::scheduler::OwnProcessSignalInputs;
113use crate::scheduler::Priority;
114use crate::scheduler::SchedResponse;
115use crate::scheduler::SchedValue;
116use crate::scheduler::Scheduler;
117use crate::scheduler::ThreadNextTurn;
118use crate::scheduler::entropy_to_priority;
119use crate::scheduler::parked::*;
120use crate::scheduler::process_signal_receiver::ProcessSignalReceiver;
121use crate::scheduler::real_timer::DequeueAck;
122use crate::scheduler::real_timer::ItimerSnapshot;
123use crate::scheduler::real_timer::TimerFailure;
124use crate::scheduler::runqueue::FIRST_PRIORITY;
125use crate::scheduler::runqueue::LAST_PRIORITY;
126use crate::scheduler::runqueue::REPLAY_DEFERRED_PRIORITY;
127use crate::scheduler::runqueue::REPLAY_FOREGROUND_PRIORITY;
128use crate::scheduler::runqueue::is_ordinary_priority;
129use crate::scheduler::sched_loop;
130use crate::scheduler::sched_loop_external;
131use crate::scheduler::timed_waiters::SignalTarget;
132use crate::shared_open_files::OpenFileControl;
133use crate::shared_open_files::OpenFileControlReply;
134use crate::shared_open_files::SharedOpenFileStore;
135use crate::tool_local::Detcore;
136use crate::tool_local::ExecFdBlockingOverrides;
137use crate::tool_local::ExecFdState;
138use crate::tool_local::RobustListWake;
139use crate::types::*;
140
141pub(crate) async fn yield_once() {
142    let mut yielded = false;
143    std::future::poll_fn(|context| {
144        if yielded {
145            Poll::Ready(())
146        } else {
147            yielded = true;
148            context.waker().wake_by_ref();
149            Poll::Pending
150        }
151    })
152    .await;
153}
154
155#[derive(Debug)]
156struct InodePool {
157    // TODO(T87258449): merge these two maps:
158    inodes: HashMap<RawInode, DetInode>,
159    detinodes_info: HashMap<DetInode, DetInodeInfo>,
160    /// Counter backing the minted [`DetInode`]s. Deliberately a plain integer:
161    /// it is the *source* of deterministic inodes, not one itself, and typing
162    /// it `RawInode` previously blurred that distinction.
163    ///
164    /// It counts *requests*, not distinct host inodes: see [`Self::add_inode`].
165    next_inode: u64,
166    /// Virtual mtimes set (by a write or `utimensat`) on host inodes that have
167    /// no deterministic number yet. Setting an mtime does not number the file:
168    /// whether those calls reach the pool depends on host state (see
169    /// [`Self::set_mtime`]), so they must not consume counter values. The
170    /// file's first numbering request takes the pending mtime.
171    pending_mtimes: HashMap<RawInode, LogicalTime>,
172    /// Mappings whose file lost its last name (see [`Self::retire`]), kept
173    /// for the descriptors that can still reach it.
174    retired: HashMap<RawInode, DetInode>,
175    /// Files that lost their last name before anything numbered them, with
176    /// the virtual mtime a write through a remaining descriptor set, if any.
177    /// A descriptor sighting numbers such a file as retired, and a later name
178    /// is another file (see [`Self::forget_retired`]), so the next file the
179    /// host gives the inode inherits neither the number nor the mtime.
180    retired_unnumbered: HashMap<RawInode, Option<LogicalTime>>,
181    /// When each retired inode, numbered or not, was retired: the value of
182    /// `retirements` then. A directory snapshot taken before that cannot show
183    /// a new file under the inode (see [`InodeSighting::Listed`]).
184    retired_at: HashMap<RawInode, u64>,
185    /// How many retirements there have been. Each follows a guest's removal
186    /// of a last name, so the count is the guest's, like the counter.
187    retirements: u64,
188    /// Every file that has held each host inode, oldest first, kept after a
189    /// later file discards a retirement: a directory snapshot taken while an
190    /// earlier file held the inode must still name that file (codex
191    /// re-review of <https://github.com/rrnewton/hermit/pull/3849>, P1).
192    generations: HashMap<RawInode, Vec<Generation>>,
193    /// How many history records the snapshot lookups have examined, for the
194    /// test that bounds the lookup's cost.
195    #[cfg(test)]
196    listed_probes: u64,
197    /// How many retired mappings a later sighting showed the host had given to
198    /// another file (see [`Self::forget_retired`]). Diagnostic only: it
199    /// follows host state and never reaches the guest.
200    reused: u64,
201    /// The key each procfs host inode, and each procfs path key, is pooled
202    /// under (see [`Self::procfs_key`]): the host inode its entry was first
203    /// numbered under. Every value is also its own key.
204    procfs_keys: HashMap<RawInode, RawInode>,
205    /// For each thread ID, the value of `threads_counted` just after each
206    /// guest thread given that ID was counted, in order (see
207    /// [`Self::count_incarnation`]).
208    incarnations: HashMap<i32, Vec<u64>>,
209    /// How many guest threads have been counted (see
210    /// [`Self::count_incarnation`]).
211    threads_counted: u64,
212    /// The state Linux keeps on a per-process `/proc` file's inode that
213    /// Detcore keeps for the files it renders, by their inode number (see
214    /// [`Self::rendered_file_state`]).
215    rendered_file_states: HashMap<u64, crate::procfs::listing::RenderedFileState>,
216    /// The placeholder directories the run's guests opened, by registration
217    /// order: the `index`th is the one whose descriptor's kernel position is
218    /// `procfs::fd_directory::tag(index)` (see [`Self::register_placeholder`]).
219    placeholders: Vec<(
220        crate::procfs::fd_directory::PlaceholderDirectory,
221        OpenFileId,
222    )>,
223}
224
225/// How a numbering request reached its file, which decides whether a retired
226/// mapping (see `InodePool::retire`) answers it.
227#[derive(PartialEq, Debug, Eq, Clone, Copy, Serialize, Deserialize)]
228pub enum InodeSighting {
229    /// Through a descriptor without a link count, or a stat that reports no
230    /// link (`fstat` after `unlink`, `/proc/self/fd/N` of an unlinked file).
231    /// Either can reach the file a retired mapping describes.
232    Descriptor,
233    /// A file that has a name: a directory entry read now, or any stat that
234    /// reports a nonzero `st_nlink`. A retired file has no link and can never
235    /// get one back, so this is another file that the host gave the freed
236    /// inode.
237    Name,
238    /// A directory entry from a snapshot taken when there had been this many
239    /// retirements. The snapshot can be older than the entry's removal: a
240    /// stream reads the whole directory at its first `getdents` and returns
241    /// the rest later (codex review of
242    /// <https://github.com/rrnewton/hermit/pull/3849>, P1). So it shows a
243    /// new file only for an inode retired before it was taken.
244    Listed(u64),
245}
246
247/// What a directory snapshot records of the inode pool when it is taken, so
248/// that the entries it returns later are numbered as they were then.
249#[derive(PartialEq, Debug, Eq, Clone, Copy, Default, Serialize, Deserialize)]
250pub struct SnapshotMark {
251    /// How many retirements there had been (see [`InodeSighting::Listed`]):
252    /// an entry can be returned after its file's last name is gone.
253    pub retirements: u64,
254    /// How many guest threads had been counted (see
255    /// `InodePool::count_incarnation`): an entry can be returned after a task
256    /// it names exited and a new thread took its ID.
257    pub threads_counted: u64,
258}
259
260/// One file that held a host inode (see `InodePool::generations`). It held
261/// the inode from just after the previous file's retirement until its own.
262#[derive(Debug)]
263struct Generation {
264    /// Its number, or `None` while nothing has numbered it.
265    number: Option<DetInode>,
266    /// The retirement count at which it lost its last name, if it has.
267    retired: Option<u64>,
268    /// The mtime a write left on it before anything numbered it.
269    pending: Option<LogicalTime>,
270}
271
272/// Everything we know (globally) about a DetInode.
273#[derive(Debug)]
274struct DetInodeInfo {
275    /// The virtual mtime, or `None` while no stat has observed the file and no
276    /// write or explicit time update has set it. Many paths (open, read,
277    /// getdents) mint a DetInode before any stat, so the first-seen policy is
278    /// applied at the first stat rather than at minting; otherwise whether a
279    /// file kept a canonical mtime would depend on which syscall touched it
280    /// first. An unresolved mtime reads as the epoch.
281    mtime: Option<LogicalTime>,
282}
283
284/// Whole-second host mtimes, with a zero nanosecond part, that a file keeps
285/// when Hermit first sees it, instead of having its mtime replaced by the
286/// epoch (<https://github.com/rrnewton/hermit/issues/3639>).
287///
288/// These values are canonical and content-independent: they are written
289/// deliberately by tools that erase timestamps, so they carry no host clock
290/// or run-specific information. Revealing them makes whether a file's host
291/// mtime is canonical part of Hermit's input, like the file's contents: a
292/// guest that rewrites the host mtime of an input file (`touch f` after
293/// `stat f` on a canonical file) changes the input of the next run, exactly
294/// as rewriting its contents would.
295/// - `1` (1970-01-01T00:00:01Z) is the mtime of every file and directory in
296///   the Nix store (seconds 1, nanoseconds 0). nixpkgs' stdenv derives
297///   `SOURCE_DATE_EPOCH` from the newest source mtime, so reporting the epoch
298///   instead moves embedded build timestamps away from a native build's.
299/// - `0` is what `SOURCE_DATE_EPOCH=0` tooling and archive normalizers write.
300///
301/// Matching is exact. A non-zero nanosecond part, or any other second, is a
302/// real timestamp, and the file gets the epoch as before.
303pub const CANONICAL_FILE_MTIME_SECONDS: [i64; 2] = [0, 1];
304
305/// What a syscall observed about a file's host mtime, already reduced to the
306/// only part the virtual mtime may depend on.
307#[derive(PartialEq, Debug, Eq, Clone, Copy, Serialize, Deserialize)]
308pub enum ObservedMtime {
309    /// The caller did not read the file's metadata (open, read, getdents...).
310    Unobserved,
311    /// A stat observed a host mtime that is a real timestamp.
312    HostSpecific,
313    /// A stat observed one of [`CANONICAL_FILE_MTIME_SECONDS`], which is kept.
314    Canonical(LogicalTime),
315}
316
317impl ObservedMtime {
318    /// Classify a host mtime that a stat reported.
319    pub fn from_host_mtime(tv_sec: i64, tv_nsec: i64) -> Self {
320        match u64::try_from(tv_sec) {
321            Ok(secs) if tv_nsec == 0 && CANONICAL_FILE_MTIME_SECONDS.contains(&tv_sec) => {
322                ObservedMtime::Canonical(LogicalTime::from_secs(secs))
323            }
324            _ => ObservedMtime::HostSpecific,
325        }
326    }
327
328    /// The virtual mtime to assign to a file whose mtime is still unresolved:
329    /// a canonical value is kept, a real timestamp becomes the epoch, and no
330    /// observation leaves it unresolved.
331    fn first_seen_mtime(self, epoch: LogicalTime) -> Option<LogicalTime> {
332        match self {
333            ObservedMtime::Unobserved => None,
334            ObservedMtime::HostSpecific => Some(epoch),
335            ObservedMtime::Canonical(mtime) => Some(mtime),
336        }
337    }
338}
339
340/// Everything the global scheduler needs to register a new child thread. A
341/// normal clone is registered by the parent; a `CLONE_VFORK` child registers
342/// itself (with `parent_is_kernel_blocked` set) because its parent is blocked
343/// inside the kernel until the child execs or exits.
344struct ChildRegistration {
345    parent_dettid: DetTid,
346    parent_detpid: DetPid,
347    child_dettid: DetTid,
348    child_tid_addr: usize,
349    flags: Option<CloneFlags>,
350    exit_signal: libc::c_int,
351    physical_ids: Option<(i32, i32)>,
352    maybe_priority: Option<Priority>,
353    parent_is_kernel_blocked: bool,
354}
355
356#[derive(Debug)]
357struct PendingInheritedInput {
358    lease: reverie::InheritedInputLease,
359    root: Option<(DetTid, MmId)>,
360}
361
362#[derive(Clone, Debug, PartialEq, Eq)]
363struct PendingExecState {
364    caller: DetTid,
365    process: DetPid,
366    mm: MmId,
367    fd_blocking: ExecFdState,
368}
369
370/// Separate terminal cleanup outcomes; neither replaces the backend failure.
371pub struct BackendFailureCleanup {
372    /// Natural scheduler completion, retaining a task panic or cancellation.
373    pub scheduler: Result<(), tokio::task::JoinError>,
374    /// The requested partial recording's write result; no destination is success.
375    pub preemption_recording: Result<(), String>,
376}
377
378#[derive(Clone, Copy)]
379struct RpcIncarnation {
380    dettid: DetTid,
381    mm: MmId,
382}
383
384impl Default for InodePool {
385    fn default() -> Self {
386        InodePool::new()
387    }
388}
389
390impl InodePool {
391    fn new() -> Self {
392        InodePool {
393            inodes: HashMap::new(),
394            detinodes_info: HashMap::new(),
395            // Above the fixed stdio inodes (`DET_SPECIAL_INODE_OFFSET`), so a
396            // pooled number can never collide with one of them.
397            next_inode: crate::consts::DET_INODE_OFFSET.as_raw(),
398            pending_mtimes: HashMap::new(),
399            retired: HashMap::new(),
400            retired_unnumbered: HashMap::new(),
401            retired_at: HashMap::new(),
402            retirements: 0,
403            generations: HashMap::new(),
404            #[cfg(test)]
405            listed_probes: 0,
406            reused: 0,
407            procfs_keys: HashMap::new(),
408            incarnations: HashMap::new(),
409            threads_counted: 0,
410            rendered_file_states: HashMap::new(),
411            placeholders: Vec::new(),
412        }
413    }
414
415    /// Register a placeholder directory a guest opened (`/proc`, or a
416    /// descriptor directory and its binding), and return the kernel position
417    /// that tags its descriptor (`procfs::fd_directory::tag`), or `None` once
418    /// the tags are used up. Every registration comes in its guest thread's
419    /// turn, so the tags follow the scheduler's order. Registrations are kept
420    /// for the run: a descriptor can carry its tag to another process (over
421    /// `SCM_RIGHTS`) at any later time, and one entry per open is bounded by
422    /// the opens.
423    // TODO-HUMAN-REVIEW(#4604): Review the run-wide registry of descriptor
424    // directories, keyed by the kernel position of the descriptor on `/proc`
425    // that stands for each.
426    fn register_placeholder(
427        &mut self,
428        directory: crate::procfs::fd_directory::PlaceholderDirectory,
429        open_file: OpenFileId,
430    ) -> Option<u64> {
431        let tag = crate::procfs::fd_directory::tag(self.placeholders.len())?;
432        self.placeholders.push((directory, open_file));
433        Some(tag)
434    }
435
436    /// The placeholder directory a kernel position tags, and the open file
437    /// description it was opened in, if one was registered with it.
438    fn placeholder(
439        &self,
440        tag: u64,
441    ) -> Option<(
442        crate::procfs::fd_directory::PlaceholderDirectory,
443        OpenFileId,
444    )> {
445        let index = crate::procfs::fd_directory::tag_index(tag)?;
446        self.placeholders.get(index).copied()
447    }
448
449    /// How many guest threads have been given task ID `tid` in this run (see
450    /// [`Self::count_incarnation`]): the incarnation of the one holding it
451    /// now, 0 for an ID no guest thread has held.
452    fn incarnation(&self, tid: i32) -> u32 {
453        self.incarnations.get(&tid).map_or(0, |counted| {
454            u32::try_from(counted.len()).unwrap_or(u32::MAX)
455        })
456    }
457
458    /// What Detcore's process table says of the task `named` and its thread
459    /// `thread` (`existence`: `Scheduler::procfs_task`'s whether it exists
460    /// and whether it has exited), with the incarnation each number has now
461    /// ([`Self::incarnation`]).
462    fn procfs_task_answer(
463        &self,
464        named: DetTid,
465        thread: Option<DetTid>,
466        (exists, without_mm): (bool, bool),
467    ) -> crate::procfs::listing::ProcfsTaskAnswer {
468        crate::procfs::listing::ProcfsTaskAnswer {
469            exists,
470            without_mm,
471            process_incarnation: self.incarnation(named.as_raw()),
472            thread_incarnation: thread.map(|thread| self.incarnation(thread.as_raw())),
473            mm: None,
474            runtime_private_memory: None,
475        }
476    }
477
478    /// Apply `change` to the state of the rendered per-process `/proc` file
479    /// numbered `inode`, and return the state. Every change comes in its
480    /// guest thread's turn, so their order is the scheduler's.
481    ///
482    /// Only a file whose owner wrote its state (an mtime, a write hint) has
483    /// an entry; a read of any other inserts none. Entries are kept for the
484    /// run: a per-process inode names one task generation
485    /// (`procfs_inode::per_process_inode`), and a descriptor or a directory
486    /// of a task that is gone can still reach its file (an `fstat` of a
487    /// descriptor kept past a reap reads its mtime), so no entry can be
488    /// retired exactly. The table holds one entry per per-process inode whose
489    /// owner set a time or a hint, and an inode is a spelling's identity
490    /// (`procfs_inode::per_process_inode`): for a thread group of N threads,
491    /// up to `(N + N²) × PER_PROCESS_NAMES.len()` inodes per group of task
492    /// generations (N process spellings, N² `/proc/<n>/task/<m>` spellings,
493    /// `procfs::per_process_path`), and only those the guest wrote.
494    // TODO-HUMAN-REVIEW(#4213): Review the per-inode mtime and write-hint
495    // state Detcore keeps for its rendered per-process files, in the global
496    // tool, changed in the changing thread's turn.
497    fn rendered_file_state(
498        &mut self,
499        inode: u64,
500        change: crate::procfs::listing::RenderedFileChange,
501    ) -> crate::procfs::listing::RenderedFileState {
502        use crate::procfs::listing::RenderedFileChange;
503        let state = match change {
504            RenderedFileChange::Read => {
505                return self
506                    .rendered_file_states
507                    .get(&inode)
508                    .copied()
509                    .unwrap_or_default();
510            }
511            _ => self.rendered_file_states.entry(inode).or_default(),
512        };
513        match change {
514            RenderedFileChange::Read => {}
515            RenderedFileChange::Mtime(seconds, nanoseconds) => {
516                state.mtime = Some((seconds, nanoseconds));
517            }
518            RenderedFileChange::WriteHint(hint) => state.write_hint = hint,
519        }
520        *state
521    }
522
523    /// The key to pool the procfs entry at host inode `host` under, given the
524    /// key of the path it was reached by (see [`Self::procfs_path_key`]).
525    ///
526    /// Linux numbers per-process procfs entries when it builds their
527    /// dentries, and builds an evicted one again under a new host inode
528    /// (see `procfs_inode`). A host inode the pool has seen keeps its key,
529    /// so aliases (`/proc/<pid>/net/tcp` and `/proc/<pid>/task/<tid>/net/tcp`
530    /// are one inode) and sightings without a path agree. A host inode the
531    /// pool has not seen, reached by a path that already named an entry, is
532    /// that entry rebuilt: it takes the entry's key, and with it its number
533    /// and virtual mtime. Consumes no counter value.
534    fn procfs_key(&mut self, host: RawInode, path: RawInode) -> RawInode {
535        let key = match self.procfs_keys.get(&host) {
536            Some(key) => *key,
537            None if self.inodes.contains_key(&host)
538                || self.retired.contains_key(&host)
539                || self.retired_unnumbered.contains_key(&host) =>
540            {
541                host
542            }
543            None => self.procfs_keys.get(&path).copied().unwrap_or(host),
544        };
545        self.procfs_keys.insert(host, key);
546        self.procfs_keys.insert(path, key);
547        // A write through a descriptor nothing had registered left its mtime
548        // under the host inode.
549        if key != host
550            && let Some(mtime) = self.pending_mtimes.remove(&host)
551        {
552            self.set_mtime(key, mtime);
553        }
554        key
555    }
556
557    /// The key `raw_inode` is pooled under: its procfs key if it has one
558    /// (see [`Self::procfs_key`]), otherwise itself.
559    fn key(&self, raw_inode: RawInode) -> RawInode {
560        self.procfs_keys
561            .get(&raw_inode)
562            .copied()
563            .unwrap_or(raw_inode)
564    }
565
566    /// Counts a new guest thread with thread ID `tid`. A process or thread
567    /// ID the kernel gives out again names a new task, and Linux drops the
568    /// old task's procfs entries when it exits, so the new task's entries
569    /// are new files (see [`Self::procfs_path_key`]).
570    fn count_incarnation(&mut self, tid: i32) {
571        self.threads_counted += 1;
572        self.incarnations
573            .entry(tid)
574            .or_default()
575            .push(self.threads_counted);
576    }
577
578    /// The path key of the procfs entry `name`: the key of its path, salted
579    /// with the incarnation of each task the path names (see
580    /// [`Self::count_incarnation`]), so that an entry of a task whose ID was
581    /// reused does not adopt the number of the old task's entry, which a
582    /// descriptor may still hold. An entry of a directory snapshot names the
583    /// incarnation that held the ID when the snapshot was taken.
584    fn procfs_path_key(&self, name: &ProcfsName) -> RawInode {
585        name.tasks.iter().fold(name.path, |key, task| {
586            let counted = self.incarnations.get(task).map_or(&[][..], Vec::as_slice);
587            let incarnation = match name.threads_counted {
588                Some(snapshot) => counted.partition_point(|at| *at <= snapshot),
589                None => counted.len(),
590            };
591            match u32::try_from(incarnation).unwrap_or(u32::MAX) {
592                0 => key,
593                incarnation => procfs_inode::with_incarnation(key, *task, incarnation),
594            }
595        })
596    }
597
598    // Allocate the next deterministic inode.  This takes the raw-inode and
599    // can return an existing mapping or extend the mapping by creating a
600    // new deterministic inode. Minted numbers are strictly increasing and
601    // never handed out twice.
602    //
603    // Host inode reuse: a filesystem such as ext4 or XFS gives a freed
604    // `(dev, ino)` to a new file, and whether it does differs between runs
605    // (https://github.com/rrnewton/hermit/issues/3840). A file's mapping, or
606    // its pending mtime if nothing numbered it yet, is retired when the guest
607    // removes its last name (`Self::retire`). A `Name` sighting of a retired
608    // `(dev, ino)` discards it, so the new file is numbered by this request
609    // in every run, reused or not, and inherits no mtime. A `Descriptor`
610    // sighting still gets the retired number: an unlinked file reached through
611    // a descriptor keeps its inode number, as on Linux, and a descriptor to a
612    // new file was opened by name, which discarded the retired mapping
613    // (`Self::forget_retired`).
614    //
615    // Still only contained, not handled: a file that never had a name
616    // (`O_TMPFILE`, never linked) is never retired, nor is a file that
617    // disappears without the guest removing its name (another process
618    // deletes it, or its filesystem is unmounted). A named file the host
619    // gives such a file's freed inode inherits its number and mtime.
620    //
621    // Every call consumes the next counter value, whether or not it mints.
622    // Whether a host inode is "new" depends on host state the guest does not
623    // control (a file replaced by another process, an inode the filesystem
624    // reused, a hard link), while the sequence of requests is the guest's
625    // own. Counting only mints let one such host-dependent sighting shift the
626    // number of every file minted after it, on every device
627    // (https://github.com/rrnewton/hermit/issues/2897); counting requests
628    // confines the difference to the file whose host identity differed.
629    // Numbers stay unique and increasing, just not dense.
630    //
631    // Also return the virtual mtime. When it is still unresolved, `observed`
632    // resolves it under the first-seen policy (see `ObservedMtime`); an
633    // unresolved mtime reads as `epoch`.
634    fn add_sighted_inode(
635        &mut self,
636        raw_inode: RawInode,
637        observed: ObservedMtime,
638        epoch: LogicalTime,
639        sighting: InodeSighting,
640    ) -> (DetInode, LogicalTime) {
641        let raw_inode = self.key(raw_inode);
642        let ordinal = self.next_inode;
643        self.next_inode += 1;
644        match sighting {
645            InodeSighting::Name => self.forget_retired(raw_inode),
646            InodeSighting::Listed(snapshot) => {
647                if let Some(answer) = self.listed_generation(raw_inode, snapshot, ordinal) {
648                    let mtime = self
649                        .detinodes_info
650                        .get_mut(&answer)
651                        .expect("Internal invariant broken, det_ino missing entry");
652                    if mtime.mtime.is_none() {
653                        mtime.mtime = observed.first_seen_mtime(epoch);
654                    }
655                    return (answer, mtime.mtime.unwrap_or(epoch));
656                }
657                if self
658                    .retired_at
659                    .get(&raw_inode)
660                    .is_some_and(|retired| *retired < snapshot)
661                {
662                    self.forget_retired(raw_inode);
663                }
664            }
665            InodeSighting::Descriptor => {}
666        }
667        let dino = match self
668            .inodes
669            .get(&raw_inode)
670            .or_else(|| self.retired.get(&raw_inode))
671        {
672            Some(dino) => *dino,
673            None => {
674                // THE determinization boundary: the single place a host inode
675                // is deliberately mapped to a deterministic one. The value is
676                // minted from a monotonic counter, never derived from the host
677                // inode's bits.
678                let new = DetInode::mint(ordinal);
679                // A file whose last name went away before it was numbered,
680                // reached through a descriptor, is numbered as retired.
681                let mtime = match self.retired_unnumbered.remove(&raw_inode) {
682                    Some(mtime) => {
683                        self.retired.insert(raw_inode, new);
684                        if let Some(last) = self
685                            .generations
686                            .get_mut(&raw_inode)
687                            .and_then(|generations| generations.last_mut())
688                        {
689                            last.number = Some(new);
690                        }
691                        mtime
692                    }
693                    None => {
694                        assert!(self.inodes.insert(raw_inode, new).is_none());
695                        self.generations
696                            .entry(raw_inode)
697                            .or_default()
698                            .push(Generation {
699                                number: Some(new),
700                                retired: None,
701                                pending: None,
702                            });
703                        self.pending_mtimes.remove(&raw_inode)
704                    }
705                };
706                let prev = self.detinodes_info.insert(new, DetInodeInfo { mtime });
707                assert!(prev.is_none()); // Should not have been previously used.
708                new
709            }
710        };
711        let info = self
712            .detinodes_info
713            .get_mut(&dino)
714            .expect("Internal invariant broken, det_ino missing entry");
715        if info.mtime.is_none() {
716            info.mtime = observed.first_seen_mtime(epoch);
717        }
718        (dino, info.mtime.unwrap_or(epoch))
719    }
720
721    /// [`Self::add_sighted_inode`] for a [`InodeSighting::Descriptor`].
722    fn add_inode(
723        &mut self,
724        raw_inode: RawInode,
725        observed: ObservedMtime,
726        epoch: LogicalTime,
727    ) -> (DetInode, LogicalTime) {
728        self.add_sighted_inode(raw_inode, observed, epoch, InodeSighting::Descriptor)
729    }
730
731    // Set a file's virtual mtime, replacing whatever the first-seen policy
732    // chose or would choose. This never consumes a counter value: its callers
733    // (a write's mtime bump, `utimensat`) reach the pool or not depending on
734    // host state, such as whether the target's host identity held still
735    // during the call, and a request counted here would shift the number of
736    // every file numbered after it. A file not yet numbered keeps the mtime
737    // pending until its first numbering request.
738    fn set_mtime(&mut self, raw_inode: RawInode, mtime: LogicalTime) {
739        let raw_inode = self.key(raw_inode);
740        // A retired file is written through a descriptor opened before its
741        // last name went away; a new file's descriptor was opened by name,
742        // which discarded the retired mapping.
743        match self
744            .inodes
745            .get(&raw_inode)
746            .or_else(|| self.retired.get(&raw_inode))
747        {
748            Some(dino) => {
749                self.detinodes_info
750                    .get_mut(dino)
751                    // TODO(T87258449): remove this `expect`:
752                    .expect("Invariant violation: det inode missing from map.")
753                    .mtime = Some(mtime);
754            }
755            None => match self.retired_unnumbered.get_mut(&raw_inode) {
756                Some(retired) => {
757                    *retired = Some(mtime);
758                    // Also in its history record, which a directory snapshot
759                    // consults once the host has reused the inode (claude
760                    // re-check of https://github.com/rrnewton/hermit/pull/3849).
761                    if let Some(last) = self
762                        .generations
763                        .get_mut(&raw_inode)
764                        .and_then(|generations| generations.last_mut())
765                    {
766                        last.pending = Some(mtime);
767                    }
768                }
769                None => {
770                    self.pending_mtimes.insert(raw_inode, mtime);
771                }
772            },
773        }
774    }
775
776    /// The file behind `raw_inode` lost its last name (an `unlink`,
777    /// `rmdir` or replacing `rename` removed it). Its number stays for the
778    /// descriptors that can still reach it, but a later [`InodeSighting::Name`]
779    /// of `raw_inode` is another file. Consumes no counter value: whether a
780    /// removal reaches here follows host state (its `st_nlink`), and must not
781    /// shift other files' numbers.
782    fn retire(&mut self, raw_inode: RawInode) {
783        let raw_inode = self.key(raw_inode);
784        let stamp = self.retirements;
785        self.retired_at.insert(raw_inode, stamp);
786        self.retirements += 1;
787        let generations = self.generations.entry(raw_inode).or_default();
788        match self.inodes.remove(&raw_inode) {
789            Some(dino) => {
790                self.retired.insert(raw_inode, dino);
791                match generations.last_mut() {
792                    Some(last) if last.number == Some(dino) && last.retired.is_none() => {
793                        last.retired = Some(stamp);
794                    }
795                    _ => generations.push(Generation {
796                        number: Some(dino),
797                        retired: Some(stamp),
798                        pending: None,
799                    }),
800                }
801            }
802            // Nothing numbered it yet, but a write may have left a pending
803            // mtime, which must stay with this file (claude review of
804            // https://github.com/rrnewton/hermit/pull/3849, P2-2).
805            None => {
806                let pending = self.pending_mtimes.remove(&raw_inode);
807                self.retired_unnumbered.insert(raw_inode, pending);
808                generations.push(Generation {
809                    number: None,
810                    retired: Some(stamp),
811                    pending,
812                });
813            }
814        }
815    }
816
817    /// The number of the file that held `raw_inode` when a directory snapshot
818    /// was taken after `snapshot` retirements, if that file has since lost its
819    /// last name. A file that held it then and was never numbered is numbered
820    /// now, as `ordinal`. `None` when the inode's holder at the snapshot still
821    /// holds it, or nothing ever retired one of its holders, so that the
822    /// current file answers.
823    ///
824    /// A file held the inode from just after the previous holder's retirement
825    /// (the host cannot reuse an inode before it is freed) through its own:
826    /// the retirement with stamp `r` happened when the count was `r`, so a
827    /// snapshot that saw it still named has `snapshot <= r`.
828    fn listed_generation(
829        &mut self,
830        raw_inode: RawInode,
831        snapshot: u64,
832        ordinal: u64,
833    ) -> Option<DetInode> {
834        let generations = self.generations.get_mut(&raw_inode)?;
835        // Holders are retired in order and the stamps only grow, and only the
836        // last can still hold the inode, so the stamps are sorted. A snapshot
837        // newer than the last retirement saw the current holder, the common
838        // case, answered without a search; an older one finds its holder by
839        // binary search: the first holder retired at or after the snapshot.
840        // The previous holder was retired before the snapshot, so this one
841        // held the inode then (codex re-check of
842        // <https://github.com/rrnewton/hermit/pull/3849>: a scan from the
843        // oldest made churn on one inode quadratic).
844        let last_retired = match generations.last()?.retired {
845            Some(retired) => Some(retired),
846            None => generations.iter().rev().nth(1).and_then(|g| g.retired),
847        };
848        if last_retired.is_none_or(|retired| snapshot > retired) {
849            return None;
850        }
851        #[cfg(test)]
852        let mut probes = 0;
853        let index = generations.partition_point(|generation| {
854            #[cfg(test)]
855            {
856                probes += 1;
857            }
858            generation.retired.is_some_and(|retired| retired < snapshot)
859        });
860        #[cfg(test)]
861        {
862            self.listed_probes += probes;
863        }
864        // The holder at the snapshot still holds the inode: the current file
865        // answers.
866        generations.get(index)?.retired?;
867        let current = index + 1 == generations.len();
868        let generation = &mut generations[index];
869        if let Some(number) = generation.number {
870            return Some(number);
871        }
872        let new = DetInode::mint(ordinal);
873        generation.number = Some(new);
874        let mut mtime = generation.pending;
875        // The latest holder, still retired and reached first by this entry, is
876        // numbered as a descriptor would number it.
877        if current && let Some(pending) = self.retired_unnumbered.remove(&raw_inode) {
878            self.retired.insert(raw_inode, new);
879            mtime = pending;
880        }
881        let prev = self.detinodes_info.insert(new, DetInodeInfo { mtime });
882        assert!(prev.is_none()); // Should not have been previously used.
883        Some(new)
884    }
885
886    /// `raw_inode` names a file that has a name, or that was just created
887    /// (an `open` that found it linked, or `O_TMPFILE`), so a retired mapping
888    /// of it described a file the host has freed: discard it. Consumes no
889    /// counter value.
890    fn forget_retired(&mut self, raw_inode: RawInode) {
891        let raw_inode = self.key(raw_inode);
892        self.retired_at.remove(&raw_inode);
893        // Its number and mtime stay in `generations` and `detinodes_info` for
894        // directory snapshots taken while it held the inode.
895        if self.retired.remove(&raw_inode).is_some() {
896            self.reused += 1;
897        }
898        if self.retired_unnumbered.remove(&raw_inode).is_some() {
899            self.reused += 1;
900        }
901    }
902}
903
904// AUTONOMOUS-BOT-IMPLEMENTED
905// TODO-HUMAN-REVIEW(PR-1056): Deterministic remapping of device numbers (st_dev).
906/// Deterministic remapping of device numbers (`st_dev`).
907///
908/// The kernel assigns anonymous block-device numbers to filesystems without a
909/// backing block device (procfs, sysfs, tmpfs, devpts) from a global,
910/// host-wide counter (`get_anon_bdev`). The raw `st_dev` a guest observes for
911/// such a filesystem therefore drifts between otherwise-identical runs — and
912/// even between the two runs of `--verify`, because the first run's mounts are
913/// still live when the second run mounts fresh copies, so the second run's
914/// procfs gets a different anonymous device number. That leaked host state into
915/// a guest-visible `stat`/`statx` field.
916///
917/// We replace each distinct raw device number with a strictly-increasing
918/// synthetic id assigned in first-observation order. Both `stat`/`statx` and a
919/// virtualized mountinfo snapshot use this pool. A mountinfo read intentionally
920/// pre-populates it in that snapshot's row order, and later metadata syscalls
921/// reuse those assignments.
922///
923/// This guarantees identity consistency within one Detcore run. It is not an
924/// unconditional cross-machine guarantee: hosts with different filesystem
925/// layouts can expose different device equivalence classes or first-observation
926/// order. The remapping still preserves distinctness and equality within the
927/// run, so `find -xdev`, `du -x`, and `(st_dev, st_ino)` checks behave
928/// consistently with the mountinfo device column.
929#[derive(Debug)]
930struct DevicePool {
931    devices: HashMap<u64, u64>,
932    next_device: u64,
933}
934
935/// Run-global guest-visible mount IDs, shared by `/proc/*/mountinfo` and
936/// `/proc/*/fdinfo/*`.
937///
938/// Linux gives pseudo filesystems such as pipefs, sockfs, anon_inodefs, nsfs,
939/// and pidfs their own mount IDs without listing those mounts in
940/// `/proc/*/mountinfo`. The raw numbers are host-assigned. Preserve equality
941/// and distinctness by keying on the raw mount ID, and number raw IDs in the
942/// order the guest first observes them: an fdinfo read assigns its one ID, and
943/// a mountinfo read assigns its not-yet-numbered row and parent IDs in row
944/// order. The namespace's row order is kept only to validate later views.
945///
946/// Numbering must not depend on mounts the guest never observed. A run's mount
947/// namespace starts as a copy of the host's, so its row count changes with
948/// unrelated host activity, such as another process's loop mount under a
949/// shared-propagation directory. Numbering by position in that table made the
950/// fdinfo `mnt_id` of a pipe or of `/dev/null`, neither of which mountinfo
951/// lists, differ between two runs of the same guest.
952#[derive(Debug)]
953enum MountIdPool {
954    Uninitialized,
955    Invalid,
956    Ready {
957        /// Raw mount ID to guest-visible mount ID, for every raw ID numbered
958        /// so far. `mount_ids[&assignment_order[i]] == i + 1`.
959        mount_ids: BTreeMap<u64, u64>,
960        /// Raw mount IDs in the order they were numbered.
961        assignment_order: Vec<u64>,
962        /// The namespace's raw row/parent order. Validation only.
963        mountinfo_order: Vec<u64>,
964        allow_visible_subsets: bool,
965    },
966}
967
968/// The raw identity orders observed by the producer and required to reconstruct
969/// the same guest-visible mount IDs during replay.
970pub struct MountIdentityProvenance {
971    /// The namespace's raw mountinfo row/parent order, which later mountinfo
972    /// views are validated against.
973    pub mountinfo_order: Vec<u64>,
974    /// Raw mount IDs in the order the run numbered them.
975    pub assignment_order: Vec<u64>,
976}
977
978impl MountIdPool {
979    fn from_config(mount_ids: &[u64], captured: bool, assignment_order: &[u64]) -> Self {
980        if !captured {
981            if !mount_ids.is_empty() || !assignment_order.is_empty() {
982                return Self::Invalid;
983            }
984            return Self::Uninitialized;
985        }
986        Self::from_orders(mount_ids, assignment_order, true).unwrap_or(Self::Invalid)
987    }
988
989    fn from_orders(
990        mount_ids: &[u64],
991        assignment_order: &[u64],
992        allow_visible_subsets: bool,
993    ) -> Option<Self> {
994        // A recorded assignment may include IDs mountinfo does not list, such
995        // as pipefs. It may include raw 0 only if the namespace order names 0
996        // as a row or parent: an fdinfo zero is the anonymous class and is
997        // never numbered, and a mountinfo zero must pass order validation.
998        if !detcore_model::procfs::mount_identity_provenance_is_well_formed(
999            mount_ids,
1000            assignment_order,
1001        ) {
1002            return None;
1003        }
1004        let mut mappings = BTreeMap::new();
1005        for (index, raw) in assignment_order.iter().enumerate() {
1006            let virtual_mount_id = u64::try_from(index).ok()?.checked_add(1)?;
1007            if mappings.insert(*raw, virtual_mount_id).is_some() {
1008                return None;
1009            }
1010        }
1011        Some(Self::Ready {
1012            mount_ids: mappings,
1013            assignment_order: assignment_order.to_vec(),
1014            mountinfo_order: mount_ids.to_vec(),
1015            allow_visible_subsets,
1016        })
1017    }
1018
1019    fn validate_mountinfo_order(&mut self, mountinfo_order: &[u64]) -> bool {
1020        if matches!(self, Self::Uninitialized) {
1021            *self = Self::from_orders(mountinfo_order, &[], false).unwrap_or(Self::Invalid);
1022        }
1023        let Self::Ready {
1024            mountinfo_order: expected,
1025            allow_visible_subsets,
1026            ..
1027        } = self
1028        else {
1029            return false;
1030        };
1031        if *allow_visible_subsets {
1032            mount_ids_are_ordered_subset(mountinfo_order, expected)
1033        } else {
1034            mountinfo_order == expected
1035        }
1036    }
1037
1038    /// Return the guest-visible ID for `raw_mount_id`, numbering it next if
1039    /// this is its first observation. The pool must be `Ready`.
1040    fn assign(&mut self, raw_mount_id: u64) -> Option<u64> {
1041        let Self::Ready {
1042            mount_ids,
1043            assignment_order,
1044            ..
1045        } = self
1046        else {
1047            return None;
1048        };
1049        if let Some(virtual_mount_id) = mount_ids.get(&raw_mount_id) {
1050            return Some(*virtual_mount_id);
1051        }
1052        let virtual_mount_id = u64::try_from(assignment_order.len()).ok()?.checked_add(1)?;
1053        mount_ids.insert(raw_mount_id, virtual_mount_id);
1054        assignment_order.push(raw_mount_id);
1055        Some(virtual_mount_id)
1056    }
1057
1058    fn determinize(&mut self, raw_mount_id: u64, mountinfo_order: Option<&[u64]>) -> Option<u64> {
1059        // Linux uses zero for anonymous objects such as memfd. It is one
1060        // equivalence class regardless of Detcore's descriptor classification.
1061        if raw_mount_id == 0 {
1062            return Some(0);
1063        }
1064        if let Some(order) = mountinfo_order {
1065            if !self.validate_mountinfo_order(order) {
1066                return None;
1067            }
1068        } else if matches!(self, Self::Uninitialized) {
1069            return None;
1070        }
1071        self.assign(raw_mount_id)
1072    }
1073
1074    /// Validate one mountinfo snapshot and return the guest-visible ID of each
1075    /// entry of `observation_order`, numbering unseen entries in that order.
1076    ///
1077    /// `mountinfo_order` is the snapshot's canonical order, checked against
1078    /// the namespace order. `observation_order` is the same set of raw IDs in
1079    /// the order the view presents them (rows, then parent-only IDs). Only it
1080    /// decides numbering, so a captured namespace order that the guest cannot
1081    /// see never changes a guest-visible number.
1082    fn resolve_mountinfo(
1083        &mut self,
1084        mountinfo_order: &[u64],
1085        observation_order: &[u64],
1086    ) -> Option<Vec<u64>> {
1087        let entries = mountinfo_order.iter().copied().collect::<BTreeSet<_>>();
1088        let mut observed = BTreeSet::new();
1089        if entries.len() != mountinfo_order.len()
1090            || observation_order.len() != mountinfo_order.len()
1091            || !observation_order
1092                .iter()
1093                .all(|raw| entries.contains(raw) && observed.insert(*raw))
1094        {
1095            return None;
1096        }
1097        if !self.validate_mountinfo_order(mountinfo_order) {
1098            return None;
1099        }
1100        observation_order
1101            .iter()
1102            .map(|raw| self.assign(*raw))
1103            .collect()
1104    }
1105
1106    fn provenance(&self) -> Result<Option<MountIdentityProvenance>, &'static str> {
1107        match self {
1108            Self::Uninitialized => Ok(None),
1109            Self::Invalid => Err("mount identity provenance is invalid"),
1110            Self::Ready {
1111                mountinfo_order,
1112                assignment_order,
1113                ..
1114            } => Ok(Some(MountIdentityProvenance {
1115                mountinfo_order: mountinfo_order.clone(),
1116                assignment_order: assignment_order.clone(),
1117            })),
1118        }
1119    }
1120}
1121
1122impl Default for DevicePool {
1123    fn default() -> Self {
1124        DevicePool::new()
1125    }
1126}
1127
1128impl DevicePool {
1129    fn new() -> Self {
1130        // Start at 1 so no file reports st_dev == 0, which some tools treat as
1131        // "no device".
1132        DevicePool {
1133            devices: HashMap::new(),
1134            next_device: 1,
1135        }
1136    }
1137
1138    /// The pool of a run whose guest mount namespace is this process's: its
1139    /// procfs device reports Detcore's procfs device, as the procfs files
1140    /// Detcore renders do (<https://github.com/rrnewton/hermit/issues/4213>).
1141    /// Outside record and replay, the process holding the global state is in
1142    /// the guest's mount namespace on every backend. Replay's tracer is not,
1143    /// and its devices are the recording's, so record and replay number
1144    /// devices as before ([`Self::new`]).
1145    fn for_this_mount_namespace() -> Self {
1146        let mut pool = Self::new();
1147        match std::fs::metadata("/proc") {
1148            Ok(proc) => {
1149                pool.devices.insert(
1150                    std::os::unix::fs::MetadataExt::dev(&proc),
1151                    procfs_inode::VIRTUAL_PROC_DEVICE,
1152                );
1153            }
1154            Err(error) => warn!(
1155                "[detcore] /proc could not be read, so its device is numbered when first seen: {error}"
1156            ),
1157        }
1158        pool
1159    }
1160
1161    /// Return the deterministic device id for `raw_device`, allocating a new one
1162    /// (in first-observation order) the first time a raw device is seen. The
1163    /// procfs device is never allocated to another device.
1164    fn determinize(&mut self, raw_device: u64) -> u64 {
1165        match self.devices.get(&raw_device) {
1166            Some(dev) => *dev,
1167            None => {
1168                if self.next_device == procfs_inode::VIRTUAL_PROC_DEVICE {
1169                    self.next_device += 1;
1170                }
1171                let new = self.next_device;
1172                self.next_device += 1;
1173                self.devices.insert(raw_device, new);
1174                new
1175            }
1176        }
1177    }
1178}
1179
1180/// Global state associated with the detcore tool.
1181///
1182/// This is a singleton, and the one object of this type lives inside a central
1183/// address space, generally the "tracer" in a Reverie backend.
1184#[derive(Debug)]
1185pub struct GlobalState {
1186    sched: Arc<Mutex<Scheduler>>,
1187    #[cfg(feature = "physical-mm-test-support")]
1188    initial_mm_probe: Mutex<Option<Arc<initial_mm_probe::Probe>>>,
1189
1190    inodes: Arc<Mutex<InodePool>>,
1191
1192    // AUTONOMOUS-BOT-IMPLEMENTED
1193    // TODO-HUMAN-REVIEW(PR-1056): Deterministic st_dev remapping state.
1194    devices: Arc<Mutex<DevicePool>>,
1195
1196    /// Shared fdinfo mount-ID equivalence classes for this Detcore run.
1197    mount_ids: Mutex<MountIdPool>,
1198
1199    // next port to use if input port is 0
1200    next_port: AtomicU16,
1201
1202    /// The ID the next guest task gets when Detcore chooses the IDs
1203    /// (`GuestIds::Chosen`): one past the first process's at the start, one
1204    /// more for every clone-family call that asks (`NextGuestId`). Zero when
1205    /// the kernel numbers the guest or the positive ID space is exhausted.
1206    next_guest_id: AtomicI32,
1207
1208    /// How many host-ID reports Detcore accepted while choosing IDs
1209    /// (`GuestIds::Chosen` without `refuse_host_ids`). This includes
1210    /// conservative reports before vfork attempts that can fail or restart,
1211    /// so it does not count distinct created tasks. The summary disclaims
1212    /// chosen-ID guarantees and the preemption record makes replay refuse.
1213    host_id_tasks: AtomicU64,
1214
1215    // used ports
1216    used_ports: Mutex<HashSet<u16>>,
1217
1218    // Unsupported syscall names observed across every process in this run.
1219    unsupported_syscalls: Mutex<BTreeSet<String>>,
1220
1221    /// Why the syscall histogram may miss calls, as the launcher reports it
1222    /// (`GlobalState::note_syscall_observation_gap`); `--passthru-opt` is
1223    /// added from the configuration.
1224    syscall_observation_gaps: Mutex<BTreeSet<SyscallObservationGap>>,
1225    /// What the run learned of its store-exclusive sequences, for its
1226    /// summary, recording and warnings (`crate::store_exclusive`).
1227    #[cfg(target_arch = "aarch64")]
1228    store_exclusive: Mutex<crate::store_exclusive::RunTally>,
1229
1230    // Signals whose requested disposition Detcore did not install, already
1231    // announced in this run (`GlobalRequest::AnnounceSignalHandlerNotInstalled`).
1232    announced_handlers_not_installed: Mutex<BTreeSet<i32>>,
1233    /// The signal controls an in-guest runtime refused that this run has
1234    /// announced (`GlobalRequest::AnnounceRefusedSignalControls`): the signals,
1235    /// bit `n - 1` for signal `n`, and whether an alternate stack.
1236    announced_refused_signal_controls: Mutex<(u64, bool)>,
1237    /// The host's `/proc/diskstats` as this run first read it, in the
1238    /// coordinator's own process, or the errno of that read: every
1239    /// `/proc/diskstats` Detcore renders in the run starts from it
1240    /// (`GlobalRequest::HostDiskstats`), whatever process asks and whatever
1241    /// its descriptor table holds.
1242    host_diskstats: std::sync::OnceLock<Result<Vec<u8>, i32>>,
1243    /// The record sequence of each per-process file's open file description
1244    /// that a fork copied where the tool runs in each guest process, as the
1245    /// last read, positioned read or seek of it left it
1246    /// (`GlobalRequest::ProcfsSequence`, `ProcfsFile::sequence_is_shared`).
1247    /// Each lives until the run ends, as committed shared open file models do
1248    /// (`crate::shared_open_files`): one per description open at a fork.
1249    procfs_sequences: Mutex<BTreeMap<OpenFileId, crate::procfs::ProcfsSequence>>,
1250
1251    // Why the fail-closed policy ended the run, from the Tool image that
1252    // refused (`GlobalRequest::PolicyRefusal`); the first report is kept.
1253    policy_refusal: Mutex<Option<String>>,
1254
1255    // The host identity of each file the guest opened in this run, in the
1256    // order the opens were reported, when `Config::record_host_inputs` is set
1257    // (see `detcore_model::host_input`). Written to `Config::host_input_log`
1258    // when the run ends normally; dropped with this state otherwise.
1259    host_inputs: Mutex<Vec<HostInputRecord>>,
1260
1261    // The paths the guest itself rebound in its file namespace in this run,
1262    // when `Config::record_host_inputs` is set (see
1263    // `detcore_model::host_input::HostMutationRecord`). Written beside
1264    // `host_inputs`.
1265    host_mutations: Mutex<BTreeSet<String>>,
1266
1267    // When this run replays a schedule whose recording names the host files
1268    // its run opened, and `Config::record_host_inputs` is set: the comparison
1269    // of this run's opens against them (`crate::host_inputs`). Information
1270    // only; no scheduling decision reads it.
1271    replay_host_inputs: Option<Mutex<crate::host_inputs::ReplayHostInputs>>,
1272
1273    // For each record in `host_inputs` whose file the run's end may read
1274    // (`crate::host_inputs::content_at_end`): its index there and where the
1275    // tool found the file at the open. Read once every guest process has
1276    // exited, by `settle_host_inputs`, never while the guest runs.
1277    // The physical identity remains separate from a captured input's origin:
1278    // it authenticates the file read at settlement, not the published source.
1279    host_input_sources: Mutex<Vec<(usize, PathBuf, HostFileIdentity)>>,
1280
1281    // Whether `settle_host_inputs` has run.
1282    host_inputs_settled: AtomicBool,
1283
1284    // The canonical models of open file descriptions that several guest
1285    // processes share (see `crate::shared_open_files`). Locked after `sched`,
1286    // never before it.
1287    shared_open_files: Arc<Mutex<SharedOpenFileStore>>,
1288
1289    // Optional append-only sink shared by DBT fork descendants.
1290    unsupported_syscall_report_fd: Option<Mutex<File>>,
1291
1292    // Open file description to bound port.
1293    open_file_to_port: Mutex<HashMap<OpenFileId, u16>>,
1294
1295    /// Inotify rename cookies renumbered by first observation across the run
1296    /// (`GlobalRequest::VirtualInotifyCookies`, `crate::syscalls::inotify`).
1297    inotify_cookies: Mutex<crate::syscalls::inotify::CookieTable>,
1298
1299    port_start_range: AtomicU16,
1300    port_end_range: AtomicU16,
1301
1302    // False initially after fork, and true when we begin executing the guest binary.
1303    past_first_execve: AtomicBool,
1304    /// Unlike the logging flag above, this changes only at successful image completion.
1305    initial_descriptor_image_complete: AtomicBool,
1306    /// Actual controller input, held only until the registered root image
1307    /// consumes it. The lease keeps its raw identity live, not guest-visible.
1308    inherited_input: Mutex<Option<PendingInheritedInput>>,
1309    // Installed only on the external controller by a contained launcher.
1310    mapping_capture_run: Option<reverie::MappingCaptureRun>,
1311    mapping_capture_pending: Mutex<Option<mapping_capture::Pending>>,
1312
1313    // AUTONOMOUS-BOT-IMPLEMENTED
1314    // TODO-HUMAN-REVIEW(PR-1154): Review the SaBRe exec descriptor-status handoff.
1315    /// Pre-exec identity and descriptor state awaiting a successful image transition.
1316    // TODO-HUMAN-REVIEW(PR-1173): Review SaBRe exec incarnation fencing.
1317    pending_exec_states: Arc<Mutex<BTreeMap<DetPid, PendingExecState>>>,
1318    // Installed only by the trusted watched native launcher, never by an RPC.
1319    native_exec_registry: OnceLock<Mutex<native_exec::NativeExecRegistry>>,
1320    /// Successful identity transfers whose RPC acknowledgment may still be in flight.
1321    /// Nesting either exec map with global_time requires holding sched first;
1322    /// the scheduler mutex serializes their inner lock orders. Process
1323    /// cleanup also holds sched before acquiring these stores.
1324    completed_exec_transfers: Arc<Mutex<BTreeMap<DetPid, exec_identity::ExecTransferReceipt>>>,
1325
1326    /// Descriptor state retained after the one-shot scheduler transition is consumed.
1327    post_exec_fd_blocking: Arc<Mutex<BTreeMap<DetTid, ExecFdState>>>,
1328
1329    /// The image each guest process executes, as its last successful exec
1330    /// recorded and the kernel's link confirmed (`crate::exec_record`), for
1331    /// another process's `/proc/<pid>/exe`. A new process starts with its
1332    /// creator's; the record goes with the process.
1333    exec_images: Arc<Mutex<BTreeMap<DetPid, crate::exec_record::ExecRecord>>>,
1334    /// The auxiliary vector of each guest process's current image, for its
1335    /// `/proc/<pid>/auxv` ([`ProcessAuxvTable`]).
1336    process_auxv: Arc<Mutex<ProcessAuxvTable>>,
1337    /// Which guest descriptor tables reference each open file description
1338    /// whose OFD or flock locks Detcore has read, and which locks it holds,
1339    /// so that `/proc/locks` keeps exactly the guest's own rows
1340    /// (`procfs::locks::LockLedger`). Locked after `sched`, never before it.
1341    lock_ledger: Mutex<crate::procfs::locks::LockLedger>,
1342    /// The descriptor table each guest thread holds, as the thread last named
1343    /// it (`GlobalRequest::HoldTable`, `Detcore::name_table_for_death`): at its
1344    /// start, and after an exec gave it a new one; forgotten when the thread
1345    /// deregisters or dies. The terminal receipt of a death by a fatal signal
1346    /// lets go of the dying threads' tables from here
1347    /// ([`Self::release_tables_at_death`]). Only a run whose backend's process
1348    /// signal control is `ToolControlled` (KVM) names any, and its tool runs
1349    /// in this process. Held weakly: a table goes with its last thread state,
1350    /// as before. Locked after `sched`, never before it, and before
1351    /// `lock_ledger`.
1352    held_tables: Mutex<BTreeMap<DetTid, std::sync::Weak<Mutex<crate::tool_local::FileMetadata>>>>,
1353    /// The registered root thread, whose first image
1354    /// `Config::launch_image` names, and whether that image was completed.
1355    root_dettid: OnceLock<DetTid>,
1356    root_image_marked: AtomicBool,
1357    /// The device and inode of `Config::launch_image`'s launcher, read once.
1358    launcher_identity: OnceLock<Option<(u64, u64)>>,
1359
1360    sched_handle: Option<tokio::task::JoinHandle<()>>,
1361
1362    /// Global time is a *volatile* vector clock of individual thread progress. Each
1363    /// thread can independently update its own progress, even (potentially) asynchronously.
1364    ///
1365    /// LockOrdering: this lock can be acquired while holding the sched lock (but not vice
1366    /// versa).
1367    //
1368    // TODO: it would be more future-proof to provide a non-blocking way to retrieve a
1369    // (nondeterministic) monotonic lower bound on global time.
1370    global_time: Arc<Mutex<GlobalTime>>,
1371
1372    /// Just cache the config so we can access it from everywhere.
1373    cfg: Config,
1374
1375    /// Storage for the preemption record read from `replay_preemptions_from`.
1376    preemptions_to_replay: Option<HistoryReader>,
1377
1378    /// The start is when we construct the global state.  Close enough.
1379    realtime_start: SystemTime,
1380
1381    /// `crate::STOPLESS_SLICE_ENDS` when this run started: the run's own
1382    /// count is the counter's growth since.
1383    stopless_slice_ends_at_start: u64,
1384
1385    /// External network record or replay state, present only when a network
1386    /// trace mode is configured. Every operation reads global time under this
1387    /// lock, so an input's release point is fixed by the schedule alone.
1388    network: Option<Mutex<NetworkEngine>>,
1389}
1390
1391/// The stores a process's physical exit must clean up, shared with the
1392/// GlobalState that owns them, so that the cleanup can run as one step under
1393/// the scheduler lock. Locked after `sched`, never before it, as every other
1394/// nested user of these stores does.
1395#[derive(Clone, Debug, Default)]
1396struct ProcessExitCleanup {
1397    shared_open_files: Arc<Mutex<SharedOpenFileStore>>,
1398    pending_exec_states: Arc<Mutex<BTreeMap<DetPid, PendingExecState>>>,
1399    completed_exec_transfers: Arc<Mutex<BTreeMap<DetPid, exec_identity::ExecTransferReceipt>>>,
1400    post_exec_fd_blocking: Arc<Mutex<BTreeMap<DetTid, ExecFdState>>>,
1401    exec_images: Arc<Mutex<BTreeMap<DetPid, crate::exec_record::ExecRecord>>>,
1402    process_auxv: Arc<Mutex<ProcessAuxvTable>>,
1403}
1404
1405/// What `/proc/<pid>/auxv` reads for each guest process, on every backend
1406/// that reports an initial stack pointer (package K slice K2,
1407/// <https://github.com/rrnewton/hermit/issues/4213>): the auxiliary vector its
1408/// current image found on its initial stack when it was exec'd
1409/// (`Detcore::record_process_auxv`), as Linux's `saved_auxv`. Another
1410/// process's access to it is decided by the kernel beneath at the open
1411/// (`Detcore::ptrace_read_allowed`), so nothing about access is kept here.
1412///
1413/// One entry per live process, at most `procfs::MAX_AUXV_BYTES` (Linux's
1414/// `saved_auxv`, 52 words), forgotten at the process's exit. A descriptor
1415/// keeps the snapshot its open took, through any later exec or exit.
1416///
1417/// Not kept: the vectors of earlier images for a descriptor that a backend
1418/// rebuilds from its placeholder's name after an exec. Only backends that
1419/// rebuild descriptors that way (DBT, SaBRe, LiteInst) would need them, and
1420/// none of them reports an initial stack pointer, so none renders this file.
1421/// That path returns, with end-to-end tests, when DBT implements
1422/// `Guest::initial_stack_pointer`.
1423///
1424/// It also keeps the other fact a `/proc/<pid>` file Detcore renders depends
1425/// on: which tasks have a changed root, whose `mountinfo` and `mounts` are
1426/// then the kernel's view, rooted where the task is (K3a of
1427/// <https://github.com/rrnewton/hermit/issues/4213>). Linux keeps a task's
1428/// root in its `fs_struct`, which `clone(CLONE_FS)` shares between creator
1429/// and child (thread or process) and which a clone without it copies, so the
1430/// table keeps each task's filesystem group the same way ([`FsRoots`]).
1431#[derive(Debug, Default)]
1432pub(crate) struct ProcessAuxvTable {
1433    /// Each live process's current image's auxiliary vector.
1434    processes: BTreeMap<DetPid, Vec<u8>>,
1435    /// Each live process's current image's argument and environment areas,
1436    /// which its rendered `/proc/<pid>/cmdline` reads from its memory
1437    /// (`Detcore::record_process_auxv`, `Detcore::read_rendered_cmdline`):
1438    /// one entry per live process whose image's initial stack Detcore read,
1439    /// inherited at fork and forgotten at exit, as the vectors are.
1440    cmdline_areas: BTreeMap<DetPid, crate::procfs::CmdlineArea>,
1441    /// Each task's filesystem group, and the groups whose root changed.
1442    roots: FsRoots,
1443    /// Whether this run has warned that a backend reports no initial stack
1444    /// pointer, and that an initial stack held no plausible vector
1445    /// ([`ProcessAuxvTable::first_kernel_file`]).
1446    kernel_file_warned: [bool; 2],
1447}
1448
1449/// Each task's filesystem group: the tasks that share one Linux `fs_struct`,
1450/// and so one root. A task is in its creator's group when it was created with
1451/// `CLONE_FS`, and in a new group, starting from a copy of its creator's
1452/// root, otherwise. `unshare(CLONE_FS)` would detach a task too, but Detcore
1453/// refuses every `unshare` (EPERM), so no guest task does. A task Detcore has
1454/// not seen created (none in practice) has an unchanged root.
1455#[derive(Debug, Default)]
1456struct FsRoots {
1457    /// Each task's group, by its thread number.
1458    groups: BTreeMap<DetPid, u64>,
1459    /// The groups whose root changed.
1460    changed: BTreeSet<u64>,
1461    /// The next group's number.
1462    next: u64,
1463}
1464
1465impl FsRoots {
1466    fn new_group(&mut self, changed: bool) -> u64 {
1467        let group = self.next;
1468        self.next += 1;
1469        if changed {
1470            self.changed.insert(group);
1471        }
1472        group
1473    }
1474
1475    fn clone_task(&mut self, parent: DetPid, child: DetPid, shares_fs: bool) {
1476        let group = match self.groups.get(&parent).copied() {
1477            Some(group) if shares_fs => group,
1478            parent_group => {
1479                let changed = parent_group.is_some_and(|group| self.changed.contains(&group));
1480                let group = self.new_group(changed);
1481                if shares_fs {
1482                    // A creator Detcore had not seen created shares the new
1483                    // group from now on.
1484                    self.groups.insert(parent, group);
1485                }
1486                group
1487            }
1488        };
1489        self.groups.insert(child, group);
1490    }
1491
1492    fn mark_changed(&mut self, task: DetPid) {
1493        let group = match self.groups.get(&task).copied() {
1494            Some(group) => group,
1495            None => {
1496                let group = self.new_group(false);
1497                self.groups.insert(task, group);
1498                group
1499            }
1500        };
1501        self.changed.insert(group);
1502    }
1503
1504    fn changed(&self, task: DetPid) -> bool {
1505        self.groups
1506            .get(&task)
1507            .is_some_and(|group| self.changed.contains(group))
1508    }
1509
1510    fn transfer(&mut self, former: DetPid, current: DetPid) {
1511        if let Some(group) = self.groups.remove(&former) {
1512            self.groups.insert(current, group);
1513        }
1514    }
1515
1516    fn forget(&mut self, task: DetPid) {
1517        if let Some(group) = self.groups.remove(&task)
1518            && !self.groups.values().any(|other| *other == group)
1519        {
1520            self.changed.remove(&group);
1521        }
1522    }
1523}
1524
1525/// A process's `/proc/<pid>/auxv` ([`ProcessAuxvTable::current`]).
1526#[derive(PartialEq, Debug, Eq, Clone, Serialize, Deserialize)]
1527pub struct ProcessAuxv {
1528    /// The process, which a thread's number names too.
1529    pub process: DetPid,
1530    /// Its current image's auxiliary vector.
1531    pub bytes: Vec<u8>,
1532}
1533
1534/// Why an image's `/proc/<pid>/auxv` is left to the kernel beneath
1535/// ([`ProcessAuxvTable::first_kernel_file`]).
1536#[derive(PartialEq, Debug, Eq, Clone, Copy, Serialize, Deserialize)]
1537pub enum AuxvFromKernel {
1538    /// The backend reports no initial stack pointer.
1539    NoInitialStackPointer,
1540    /// The initial stack held no plausible auxiliary vector.
1541    Implausible,
1542}
1543
1544impl ProcessAuxvTable {
1545    /// Record `bytes`, the auxiliary vector `process`'s new image found on its
1546    /// initial stack, as its current vector.
1547    pub(crate) fn record(&mut self, process: DetPid, bytes: Vec<u8>) {
1548        self.processes.insert(process, bytes);
1549    }
1550
1551    /// `process`'s current image's auxiliary vector.
1552    pub(crate) fn current(&self, process: DetPid) -> Option<ProcessAuxv> {
1553        Some(ProcessAuxv {
1554            process,
1555            bytes: self.processes.get(&process)?.clone(),
1556        })
1557    }
1558
1559    /// A new process `child` runs its creator `parent`'s image until it
1560    /// execs: its vector, and its argument and environment areas, at the same
1561    /// addresses in its copy of its creator's memory.
1562    pub(crate) fn inherit(&mut self, parent: DetPid, child: DetPid) {
1563        match self.processes.get(&parent).cloned() {
1564            Some(bytes) => self.processes.insert(child, bytes),
1565            None => self.processes.remove(&child),
1566        };
1567        match self.cmdline_areas.get(&parent).copied() {
1568            Some(area) => self.cmdline_areas.insert(child, area),
1569            None => self.cmdline_areas.remove(&child),
1570        };
1571    }
1572
1573    /// Record `area`, the argument and environment areas of `process`'s new
1574    /// image, as its current ones; `None` where Detcore read none (the
1575    /// backend reports no initial stack pointer, or the stack held no
1576    /// plausible layout), which drops what it held before the exec, so that
1577    /// its `/proc/<pid>/cmdline` is the kernel's.
1578    pub(crate) fn record_cmdline(
1579        &mut self,
1580        process: DetPid,
1581        area: Option<crate::procfs::CmdlineArea>,
1582    ) {
1583        match area {
1584            Some(area) => self.cmdline_areas.insert(process, area),
1585            None => self.cmdline_areas.remove(&process),
1586        };
1587    }
1588
1589    /// `process`'s current image's argument and environment areas, if
1590    /// Detcore recorded them.
1591    pub(crate) fn current_cmdline(&self, process: DetPid) -> Option<crate::procfs::CmdlineArea> {
1592        self.cmdline_areas.get(&process).copied()
1593    }
1594
1595    /// A new task `child` of `parent`: it shares `parent`'s filesystem group
1596    /// (its root) with `CLONE_FS`, and starts a group of its own with a copy
1597    /// of `parent`'s root without it, as Linux's `copy_fs` does.
1598    pub(crate) fn clone_root(&mut self, parent: DetPid, child: DetPid, shares_fs: bool) {
1599        self.roots.clone_task(parent, child, shares_fs);
1600    }
1601
1602    /// `task` changed its root (a successful `chroot`): so did every task in
1603    /// its filesystem group.
1604    pub(crate) fn mark_changed_root(&mut self, task: DetPid) {
1605        self.roots.mark_changed(task);
1606    }
1607
1608    /// Whether `task`'s root has changed.
1609    pub(crate) fn root_changed(&self, task: DetPid) -> bool {
1610        self.roots.changed(task)
1611    }
1612
1613    /// A successful exec by the non-leader thread `former` gave it its
1614    /// process's number `current` (Linux's `de_thread`); its filesystem group
1615    /// goes with it. An exec does not detach a shared filesystem group:
1616    /// Linux's `check_unsafe_exec` only marks such an exec unsafe for set-id
1617    /// programs, and `begin_new_exec` unshares the descriptor table, not
1618    /// `fs_struct`.
1619    pub(crate) fn transfer_root(&mut self, former: DetPid, current: DetPid) {
1620        self.roots.transfer(former, current);
1621    }
1622
1623    /// `process` is gone, or its new image has no vector Detcore recorded
1624    /// (whatever it held before the exec, its own earlier image's or its
1625    /// creator's, is stale), so its file is the kernel's. Descriptors already
1626    /// open keep their snapshots.
1627    pub(crate) fn forget(&mut self, process: DetPid) {
1628        self.processes.remove(&process);
1629    }
1630
1631    /// `process` is gone: none of its facts stay.
1632    pub(crate) fn forget_process(&mut self, process: DetPid) {
1633        self.processes.remove(&process);
1634        self.cmdline_areas.remove(&process);
1635        self.roots.forget(process);
1636    }
1637
1638    /// How many processes have a current vector.
1639    #[cfg(test)]
1640    pub(crate) fn len(&self) -> usize {
1641        self.processes.len()
1642    }
1643
1644    /// Whether this is the run's first image whose `/proc/<pid>/auxv` is left
1645    /// to the kernel beneath for `why`, which the run warns of once.
1646    pub(crate) fn first_kernel_file(&mut self, why: AuxvFromKernel) -> bool {
1647        let index = match why {
1648            AuxvFromKernel::NoInitialStackPointer => 0,
1649            AuxvFromKernel::Implausible => 1,
1650        };
1651        !std::mem::replace(&mut self.kernel_file_warned[index], true)
1652    }
1653}
1654
1655impl ProcessExitCleanup {
1656    /// Complete the exit `watch` observed, unless a later registration has
1657    /// replaced it. Nothing calls this in production yet; the daemon-point
1658    /// observation of <https://github.com/rrnewton/hermit/issues/4362> part 2
1659    /// will (2ee7190125's free-running observer was reverted).
1660    #[cfg_attr(not(test), allow(dead_code))]
1661    fn complete_observed_locked(
1662        &self,
1663        sched: &mut Scheduler,
1664        watch: &Arc<crate::scheduler::ProcessExitWatch>,
1665    ) -> Option<(bool, Option<String>)> {
1666        if !sched.process_exit_watch_is_current(watch) {
1667            return None;
1668        }
1669        sched.finish_process_exit_watch(watch);
1670        Some(self.complete_locked(sched, watch.process, true))
1671    }
1672
1673    fn complete_locked(
1674        &self,
1675        sched: &mut Scheduler,
1676        process: DetPid,
1677        consume_unreported: bool,
1678    ) -> (bool, Option<String>) {
1679        let loss = (consume_unreported && sched.has_unreported_exit(process)).then(|| {
1680            let reason = format!(
1681                "process {process} exited without deregistering; its exit was not scheduled"
1682            );
1683            // Publish refusal before cancellation can wake a request. Normal
1684            // deregistration and this entire operation share the sched lock.
1685            crate::detlog::record_determinism_loss(&reason);
1686            reason
1687        });
1688        self.pending_exec_states.lock().unwrap().remove(&process);
1689        self.completed_exec_transfers
1690            .lock()
1691            .unwrap()
1692            .remove(&process);
1693        self.post_exec_fd_blocking.lock().unwrap().remove(&process);
1694        self.exec_images.lock().unwrap().remove(&process);
1695        self.process_auxv.lock().unwrap().forget_process(process);
1696        if consume_unreported {
1697            let consumed = sched.consume_unreported_exit(process);
1698            debug_assert_eq!(consumed, loss.is_some());
1699        }
1700        self.shared_open_files
1701            .lock()
1702            .unwrap()
1703            .end_leases_of(process);
1704        (sched.complete_physical_process_exit(process), loss)
1705    }
1706}
1707
1708impl Default for GlobalState {
1709    fn default() -> Self {
1710        // TODO(T77816673): eventually we want to remove this requirement.
1711        // In the meantime... just don't call this.
1712        panic!("Detcore GlobalState Default impl should not be called");
1713    }
1714}
1715
1716impl Drop for GlobalState {
1717    fn drop(&mut self) {
1718        // TODO-HUMAN-REVIEW(PR-643): Review shutdown-time aggregate warning delivery.
1719        if let Some(message) =
1720            format_unsupported_syscall_warning(&self.unsupported_syscalls.lock().unwrap())
1721        {
1722            warn!("{}", message);
1723        }
1724        info!("detcore shut down, destroying global state");
1725    }
1726}
1727
1728impl GlobalState {
1729    /// Transfer the completed run's diagnostic aggregate to its output owner.
1730    /// The backend must first reclaim exclusive ownership after all RPC clients
1731    /// have drained. Callers that do not take it retain the ordinary Drop warning.
1732    pub fn take_unsupported_syscalls(&mut self) -> BTreeSet<String> {
1733        std::mem::take(self.unsupported_syscalls.get_mut().unwrap())
1734    }
1735
1736    /// Take a store-exclusive report. It carries no time and touches no
1737    /// scheduler state, so where it lands among other requests changes
1738    /// nothing a run compares.
1739    #[cfg(target_arch = "aarch64")]
1740    fn note_store_exclusive(
1741        &self,
1742        report: &crate::store_exclusive::Report,
1743    ) -> (Option<LogicalTime>, GlobalResponse) {
1744        self.store_exclusive.lock().unwrap().note(&self.cfg, report);
1745        (None, GlobalResponse::StoreExclusive(()))
1746    }
1747
1748    /// Ordinary RPC mutation must linearize before terminal publication under
1749    /// the same mutex as scheduler grants. A losing callback stays pending for
1750    /// the backend's failure subscription to drop; no normal reply is invented.
1751    /// Consuming exit RPCs retain their existing validation/accounting path.
1752    async fn lock_rpc_scheduler(
1753        &self,
1754        consuming_cleanup: bool,
1755    ) -> std::sync::MutexGuard<'_, Scheduler> {
1756        std::future::poll_fn(|_| {
1757            let sched = self.sched.lock().unwrap();
1758            if !consuming_cleanup && sched.backend_failed() {
1759                Poll::Pending
1760            } else {
1761                Poll::Ready(sched)
1762            }
1763        })
1764        .await
1765    }
1766
1767    /// The deterministic numbers of the files whose last name the run removed
1768    /// while they were numbered and that no later name showed to be reused
1769    /// (see `InodePool::retire`), in increasing order. Diagnostic, for tests
1770    /// of the removal paths (<https://github.com/rrnewton/hermit/issues/3840>).
1771    pub fn retired_inodes(&self) -> Vec<DetInode> {
1772        let mut retired: Vec<DetInode> = self
1773            .inodes
1774            .lock()
1775            .unwrap()
1776            .retired
1777            .values()
1778            .copied()
1779            .collect();
1780        retired.sort();
1781        retired
1782    }
1783
1784    /// How many retired inode numbers a later name or `open` showed the host
1785    /// had given to another file. Diagnostic, for tests that make a host reuse
1786    /// an inode (<https://github.com/rrnewton/hermit/issues/3840>).
1787    pub fn reused_inode_count(&self) -> u64 {
1788        self.inodes.lock().unwrap().reused
1789    }
1790
1791    /// Return the producer-observed mount identity order after a run.
1792    ///
1793    /// The first vector is the exact mountinfo row/parent order. The second is
1794    /// the order in which the run numbered raw mount IDs.
1795    pub fn mount_identity_provenance(
1796        &self,
1797    ) -> Result<Option<MountIdentityProvenance>, &'static str> {
1798        self.mount_ids.lock().unwrap().provenance()
1799    }
1800
1801    /// A global state with no scheduler task, for unit tests elsewhere in the
1802    /// crate that send RPCs to it directly.
1803    #[cfg(test)]
1804    pub(crate) fn for_tests(cfg: &Config) -> Self {
1805        Self::initialize(cfg, false)
1806    }
1807
1808    fn initialize(cfg: &Config, spawn_scheduler: bool) -> Self {
1809        // GlobalTool-only construction has erased the inner Tool type. It
1810        // still rejects raw binary-mode Config; typed facility_admission is
1811        // independently required to authenticate the actual stream role.
1812        crate::record_or_replay::admit_chosen_input_stream(cfg, reverie::ToolStreamRole::Plain);
1813        if let Some(refusal) = crate::preemptions::annotated_replay_mode_refusal(cfg) {
1814            crate::scheduler::exit_on_scheduler_refusal(&refusal);
1815        }
1816        let sched = Arc::new(Mutex::new(Scheduler::new(cfg)));
1817        let global_time = Arc::new(Mutex::new(GlobalTime::new(cfg)));
1818        let process_exit_cleanup = ProcessExitCleanup::default();
1819
1820        let preemptions_to_replay: Option<HistoryReader> =
1821            cfg.replay_preemptions_from.as_ref().map(|path| {
1822                // Keep ordinary file/JSON errors in their original panic class.
1823                // This is a separate authoritative input from the scheduler
1824                // replay above. Validate both when raw Config names both;
1825                // never authorize one through an `.or(...)` of their paths.
1826                let history =
1827                    HistoryReader::read_for_replay(path, cfg.guest_ids).unwrap_or_else(|error| {
1828                        if error
1829                            .chain()
1830                            .any(|cause| cause.is::<crate::preemptions::RecordAdmissionRefusal>())
1831                        {
1832                            crate::scheduler::exit_on_scheduler_refusal(&format!("{error:#}"));
1833                        }
1834                        panic!("{error:?}")
1835                    });
1836                history
1837                    .require_replay_mode(cfg.replay_edited_preemptions)
1838                    .unwrap_or_else(|error| crate::scheduler::exit_on_scheduler_refusal(&error));
1839                // A recording whose run retried store-exclusives may not
1840                // replay; say so before the replay starts.
1841                #[cfg(target_arch = "aarch64")]
1842                crate::store_exclusive::warn_at_replay_start(history.store_exclusive_failures());
1843                // Inherit provenance from the very history consumed below,
1844                // before any guest event or new recording can be emitted.
1845                if let Some(event) = history.replay_departed_at() {
1846                    tracing::warn!(
1847                        "HERMIT_INCOMPLETE_REPLAY_RECORD: explicit edited history exploration of {} \
1848                         retains replay_departed_at={}; it does not certify faithful replay",
1849                        path.display(), event,
1850                    );
1851                    if let Some(writer) = &mut sched.lock().unwrap().preemption_writer {
1852                        writer.mark_replay_departed(event);
1853                    }
1854                }
1855                // The scheduler takes the same record's callback checkpoints,
1856                // and checks them against the launch decision, before its
1857                // loop runs or any thread starts.
1858                sched
1859                    .lock()
1860                    .unwrap()
1861                    .install_priority_checkpoints(history.callback_checkpoints())
1862                    .unwrap_or_else(|error| crate::scheduler::exit_on_scheduler_refusal(&error));
1863                history
1864            });
1865        if preemptions_to_replay.is_none() && cfg.annotated_priority_replay {
1866            crate::scheduler::exit_on_scheduler_refusal(
1867                &"HERMIT_CALLBACK_CHECKPOINT_UNQUALIFIED_MODE: the run was prepared to replay a \
1868                  callback-annotated priority history, but names no --replay-preemptions-from",
1869            );
1870        }
1871
1872        let handle = if cfg.sequentialize_threads && spawn_scheduler {
1873            // Announce before spawning, not from inside the spawned task. The
1874            // task's first poll is unordered with respect to the rest of this
1875            // bootstrap, so emitting there raced the root thread's seeding
1876            // lines and produced a nondeterministic INFO stream. Emitting here
1877            // sequences it: `Scheduler::new`'s SCHEDRAND line above, then this,
1878            // then the root `ThreadState::new` seeding lines.
1879            info!("[scheduler] daemon task starting up, waiting for guest thread start..");
1880            Some(tokio::spawn(crate::scheduler::exit_on_scheduler_panic(
1881                sched_loop(sched.clone(), global_time.clone()),
1882            )))
1883        } else {
1884            None
1885        };
1886
1887        // A replayed recording that names the host files its run opened:
1888        // this run's opens are compared against them.
1889        let replay_host_inputs = cfg
1890            .record_host_inputs
1891            .then(|| {
1892                preemptions_to_replay
1893                    .as_ref()
1894                    .and_then(HistoryReader::host_inputs)
1895                    .cloned()
1896                    .or_else(|| sched.lock().unwrap().recorded_host_inputs.clone())
1897            })
1898            .flatten()
1899            .map(|recorded| Mutex::new(crate::host_inputs::ReplayHostInputs::new(recorded)));
1900        let range = Self::read_port_range();
1901        let network = Self::network_engine(cfg).map(Mutex::new);
1902
1903        let unsupported_syscall_report_fd = cfg.unsupported_syscall_report_fd.and_then(|fd| {
1904            // This writer is internal controller state. In an in-process DBT
1905            // runtime it must not leak into the next guest image across exec
1906            // (hence F_DUPFD_CLOEXEC), and it must not perturb the descriptor
1907            // namespace the *current* guest observes. The backend places the
1908            // report fd itself high, out of the guest's working range (e.g. 199
1909            // for the DBT backend). Duplicating with a min hint of `fd` keeps
1910            // this private copy up in that same reserved band instead of
1911            // grabbing the lowest free descriptor (fd 3), which would shift
1912            // every fd the guest subsequently opens and diverge from the golden
1913            // ptrace reference (where this fd is unset and no dup happens).
1914            let duplicate = unsafe { libc::fcntl(fd, libc::F_DUPFD_CLOEXEC, fd) };
1915            if duplicate == -1 {
1916                warn!(
1917                    "failed to duplicate unsupported-syscall report fd {fd}: {}",
1918                    std::io::Error::last_os_error()
1919                );
1920                None
1921            } else {
1922                // SAFETY: dup returned a new owned descriptor.
1923                Some(Mutex::new(unsafe { File::from_raw_fd(duplicate) }))
1924            }
1925        });
1926
1927        Self {
1928            sched,
1929            #[cfg(feature = "physical-mm-test-support")]
1930            initial_mm_probe: Mutex::new(None),
1931            next_port: AtomicU16::new(range[0]),
1932            next_guest_id: AtomicI32::new(
1933                cfg.guest_ids
1934                    .base()
1935                    .filter(|base| *base > 0)
1936                    .and_then(|base| base.checked_add(1))
1937                    .unwrap_or(0),
1938            ),
1939            host_id_tasks: AtomicU64::new(0),
1940            used_ports: Mutex::new(HashSet::new()),
1941            unsupported_syscalls: Mutex::new(BTreeSet::new()),
1942            syscall_observation_gaps: Mutex::new(BTreeSet::new()),
1943            #[cfg(target_arch = "aarch64")]
1944            store_exclusive: Mutex::new(Default::default()),
1945            announced_handlers_not_installed: Mutex::new(BTreeSet::new()),
1946            announced_refused_signal_controls: Mutex::new((0, false)),
1947            host_diskstats: std::sync::OnceLock::new(),
1948            procfs_sequences: Mutex::new(BTreeMap::new()),
1949            policy_refusal: Mutex::new(None),
1950            host_inputs: Mutex::new(Vec::new()),
1951            host_mutations: Mutex::new(BTreeSet::new()),
1952            replay_host_inputs,
1953            host_input_sources: Mutex::new(Vec::new()),
1954            host_inputs_settled: AtomicBool::new(false),
1955            shared_open_files: process_exit_cleanup.shared_open_files,
1956            unsupported_syscall_report_fd,
1957            port_start_range: AtomicU16::new(range[0]),
1958            port_end_range: AtomicU16::new(range[1]),
1959            open_file_to_port: Mutex::new(HashMap::new()),
1960            inotify_cookies: Mutex::new(Default::default()),
1961            past_first_execve: AtomicBool::new(false),
1962            initial_descriptor_image_complete: AtomicBool::new(false),
1963            inherited_input: Mutex::new(None),
1964            mapping_capture_run: None,
1965            mapping_capture_pending: Mutex::new(None),
1966            pending_exec_states: process_exit_cleanup.pending_exec_states,
1967            completed_exec_transfers: process_exit_cleanup.completed_exec_transfers,
1968            post_exec_fd_blocking: process_exit_cleanup.post_exec_fd_blocking,
1969            exec_images: process_exit_cleanup.exec_images,
1970            process_auxv: process_exit_cleanup.process_auxv,
1971            lock_ledger: Mutex::new(Default::default()),
1972            held_tables: Mutex::new(BTreeMap::new()),
1973            root_dettid: OnceLock::new(),
1974            root_image_marked: AtomicBool::new(false),
1975            launcher_identity: OnceLock::new(),
1976            native_exec_registry: OnceLock::new(),
1977            inodes: Arc::new(Mutex::new(InodePool::new())),
1978            // AUTONOMOUS-BOT-IMPLEMENTED
1979            // TODO-HUMAN-REVIEW(PR-1056): Deterministic st_dev remapping state.
1980            devices: Arc::new(Mutex::new(if cfg.recordreplay_modes {
1981                DevicePool::new()
1982            } else {
1983                DevicePool::for_this_mount_namespace()
1984            })),
1985            mount_ids: Mutex::new(MountIdPool::from_config(
1986                &cfg.mountinfo_mount_ids,
1987                cfg.mountinfo_mount_ids_captured,
1988                &cfg.mount_id_assignment_order,
1989            )),
1990            sched_handle: handle,
1991            cfg: cfg.clone(),
1992            realtime_start: SystemTime::now(),
1993            stopless_slice_ends_at_start: crate::STOPLESS_SLICE_ENDS
1994                .load(std::sync::atomic::Ordering::Relaxed),
1995            global_time,
1996            preemptions_to_replay,
1997            network,
1998        }
1999    }
2000
2001    /// The network engine for the configured trace mode. The CLI validates a
2002    /// replay trace before launch; failing here is a backstop.
2003    fn network_engine(cfg: &Config) -> Option<NetworkEngine> {
2004        let path = cfg.network_trace.path.as_ref();
2005        match cfg.network_trace.mode {
2006            NetworkTraceMode::Off => None,
2007            NetworkTraceMode::Record => Some(NetworkEngine::new_record(cfg.epoch)),
2008            NetworkTraceMode::Replay => {
2009                let path = path.expect("network trace replay requires a trace path");
2010                let trace = File::open(path)
2011                    .map_err(|error| error.to_string())
2012                    .and_then(|mut file| {
2013                        NetworkTraceV2::read_framed(&mut file).map_err(|error| error.to_string())
2014                    })
2015                    .unwrap_or_else(|error| {
2016                        panic!("cannot read network trace {}: {error}", path.display())
2017                    });
2018                Some(NetworkEngine::new_replay(trace))
2019            }
2020        }
2021    }
2022
2023    /// Close the access trace (`--record-access-trace-to`): the calls that
2024    /// never returned, then the end line that marks it complete. The trace
2025    /// is an observation, so a failure to finish it is reported and leaves
2026    /// the run's outcome alone; the missing end line tells a reader.
2027    fn finish_access_trace(&mut self) {
2028        let (writer, events) = {
2029            let mut sched = self.sched.lock().unwrap();
2030            (sched.access_trace.take(), sched.recorded_event_count)
2031        };
2032        // Output I/O runs without the scheduler mutex.
2033        if let Some(writer) = writer
2034            && let Err(error) = writer.finish(events)
2035        {
2036            let path = self.cfg.access_trace.as_ref().map(|p| p.display());
2037            warn!(
2038                "hermit: the access trace {} is incomplete: {error}",
2039                path.map_or_else(String::new, |p| p.to_string())
2040            );
2041        }
2042    }
2043
2044    /// Write the network trace of a record run, or check that a replay used
2045    /// the whole recording. Returns the refusal a replay ends with.
2046    fn finish_network_trace(&mut self) -> Option<String> {
2047        let engine = self.network.take()?;
2048        let engine = engine.into_inner().unwrap();
2049        if !engine.is_record() {
2050            return engine
2051                .finish_replay()
2052                .err()
2053                .map(|error| format!("hermit: {error}. {}", error.remedy()));
2054        }
2055        let path = self
2056            .cfg
2057            .network_trace
2058            .path
2059            .as_ref()
2060            .expect("network trace recording requires a trace path");
2061        // Encode before opening the output, so a trace that cannot be encoded
2062        // leaves the file as it was instead of truncated.
2063        let result = engine
2064            .finish()
2065            .map_err(|error| error.to_string())
2066            .and_then(|trace| trace.encode_framed().map_err(|error| error.to_string()))
2067            .and_then(|bytes| {
2068                let mut file = File::create(path).map_err(|error| error.to_string())?;
2069                file.write_all(&bytes).map_err(|error| error.to_string())
2070            });
2071        if let Err(error) = result {
2072            panic!("cannot write network trace {}: {error}", path.display());
2073        }
2074        None
2075    }
2076
2077    /// Initializes global state whose sequential scheduler is driven by an
2078    /// external backend executor.
2079    pub fn init_for_external_scheduler(cfg: &Config) -> Self {
2080        assert!(
2081            cfg.sequentialize_threads,
2082            "an external scheduler is only meaningful when threads are sequentialized"
2083        );
2084        Self::initialize(cfg, false)
2085    }
2086
2087    /// Returns the sequential scheduler loop for a backend-owned executor to run.
2088    ///
2089    /// This is deliberately not an `async fn`: the announcement is logged when
2090    /// this is called, not when the returned future is first polled. A backend
2091    /// can then order it ahead of anything it releases before polling (DBT
2092    /// publishes `READY_IMAGE`, which lets the root guest thread log its
2093    /// seeding lines). See <https://github.com/rrnewton/hermit/issues/3463>.
2094    pub fn run_external_scheduler(
2095        &self,
2096        observer: Arc<dyn Fn(&'static str) + Send + Sync>,
2097    ) -> impl Future<Output = ()> + use<> {
2098        // Emitted at the call site for the same reason as the spawned path in
2099        // `initialize`, so both ways of starting the daemon place this line at
2100        // a deterministic point in the caller's program order.
2101        info!("[scheduler] daemon task starting up, waiting for guest thread start..");
2102        sched_loop_external(self.sched.clone(), self.global_time.clone(), observer)
2103    }
2104
2105    /// Reports that a backend supervisor received a process's final kernel exit status.
2106    ///
2107    /// This records a barrier observation when the backend advertises physical-exit
2108    /// reporting or asynchronous exit completion; it is a no-op for ptrace, DBT and KVM.
2109    /// The exact process's barrier is released at this physical-waitability boundary.
2110    ///
2111    /// With asynchronous exit completion, registrations the process did not deregister
2112    /// itself are retired here first, in the same locked step, and a determinism loss is
2113    /// recorded: such a process died without finishing its exit hook, at a moment the host
2114    /// chose, so the run must not be compared.
2115    pub fn complete_physical_process_exit(&self, raw_pid: i32) {
2116        let detpid = DetPid::from_raw(raw_pid);
2117        let (completed, loss) = {
2118            let mut sched = self.sched.lock().unwrap();
2119            self.process_exit_cleanup().complete_locked(
2120                &mut sched,
2121                detpid,
2122                self.cfg.backend.process_exits_complete_asynchronously,
2123            )
2124        };
2125        if completed {
2126            trace!(
2127                "[detcore, dpid {}] backend completed final physical process exit",
2128                detpid
2129            );
2130        }
2131        if let Some(loss) = loss {
2132            crate::detlog::write_loss_notice(&loss);
2133        }
2134    }
2135
2136    fn process_exit_cleanup(&self) -> ProcessExitCleanup {
2137        ProcessExitCleanup {
2138            shared_open_files: self.shared_open_files.clone(),
2139            pending_exec_states: self.pending_exec_states.clone(),
2140            completed_exec_transfers: self.completed_exec_transfers.clone(),
2141            post_exec_fd_blocking: self.post_exec_fd_blocking.clone(),
2142            exec_images: self.exec_images.clone(),
2143            process_auxv: self.process_auxv.clone(),
2144        }
2145    }
2146
2147    /// The device and inode of the launcher `Config::launch_image` names,
2148    /// the file the kernel's `exe` link names in a launched guest's place.
2149    fn launcher_identity(&self) -> Option<(u64, u64)> {
2150        *self.launcher_identity.get_or_init(|| {
2151            use std::os::unix::fs::MetadataExt;
2152            let launcher = self.cfg.launch_image.as_ref()?.launcher.as_ref()?;
2153            let meta = std::fs::metadata(launcher).ok()?;
2154            Some((meta.dev(), meta.ino()))
2155        })
2156    }
2157
2158    /// Answers a control message about a shared open file description. Only
2159    /// the thread that holds the scheduler's serial grant may send one, so
2160    /// access to a shared description is exclusive and happens in canonical
2161    /// order (`crate::shared_open_files`).
2162    ///
2163    /// A refusal is an infrastructure failure, not something the guest's
2164    /// syscall can report: the caller's process stops, and this reports a
2165    /// backend failure, which ends the whole run before any further grant.
2166    fn shared_open_file_control(
2167        &self,
2168        thread: DetTid,
2169        control: OpenFileControl,
2170    ) -> Result<OpenFileControlReply, SharedOpenFileError> {
2171        // The refusal and the failure it reports are one locked step, so the
2172        // scheduler can grant no other thread in between.
2173        let (result, wakes) = {
2174            let mut sched = self.sched.lock().unwrap();
2175            let process = sched.registered_process(thread);
2176            let result = if !sched.holds_serial_grant(thread) {
2177                Err(SharedOpenFileError(format!(
2178                    "thread {thread} sent {control:?} without holding the serial grant"
2179                )))
2180            } else if let Some(process) = process {
2181                self.shared_open_files
2182                    .lock()
2183                    .unwrap()
2184                    .handle(process, thread, control)
2185            } else {
2186                Err(SharedOpenFileError(format!(
2187                    "thread {thread} belongs to no registered process"
2188                )))
2189            };
2190            let wakes = result.as_ref().err().map(|error| {
2191                tracing::error!("[detcore] shared open file control refused: {error}");
2192                let wake = sched.report_backend_failure(reverie::BackendFailure {
2193                    pid: reverie::Pid::from_raw(process.unwrap_or(thread).as_raw()),
2194                    tid: Tid::from_raw(thread.as_raw()),
2195                    phase: "shared open file control",
2196                });
2197                (wake, sched.take_signal_failure_wakes())
2198            });
2199            (result, wakes)
2200        };
2201        // Notified after the lock is released, as `report_backend_failure`
2202        // does.
2203        if let Some((wake, deferred)) = wakes {
2204            for wake in deferred {
2205                let _ = wake.send(());
2206            }
2207            if let Some(wake) = wake {
2208                let _ = wake.send(());
2209            }
2210        }
2211        result
2212    }
2213
2214    /// Releases all physical-process-exit barriers after a backend supervisor has drained every
2215    /// tracee and no guest thread can race another lifecycle event.
2216    pub fn release_all_physical_process_exits(&self) {
2217        self.pending_exec_states.lock().unwrap().clear();
2218        self.completed_exec_transfers.lock().unwrap().clear();
2219        self.post_exec_fd_blocking.lock().unwrap().clear();
2220        self.exec_images.lock().unwrap().clear();
2221        *self.process_auxv.lock().unwrap() = ProcessAuxvTable::default();
2222        let released = self
2223            .sched
2224            .lock()
2225            .unwrap()
2226            .release_all_physical_process_exits();
2227        if released != 0 {
2228            trace!("released {released} final physical process-exit barrier(s)");
2229        }
2230    }
2231
2232    /// Record that the run's syscall histogram cannot count every syscall the
2233    /// guest makes, for `gap`. Its summary then reports a partial observation
2234    /// scope.
2235    pub fn note_syscall_observation_gap(&self, gap: SyscallObservationGap) {
2236        self.syscall_observation_gaps.lock().unwrap().insert(gap);
2237    }
2238
2239    /// Why the fail-closed policy ended the run, when a Tool image reported
2240    /// it before exiting (`GlobalRequest::PolicyRefusal`).
2241    pub fn policy_refusal(&self) -> Option<String> {
2242        self.policy_refusal.lock().unwrap().clone()
2243    }
2244
2245    /// Unrecoverable fatal erorr. Bring things to a close cleanly, but as quickly as
2246    /// possible.
2247    pub fn force_shutdown_with_error(&self) {
2248        let start = std::time::Instant::now();
2249        let sched = loop {
2250            if start.elapsed().as_millis() > 1000 {
2251                eprintln!(
2252                    "Could not acquire scheduler lock during forced shutdown (timeout)... proceeding anyway."
2253                );
2254                return;
2255            }
2256            match self.sched.try_lock() {
2257                Ok(guard) => {
2258                    break guard;
2259                }
2260                Err(std::sync::TryLockError::WouldBlock) => {
2261                    std::thread::yield_now();
2262                    continue;
2263                }
2264                Err(e) => {
2265                    eprintln!(
2266                        "Could not acquire scheduler lock during forced shutdown ({})... proceeding anyway.",
2267                        e
2268                    );
2269                    return;
2270                }
2271            }
2272        };
2273        info!("Scheduler state at exit:\n{}", sched.full_summary());
2274    }
2275
2276    // AUTONOMOUS-BOT-IMPLEMENTED
2277    // TODO-HUMAN-REVIEW(PR-744): Review explicit abnormal-backend scheduler cancellation.
2278    /// Cancels the internally spawned scheduler task after a backend guest exits abnormally.
2279    ///
2280    /// External-scheduler states do not own a task and are left unchanged.
2281    pub async fn cancel_internal_scheduler(&mut self) {
2282        if let Some(handle) = self.sched_handle.take() {
2283            handle.abort();
2284            match handle.await {
2285                Ok(()) => {}
2286                Err(error) if error.is_cancelled() => {}
2287                Err(error) => panic!("cancelled scheduler task panicked: {error}"),
2288            }
2289        }
2290    }
2291
2292    /// The terminal outcome this run ended with, when its backend unwound the
2293    /// guest for it (`TerminalOutcomeUnwindGuard`): the status the run
2294    /// completed with is then the outcome's, not one the guest exited with.
2295    pub fn terminal_outcome(&self) -> Option<crate::TerminalOutcome> {
2296        self.sched.lock().unwrap().terminal_outcome()
2297    }
2298
2299    /// Consume failed-run state after the scheduler has naturally finished.
2300    ///
2301    /// A failed run has no successful run summary. Preserve the scheduler's
2302    /// join error and any requested partial preemption recording's write error
2303    /// for the caller, without allowing either to replace the backend failure.
2304    pub async fn clean_up_after_backend_failure(mut self) -> BackendFailureCleanup {
2305        // From here a scheduler panic is reported beside the backend's
2306        // failure, as its JoinError below, not by ending the process.
2307        crate::scheduler::note_backend_failure_cleanup();
2308        let scheduler = if let Some(handle) = self.sched_handle.take() {
2309            handle.await
2310        } else {
2311            Ok(())
2312        };
2313        // A scheduler panic can poison this mutex. Its JoinError is returned
2314        // below; recovering only to finish output must not replace that error.
2315        let mut writer = self
2316            .sched
2317            .lock()
2318            .unwrap_or_else(std::sync::PoisonError::into_inner)
2319            .preemption_writer
2320            .take();
2321        if let (Some(writer), true) = (writer.as_mut(), self.cfg.record_preemptions_to.is_some())
2322            && let Some(inputs) = self.host_input_set()
2323        {
2324            writer.set_host_inputs(inputs);
2325        }
2326        let preemption_recording = if self.cfg.record_preemptions_to.is_some() {
2327            writer.map_or(Ok(()), |writer| writer.flush())
2328        } else {
2329            // The writer's destination is fixed from this same configuration.
2330            // In-memory-only recordings have no Drop write to finish.
2331            drop(writer);
2332            Ok(())
2333        };
2334        BackendFailureCleanup {
2335            scheduler,
2336            preemption_recording,
2337        }
2338    }
2339
2340    /// Shut down anything running, in particular wait on the scheduler.
2341    ///
2342    /// This is basically the destructor for the global state, but is here rather than in the
2343    /// Drop instance so that it can be async, and is more explicitly sequenced in the program.
2344    ///
2345    /// Print a summary of the execution, typically called when it is complete.
2346    ///
2347    /// If the boolean argument is true, print to stderr, otherwise only print the summary
2348    /// to the log.
2349    ///
2350    /// Returns terminal replay, recording, or summary errors after consuming
2351    /// the remaining recording writer. Callers retain their outer owners until
2352    /// this returns and must not publish completed metadata on failure.
2353    pub async fn clean_up(
2354        self,
2355        to_stderr: bool,
2356        print_summary_to_json_file: &Option<PathBuf>,
2357    ) -> anyhow::Result<()> {
2358        self.clean_up_with_dispatch_stats(to_stderr, print_summary_to_json_file, None)
2359            .await
2360    }
2361
2362    /// [`Self::clean_up`], also carrying the backend's dispatch record into the
2363    /// machine-readable summary. The human-readable report, including the INFO
2364    /// view `--verify` compares, never shows it.
2365    pub async fn clean_up_with_dispatch_stats(
2366        mut self,
2367        to_stderr: bool,
2368        print_summary_to_json_file: &Option<PathBuf>,
2369        dispatch_stats: Option<reverie::DispatchStats>,
2370    ) -> anyhow::Result<()> {
2371        if let Some(handle) = self.sched_handle.take() {
2372            debug!("Global state cleanup, confirming scheduler has shut down...");
2373            handle.await.expect("Global scheduler clean shutdown");
2374            debug!("Global state cleanup, continuing...");
2375        }
2376        let departure = {
2377            let scheduler = self.sched.lock().unwrap();
2378            let summary = scheduler.replay_departure().cloned();
2379            let publication_refusal = scheduler.replay_publication_refusal();
2380            (summary.is_some() || publication_refusal.is_some()).then_some(
2381                crate::ReplayDepartureError {
2382                    summary,
2383                    publication_refusal,
2384                },
2385            )
2386        };
2387        let network_refusal = self.finish_network_trace();
2388        self.finish_access_trace();
2389        let banner =
2390            "  ------------------------------ hermit run report ------------------------------";
2391        let recording_destination = self.cfg.record_preemptions_to.clone();
2392        // The run is over: write what it opened for --verify.
2393        if let Some(log) = &self.cfg.host_input_log {
2394            self.settle_host_inputs();
2395            crate::host_inputs::write(
2396                log,
2397                &self.host_inputs.lock().unwrap(),
2398                &self.host_mutations.lock().unwrap(),
2399            );
2400        }
2401        // After the scheduler shut down: every guest task has exited, and its
2402        // store-exclusive counts have folded into the run's counters.
2403        #[cfg(target_arch = "aarch64")]
2404        for warning in self.store_exclusive.lock().unwrap().warnings(&self.cfg) {
2405            warn!("{}", warning);
2406        }
2407        let summary_result = self
2408            .finalize_run_summary()
2409            .and_then(|(mut summary, description)| {
2410                summary.dispatch_stats = dispatch_stats;
2411                // No summary is published unless terminal validation and recording
2412                // output both succeeded. Output errors retain their ordinary class.
2413                if let Some(path) = print_summary_to_json_file {
2414                    let json = serde_json::to_string_pretty(&summary)?;
2415                    fs::write(path, json + "\n")?;
2416                }
2417                Ok((summary, description))
2418            });
2419        // A saved network refusal is an additional observed cause on this new
2420        // error path. The existing network-only report/exit path stays below.
2421        let (mut summary, info_reprio_descrip) = match summary_result {
2422            Ok(summary) => summary,
2423            Err(error) => {
2424                return crate::combine_finalization(
2425                    crate::combine_finalization(
2426                        Err(error),
2427                        network_refusal.map_or(Ok(()), |refusal| Err(anyhow::anyhow!(refusal))),
2428                    ),
2429                    departure.map_or(Ok(()), |refusal| Err(refusal.into())),
2430                );
2431            }
2432        };
2433        // Print human-readable summary:
2434        if to_stderr {
2435            // In this case, print summary irrespective of logging level.
2436            // TODO: output summary in machine-readable, JSON form.
2437            //
2438            // NOT `eprint!`: a guest that set O_NONBLOCK on the inherited fd 2
2439            // makes `write_all` fail with EAGAIN here, which panics the print
2440            // macro and loses both the summary and the panic message. See
2441            // `crate::util::RetryingStderr`.
2442            {
2443                use std::io::Write;
2444                let _ = write!(crate::util::RetryingStderr, "{}\n{}", banner, summary);
2445            }
2446        } else {
2447            // Separate out the nondeterministic bits and print them at debug level:
2448            let rt = summary.realtime_elapsed.take();
2449            log_run_summary(
2450                banner,
2451                &summary,
2452                info_reprio_descrip.as_deref(),
2453                recording_destination.as_deref(),
2454            );
2455            if let Some(x) = rt {
2456                debug!("Nondeterministic realtime elapsed: {:?}", x);
2457            }
2458        }
2459        // After the report, so a refused replay still describes the run.
2460        if let Some(refusal) = network_refusal {
2461            {
2462                use std::io::Write;
2463                let _ = writeln!(crate::util::RetryingStderr, "{refusal}");
2464            }
2465            std::process::exit(detcore_model::HERMIT_POLICY_REFUSAL_EXIT);
2466        }
2467        departure.map_or(Ok(()), |refusal| Err(refusal.into()))
2468    }
2469
2470    #[cfg(test)]
2471    fn into_run_summary(self) -> anyhow::Result<RunSummary> {
2472        self.finalize_run_summary().map(|(summary, _)| summary)
2473    }
2474
2475    // Keep GlobalState alive until every remaining writer has been consumed.
2476    // In particular, strict finish can fail before the summary takes it. A
2477    // consumed failed write is absent and must never be retried by Drop.
2478    fn finalize_run_summary(&self) -> anyhow::Result<(RunSummary, Option<String>)> {
2479        let summary = self.run_summary_for_log();
2480        let writer = self.sched.lock().unwrap().preemption_writer.take();
2481        // Do not hold the scheduler mutex while doing output I/O.
2482        let flushed = if self.cfg.record_preemptions_to.is_some() {
2483            writer.map_or(Ok(()), |writer| writer.flush().map_err(anyhow::Error::msg))
2484        } else {
2485            drop(writer);
2486            Ok(())
2487        };
2488        crate::combine_finalization(summary, flushed)
2489    }
2490
2491    /// The host files this run's guest opened (`crate::host_inputs::input_set`),
2492    /// when `Config::record_host_inputs` is set.
2493    fn host_input_set(&self) -> Option<HostInputSet> {
2494        if !self.cfg.record_host_inputs {
2495            return None;
2496        }
2497        self.settle_host_inputs();
2498        let mutated = self
2499            .host_mutations
2500            .lock()
2501            .unwrap_or_else(PoisonError::into_inner);
2502        // A rebinding Detcore could not place (untraced guest code) rebinds
2503        // `/`: every path may have been changed by the guest, so the set
2504        // would be empty and look complete. Report none instead.
2505        if mutated.contains("/") {
2506            return None;
2507        }
2508        Some(crate::host_inputs::input_set(
2509            &self
2510                .host_inputs
2511                .lock()
2512                .unwrap_or_else(PoisonError::into_inner),
2513            &mutated,
2514        ))
2515    }
2516
2517    /// Read what the run's host-input records need from the files
2518    /// themselves, once, when every guest process has exited (the callers
2519    /// run after the scheduler has finished): for the first open of each
2520    /// path whose file the tool could name, the content
2521    /// `crate::host_inputs::content_at_end` reads. An exec's image and
2522    /// program interpreter were recorded at the exec, with the identities
2523    /// the process ran with (`Detcore::record_exec_host_inputs`). Reading
2524    /// while the guest ran would raise inotify events in the guest's
2525    /// watches. A schedule
2526    /// replay then decides the opens it left (`ReplayHostInputs::finish`)
2527    /// and reports each change it finds, as an open's change is reported.
2528    fn settle_host_inputs(&self) {
2529        if !self.cfg.record_host_inputs || self.host_inputs_settled.swap(true, SeqCst) {
2530            return;
2531        }
2532        let mutated = self
2533            .host_mutations
2534            .lock()
2535            .unwrap_or_else(PoisonError::into_inner)
2536            .clone();
2537        let sources = std::mem::take(
2538            &mut *self
2539                .host_input_sources
2540                .lock()
2541                .unwrap_or_else(PoisonError::into_inner),
2542        );
2543        let mut records = self
2544            .host_inputs
2545            .lock()
2546            .unwrap_or_else(PoisonError::into_inner);
2547        let mut first: HashMap<String, usize> = HashMap::new();
2548        for (index, record) in records.iter().enumerate() {
2549            first.entry(record.path.clone()).or_insert(index);
2550        }
2551        let mut authenticated_snapshots: HashMap<HostFileIdentity, bool> = HashMap::new();
2552        for (index, source, physical) in &sources {
2553            let record = &mut records[*index];
2554            if first.get(&record.path) != Some(index) {
2555                continue;
2556            }
2557            record.content =
2558                crate::host_inputs::content_at_end(source, &record.path, *physical, &mutated);
2559            if record.identity != *physical {
2560                let authenticated = self.cfg.host_input_snapshots.iter().any(|snapshot| {
2561                    snapshot.physical == *physical
2562                        && record.content.as_deref() == Some(snapshot.content.as_str())
2563                });
2564                authenticated_snapshots
2565                    .entry(*physical)
2566                    .and_modify(|valid| *valid &= authenticated)
2567                    .or_insert(authenticated);
2568            }
2569        }
2570        let mut failed_snapshots = Vec::new();
2571        for (index, _, physical) in sources {
2572            let record = &mut records[index];
2573            if record.identity != physical && authenticated_snapshots.get(&physical) != Some(&true)
2574            {
2575                // Revoke provenance on every open, including later opens that
2576                // supply no content of their own. Missing evidence or a digest
2577                // mismatch must not leave an unchecked origin in the log.
2578                record.identity = physical;
2579                if first.get(&record.path) == Some(&index) {
2580                    failed_snapshots.push((record.path.clone(), physical));
2581                }
2582            }
2583        }
2584        let mut contents: HashMap<String, Option<String>> = HashMap::new();
2585        for record in records.iter() {
2586            contents
2587                .entry(record.path.clone())
2588                .or_insert_with(|| record.content.clone());
2589        }
2590        drop(records);
2591        let Some(replay) = &self.replay_host_inputs else {
2592            return;
2593        };
2594        let mut replay = replay.lock().unwrap_or_else(PoisonError::into_inner);
2595        for (path, physical) in failed_snapshots {
2596            replay.restore_snapshot_identity(&path, physical);
2597        }
2598        let mut changes = Vec::new();
2599        changes.extend_from_slice(
2600            replay.finish(|path| contents.get(path).cloned().flatten(), &mutated),
2601        );
2602        drop(replay);
2603        for change in &changes {
2604            report_host_input_change(change);
2605        }
2606    }
2607
2608    fn run_summary_for_log(&self) -> anyhow::Result<(RunSummary, Option<String>)> {
2609        // The host files the guest opened go into the recorded schedule,
2610        // which the scheduler flushes below, and into the summary.
2611        let host_inputs = self.host_input_set();
2612        // First, the scheduler can generate part of the summary (and flush once).
2613        let (mut summary, info_reprio_descrip) = {
2614            let mut sched = self.sched.lock().unwrap();
2615            // Only a schedule written to a file carries the set; an in-memory
2616            // record's text goes into the INFO description.
2617            if let (Some(writer), Some(inputs), true) = (
2618                sched.preemption_writer.as_mut(),
2619                &host_inputs,
2620                self.cfg.record_preemptions_to.is_some(),
2621            ) {
2622                writer.set_host_inputs(inputs.clone());
2623            }
2624            // Every guest task has exited: the recording keeps the run's failed
2625            // store-exclusives for a replay to warn about.
2626            #[cfg(target_arch = "aarch64")]
2627            if let Some(writer) = sched.preemption_writer.as_mut() {
2628                writer.record_store_exclusive_failures(
2629                    self.store_exclusive
2630                        .lock()
2631                        .unwrap()
2632                        .failures_for_recording(),
2633                );
2634            }
2635            sched.generate_partial_run_summary_for_log(self.cfg.record_preemptions_to.as_ref())?
2636        };
2637        #[cfg(target_arch = "aarch64")]
2638        self.store_exclusive
2639            .lock()
2640            .unwrap()
2641            .fill_summary(&mut summary);
2642        // Second, we fill in the rest based on global state.
2643        // Record configuration and the runtime's timer mechanism, not whether
2644        // a timer is currently armed or has delivered an event. This metadata
2645        // stays separate from the INFO guest-behavior view.
2646        summary.preemption = Some(PreemptionSummary {
2647            max_timeslice_ns: self.cfg.max_timeslice.map(|maximum| maximum.get()),
2648            timer_enabled: Some(self.cfg.use_preemption_timer()),
2649            stopless_slice_ends: self.cfg.replays_lone_preemptions().then(|| {
2650                crate::STOPLESS_SLICE_ENDS
2651                    .load(std::sync::atomic::Ordering::Relaxed)
2652                    .saturating_sub(self.stopless_slice_ends_at_start)
2653            }),
2654        });
2655        // The host files the guest opened, and those a replay found changed
2656        // since its recording. Like `preemption`, outside the INFO view.
2657        summary.host_inputs = host_inputs;
2658        if let Some(replay) = &self.replay_host_inputs {
2659            summary.host_inputs_changed = replay.lock().unwrap().changes().to_vec();
2660        }
2661        summary.guest_ids = (self.cfg.guest_ids != detcore_model::pid::GuestIds::Namespace)
2662            .then_some(self.cfg.guest_ids);
2663        summary.host_id_tasks = self.host_id_tasks.load(SeqCst);
2664        //
2665        // Real time report:
2666        // N.B.: We don't have a job-level exit hook atm (T76248597), so we use the
2667        // CURRENT time -- that we are calling summarize -- as the end time:
2668        summary.realtime_elapsed = Some(self.realtime_start.elapsed()?);
2669        if let Some(histogram) = &mut summary.syscall_histogram {
2670            let mut gaps = self.syscall_observation_gaps.lock().unwrap().clone();
2671            if self.cfg.passthru_opt {
2672                gaps.insert(SyscallObservationGap::PassthruOpt);
2673            }
2674            histogram.observation_scope = SyscallObservationScope::from_gaps(gaps);
2675        }
2676
2677        if self.cfg.virtualize_time {
2678            let final_time = self.global_time.lock().unwrap();
2679            summary.virttime_final = final_time.as_nanos().as_nanos();
2680            summary.virttime_elapsed = match final_time.elapsed_nanos() {
2681                Some(elapsed) => elapsed.as_nanos(),
2682                None => bail!(
2683                    "Internal invariant violated! Global time {} is before its epoch baseline",
2684                    final_time.as_nanos()
2685                ),
2686            };
2687        }
2688
2689        Ok((summary, info_reprio_descrip))
2690    }
2691}
2692
2693fn log_run_summary(
2694    banner: &str,
2695    summary: &RunSummary,
2696    info_reprio_descrip: Option<&str>,
2697    recording_destination: Option<&std::path::Path>,
2698) {
2699    info!("\n{}\n{}", banner, summary.info(info_reprio_descrip));
2700    debug!(
2701        replayed_events = summary.schedevent_replayed,
2702        ?recording_destination,
2703        "Run recording/replay bookkeeping"
2704    );
2705}
2706
2707#[reverie::global_tool]
2708impl GlobalTool for GlobalState {
2709    type Config = Config;
2710
2711    /// A request asks the scheduler to perform an RPC, which includes multiple kinds of
2712    /// actions, and, most importantly, permission to acquire resources and run the guest thread.
2713    ///
2714    /// Irrespective of which method we execute, we can "tick" our local component of the
2715    /// global time in the process.
2716    type Request = (DetTime, MmId, GlobalRequest);
2717
2718    /// Response from the global portion of the Detcore instrumentation tool.
2719    /// The exact form of the response depends on which method was executed.
2720    ///
2721    /// Irrespective of which method was called, the global handling may have consumed
2722    /// logical time, in which case the scheduler can send a new thread-local time back to
2723    /// the caller.  Unfortunately, information is lost as this is collapsed to a flat
2724    /// scalar instead of a rich `DetTime`.
2725    type Response = (Option<LogicalTime>, GlobalResponse);
2726
2727    /// Called once during startup.
2728    async fn init_global_state(cfg: &Config) -> GlobalState {
2729        // A backend whose guest thread IDs are not host tasks never reads a
2730        // host task's `/proc` signal state for them (`read_kernel_signal_state`).
2731        crate::syscalls::refuse_host_signal_state_reads(&cfg.backend);
2732        // How the guest is numbered, where no PID namespace of its own does
2733        // it, so that a run's log says which IDs it can be compared by.
2734        if cfg.guest_ids != detcore_model::pid::GuestIds::Namespace {
2735            info!("[detcore] guest process IDs: {}", cfg.guest_ids);
2736        }
2737        GlobalState::initialize(cfg, true)
2738    }
2739
2740    fn install_inherited_input(&mut self, input: reverie::InheritedInputLease) {
2741        // Called once by the launcher before this global state is published.
2742        // All fallible endpoint/lease creation happened before initialization.
2743        *self.inherited_input.get_mut().unwrap() = Some(PendingInheritedInput {
2744            lease: input,
2745            root: None,
2746        });
2747    }
2748
2749    fn install_mapping_capture_run(&mut self, run: reverie::MappingCaptureRun) {
2750        self.mapping_capture_run = Some(run);
2751    }
2752
2753    fn install_reserved_input_control(
2754        &mut self,
2755        control: reverie::reserved_input::ReservedInputControl,
2756    ) -> Result<(), reverie::Error> {
2757        let mut scheduler = self.sched.lock().unwrap();
2758        if scheduler.reserved_input_control.is_some() {
2759            return Err(anyhow::anyhow!("reserved-input control installed twice").into());
2760        }
2761        crate::reserved_input::install_native_history(&control)?;
2762        scheduler.reserved_input_operations =
2763            Some(crate::reserved_input::NativeOperations::install(&control)?);
2764        scheduler.reserved_input_control = Some(control);
2765        Ok(())
2766    }
2767
2768    fn on_reserved_input_task(
2769        &self,
2770        task: reverie::reserved_input::InputTaskRegistration,
2771    ) -> Result<(), reverie::Error> {
2772        // These are explicit backend-to-Tool bindings, not an assumption
2773        // that a native PID is the scheduler's logical identity. The backend
2774        // publishes them before either parent- or child-side registration.
2775        let tool_tid = task
2776            .tool_tid
2777            .filter(|tid| *tid > 0)
2778            .ok_or_else(|| anyhow::anyhow!("reserved-input report lacks Tool thread identity"))?;
2779        let _tool_pid = task
2780            .tool_pid
2781            .filter(|pid| *pid > 0)
2782            .ok_or_else(|| anyhow::anyhow!("reserved-input report lacks Tool process identity"))?;
2783        let mut scheduler = self.sched.lock().unwrap();
2784        let control = scheduler.reserved_input_control.as_ref().ok_or_else(|| {
2785            anyhow::anyhow!("reserved-input task reported before control installation")
2786        })?;
2787        if control.registration(task.task) != Some(task) {
2788            return Err(
2789                anyhow::anyhow!("reserved-input report differs from retained generation").into(),
2790            );
2791        }
2792        scheduler
2793            .reserved_input_native_tasks
2794            .insert(task.native_tid, task.task);
2795        // Preserve the old logical image until its original exec teardown
2796        // fence completes. Birth/reconnect consumes this pending binding.
2797        scheduler
2798            .reserved_input_tool_tasks
2799            .insert(DetTid::from_raw(tool_tid), task.task);
2800        Ok(())
2801    }
2802
2803    fn admit_reserved_input_initial_state(
2804        &self,
2805        facts: &reverie::reserved_input::InputNativeFacts,
2806    ) -> Result<reverie::reserved_input::InputAdmission, reverie::Error> {
2807        let control = self
2808            .sched
2809            .lock()
2810            .unwrap()
2811            .reserved_input_control
2812            .clone()
2813            .ok_or_else(|| {
2814                anyhow::anyhow!("initial native admission precedes control installation")
2815            })?;
2816        crate::reserved_input::register_initial_history(&control, facts.task)?;
2817        let admission = crate::reserved_input::admit_initial(facts);
2818        let descriptors = crate::reserved_input::observe_initial_descriptors(&control, facts)?;
2819        Ok(match (admission, descriptors) {
2820            (reverie::reserved_input::InputAdmission::Qualified, Some(cause)) => {
2821                reverie::reserved_input::InputAdmission::Unqualified(cause)
2822            }
2823            (admission, _) => admission,
2824        })
2825    }
2826
2827    fn on_reserved_input_setup(
2828        &self,
2829        receipt: reverie::reserved_input::InputSetupReceipt,
2830    ) -> Result<(), reverie::Error> {
2831        if let Some(writer) = self.sched.lock().unwrap().preemption_writer.as_mut() {
2832            writer
2833                .observe_reserved_input_setup(&receipt)
2834                .map_err(anyhow::Error::new)?;
2835        }
2836        Ok(())
2837    }
2838
2839    fn install_backend_signal_control(
2840        &self,
2841        control: Option<reverie::BackendSignalControl>,
2842    ) -> Result<reverie::BackendSignalControlMode, reverie::Error> {
2843        self.sched
2844            .lock()
2845            .unwrap()
2846            .install_signal_control(control, self.cfg.sequentialize_threads)
2847    }
2848
2849    /// Dequeues carry real-timer signals only in a run that installed the
2850    /// backend's control; any other run leaves them to the guest kernel.
2851    fn observe_signal_dequeues(&self, mode: reverie::BackendSignalControlMode) -> bool {
2852        mode == reverie::BackendSignalControlMode::ToolControlled
2853    }
2854
2855    /// Detcore lets the backend skip an empty user-return boundary. Only
2856    /// reverie-kvm reads this answer, and only once Detcore's
2857    /// `install_signal_control` returned `ToolControlled`, which needs the
2858    /// backend's process signal control and `--sequentialize-threads`. The
2859    /// ptrace, DBT, SaBRe and LiteInst backends never ask, so the constant is
2860    /// fixed for the run on every backend, as the contract requires.
2861    ///
2862    /// An empty boundary is one where the thread holds no permit and the
2863    /// backend shows no pending signal its mask leaves unblocked, no parked
2864    /// ledger, no signalfd in the process, no dequeue-journal entry of the
2865    /// thread, and no stored dequeue failure. There the full round trip is
2866    /// `authorize_signal_boundary`, a selection of nothing, and a `NoHandler`
2867    /// receipt to `consume_signal_boundary`. Skipping it is unobservable:
2868    ///
2869    /// - Authorization publishes nothing. Detcore's own signals (real-timer
2870    ///   and POSIX-timer expiry, child-exit SIGCHLD) are published under the
2871    ///   scheduler lock at scheduler time into the backend's pending sets or
2872    ///   dequeue journal, which make a boundary non-empty, so none is lost.
2873    /// - The round trip's permit is read by no other decision. Permits are
2874    ///   per thread (<https://github.com/rrnewton/hermit/issues/4899>), and
2875    ///   apart from the holder's own authorization and receipt, every reader
2876    ///   of `parked.permits` reads one named thread's entry, or only the
2877    ///   observation permits of parked waits, which a boundary's permit never
2878    ///   is:
2879    ///   - another thread's authorization, the exit reservation
2880    ///     (`reserve_exit_boundary`) and the parked-request retirements
2881    ///     (`retire_parked_requests`, `retire_replaced_parked_request`,
2882    ///     `retire_child_wait_signal_request`) read their own thread's entry;
2883    ///   - alarm selection (`select_parked_alarm`) skips a process only for an
2884    ///     observation permit (one with a `site`) and otherwise reads each
2885    ///     recipient's own entry, and a thread at a boundary is no parked-wait
2886    ///     recipient;
2887    ///   - an observation's finish (`finish_observation`) and another
2888    ///     thread's receipt remove only their own thread's entry;
2889    ///   - process retirement (`finish_process_retirement`) reads every entry
2890    ///     of the process, but only once its leader has joined every thread,
2891    ///     never during a live thread's boundary.
2892    ///
2893    ///   So with or without the round trip, every decision sees the same
2894    ///   permits of every other thread, and the boundary's thread holds none
2895    ///   once the round trip ends.
2896    /// - The round trip runs inside the thread's own serial grant.
2897    ///   Authorization succeeds only for `parked.running`. The only skippable
2898    ///   boundaries follow a completed Tool callback or a backend-owned
2899    ///   `rt_sigreturn` (thread entry checks only when a signal is eligible).
2900    ///   Every request is awaited, and every background release
2901    ///   (`BlockingExternalIO`, `BlockingRtSigsuspend`, a vfork parent) awaits
2902    ///   its `BlockedExternalContinue` grant before the callback returns, so
2903    ///   such a boundary is always inside the thread's own serial grant
2904    ///   (`holds_serial_grant`). No grant, alarm selection or step2 drain runs
2905    ///   then: they wait for quiescence, and `are_all_quiesced` waits on the
2906    ///   running thread's empty request. The control-intent drain
2907    ///   (`drain_control_intents`) does run on every pass of the turn loop,
2908    ///   but its intents only finish or resume an observed parked wait, and
2909    ///   touch only that wait's own permit.
2910    /// - The receipt's cleanup is redundant there. Every grant settles the
2911    ///   parked request it answers (`settle_parked_grant`), and every request
2912    ///   the scheduler substitutes for a filed one retires the replaced
2913    ///   request's ownership (`retire_replaced_parked_request`), so no
2914    ///   ownership outlives its request. An observed wait's request and
2915    ///   `Observation` owner, and a `ReturningCaught` owner, always come with
2916    ///   a permit, which makes the boundary non-empty.
2917    /// - The rest is bookkeeping no decision reads: `parked.nonce` stays
2918    ///   monotonic, only smaller, and is never logged; `parked.completed`
2919    ///   only recognises an exact duplicate receipt.
2920    ///
2921    /// The cost is that Detcore's identity checks at authorization (ESRCH,
2922    /// EINVAL) now run only at non-empty boundaries.
2923    ///
2924    /// Held once before, while a backgrounded KVM `rt_sigsuspend` race was
2925    /// open (<https://github.com/rrnewton/hermit/issues/4360>). That race is
2926    /// fixed: a KVM `rt_sigsuspend` now runs in its caller's turn. Before that
2927    /// fix the race fired about as often with this opt-in as without it.
2928    ///
2929    /// An earlier form of this proof called an observed wait's permit
2930    /// short-lived and unseen by other decisions. It is not short-lived: it
2931    /// lasts until the observed thread's return-boundary receipt, across that
2932    /// thread's `InboundSignal` turn, during which siblings run. While permits
2933    /// were one per process, a sibling's boundary saw it and was refused
2934    /// (<https://github.com/rrnewton/hermit/issues/4899>). The argument above
2935    /// no longer depends on any permit's lifetime.
2936    ///
2937    /// Re-prove or drop this if Detcore ever holds pending signals, or its
2938    /// own mask copy, that the backend cannot see and delivers them through
2939    /// authorization, or if the backend's KVM signal policy moves into
2940    /// Detcore: the skip trusts the backend's pending sets and mask.
2941    fn skip_empty_signal_boundaries(&self) -> bool {
2942        true
2943    }
2944
2945    fn authorize_backend_signal_boundary(
2946        &self,
2947        task: reverie::SignalTaskIdentity,
2948    ) -> Result<Option<reverie::SignalDeliveryPermit>, reverie::Error> {
2949        self.sched.lock().unwrap().authorize_signal_boundary(task)
2950    }
2951
2952    /// A terminal receipt of a death by a fatal signal also lets go of the
2953    /// dying threads' descriptor tables (`GlobalState::release_tables_at_death`),
2954    /// before the receipt returns to the backend, so before the backend
2955    /// publishes the death to a parent or retires the process: the control
2956    /// barrier the receipt installed holds every later turn until then.
2957    async fn on_backend_signal_boundary(
2958        &self,
2959        receipt: reverie::SignalBoundaryReceipt,
2960    ) -> Result<(), reverie::Error> {
2961        let death = self
2962            .sched
2963            .lock()
2964            .unwrap()
2965            .consume_signal_boundary(receipt)?;
2966        if let Some(death) = death {
2967            self.release_tables_at_death(death);
2968        }
2969        Ok(())
2970    }
2971
2972    fn on_backend_process_retired(
2973        &self,
2974        event: reverie::BackendProcessRetirement,
2975    ) -> Result<(), reverie::Error> {
2976        let (result, wakes) = {
2977            let mut sched = self.sched.lock().unwrap();
2978            let result = sched.consume_process_retirement(event);
2979            (result, sched.take_signal_failure_wakes())
2980        };
2981        for wake in wakes {
2982            let _ = wake.send(());
2983        }
2984        result
2985    }
2986
2987    /// A backend whose process exits complete asynchronously reports each
2988    /// physical exit here; see [`GlobalState::complete_physical_process_exit`].
2989    fn on_backend_process_exited(&self, pid: i32) {
2990        if self.cfg.backend.reports_child_exit_publication {
2991            // The kernel has published this process's exit to its parent. It
2992            // is not a physical-exit barrier report on this backend.
2993            let released = self
2994                .sched
2995                .lock()
2996                .unwrap()
2997                .complete_child_exit_publication(DetPid::from_raw(pid));
2998            // This branch bypasses `ProcessExitCleanup::complete_locked`: the
2999            // process's `/proc/<pid>/auxv` vector goes here. A zombie's file
3000            // is served on the rendered path whether or not its vector is
3001            // still kept (`Detcore::auxv_kept_by_kernel`), so the host-timed
3002            // moment of this report changes nothing the guest sees.
3003            self.process_auxv
3004                .lock()
3005                .unwrap()
3006                .forget_process(DetPid::from_raw(pid));
3007            if released {
3008                trace!("[detcore, dpid {}] backend published the child exit", pid);
3009            }
3010        } else {
3011            self.complete_physical_process_exit(pid);
3012        }
3013    }
3014
3015    /// The processes held between their exit grant and their physical exit,
3016    /// for the backend's watchdog.
3017    fn backend_pending_process_exits(&self) -> Vec<i32> {
3018        self.sched
3019            .lock()
3020            .unwrap()
3021            .pending_async_process_exits()
3022            .into_iter()
3023            .map(|detpid| detpid.as_raw())
3024            .collect()
3025    }
3026
3027    fn on_fatal_signal_exit(&self, exit: &reverie::FatalSignalExit) {
3028        if let Some(capture) = &self.cfg.fatal_core_capture {
3029            crate::fatal_core::capture_and_log(capture, exit);
3030        }
3031    }
3032
3033    fn report_backend_failure(&self, event: reverie::BackendFailure) {
3034        let (wake, deferred) = {
3035            let mut sched = self.sched.lock().unwrap();
3036            (
3037                sched.report_backend_failure(event),
3038                sched.take_signal_failure_wakes(),
3039            )
3040        };
3041        for wake in deferred {
3042            let _ = wake.send(());
3043        }
3044        if let Some(wake) = wake {
3045            // No waiter can begin consuming cleanup until the scheduler has
3046            // closed its selected transaction under the grant/commit mutex.
3047            let _ = wake.send(());
3048        }
3049    }
3050
3051    async fn wait_for_backend_failure(&self) {
3052        let wake = self.sched.lock().unwrap().backend_failure_waiter();
3053        wake.await
3054            .expect("GlobalState owns the failure sender until publication");
3055    }
3056
3057    async fn on_backend_child_wait_event(
3058        &self,
3059        event: reverie::BackendChildWaitEvent,
3060    ) -> Result<(), reverie::Error> {
3061        // KVM polls this future through its first suspension before exposing
3062        // waitability. The helper's first poll performs the complete
3063        // generation-bound publication under the scheduler mutex, then
3064        // self-wakes and returns Pending; its second poll consumes the typed
3065        // result and releases the stage-2 control barrier.
3066        crate::scheduler::signal_control::ChildExitPublicationFuture::new(self.sched.clone(), event)
3067            .await
3068    }
3069
3070    async fn receive_rpc(&self, from: Tid, gr: Self::Request) -> Self::Response {
3071        if replay_departure::is_report(&gr.2) {
3072            return self.accept_replay_departure(from, gr);
3073        }
3074        #[cfg(target_arch = "aarch64")]
3075        if let GlobalRequest::StoreExclusive(report) = &gr.2 {
3076            return self.note_store_exclusive(report);
3077        }
3078        if let GlobalRequest::MappingCapture(request) = &gr.2 {
3079            return (
3080                None,
3081                GlobalResponse::MappingCapture(self.mapping_capture_request(from, gr.1, request)),
3082            );
3083        }
3084        if let GlobalRequest::CheckBrkMappings(check, fact) = &gr.2 {
3085            return (
3086                None,
3087                GlobalResponse::CheckBrkMappings(
3088                    self.capture_brk_mappings(from, gr.1, *check, *fact),
3089                ),
3090            );
3091        }
3092        if let GlobalRequest::HoldTable(table) = &gr.2 {
3093            return self.hold_table(from, table);
3094        }
3095        // Intercept even malformed counted envelopes before taking a deadline
3096        // hint. Native controls belong to the already owned exec cut.
3097        if native_exec::is_native_exec_control(&gr.2) {
3098            return self.dispatch_native_exec_control(from, gr);
3099        }
3100        // The deadline hint of https://github.com/rrnewton/hermit/issues/4121
3101        // rides on whichever reply resumes the thread after its grant: an
3102        // ordinary or parked resource grant, a futex wake, a parent's
3103        // continuation, thread start, the exec continuation. Clearing on
3104        // arrival means a reply carries only a hint recorded while its own
3105        // request was pending (a grant, or a timer the request armed).
3106        // Only a request whose reply resumes the thread from a grant, or arms
3107        // a timer, touches the hint: control messages sent on another task's
3108        // behalf must neither consume nor carry it.
3109        //
3110        // Whether the scheduler resumed the thread from a wait while its
3111        // request was pending rides on the reply too (`GlobalResponse::Granted`),
3112        // whatever the request: the scheduler marks where it resumes a waiting
3113        // thread (`Scheduler::granted`: a grant, or a parked wait resumed to
3114        // observe a signal), so no request kind has to be listed for it. A
3115        // request answered without either carries no mark.
3116        let dtid = DetTid::from_raw(from.as_raw());
3117        let request_mm = gr.1;
3118        // Root/reloaded image self-registration precedes StartNewThread. Its
3119        // existing ParentContinue grant must not initialize callback counters
3120        // or consume the budget which belongs after the startup reset.
3121        let self_registration = registers_own_image(dtid, &gr.2);
3122        {
3123            let mut scheduler = self.sched.lock().unwrap();
3124            scheduler.take_grant(dtid);
3125            scheduler.take_lone_grant(dtid);
3126        }
3127        let (time, response) = if !resumes_from_grant(&gr.2) {
3128            self.dispatch_rpc(from, gr).await
3129        } else {
3130            self.sched.lock().unwrap().take_deadline_hint(dtid);
3131            let (time, response) = self.dispatch_rpc(from, gr).await;
3132            let hint = self.sched.lock().unwrap().take_deadline_hint(dtid);
3133            match hint {
3134                Some(remaining)
3135                    if response != GlobalResponse::ThreadExited
3136                        && response != GlobalResponse::Retired =>
3137                {
3138                    (
3139                        time,
3140                        GlobalResponse::WithDeadline(remaining, Box::new(response)),
3141                    )
3142                }
3143                _ => (time, response),
3144            }
3145        };
3146        let (granted, boundary_budget, boundary_owner, lone) = {
3147            let mut scheduler = self.sched.lock().unwrap();
3148            let granted = scheduler.take_grant(dtid);
3149            let lone = scheduler.take_lone_grant(dtid);
3150            let terminal =
3151                response == GlobalResponse::ThreadExited || response == GlobalResponse::Retired;
3152            let mut budget = scheduler
3153                .boundary_budgets
3154                .finish(dtid, request_mm, granted && !self_registration, terminal)
3155                .unwrap_or_else(|error| crate::scheduler::exit_on_scheduler_refusal(&error));
3156            // Same-leader exec keeps its existing turn; its authenticated
3157            // image-completion reply carries the new image's expectation
3158            // without pretending another resource turn was committed.
3159            if !terminal && matches!(response, GlobalResponse::MarkPastFirstExecve(_)) {
3160                assert!(
3161                    budget.is_none(),
3162                    "exec completion cannot steal an old grant budget"
3163                );
3164                budget = scheduler
3165                    .boundary_budget_for_live_owner(dtid)
3166                    .unwrap_or_else(|error| crate::scheduler::exit_on_scheduler_refusal(&error));
3167            }
3168            let owner = if !terminal
3169                && !self_registration
3170                && (granted || matches!(response, GlobalResponse::MarkPastFirstExecve(_)))
3171            {
3172                scheduler.boundary_owners.as_ref().map(|owners| {
3173                    owners
3174                        .owner(dtid, request_mm)
3175                        .unwrap_or_else(|error| crate::scheduler::exit_on_scheduler_refusal(&error))
3176                })
3177            } else {
3178                None
3179            };
3180            (granted, budget, owner, lone)
3181        };
3182        let response = match boundary_budget {
3183            Some(budget) => GlobalResponse::WithSliceBoundaryBudget(budget, Box::new(response)),
3184            None => response,
3185        };
3186        let response = match boundary_owner {
3187            Some(owner) => GlobalResponse::WithSliceBoundaryOwner(owner, Box::new(response)),
3188            None => response,
3189        };
3190        if granted
3191            && response != GlobalResponse::ThreadExited
3192            && response != GlobalResponse::Retired
3193        {
3194            let response = GlobalResponse::Granted(Box::new(response));
3195            if lone {
3196                (time, GlobalResponse::WithLoneGrant(Box::new(response)))
3197            } else {
3198                (time, response)
3199            }
3200        } else {
3201            (time, response)
3202        }
3203    }
3204}
3205
3206impl GlobalState {
3207    /// Hold `table` as the descriptor table thread `from` holds
3208    /// (`GlobalRequest::HoldTable`, [`Self::held_tables`]), until the thread
3209    /// names another, deregisters, or dies. Answered at once: as for a mapping
3210    /// capture, no scheduler state changes, no clock moves, and no forwarded
3211    /// record is drained, so naming a table changes nothing else in the run.
3212    ///
3213    /// A request that reached the global state through a transport that
3214    /// encodes its requests carries no table (`HeldTable`): it is answered
3215    /// with the reason, by which the sender stops the run.
3216    fn hold_table(&self, from: Tid, table: &HeldTable) -> <Self as GlobalTool>::Response {
3217        let Some(table) = &table.0 else {
3218            return (
3219                None,
3220                GlobalResponse::HoldTable(Err(format!(
3221                    "thread {from} named its descriptor table through a transport that encodes \
3222                     its requests, which carries no table: {HELD_TABLE_IS_IN_PROCESS}, so the \
3223                     terminal receipt of a death by a fatal signal could not let go of it"
3224                ))),
3225            );
3226        };
3227        self.held_tables
3228            .lock()
3229            .unwrap()
3230            .insert(DetTid::from_raw(from.as_raw()), Arc::downgrade(table));
3231        (None, GlobalResponse::HoldTable(Ok(())))
3232    }
3233
3234    /// Release the port reserved for the socket `open_file` names, as its
3235    /// last reference's going does (`GlobalRequest::ReleasePort`): the port,
3236    /// if one was reserved.
3237    fn release_port(&self, open_file: OpenFileId) -> Option<u16> {
3238        let mut used_ports = self.used_ports.lock().unwrap();
3239        let mut open_file_to_port = self.open_file_to_port.lock().unwrap();
3240        let port = open_file_to_port.remove(&open_file);
3241        if let Some(port) = port {
3242            used_ports.remove(&port);
3243        }
3244        port
3245    }
3246
3247    /// `death` ended its process by the default action of a fatal signal, as
3248    /// its terminal receipt says (`Scheduler::consume_signal_boundary`): the
3249    /// process's own `SIGTERM`, an `abort()`, a `SIGSEGV`, which no handler of
3250    /// the dying process sees end it. Linux's `do_exit` releases the dying
3251    /// task's descriptor table with `exit_files` before `exit_notify` lets a
3252    /// parent reap it, and a table's last holder's going closes every
3253    /// descriptor of it; a socket's last reference purges its receive queue
3254    /// (`unix_release_sock`), and the last reference of a description that a
3255    /// queued message held removes its locks (`locks_remove_file`), so a lock
3256    /// only such a message held is gone before the parent's `wait` returns.
3257    /// Here, before the receipt returns to the backend, while the control
3258    /// barrier the receipt installed holds every later turn: each table the
3259    /// dying threads named ([`Self::held_tables`]) that no other process holds
3260    /// goes (`tool_local::FileMetadata::release_at_death`), and what the queues
3261    /// of its sockets held is released (`scm_rights::settle`): the lock ledger
3262    /// uncounts the copies in flight it counted, and the sockets among the
3263    /// descriptions that went release their ports, as an exit Detcore grants
3264    /// does (`Detcore::release_table_at_exit`). Decided from the receipt alone,
3265    /// at the same point of the schedule in every run. A table the death
3266    /// cannot decide about (a dying thread named none, or its holders do not
3267    /// name the process) is refused by name instead: if the ledger counts any
3268    /// copy in flight, every later `/proc/locks` snapshot it renders is
3269    /// refused (`procfs::locks::LockLedger::refuse`), as it could show a lock
3270    /// Linux has released. Only where tables go at their last holder's exit
3271    /// (`scm_rights::releases_tables_at_exits`).
3272    fn release_tables_at_death(&self, death: crate::scheduler::signal_control::SignalDeath) {
3273        use crate::syscalls::scm_rights;
3274        use crate::tool_local::TableAtDeath;
3275        if !scm_rights::releases_tables_at_exits(&self.cfg) {
3276            return;
3277        }
3278        let mut unnamed = None;
3279        let tables = {
3280            let mut held = self.held_tables.lock().unwrap();
3281            let mut tables: Vec<Arc<Mutex<crate::tool_local::FileMetadata>>> = Vec::new();
3282            for thread in &death.threads {
3283                match held.remove(thread).and_then(|table| table.upgrade()) {
3284                    Some(table) => {
3285                        if !tables.iter().any(|known| Arc::ptr_eq(known, &table)) {
3286                            tables.push(table);
3287                        }
3288                    }
3289                    None => {
3290                        unnamed.get_or_insert(*thread);
3291                    }
3292                }
3293            }
3294            tables
3295        };
3296        // A thread that named no table has not started yet. A thread of the
3297        // process holds the table its creator held (CLONE_FILES) unless one of
3298        // the process's threads has a table of its own, which marks the
3299        // process in the ledger (`LockLedger::mark_private_tables`): only then
3300        // may an unnamed thread hold a table no other thread named.
3301        let mut undecided = unnamed
3302            .filter(|_| {
3303                tables.is_empty()
3304                    || self
3305                        .lock_ledger
3306                        .lock()
3307                        .unwrap()
3308                        .has_private_tables(death.process)
3309            })
3310            .map(|thread| format!("its thread {thread} named no descriptor table"));
3311        let mut released = Vec::new();
3312        for table in tables {
3313            match table.lock().unwrap().release_at_death(death.process) {
3314                TableAtDeath::Released(rights) => released.extend(rights),
3315                TableAtDeath::Kept => {}
3316                TableAtDeath::Undecided => {
3317                    undecided.get_or_insert_with(|| {
3318                        "a table of its threads does not count it among its holders".to_owned()
3319                    });
3320                }
3321            }
3322        }
3323        let scm_rights::Settled { lock_copies, ports } = scm_rights::settle(released);
3324        if scm_rights::follows_lock_copies(&self.cfg) {
3325            let mut ledger = self.lock_ledger.lock().unwrap();
3326            if !lock_copies.is_empty() {
3327                // Uncounting never refuses: only a send names a locked copy.
3328                let _ = ledger.placeholder_copies(crate::procfs::locks::PlaceholderCopies::Unsent(
3329                    lock_copies,
3330                ));
3331            }
3332            if let Some(reason) = undecided
3333                && ledger.counts_copies_in_flight()
3334            {
3335                ledger.refuse(format!(
3336                    "process {} died by a fatal signal while a copy of a description was in \
3337                     flight, and {reason}, so which locks its death released is unknown",
3338                    death.process
3339                ));
3340            }
3341        }
3342        for open_file in ports {
3343            self.release_port(open_file);
3344        }
3345    }
3346
3347    /// The ID for the next guest task, when Detcore chooses the IDs: each
3348    /// call takes the next one, whether or not the clone that asked succeeds,
3349    /// skipping IDs any guest task already used, including an explicit
3350    /// `clone3(set_tid)` choice. Registration survives exit and reaping, so
3351    /// an ID is never given out twice. Asked in the caller's turn, so the
3352    /// order of the takes is the guest's own order of clone-family calls.
3353    /// `None` when the kernel numbers the guest or no positive ID remains.
3354    async fn take_guest_id(&self) -> Option<i32> {
3355        self.cfg.guest_ids.base()?;
3356        loop {
3357            let (id, registered) = {
3358                let sched = self.lock_rpc_scheduler(false).await;
3359                let id = self
3360                    .next_guest_id
3361                    .try_update(SeqCst, SeqCst, |id| {
3362                        (id > 0).then(|| id.checked_add(1).unwrap_or(0))
3363                    })
3364                    .ok()?;
3365                (id, sched.thread_was_registered(DetTid::from_raw(id)))
3366            };
3367            let clocked = self
3368                .global_time
3369                .lock()
3370                .unwrap()
3371                .contains_thread(DetTid::from_raw(id));
3372            if !registered && !clocked {
3373                return Some(id);
3374            }
3375        }
3376    }
3377
3378    /// Evaluate a brk predicate in the controller's unchanged procfs view.
3379    /// The scheduler lock retains the caller's grant and physical identity
3380    /// until all temporary descriptors have closed. The backend fact excludes
3381    /// native tasks that the scheduler cannot stop; it is not inferred here.
3382    fn capture_brk_mappings(
3383        &self,
3384        from: Tid,
3385        mm: MmId,
3386        check: crate::syscalls::memory::BrkMappingCheck,
3387        fact: reverie::ControllerCaptureFact,
3388    ) -> Option<bool> {
3389        use std::io::Read;
3390        use std::os::fd::AsRawFd;
3391        use std::os::unix::fs::MetadataExt;
3392
3393        if !self.cfg.sequentialize_threads {
3394            return None;
3395        }
3396        let sched = self.sched.lock().unwrap();
3397        let dettid = DetTid::from_raw(from.as_raw());
3398        let (pid, tid) = sched.controller_capture_task(dettid, mm, fact)?;
3399        let root = File::open("/proc").ok()?;
3400        let metadata = root.metadata().ok()?;
3401        if !metadata.is_dir()
3402            || metadata.dev() != fact.procfs_device
3403            || metadata.ino() != fact.procfs_inode
3404        {
3405            return None;
3406        }
3407        let mut filesystem = std::mem::MaybeUninit::<libc::statfs>::uninit();
3408        // SAFETY: root is live and filesystem has space for the complete result.
3409        if unsafe { libc::fstatfs(root.as_raw_fd(), filesystem.as_mut_ptr()) } != 0
3410            || unsafe { filesystem.assume_init() }.f_type as u64
3411                != reverie::syscalls::linux_abi::PROC_SUPER_MAGIC as u64
3412        {
3413            return None;
3414        }
3415        // A live nonleader remains addressable by its TID after its leader
3416        // exits, when the leader's /task directory may no longer be readable.
3417        let relative = std::ffi::CString::new(format!("{tid}/smaps")).ok()?;
3418        // SAFETY: both the directory and nul-terminated relative path are live.
3419        let raw = unsafe {
3420            libc::openat(
3421                root.as_raw_fd(),
3422                relative.as_ptr(),
3423                libc::O_RDONLY | libc::O_CLOEXEC,
3424            )
3425        };
3426        if raw < 0 {
3427            return None;
3428        }
3429        // SAFETY: openat returned a new descriptor exclusively owned here.
3430        let mut smaps_file = unsafe { File::from_raw_fd(raw) };
3431        let mut smaps = Vec::new();
3432        smaps_file.read_to_end(&mut smaps).ok()?;
3433        // External death cannot turn a partial or former task's capture into
3434        // authority for a replacement. The registered pidfd is still held.
3435        if sched.controller_capture_task(dettid, mm, fact) != Some((pid, tid)) {
3436            return None;
3437        }
3438        Some(check.allows(&smaps))
3439        // smaps_file and root close before sched releases the grant guard and
3440        // before the caller receives the fixed, allocation-free answer.
3441    }
3442
3443    /// The host's `/proc/diskstats` as this run first read it, in this
3444    /// process (the coordinator's), or the errno of that read
3445    /// (`GlobalState::host_diskstats`). Read once: every rendered
3446    /// `/proc/diskstats` of the run starts from the same table, in every
3447    /// process and after every `exec`, as Linux's own reads of the file do
3448    /// while the host's block devices do not change.
3449    fn host_diskstats(&self) -> Result<Vec<u8>, i32> {
3450        self.host_diskstats
3451            .get_or_init(|| {
3452                std::fs::read("/proc/diskstats")
3453                    .map_err(|error| error.raw_os_error().unwrap_or(libc::EIO))
3454            })
3455            .clone()
3456    }
3457
3458    /// Prints, once per run, a WARN for each signal whose handler an in-guest
3459    /// runtime refused and one for a refused alternate stack
3460    /// (`crate::refused_signal_controls`), whichever process reports it.
3461    fn announce_refused_signal_controls(&self, actions: u64, alternate_stack: bool) {
3462        let mut announced = self.announced_refused_signal_controls.lock().unwrap();
3463        let new_actions = actions & !announced.0;
3464        announced.0 |= actions;
3465        let new_stack = alternate_stack && !announced.1;
3466        announced.1 |= alternate_stack;
3467        drop(announced);
3468        for signum in (1..=64).filter(|signum| new_actions & (1 << (signum - 1)) != 0) {
3469            warn!(
3470                "{}",
3471                crate::refused_signal_controls::handler_refused_warning(signum)
3472            );
3473        }
3474        if new_stack {
3475            warn!(
3476                "{}",
3477                crate::refused_signal_controls::alternate_stack_refused_warning()
3478            );
3479        }
3480    }
3481
3482    async fn dispatch_rpc(
3483        &self,
3484        from: Tid,
3485        gr: <Self as GlobalTool>::Request,
3486    ) -> <Self as GlobalTool>::Response {
3487        if replay_departure::is_report(&gr.2) {
3488            return self.accept_replay_departure(from, gr);
3489        }
3490        // This read stays inside the caller's existing grant. In particular,
3491        // it neither drains logs nor updates clocks or deadline ownership.
3492        if let GlobalRequest::MappingCapture(request) = &gr.2 {
3493            return (
3494                None,
3495                GlobalResponse::MappingCapture(self.mapping_capture_request(from, gr.1, request)),
3496            );
3497        }
3498        #[cfg(target_arch = "aarch64")]
3499        if let GlobalRequest::StoreExclusive(report) = &gr.2 {
3500            return self.note_store_exclusive(report);
3501        }
3502        if let GlobalRequest::CheckBrkMappings(check, fact) = &gr.2 {
3503            return (
3504                None,
3505                GlobalResponse::CheckBrkMappings(
3506                    self.capture_brk_mappings(from, gr.1, *check, *fact),
3507                ),
3508            );
3509        }
3510        if let GlobalRequest::HoldTable(table) = &gr.2 {
3511            return self.hold_table(from, table);
3512        }
3513        // This entry is also used directly. The strict native cut must precede
3514        // ordinary forwarding drain/checks and all sender-clock accounting.
3515        if native_exec::is_native_exec_control(&gr.2) {
3516            return self.dispatch_native_exec_control(from, gr);
3517        }
3518        // A Detcore warning from inside the guest (`crate::guest_log`) carries
3519        // no logical time, changes no scheduler state, and its answer carries
3520        // no time back. It is answered before the forwarded-record drain below,
3521        // whose order state it must not touch: verification never compares a
3522        // warning's place, and a run's ordered records must not depend on
3523        // whether it warned. (`receive_rpc` takes no deadline hint for it.)
3524        if let GlobalRequest::GuestLog(record, unsent_before) = &gr.2 {
3525            crate::guest_log::emit(record.as_ref(), *unsent_before);
3526            return (None, GlobalResponse::GuestLog(()));
3527        }
3528        // A thread a SIGKILL ended asks, from its exit callback, for the
3529        // robust list its sender recorded for it. The thread is retired and
3530        // its request is no turn: like a warning, it carries no logical time,
3531        // touches no ordering state and is answered before the forwarded-record
3532        // drain, and a committed victim's request is answered here, before the
3533        // gate that leaves its other requests unanswered.
3534        if let GlobalRequest::TakeSigkillRobustList = &gr.2 {
3535            let list = self
3536                .sched
3537                .lock()
3538                .unwrap()
3539                .take_sigkill_robust_list(DetTid::from_raw(from.as_raw()), gr.1);
3540            return (None, GlobalResponse::TakeSigkillRobustList(list));
3541        }
3542        // Signal controls an in-guest runtime refused
3543        // (`crate::refused_signal_controls`): announced once per run, and,
3544        // like a warning, answered before the forwarded-record drain, with no
3545        // time either way.
3546        if let GlobalRequest::AnnounceRefusedSignalControls(actions, alternate_stack) = &gr.2 {
3547            self.announce_refused_signal_controls(*actions, *alternate_stack);
3548            return (None, GlobalResponse::AnnounceRefusedSignalControls(()));
3549        }
3550        // The host's block-device table, a fact of the host the run reads
3551        // once (`GlobalState::host_diskstats`): answered here too, with no
3552        // time either way.
3553        if let GlobalRequest::HostDiskstats = &gr.2 {
3554            return (None, GlobalResponse::HostDiskstats(self.host_diskstats()));
3555        }
3556        // The record sequence of a per-process file's open file description a
3557        // fork copied where the tool runs in each guest process
3558        // (`Detcore::take_procfs_sequence`): taken and kept by the thread that
3559        // reads or moves it, in its turn, so in the order of the guest's
3560        // operations, and answered here too, with no time either way.
3561        if let GlobalRequest::ProcfsSequence(id) = &gr.2 {
3562            let sequence = self.procfs_sequences.lock().unwrap().get(id).cloned();
3563            return (None, GlobalResponse::ProcfsSequence(sequence));
3564        }
3565        if let GlobalRequest::StoreProcfsSequence(id, sequence) = &gr.2 {
3566            self.procfs_sequences
3567                .lock()
3568                .unwrap()
3569                .insert(*id, sequence.clone());
3570            return (None, GlobalResponse::StoreProcfsSequence(()));
3571        }
3572        // Records an in-guest Tool forwarded before sending this request are queued,
3573        // and the sender's are written now if it runs alone (the order rule at
3574        // `detlog::set_forwarded_source`).
3575        crate::detlog::drain_forwarded_at_request(from.as_raw());
3576        type R = GlobalResponse;
3577        let dtid = DetTid::from_raw(from.into()); // TODO(T78538674): FIXME
3578        let (guest_time, request_mm, request) = gr;
3579        let request = match request {
3580            GlobalRequest::WithForwardedCount(produced, request) => {
3581                crate::detlog::check_forwarded_count(from.as_raw(), produced);
3582                *request
3583            }
3584            request => request,
3585        };
3586        let time_from_guest = guest_time.as_nanos();
3587        // The thread's own computation and syscalls, excluding clock advances
3588        // (`DetTime::advance_to`), for `--work-span-report`.
3589        let work_span_cpu = self.cfg.work_span_report.is_some().then(|| {
3590            guest_time.user_cpu_time().as_nanos() + guest_time.system_cpu_time().as_nanos()
3591        });
3592        if let Some(cpu) = work_span_cpu {
3593            use crate::scheduler::work_span::Arrival;
3594            let arrival = match &request {
3595                GlobalRequest::StartNewThread(child, ..) if *child == dtid => Arrival::StartsThread,
3596                GlobalRequest::RequestResources(resources, _) if resources.poll_attempt > 0 => {
3597                    Arrival::Retry(resources)
3598                }
3599                GlobalRequest::ReconnectExec { .. } | GlobalRequest::RetireExec { .. } => {
3600                    Arrival::ExecTransfer
3601                }
3602                _ => Arrival::Other,
3603            };
3604            self.sched
3605                .lock()
3606                .unwrap()
3607                .work_span_note_arrival(dtid, cpu, arrival);
3608        }
3609        // Exec transfer messages authenticate both incarnations before ordinary
3610        // sender-clock accounting. The carried clock still belongs to the former
3611        // thread until the successful reconnect commits.
3612        match &request {
3613            GlobalRequest::ReconnectExec { former, process } => {
3614                return self
3615                    .recv_exec_transfer(from, guest_time, request_mm, *former, *process)
3616                    .await;
3617            }
3618            GlobalRequest::RetireExec { thread, signaled } => {
3619                return self
3620                    .recv_retire_exec_transfer(
3621                        from,
3622                        guest_time,
3623                        request_mm,
3624                        thread.clone(),
3625                        *signaled,
3626                    )
3627                    .await;
3628            }
3629            _ => {}
3630        }
3631        // A committed SIGKILL victim's requests that return before ordinary
3632        // admission (https://github.com/rrnewton/hermit/issues/3994). Exec
3633        // transfers above, the child-exit SIGCHLD control (sent in the
3634        // sender's own turn, which a committed victim no longer gets, or by
3635        // the tracer at a signal stop) and the consuming `SignalDequeued` keep
3636        // their own answers. Shared open file control is in-guest LiteInst
3637        // transport, never sent on ptrace, where alone a SIGKILL is retired
3638        // at the commit. Observations and loss notices are still recorded,
3639        // since dropping one only loses information, but not answered. A
3640        // SIGALRM ledger change from a thread that is already logically dead
3641        // would land at a host-chosen time: it is refused, and not answered
3642        // either.
3643        let early_victim_request = match &request {
3644            GlobalRequest::RecordHostInput { .. }
3645            | GlobalRequest::RecordHostMutation { .. }
3646            | GlobalRequest::RecordDeterminismLoss(_)
3647            | GlobalRequest::Sigalrm(_) => self
3648                .sched
3649                .lock()
3650                .unwrap()
3651                .is_unaccounted_sigkill_victim(dtid),
3652            _ => false,
3653        };
3654        if early_victim_request && matches!(&request, GlobalRequest::Sigalrm(_)) {
3655            return (None, R::Retired);
3656        }
3657        // An observation for `hermit run --verify`, answered before any clock
3658        // or scheduler accounting: it carries no logical time, changes no
3659        // scheduler state, and its answer carries no time back.
3660        if let GlobalRequest::RecordHostInput {
3661            path,
3662            syscall,
3663            identity,
3664            source,
3665            regular,
3666            exec,
3667            kernel_object,
3668        } = request
3669        {
3670            let physical = identity;
3671            // A pathname alone is never provenance. The launcher owns and
3672            // pins these exact copies; a user mount or a changed copy cannot
3673            // match. Without a source for the later content check, retain the
3674            // physical identity throughout.
3675            let identity = source
3676                .as_ref()
3677                .filter(|_| regular && !kernel_object)
3678                .and_then(|_| {
3679                    self.cfg
3680                        .host_input_snapshots
3681                        .iter()
3682                        .find(|snapshot| snapshot.physical == physical)
3683                })
3684                .map_or(physical, |snapshot| snapshot.origin);
3685            let change = self.replay_host_inputs.as_ref().and_then(|replay| {
3686                let mutated = self
3687                    .host_mutations
3688                    .lock()
3689                    .unwrap_or_else(PoisonError::into_inner);
3690                replay
3691                    .lock()
3692                    .unwrap_or_else(PoisonError::into_inner)
3693                    .observe(&path, identity, regular, kernel_object, &mutated)
3694                    .cloned()
3695            });
3696            if let Some(change) = change {
3697                report_host_input_change(&change);
3698            }
3699            let mut records = self
3700                .host_inputs
3701                .lock()
3702                .unwrap_or_else(PoisonError::into_inner);
3703            if let Some(source) = source {
3704                self.host_input_sources
3705                    .lock()
3706                    .unwrap_or_else(PoisonError::into_inner)
3707                    .push((records.len(), PathBuf::from(source), physical));
3708            }
3709            records.push(HostInputRecord {
3710                path,
3711                dtid: dtid.as_raw() as u64,
3712                syscall,
3713                identity,
3714                // Read by `settle_host_inputs`, once the guest has exited.
3715                content: None,
3716                exec,
3717                kernel_object,
3718            });
3719            drop(records);
3720            if early_victim_request {
3721                return (None, R::Retired);
3722            }
3723            return (None, R::RecordHostInput(()));
3724        }
3725        if let GlobalRequest::RecordHostMutation { path } = request {
3726            self.host_mutations.lock().unwrap().insert(path);
3727            if early_victim_request {
3728                return (None, R::Retired);
3729            }
3730            return (None, R::RecordHostMutation(()));
3731        }
3732        // A determinism loss is recorded where verification reads it, and,
3733        // like the observations above, moves no clock: whether the host made
3734        // a guest-side handler lose determinism must not change guest time.
3735        if let GlobalRequest::RecordDeterminismLoss(reason) = request {
3736            crate::detlog::write_loss_notice(&reason);
3737            if early_victim_request {
3738                return (None, R::Retired);
3739            }
3740            return (None, R::RecordDeterminismLoss(()));
3741        }
3742        if let GlobalRequest::CallbackCheckpointMismatch {
3743            checkpoint,
3744            hard,
3745            actual,
3746        } = request
3747        {
3748            let mut sched = self.lock_rpc_scheduler(false).await;
3749            if sched.thread_is_logically_killed(dtid)
3750                || !sched.rpc_incarnation_matches(dtid, request_mm)
3751            {
3752                return (None, R::ThreadExited);
3753            }
3754            sched.callback_checkpoint_mismatch(dtid, &checkpoint, hard, &actual);
3755            return (None, R::CallbackCheckpointMismatch(()));
3756        }
3757        // Control traffic for a shared open file description: like the
3758        // observations above, it carries no logical time, changes no
3759        // scheduler state, and its answer carries no time back.
3760        if let GlobalRequest::SharedOpenFile(control) = request {
3761            return (
3762                None,
3763                R::SharedOpenFile(self.shared_open_file_control(dtid, control)),
3764            );
3765        }
3766        // A SIGALRM ledger control message: like the messages above, it
3767        // carries no logical time and its answer carries none back. It reads
3768        // or records ledger state, which changes only at scheduler commits,
3769        // in the sender's own turn.
3770        if let GlobalRequest::Sigalrm(control) = request {
3771            let refused = self.sched.lock().unwrap().sigalrm_control(dtid, control);
3772            return (None, R::Sigalrm(refused));
3773        }
3774        // A child-exit SIGCHLD control message: like the SIGALRM ledger's, it
3775        // carries no logical time and its answer carries none back. It
3776        // records an exit status in the sender's own turn, or answers at a
3777        // signal stop, which no scheduler state waits for.
3778        if let GlobalRequest::ChildExitSigchld(control) = request {
3779            let answer = self
3780                .sched
3781                .lock()
3782                .unwrap()
3783                .child_exit_sigchld_control_for_receiver(dtid, control);
3784            return (None, R::ChildExitSigchld(answer));
3785        }
3786        // A discarded-at-send record message
3787        // (`scheduler::discarded_at_send`): like the child-exit ledger's, it
3788        // carries no logical time and its answer carries none back. It records
3789        // a send in the sender's own turn, or a consumption at a signal stop.
3790        if let GlobalRequest::DiscardedSignal(control) = request {
3791            let answer = self
3792                .sched
3793                .lock()
3794                .unwrap()
3795                .discarded_signal_control(dtid, control);
3796            return (None, R::DiscardedSignal(answer));
3797        }
3798        // A thread's copied signal dispositions
3799        // (`syscalls::signal_shadow::publish_dispositions`): like the control
3800        // messages above, it carries no logical time and its answer carries
3801        // none back. It replaces what the scheduler holds for the sender's
3802        // process, which the sender's own operations changed while it held
3803        // the turn.
3804        if let GlobalRequest::PublishSignalDispositions(generation, dispositions) = request {
3805            let held = self.sched.lock().unwrap().publish_signal_dispositions(
3806                dtid,
3807                request_mm,
3808                generation,
3809                dispositions,
3810            );
3811            return (None, R::PublishSignalDispositions(held));
3812        }
3813        // A thread's mask from Detcore's copy
3814        // (`syscalls::signal_shadow::publish_mask`): the same kind of message.
3815        if let GlobalRequest::PublishSignalMask(mask) = request {
3816            let taken = self
3817                .sched
3818                .lock()
3819                .unwrap()
3820                .publish_signal_mask(dtid, request_mm, mask);
3821            return (None, R::PublishSignalMask(taken));
3822        }
3823        if let GlobalRequest::PublishRuntimePrivateMemory(facts) = request {
3824            let accepted = self
3825                .sched
3826                .lock()
3827                .unwrap()
3828                .publish_runtime_private_memory(dtid, request_mm, facts);
3829            return (None, R::PublishRuntimePrivateMemory(accepted));
3830        }
3831        if let GlobalRequest::SignalDequeued {
3832            detpid,
3833            identity,
3834            dequeue,
3835        } = &request
3836        {
3837            return self
3838                .recv_signal_dequeued(dtid, request_mm, guest_time, *detpid, *identity, *dequeue)
3839                .await;
3840        }
3841
3842        // A vfork child registers itself while the parent is kernel-blocked.
3843        // Reopening a former exec worker's TID requires that exact pending
3844        // parent grant and its address-space lineage, not an ordinary RPC.
3845        if let GlobalRequest::CreateVforkChildThread(parent, process, child, _, flags, ..) =
3846            &request
3847        {
3848            let mut sched = self.lock_rpc_scheduler(false).await;
3849            if sched.transferred_exec_tid_requires_registration(dtid) {
3850                if *child != dtid
3851                    || !flags.contains(CloneFlags::CLONE_VFORK)
3852                    || !sched.pending_vfork_registration_matches(
3853                        *parent,
3854                        *process,
3855                        *child,
3856                        request_mm,
3857                        flags.contains(CloneFlags::CLONE_VM),
3858                    )
3859                {
3860                    return (None, R::ThreadExited);
3861                }
3862                sched.register_reused_transferred_exec_tid(dtid, request_mm);
3863            }
3864        }
3865
3866        // Ptrace may deliver a newborn's startup before the parent's clone
3867        // callback registers it. A former exec worker's reused Linux TID must
3868        // wait for that authenticated registration, not inherit the old
3869        // incarnation or turn a premature refusal into a silent exit(0).
3870        // So must any other reused TID (`id_awaits_new_registration`): its
3871        // registration retires the earlier thread's clock, and a clock this
3872        // start published first would be retired with it.
3873        // No clock or scheduler request is published while it waits.
3874        if let GlobalRequest::StartNewThread(child, detpid, ..) = &request
3875            && *child == dtid
3876        {
3877            loop {
3878                {
3879                    let sched = self.lock_rpc_scheduler(false).await;
3880                    let awaits_registration = sched
3881                        .transferred_exec_tid_requires_registration(dtid)
3882                        || id_awaits_new_registration(&sched, dtid);
3883                    // These starts end without a registration, answered below
3884                    // or by `recv_start_new_thread` as before. An exec
3885                    // worker's reused TID mismatches its incarnation until its
3886                    // registration rebinds it, so only another mismatch ends
3887                    // the wait.
3888                    let ends_unregistered = sched.thread_is_logically_killed(dtid)
3889                        || sched.is_unaccounted_sigkill_victim(dtid)
3890                        || (!sched.rpc_incarnation_matches(dtid, request_mm)
3891                            && !sched.transferred_exec_tid_requires_registration(dtid))
3892                        || (self.cfg.backend.process_exits_complete_asynchronously
3893                            && sched.process_physically_gone(*detpid));
3894                    if !awaits_registration || ends_unregistered {
3895                        break;
3896                    }
3897                }
3898                yield_once().await;
3899            }
3900        }
3901
3902        let is_deregister = matches!(&request, GlobalRequest::DeregisterThread(_));
3903        let consuming_cleanup =
3904            is_deregister || matches!(&request, GlobalRequest::RobustListWakes(_));
3905        // What a committed SIGKILL victim may still send and be answered
3906        // (`Scheduler::is_unaccounted_sigkill_victim`): its consuming cleanup,
3907        // and the failure reports that end a run.
3908        let answered_for_sigkill_victim = consuming_cleanup
3909            || matches!(
3910                &request,
3911                GlobalRequest::ParkedProtocolFailure(_)
3912                    | GlobalRequest::UnrecoverableShutdown
3913                    | GlobalRequest::PolicyRefusal(_)
3914                    | GlobalRequest::StoppingRefusal(_)
3915            );
3916
3917        let (exec_reconnect, is_exec_caller_after_local_mm_swap) = {
3918            let pending = self.pending_exec_states.lock().unwrap();
3919            let reconnect = match &request {
3920                GlobalRequest::CreateChildThread(child, process, _, None, _, _, _)
3921                    if *child == dtid && *child == *process =>
3922                {
3923                    pending.get(process).cloned()
3924                }
3925                _ => None,
3926            };
3927            let is_exec_caller_after_local_mm_swap = pending.values().any(|state| {
3928                state.caller == dtid && state.mm.for_exec(state.process) == request_mm
3929            });
3930            (reconnect, is_exec_caller_after_local_mm_swap)
3931        };
3932
3933        // Retired incarnations cannot publish clocks. A transferred former TID
3934        // reopens only through fresh child registration; successful exec binds
3935        // the surviving leader separately. Hold admission through clock
3936        // accounting so logical teardown cannot linearize between the two.
3937        let mut tombstoned_deregistration = None;
3938        {
3939            let mut sched = self.lock_rpc_scheduler(consuming_cleanup).await;
3940            if exec_reconnect.is_none()
3941                && !is_exec_caller_after_local_mm_swap
3942                && !sched.rpc_incarnation_matches(dtid, request_mm)
3943            {
3944                debug!(
3945                    "[detcore, dtid {}] rejecting {:?} RPC from retired exec incarnation {:?}",
3946                    dtid, request, request_mm,
3947                );
3948                return if is_deregister {
3949                    (None, R::DeregisterThread(()))
3950                } else {
3951                    (None, R::ThreadExited)
3952                };
3953            }
3954            if let GlobalRequest::DeregisterThread(owner) = &request {
3955                assert_eq!(
3956                    owner.dettid, dtid,
3957                    "deregistration must belong to its sender"
3958                );
3959                assert_eq!(owner.mm, request_mm, "deregistration must retain its MmId");
3960                // A vfork child torn down before its registration completed
3961                // (it died before its first stop, or its registration was
3962                // refused) never registers; the barrier of the vfork that
3963                // created it must not wait for it.
3964                if let Some(origin) = owner.vfork_origin {
3965                    sched.record_unregistered_vfork_child_exit(origin, owner.dettid);
3966                }
3967                // DBT can reject StartNewThread before parent registration.
3968                // Its tombstone still needs the existing final accounting path.
3969                if !sched.thread_was_registered(dtid) && !sched.thread_is_logically_killed(dtid) {
3970                    assert!(
3971                        sched.backend_failed() || !owner.thread_start_entered,
3972                        "a started thread must have a scheduler registration before deregistration"
3973                    );
3974                    // The backend still consumes this constructed ThreadState,
3975                    // but no guest start/registration happened. Acknowledge its
3976                    // cleanup without creating a clock, tree entry or admission.
3977                    return (None, R::DeregisterThread(()));
3978                }
3979            }
3980            let child = match &request {
3981                GlobalRequest::CreateChildThread(child, ..)
3982                | GlobalRequest::CreateVforkChildThread(_, _, child, ..) => Some(*child),
3983                _ => None,
3984            };
3985            if sched.thread_is_logically_killed(dtid) && exec_reconnect.is_none() {
3986                trace!(
3987                    "[detcore, dtid {}] rejecting RPC after permanent logical-thread removal",
3988                    dtid
3989                );
3990                if let GlobalRequest::DeregisterThread(deregistration) = &request {
3991                    tombstoned_deregistration = Some(deregistration.clone());
3992                } else {
3993                    return (None, R::ThreadExited);
3994                }
3995            }
3996            if !answered_for_sigkill_victim && sched.is_unaccounted_sigkill_victim(dtid) {
3997                trace!(
3998                    "[detcore, dtid {}] leaving a SIGKILLed thread's request unanswered",
3999                    dtid
4000                );
4001                return (None, R::Retired);
4002            }
4003            if child.is_some_and(|child| sched.thread_is_logically_killed(child))
4004                && exec_reconnect.is_none()
4005            {
4006                trace!(
4007                    "[detcore, dtid {}] rejecting registration that reuses a tombstoned child TID",
4008                    dtid
4009                );
4010                return (None, R::ThreadExited);
4011            }
4012
4013            if let GlobalRequest::ResumeExec(process) = &request
4014                && (dtid != *process
4015                    || sched.registered_process(dtid) != Some(*process)
4016                    || !sched.next_turns.contains_key(&dtid))
4017            {
4018                return (None, R::ThreadExited);
4019            }
4020
4021            // An admitted ordinary request under the new identity proves the
4022            // Tool received the transfer acknowledgment and rebound its state.
4023            self.acknowledge_exec_transfer(dtid, request_mm);
4024
4025            let is_thread_reconnect = matches!(
4026                &request,
4027                GlobalRequest::StartNewThread(child_dettid, ..) if *child_dettid == dtid
4028            ) && self.global_time.lock().unwrap().contains_thread(dtid);
4029
4030            // A vfork child registers itself, so this message is its
4031            // registration and the first to carry its clock.
4032            if matches!(&request, GlobalRequest::CreateVforkChildThread(_, _, child, ..) if *child == dtid)
4033                && self.clock_belongs_to_earlier_thread(&sched, dtid)
4034            {
4035                self.global_time.lock().unwrap().retire_thread(dtid);
4036            }
4037
4038            // This portion of the time updates "asynchronously", and we can tick it on every rpc:
4039            // TODO: eventually the vector clock should be in shared memory, and
4040            // the local clocks should update truly asynchrously.  Therefore it
4041            // SHOULD be safe to always push through this update on any rpc.
4042            if tombstoned_deregistration.is_none()
4043                && exec_reconnect.is_none()
4044                && !is_thread_reconnect
4045            {
4046                self.global_time.lock().unwrap().update_global_time(
4047                    dtid,
4048                    time_from_guest,
4049                    guest_time.inherited_nanos(),
4050                );
4051            }
4052        }
4053        if let Some(deregistration) = tombstoned_deregistration {
4054            self.recv_deregister_thread(from, deregistration).await;
4055            return (None, R::DeregisterThread(()));
4056        }
4057
4058        // RPC boilerplate. (Hard to generate systematically now though, because of the
4059        // time payload piggy-backing on each rpc. Maybe eventually once ticking a
4060        // threads' own clock happens through shared memory.)
4061        #[allow(clippy::unit_arg)]
4062        let resp = match request {
4063            GlobalRequest::PrepareNativeExec(_)
4064            | GlobalRequest::SealNativeExecStats { .. }
4065            | GlobalRequest::IssueNativeExec { .. }
4066            | GlobalRequest::CancelNativeExec { .. }
4067            | GlobalRequest::CompleteNativeExec { .. } => {
4068                unreachable!("native controls handled before ordinary admission")
4069            }
4070            GlobalRequest::ReconnectExec { .. } | GlobalRequest::RetireExec { .. } => {
4071                unreachable!("exec transfer handled before ordinary admission")
4072            }
4073            GlobalRequest::WithForwardedCount(..) => {
4074                unreachable!("a counted request is unwrapped on arrival")
4075            }
4076            GlobalRequest::ReplayDeparture { .. } => {
4077                unreachable!("replay findings use the authenticated neutral path")
4078            }
4079            GlobalRequest::ResumeExec(process) => {
4080                let mut resources = Resources::new(dtid);
4081                resources.insert(ResourceID::MemAddrSpace(process), Permission::RW);
4082                let (response, duration) = self
4083                    .recv_request_resources(from, process, resources, Some(request_mm))
4084                    .await;
4085                match response {
4086                    SchedulerRpcResult::Continue(status) => R::ResumeExec(status, duration),
4087                    SchedulerRpcResult::ThreadExited => R::ThreadExited,
4088                }
4089            }
4090            GlobalRequest::SignalDequeued { .. } => {
4091                unreachable!("consuming path handled before ordinary cancellation")
4092            }
4093            GlobalRequest::RecordHostInput { .. } | GlobalRequest::RecordHostMutation { .. } => {
4094                unreachable!("host-input observation answered before clock accounting")
4095            }
4096            GlobalRequest::SharedOpenFile(_) => {
4097                unreachable!("shared open file control answered before clock accounting")
4098            }
4099            GlobalRequest::GuestLog(..) => {
4100                unreachable!("a guest log record is answered before clock accounting")
4101            }
4102            GlobalRequest::TakeSigkillRobustList => {
4103                unreachable!("a SIGKILL victim's robust list is taken before clock accounting")
4104            }
4105            GlobalRequest::SigkillDyingThreads(token) => R::SigkillDyingThreads(
4106                self.lock_rpc_scheduler(false)
4107                    .await
4108                    .sigkill_dying_threads(token),
4109            ),
4110            GlobalRequest::RecordSigkillRobustLists(token, lists) => {
4111                self.lock_rpc_scheduler(false)
4112                    .await
4113                    .record_sigkill_robust_lists(token, lists);
4114                R::RecordSigkillRobustLists(())
4115            }
4116            GlobalRequest::AnnounceRefusedSignalControls(..) => {
4117                unreachable!(
4118                    "refused signal controls are answered before the forwarded-record drain"
4119                )
4120            }
4121            GlobalRequest::HostDiskstats => {
4122                unreachable!("the host's diskstats are answered before the forwarded-record drain")
4123            }
4124            GlobalRequest::ProcfsSequence(..) | GlobalRequest::StoreProcfsSequence(..) => {
4125                unreachable!("record sequences are answered before the forwarded-record drain")
4126            }
4127            GlobalRequest::MappingCapture(_) => {
4128                unreachable!("coherent mapping capture precedes clock accounting")
4129            }
4130            GlobalRequest::CheckBrkMappings(..) => {
4131                unreachable!("mapping capture is answered before clock accounting")
4132            }
4133            GlobalRequest::HoldTable(_) => {
4134                unreachable!("a held table is answered before clock accounting")
4135            }
4136            #[cfg(target_arch = "aarch64")]
4137            GlobalRequest::StoreExclusive(_) => {
4138                unreachable!("a store-exclusive report is answered before clock accounting")
4139            }
4140            GlobalRequest::Sigalrm(_) => {
4141                unreachable!("SIGALRM ledger control answered before clock accounting")
4142            }
4143            GlobalRequest::ChildExitSigchld(_) => {
4144                unreachable!("child-exit SIGCHLD control answered before clock accounting")
4145            }
4146            GlobalRequest::DiscardedSignal(_) => {
4147                unreachable!("discarded-at-send control answered before clock accounting")
4148            }
4149            GlobalRequest::RecordDeterminismLoss(_) => {
4150                unreachable!("determinism loss answered before clock accounting")
4151            }
4152            GlobalRequest::CallbackCheckpointMismatch { .. } => {
4153                unreachable!("callback mismatch answered before clock accounting")
4154            }
4155            GlobalRequest::PublishSignalDispositions(..) => {
4156                unreachable!("published dispositions answered before clock accounting")
4157            }
4158            GlobalRequest::PublishSignalMask(_) => {
4159                unreachable!("a published mask is answered before clock accounting")
4160            }
4161            GlobalRequest::PublishRuntimePrivateMemory(_) => {
4162                unreachable!("private-memory publication precedes clock accounting")
4163            }
4164            GlobalRequest::ParkedRequest(rs, pid, capability) => {
4165                let (response, _, stack) = self
4166                    .recv_resources_with_origin(
4167                        from,
4168                        pid,
4169                        rs,
4170                        Some(request_mm),
4171                        RpcOrigin::DirectRequestResources,
4172                        capability,
4173                    )
4174                    .await;
4175                match response {
4176                    SchedulerRpcResult::Continue(r) => {
4177                        Self::with_boundary_stack(R::ParkedRequest(r), stack)
4178                    }
4179                    SchedulerRpcResult::ThreadExited => R::ThreadExited,
4180                }
4181            }
4182            GlobalRequest::ResumeParkedRequest {
4183                ticket,
4184                current_site,
4185            } => {
4186                self.recv_resume_parked(from, request_mm, ticket, current_site)
4187                    .await
4188            }
4189            GlobalRequest::FinishParkedObservation {
4190                wait,
4191                lease,
4192                site,
4193                finish,
4194            } => {
4195                let ack = Ivar::new();
4196                let intent = ControlIntent::Finish {
4197                    wait,
4198                    lease,
4199                    site,
4200                    finish,
4201                    ack: ack.clone(),
4202                };
4203                let posted = self
4204                    .sched
4205                    .lock()
4206                    .unwrap()
4207                    .post_control(dtid, request_mm, intent);
4208                if let Err(error) = posted {
4209                    self.sched.lock().unwrap().fail_parked(dtid, error);
4210                    return (None, R::ThreadExited);
4211                }
4212                R::FinishParkedObservation(ack.await)
4213            }
4214            GlobalRequest::ParkedProtocolFailure(error) => {
4215                self.sched.lock().unwrap().fail_parked(dtid, error);
4216                return (None, R::ThreadExited);
4217            }
4218
4219            GlobalRequest::RequestResources(rs, pid) => {
4220                let (response, time, stack) = self
4221                    .recv_resources_with_origin(
4222                        from,
4223                        pid,
4224                        rs,
4225                        Some(request_mm),
4226                        RpcOrigin::DirectRequestResources,
4227                        ControlCapability::None,
4228                    )
4229                    .await;
4230                match self.resource_grant_reply(from, response, time).0 {
4231                    SchedulerRpcResult::Continue(response) => {
4232                        Self::with_boundary_stack(R::RequestResources(response), stack)
4233                    }
4234                    SchedulerRpcResult::ThreadExited => R::ThreadExited,
4235                }
4236            }
4237            GlobalRequest::ReleaseResources(rs) => {
4238                R::ReleaseResources(self.recv_release_resources(from, rs).await)
4239            }
4240            GlobalRequest::ReleaseAllResources => {
4241                R::ReleaseAllResources(self.recv_release_all_resources(from).await)
4242            }
4243            GlobalRequest::PolicyRefusal(reason) => {
4244                self.policy_refusal.lock().unwrap().get_or_insert(reason);
4245                R::PolicyRefusal(())
4246            }
4247            // Acted on even from a committed SIGKILL victim (it is in the
4248            // victim's answered list, as PolicyRefusal is). Whether a victim
4249            // gets to file one before it dies is host-timed once 3c.1c and
4250            // 3c.1e add producers; to be re-examined there. Every 3c.1c
4251            // producer files it in the producing thread's own turn, which a
4252            // committed SIGKILL victim never gets again, so none of them can
4253            // race its victim's death.
4254            //
4255            // IR-16 (A5 step 3c.1c): the run-wide exit is taken only under
4256            // the in-guest delivery gate, whose conditions include that the
4257            // scheduler is the init of the guest's PID namespace (D51, D52),
4258            // which is what makes the exit end every guest. Without the gate
4259            // the request is refused that guarantee: the reason is recorded as
4260            // a policy refusal, which ends the run with
4261            // `HERMIT_POLICY_REFUSAL_EXIT` once it completes, and the request
4262            // is answered, so the coordinator never exits on behalf of a run
4263            // whose guests it might not take with it.
4264            //
4265            // The gate-off answer is the `PolicyRefusal` arm's, and like it
4266            // goes through the bookkeeping after this `match` (the teardown
4267            // and SIGKILL-retirement checks, the time update).
4268            GlobalRequest::StoppingRefusal(reason) => {
4269                if self.cfg.in_guest_signal_delivery {
4270                    crate::scheduler::exit_on_scheduler_refusal(&reason)
4271                }
4272                warn!(
4273                    "[detcore] a stopping refusal was filed without the in-guest delivery gate; recorded as a policy refusal, without the run-wide exit: {}",
4274                    reason
4275                );
4276                self.policy_refusal.lock().unwrap().get_or_insert(reason);
4277                R::PolicyRefusal(())
4278            }
4279            // TODO-HUMAN-REVIEW(PR-643): Review run-wide unsupported-syscall aggregation.
4280            GlobalRequest::ReportUnsupportedSyscall(name) => {
4281                let _sched = self.lock_rpc_scheduler(false).await;
4282                let inserted = self
4283                    .unsupported_syscalls
4284                    .lock()
4285                    .unwrap()
4286                    .insert(name.clone());
4287                if inserted
4288                    && let Some(report) = &self.unsupported_syscall_report_fd
4289                    && let Err(error) = writeln!(report.lock().unwrap(), "{name}")
4290                {
4291                    warn!("failed to append unsupported-syscall report: {error}");
4292                }
4293                R::ReportUnsupportedSyscall(())
4294            }
4295            GlobalRequest::AnnounceSignalHandlerNotInstalled(signum) => {
4296                if self
4297                    .announced_handlers_not_installed
4298                    .lock()
4299                    .unwrap()
4300                    .insert(signum)
4301                {
4302                    warn!("{}", signal_handler_not_installed_warning(signum));
4303                }
4304                R::AnnounceSignalHandlerNotInstalled(())
4305            }
4306            GlobalRequest::PrepareExec(process, mm, fd_blocking) => {
4307                let mut sched = self.lock_rpc_scheduler(false).await;
4308                if mm != request_mm
4309                    || fd_blocking.initialize_inherited_stdio
4310                    || fd_blocking.initial_input.is_some()
4311                {
4312                    return (None, R::ThreadExited);
4313                }
4314                if self.cfg.sequentialize_threads && !sched.prepare_exec_teardown(dtid, process, mm)
4315                {
4316                    return (None, R::ThreadExited);
4317                }
4318                trace!(
4319                    "[detcore, dtid {}] preparing exec for process {} with mm {:?} and logically blocking descriptors {:?}",
4320                    dtid, process, mm, fd_blocking,
4321                );
4322                self.pending_exec_states.lock().unwrap().insert(
4323                    process,
4324                    PendingExecState {
4325                        caller: dtid,
4326                        process,
4327                        mm,
4328                        fd_blocking: *fd_blocking,
4329                    },
4330                );
4331                R::PrepareExec(())
4332            }
4333            GlobalRequest::CancelExec(process) => {
4334                let mut sched = self.lock_rpc_scheduler(false).await;
4335                let mut pending = self.pending_exec_states.lock().unwrap();
4336                if pending
4337                    .get(&process)
4338                    .is_some_and(|state| state.caller == dtid)
4339                {
4340                    let prepared = pending.remove(&process).unwrap();
4341                    sched.finish_exec_teardown(prepared.caller, process, prepared.mm, false);
4342                }
4343                R::CancelExec(())
4344            }
4345            GlobalRequest::MarkPastFirstExecve(
4346                detpid,
4347                signal_identity,
4348                unprepared_exe,
4349                kernel_image,
4350            ) => {
4351                let mut sched = self.lock_rpc_scheduler(false).await;
4352                // The exec leaves its process's descriptor table
4353                // (`LockLedger::exec`); `TableExec` names what survived.
4354                self.lock_ledger.lock().unwrap().exec(detpid);
4355                // A backend preserving the leader's ThreadState does not use
4356                // the identity-transfer RPC. This successful-exec edge still
4357                // commits the frozen sibling cohort in scheduler order.
4358                let prepared = self.pending_exec_states.lock().unwrap().get(&dtid).cloned();
4359                if sched.signal_control_installed() {
4360                    let result = (|| {
4361                        let identity = signal_identity.ok_or(ProtocolFailure::Identity)?;
4362                        let pid = sched
4363                            .registered_process(dtid)
4364                            .ok_or(ProtocolFailure::Identity)?;
4365                        let mut pending = self.pending_exec_states.lock().unwrap();
4366                        if let Some(prepared) = pending.get(&pid) {
4367                            if prepared.caller != dtid || prepared.process != pid {
4368                                return Err(ProtocolFailure::Identity);
4369                            }
4370                            sched.complete_signal_exec(
4371                                pid,
4372                                dtid,
4373                                prepared.mm,
4374                                request_mm,
4375                                identity,
4376                            )?;
4377                            pending.remove(&pid);
4378                        } else {
4379                            // A backend that re-enters thread-start already bound
4380                            // the new image. A lifecycle notification alone cannot
4381                            // authorize a new address space or backend generation.
4382                            sched
4383                                .real_timers
4384                                .validate_task(pid, dtid, request_mm, identity)?;
4385                        }
4386                        Ok(())
4387                    })();
4388                    if let Err(error) = result {
4389                        sched.fail_parked(dtid, error);
4390                        return (None, R::ThreadExited);
4391                    }
4392                }
4393                // The exec caller still owns its turn: delete process-owned
4394                // POSIX deadlines before the replacement image can yield or run.
4395                // Failed exec takes CancelExec and never reaches this point.
4396                sched.delete_exec_posix_timers(detpid);
4397                // The exec resets the process's exit signal to SIGCHLD and turns
4398                // its existing children's notifications into SIGCHLD.
4399                sched.record_successful_exec(detpid);
4400                // Reloading backends consumed the preparation in their
4401                // registration reconnect; state-preserving leaders consume it
4402                // here after the terminal exec notification was authenticated.
4403                if let Some(prepared) = &prepared
4404                    && self.cfg.sequentialize_threads
4405                    && prepared.caller == dtid
4406                    && prepared.process == dtid
4407                    && prepared.mm.for_exec(dtid) == request_mm
4408                {
4409                    // Only a reloading backend (DBT) carries owner-death
4410                    // wakes with its preparation, and it delivered them at
4411                    // its reconnect; ptrace delivers its own after the exec
4412                    // (Claude review of
4413                    // https://github.com/rrnewton/hermit/pull/4352, F5).
4414                    assert!(
4415                        prepared.fd_blocking.robust_wakes.is_empty(),
4416                        "a state-preserving exec carried robust-list wakes to the coordinator"
4417                    );
4418                    sched.reconnect_after_exec(ExecReconnect {
4419                        caller: dtid,
4420                        new_leader: dtid,
4421                        detpid: dtid,
4422                        pre_exec_mm: prepared.mm,
4423                        post_exec_mm: request_mm,
4424                        child_tid_addr: 0,
4425                        reconnect_priority: None,
4426                    });
4427                    self.pending_exec_states.lock().unwrap().remove(&dtid);
4428                    // Native-only lifecycle receipt: the actual same-leader
4429                    // Mark edge has now installed this ticket's new mm. This
4430                    // hook grants no turn and performs no clock accounting.
4431                    if let Some(registry) = self.native_exec_registry.get()
4432                        && registry
4433                            .lock()
4434                            .unwrap()
4435                            .mark_past_first_exec(dtid, request_mm)
4436                            .is_err()
4437                    {
4438                        return (None, R::ThreadExited);
4439                    }
4440                }
4441                // The exec caller's priority-inheritance futexes go to their
4442                // waiters, as Linux's `futex_exec_release` releases them after
4443                // the robust lists, which the exec's backend delivered first.
4444                if self.cfg.sequentialize_threads {
4445                    sched.release_pi_futexes_of(dtid);
4446                }
4447                self.past_first_execve.store(true, SeqCst);
4448                let first_image = !self.initial_descriptor_image_complete.swap(true, SeqCst);
4449                let mut overrides = self
4450                    .post_exec_fd_blocking
4451                    .lock()
4452                    .unwrap()
4453                    .remove(&dtid)
4454                    .unwrap_or_default();
4455                trace!(
4456                    "[detcore, dtid {}] restoring logically blocking descriptors after exec: {:?}",
4457                    dtid, overrides,
4458                );
4459                overrides.initialize_inherited_stdio =
4460                    first_image && self.cfg.backend.tool_shares_guest_descriptor_table;
4461                // The image the exec ran: a reloading backend carried it in the
4462                // preparation it reconnected with (above); a state-preserving
4463                // one left the preparation to this edge. The registered root's
4464                // first image is the one its launch named, which a backend
4465                // whose launcher loads the guest (SaBRe) never execs.
4466                // Without sequentialized threads, sibling execs may prepare
4467                // concurrently and the preparation this edge consumed may be
4468                // another thread's, which the launcher check cannot tell
4469                // apart: no prepared record is kept (a launched run then
4470                // refuses the link by name).
4471                if !self.cfg.sequentialize_threads {
4472                    overrides.exe = None;
4473                } else if overrides.exe.is_none() {
4474                    overrides.exe = prepared
4475                        .filter(|prepared| prepared.caller == dtid)
4476                        .and_then(|prepared| prepared.fd_blocking.exe)
4477                        // An exec no Detcore prepared (a backend's copied
4478                        // vfork child runs it without Detcore): the record the
4479                        // new image resolved from the path the exec was given.
4480                        .or(unprepared_exe);
4481                }
4482                if self.root_dettid.get() == Some(&dtid)
4483                    && !self.root_image_marked.swap(true, SeqCst)
4484                    && let Some(launch) = &self.cfg.launch_image
4485                {
4486                    let launched = launch.exe.as_deref().and_then(|exe| {
4487                        crate::exec_record::ExecRecord::at(std::path::Path::new("/"), exe)
4488                    });
4489                    overrides.exe = if launch.launcher.is_some() {
4490                        // In a run that declares a launcher, the image's own
4491                        // record (from its `AT_EXECFN`) is the file the
4492                        // launcher actually ran, where it names one other
4493                        // than the launcher: it outranks the launch's own
4494                        // search, which stands where the image gave none.
4495                        overrides
4496                            .exe
4497                            .take()
4498                            .filter(|record| Some(record.identity()) != self.launcher_identity())
4499                            .or(launched)
4500                    } else {
4501                        launched
4502                    };
4503                }
4504                // A record stands only where the kernel's own link, which the
4505                // new image read (`kernel_image`), names the recorded file or
4506                // the launcher the run declared: Detcore's resolution looked the
4507                // path up in its own view, which a binfmt_misc handler, another
4508                // filesystem view or a racing exec can make wrong. Where the
4509                // tool cannot read the link (KVM), the record stands.
4510                if let Some(kernel) = kernel_image {
4511                    overrides.exe = overrides.exe.filter(|record| {
4512                        record.identity() == kernel || self.launcher_identity() == Some(kernel)
4513                    });
4514                }
4515                // The launcher itself is never the guest's image (a link to it
4516                // resolved, as the kernel's link names it).
4517                overrides.exe = overrides
4518                    .exe
4519                    .filter(|record| Some(record.identity()) != self.launcher_identity());
4520                {
4521                    let mut images = self.exec_images.lock().unwrap();
4522                    match &overrides.exe {
4523                        Some(record) => images.insert(detpid, record.clone()),
4524                        None => images.remove(&detpid),
4525                    };
4526                }
4527                if self.cfg.backend.tool_shares_guest_descriptor_table {
4528                    let mut input = self.inherited_input.lock().unwrap();
4529                    if input
4530                        .as_ref()
4531                        .is_some_and(|input| input.root == Some((dtid, request_mm)))
4532                    {
4533                        // Only this registered root/image consumes the launch
4534                        // seed. A descendant or later exec cannot acquire it.
4535                        // Drop the temporary reader at this handoff; the actual
4536                        // root's descriptor remains live until local validation.
4537                        overrides.initial_input =
4538                            input.take().map(|input| input.lease.identity().clone());
4539                    }
4540                }
4541                // The actual exec bound a fresh native input generation, but
4542                // did not create a scheduler turn. Re-publish only the serial
4543                // authority this exact new image already owns, after all old
4544                // image fences, while the existing scheduler mutex is held.
4545                sched
4546                    .publish_reserved_input_exec_authority(dtid, detpid, request_mm)
4547                    .unwrap_or_else(|error| {
4548                        crate::scheduler::exit_on_scheduler_refusal(&format!(
4549                            "HERMIT_CHOSEN_INPUT_AUTHORITY: {error}"
4550                        ))
4551                    });
4552                R::MarkPastFirstExecve(Box::new(overrides))
4553            }
4554            // Requested by the parent thread:
4555            GlobalRequest::CreateChildThread(
4556                dettid,
4557                parent_detpid,
4558                ctid,
4559                flags,
4560                exit_signal,
4561                physical_ids,
4562                priority,
4563            ) => {
4564                if let Some(prepared) = &exec_reconnect {
4565                    let mut sched = self.lock_rpc_scheduler(false).await;
4566                    let (mut pending, post_exec_mm) = {
4567                        let mut states = self.pending_exec_states.lock().unwrap();
4568                        let Some(pending) = states.remove(&parent_detpid) else {
4569                            return (None, R::ThreadExited);
4570                        };
4571                        assert_eq!(&pending, prepared);
4572                        let post_exec_mm = pending.mm.for_exec(pending.process);
4573                        (pending, post_exec_mm)
4574                    };
4575                    assert_eq!(pending.process, parent_detpid);
4576                    if let Some((physical_pid, physical_tid)) = physical_ids
4577                        && let Err(open_error) = sched.register_physical_thread(
4578                            dettid,
4579                            post_exec_mm,
4580                            physical_pid,
4581                            physical_tid,
4582                        )
4583                    {
4584                        error!(
4585                            "[detcore, dtid {}] failed to register post-exec host process {} thread {}: {}",
4586                            dettid, physical_pid, physical_tid, open_error,
4587                        );
4588                        return (None, R::ThreadExited);
4589                    }
4590                    let retired = sched.reconnect_after_exec(ExecReconnect {
4591                        caller: pending.caller,
4592                        new_leader: dettid,
4593                        detpid: parent_detpid,
4594                        pre_exec_mm: pending.mm,
4595                        post_exec_mm,
4596                        child_tid_addr: ctid,
4597                        reconnect_priority: priority,
4598                    });
4599                    if pending.caller != dettid {
4600                        self.global_time
4601                            .lock()
4602                            .unwrap()
4603                            .reassign_thread(pending.caller, dettid);
4604                    }
4605                    // Linux marked the old thread group's robust-list words
4606                    // during the exec, which this message from the new image
4607                    // follows, and the reconnect has retired the threads the
4608                    // exec killed. Wake one waiter per marked word now, under
4609                    // the same lock: the scheduler commits no turn between the
4610                    // exec's grant and this reconnect, and the admissions are
4611                    // drained in canonical order
4612                    // (https://github.com/rrnewton/hermit/issues/4268).
4613                    let robust_wakes = std::mem::take(&mut pending.fd_blocking.robust_wakes);
4614                    wake_exec_robust_waiters(&mut sched, &robust_wakes, "exec");
4615                    if !pending.fd_blocking.is_empty() {
4616                        self.post_exec_fd_blocking
4617                            .lock()
4618                            .unwrap()
4619                            .insert(dettid, pending.fd_blocking);
4620                    }
4621                    debug!(
4622                        "[detcore, dtid {}] reconciled successful exec from caller {}; retired prior identities {:?}",
4623                        dtid, pending.caller, retired
4624                    );
4625                    R::CreateChildThread(Some(post_exec_mm))
4626                } else {
4627                    let registration = self
4628                        .recv_create_child_thread(
4629                            from,
4630                            request_mm,
4631                            ChildRegistration {
4632                                parent_dettid: DetTid::from_raw(from.into()),
4633                                parent_detpid,
4634                                child_dettid: dettid,
4635                                child_tid_addr: ctid,
4636                                flags,
4637                                exit_signal,
4638                                physical_ids,
4639                                maybe_priority: priority,
4640                                parent_is_kernel_blocked: false,
4641                            },
4642                        )
4643                        .await;
4644                    #[cfg(feature = "physical-mm-test-support")]
4645                    self.initial_mm_registration_result(dettid, &registration);
4646                    match registration {
4647                        SchedulerRpcResult::Continue(()) => R::CreateChildThread(None),
4648                        SchedulerRpcResult::ThreadExited => R::ThreadExited,
4649                    }
4650                }
4651            }
4652            // Requested by the vfork child on behalf of its kernel-blocked parent:
4653            GlobalRequest::CreateVforkChildThread(
4654                parent_dettid,
4655                parent_detpid,
4656                child_dettid,
4657                ctid,
4658                flags,
4659                exit_signal,
4660                priority,
4661            ) => match self
4662                .recv_create_child_thread(
4663                    from,
4664                    request_mm,
4665                    ChildRegistration {
4666                        parent_dettid,
4667                        parent_detpid,
4668                        child_dettid,
4669                        child_tid_addr: ctid,
4670                        flags: Some(flags),
4671                        exit_signal,
4672                        physical_ids: None,
4673                        maybe_priority: priority,
4674                        parent_is_kernel_blocked: true,
4675                    },
4676                )
4677                .await
4678            {
4679                SchedulerRpcResult::Continue(()) => R::CreateChildThread(None),
4680                SchedulerRpcResult::ThreadExited => R::ThreadExited,
4681            },
4682            // Requested by the child thread itself:
4683            GlobalRequest::StartNewThread(dettid, detpid, physical_ids, signal_identity) => {
4684                match self
4685                    .recv_start_new_thread(
4686                        from,
4687                        dettid,
4688                        detpid,
4689                        request_mm,
4690                        physical_ids,
4691                        signal_identity,
4692                    )
4693                    .await
4694                {
4695                    SchedulerRpcResult::Continue(history) => {
4696                        let mut sched = self.lock_rpc_scheduler(false).await;
4697                        // A callback-annotated history replays from its
4698                        // first row only at the thread's birth.
4699                        if let Err(refusal) = sched.authenticate_priority_birth(dettid) {
4700                            crate::scheduler::exit_on_scheduler_refusal(&refusal);
4701                        }
4702                        // A thread born after the replay released its schedule
4703                        // starts without the checkpoints, in both copies.
4704                        sched.drop_released_callback_checkpoints(dettid);
4705                        let startup = crate::preemptions::callback_history::Startup {
4706                            checkpoints: sched
4707                                .callback_checkpoints
4708                                .get(&dettid)
4709                                .into_iter()
4710                                .flatten()
4711                                .cloned()
4712                                .collect(),
4713                            progress: sched
4714                                .callback_progress
4715                                .get(&dettid)
4716                                .cloned()
4717                                .unwrap_or_default(),
4718                        };
4719                        let response = R::StartNewThread(history);
4720                        if sched.callback_history_active {
4721                            R::WithCallbackHistory(startup, Box::new(response))
4722                        } else {
4723                            response
4724                        }
4725                    }
4726                    SchedulerRpcResult::ThreadExited => R::ThreadExited,
4727                }
4728            }
4729            GlobalRequest::DeregisterThread(deregistration) => {
4730                // A thread that leaves holds no table for a death to release.
4731                self.held_tables
4732                    .lock()
4733                    .unwrap()
4734                    .remove(&deregistration.dettid);
4735                R::DeregisterThread(self.recv_deregister_thread(from, deregistration).await)
4736            }
4737            GlobalRequest::PublishedGuestSigmask(thread) => {
4738                let scheduler = self.lock_rpc_scheduler(false).await;
4739                // Under the in-guest delivery gate the status reader gets the
4740                // tri-state and D37's held masks (A5 step 3c.1e A); with it
4741                // off, the answer is the one it always was.
4742                if self.cfg.in_guest_signal_delivery {
4743                    R::GuestSigmaskForStatus(scheduler.status_guest_mask(thread))
4744                } else {
4745                    R::PublishedGuestSigmask(scheduler.published_guest_sigmask(thread))
4746                }
4747            }
4748            GlobalRequest::PublishTemporaryMask(control) => R::PublishTemporaryMask(
4749                self.lock_rpc_scheduler(false)
4750                    .await
4751                    .temporary_mask_control(dtid, control),
4752            ),
4753            GlobalRequest::VirtualInotifyCookies(host) => {
4754                let mut table = self
4755                    .inotify_cookies
4756                    .lock()
4757                    .expect("inotify cookie table lock poisoned");
4758                R::VirtualInotifyCookies(
4759                    host.into_iter()
4760                        .map(|cookie| table.virtualize(cookie))
4761                        .collect(),
4762                )
4763            }
4764            GlobalRequest::NextGuestId => R::NextGuestId(self.take_guest_id().await),
4765            GlobalRequest::GaveHostId => {
4766                self.host_id_tasks.fetch_add(1, SeqCst);
4767                // The run's IDs now depend on the host: a record of it says
4768                // so, and a replay refuses it.
4769                if let Some(writer) = &mut self.lock_rpc_scheduler(false).await.preemption_writer {
4770                    writer.set_guest_ids(detcore_model::pid::GuestIds::Host);
4771                }
4772                R::GaveHostId(())
4773            }
4774            GlobalRequest::InPidNamespaceInit(thread) => {
4775                let scheduler = self.lock_rpc_scheduler(false).await;
4776                R::InPidNamespaceInit(
4777                    scheduler
4778                        .thread_tree
4779                        .process_of(thread)
4780                        .is_some_and(|process| {
4781                            scheduler.thread_tree.is_pid_namespace_init(process)
4782                        }),
4783                )
4784            }
4785            GlobalRequest::SetSavedGuestSigmask(mask) => {
4786                let updated = self
4787                    .lock_rpc_scheduler(false)
4788                    .await
4789                    .set_saved_guest_sigmask(dtid, mask);
4790                if updated {
4791                    R::SetSavedGuestSigmask(())
4792                } else {
4793                    R::ThreadExited
4794                }
4795            }
4796            GlobalRequest::SetSigtimedwaitSet(set) => {
4797                let updated = self
4798                    .lock_rpc_scheduler(false)
4799                    .await
4800                    .set_sigtimedwait_set(dtid, set);
4801                if updated {
4802                    R::SetSigtimedwaitSet(())
4803                } else {
4804                    R::ThreadExited
4805                }
4806            }
4807            GlobalRequest::RecordGuestMarkerSignal => {
4808                self.lock_rpc_scheduler(false)
4809                    .await
4810                    .record_guest_marker_signal();
4811                R::RecordGuestMarkerSignal(())
4812            }
4813            GlobalRequest::OwnProcessSignalInputs => R::OwnProcessSignalInputs(
4814                self.lock_rpc_scheduler(false)
4815                    .await
4816                    .own_process_signal_inputs(dtid),
4817            ),
4818            GlobalRequest::ProcessSignalReceiver(process, named, signo) => {
4819                R::ProcessSignalReceiver(
4820                    self.lock_rpc_scheduler(false)
4821                        .await
4822                        .process_signal_receiver(process, named, dtid, signo),
4823                )
4824            }
4825            GlobalRequest::OwnProcessSignalChoice(named, signo) => {
4826                self.lock_rpc_scheduler(false)
4827                    .await
4828                    .own_process_signal_choice(dtid, named, signo);
4829                R::OwnProcessSignalChoice(())
4830            }
4831            GlobalRequest::ProcessSignalSent(process) => {
4832                self.lock_rpc_scheduler(false)
4833                    .await
4834                    .process_signal_sent(dtid, process);
4835                R::ProcessSignalSent(())
4836            }
4837            GlobalRequest::SetChildTidAddress(address) => {
4838                let updated = self
4839                    .lock_rpc_scheduler(false)
4840                    .await
4841                    .set_child_tid_address(dtid, address);
4842                if updated {
4843                    R::SetChildTidAddress(())
4844                } else {
4845                    R::ThreadExited
4846                }
4847            }
4848            GlobalRequest::FutexAction(dettid, action, futexid, init_read, mask, passed_out) => {
4849                R::FutexAction(
4850                    self.recv_futex_action(
4851                        RpcIncarnation {
4852                            dettid,
4853                            mm: request_mm,
4854                        },
4855                        action,
4856                        futexid,
4857                        init_read,
4858                        mask,
4859                        passed_out,
4860                    )
4861                    .await,
4862                )
4863            }
4864            GlobalRequest::RobustListWakes(wakes) => {
4865                R::RobustListWakes(self.recv_robust_list_wakes(dtid, wakes))
4866            }
4867            GlobalRequest::DeterminizeInode(ino, observed, sighting, procfs) => {
4868                R::DeterminizeInode(
4869                    self.recv_determinize_inode(from, ino, procfs, observed, sighting)
4870                        .await,
4871                )
4872            }
4873            GlobalRequest::DeterminizeMappingInodes(inodes) => R::DeterminizeMappingInodes(
4874                self.recv_determinize_mapping_inodes(from, inodes).await,
4875            ),
4876            // AUTONOMOUS-BOT-IMPLEMENTED
4877            // TODO-HUMAN-REVIEW(PR-1056): Deterministic st_dev remapping RPC.
4878            GlobalRequest::DeterminizeDevice(dev) => {
4879                R::DeterminizeDevice(self.recv_determinize_device(from, dev).await)
4880            }
4881            GlobalRequest::DeterminizeMountId(raw_mount_id, fallback_order) => {
4882                R::DeterminizeMountId(
4883                    self.recv_determinize_mount_id(from, raw_mount_id, fallback_order.as_deref())
4884                        .await,
4885                )
4886            }
4887            GlobalRequest::ResolveMountinfoIds(mountinfo_order, observation_order) => {
4888                R::ResolveMountinfoIds(
4889                    self.recv_resolve_mountinfo_ids(from, &mountinfo_order, &observation_order)
4890                        .await,
4891                )
4892            }
4893            GlobalRequest::RetireInode(ino) => {
4894                R::RetireInode(self.recv_retire_inode(from, ino).await)
4895            }
4896            GlobalRequest::ForgetRetiredInode(ino, procfs) => {
4897                R::ForgetRetiredInode(self.recv_forget_retired_inode(from, ino, procfs).await)
4898            }
4899            GlobalRequest::DirectorySnapshotMark => {
4900                let _sched = self.lock_rpc_scheduler(false).await;
4901                let inodes = self.inodes.lock().unwrap();
4902                R::DirectorySnapshotMark(SnapshotMark {
4903                    retirements: inodes.retirements,
4904                    threads_counted: inodes.threads_counted,
4905                })
4906            }
4907            GlobalRequest::ReadyChannel(channel, wants) => R::ReadyChannel(
4908                self.lock_rpc_scheduler(false)
4909                    .await
4910                    .ready_channel(dtid, channel, wants),
4911            ),
4912            GlobalRequest::HandOffTurn => {
4913                R::HandOffTurn(self.lock_rpc_scheduler(false).await.hand_off_turn(dtid))
4914            }
4915            GlobalRequest::CommitFatalDelivery(retire_others) => R::CommitFatalDelivery(
4916                self.lock_rpc_scheduler(false)
4917                    .await
4918                    .commit_fatal_delivery(dtid, retire_others),
4919            ),
4920            GlobalRequest::TouchFile(ino) => R::TouchFile(self.recv_touch_file(from, ino).await),
4921            GlobalRequest::SetFileMtime(ino, mtime) => {
4922                R::SetFileMtime(self.recv_set_file_mtime(from, ino, mtime).await)
4923            }
4924            GlobalRequest::GlobalTimeLowerBound => {
4925                let ns = self.global_time.lock().unwrap().as_nanos();
4926                R::GlobalTimeLowerBound(ns)
4927            }
4928            GlobalRequest::TraceSchedEvent(ev, detpid, command_bootstrap, access) => {
4929                match self
4930                    .recv_trace_schedevent(ev, detpid, request_mm, command_bootstrap, access)
4931                    .await
4932                {
4933                    SchedulerRpcResult::Continue(response) => R::TraceSchedEvent(response),
4934                    SchedulerRpcResult::ThreadExited => R::ThreadExited,
4935                }
4936            }
4937            // AUTONOMOUS-BOT-IMPLEMENTED
4938            // TODO-HUMAN-REVIEW(#663)
4939            // TODO-HUMAN-REVIEW(#869)
4940            GlobalRequest::RegisterAlarm(dpid, dtid, duration, interval, sig) => {
4941                let now = self.global_time.lock().unwrap().as_nanos();
4942                // O1: a dequeue earlier in this turn restarted the timer, so
4943                // the old value reports that restart
4944                // (https://github.com/rrnewton/hermit/issues/4139).
4945                self.sched.lock().unwrap().reconcile_alarm(dpid, now);
4946                let response = match self
4947                    .recv_register_alarm(
4948                        dpid,
4949                        RpcIncarnation {
4950                            dettid: dtid,
4951                            mm: request_mm,
4952                        },
4953                        now,
4954                        duration,
4955                        interval,
4956                        sig,
4957                    )
4958                    .await
4959                {
4960                    SchedulerRpcResult::Continue(remaining) => R::RegisterAlarm(remaining),
4961                    SchedulerRpcResult::ThreadExited => R::ThreadExited,
4962                };
4963                self.sched.lock().unwrap().note_arming_hint(dtid, now);
4964                response
4965            }
4966            // AUTONOMOUS-BOT-IMPLEMENTED
4967            // TODO-HUMAN-REVIEW(PR-841): Review logical alarm query RPC.
4968            GlobalRequest::AlarmRemaining(dpid) => {
4969                let now = self.global_time.lock().unwrap().as_nanos();
4970                let mut sched = self.lock_rpc_scheduler(false).await;
4971                // O1: reconcile a dequeue earlier in this turn first, and end
4972                // the caller's slice at a deadline it restarts
4973                // (https://github.com/rrnewton/hermit/issues/4139).
4974                if sched.reconcile_alarm(dpid, now).is_some() {
4975                    sched.note_arming_hint(dtid, now);
4976                }
4977                match sched.itimer_snapshot(dpid, now) {
4978                    Ok(snapshot) => R::AlarmRemaining(snapshot),
4979                    Err(error) => {
4980                        sched.fail_parked(dtid, ProtocolFailure::Timer(error));
4981                        R::ThreadExited
4982                    }
4983                }
4984            }
4985            // O2: a signalfd read or `rt_sigtimedwait` completion in this
4986            // turn may have dequeued the process's ITIMER_REAL copy
4987            // (https://github.com/rrnewton/hermit/issues/4139).
4988            GlobalRequest::NoteGuestSigalrm(target) => {
4989                self.sched.lock().unwrap().note_guest_sigalrm(dtid, target);
4990                R::NoteGuestSigalrm
4991            }
4992            GlobalRequest::NoteSigalrmDisposition => {
4993                self.sched.lock().unwrap().note_sigalrm_disposition(dtid);
4994                R::NoteGuestSigalrm
4995            }
4996            GlobalRequest::ReconcileAlarm(dpid) => {
4997                let now = self.global_time.lock().unwrap().as_nanos();
4998                let mut sched = self.sched.lock().unwrap();
4999                if sched.reconcile_alarm(dpid, now).is_some() {
5000                    sched.note_arming_hint(dtid, now);
5001                }
5002                R::ReconcileAlarm
5003            }
5004            // AUTONOMOUS-BOT-IMPLEMENTED
5005            // TODO-HUMAN-REVIEW(#869)
5006            GlobalRequest::RegisterPosixTimer(
5007                dpid,
5008                dtid,
5009                timer_id,
5010                deadline,
5011                interval,
5012                notification,
5013            ) => {
5014                let now = self.global_time.lock().unwrap().as_nanos();
5015                let response = match self
5016                    .recv_register_posix_timer(
5017                        dpid,
5018                        RpcIncarnation {
5019                            dettid: dtid,
5020                            mm: request_mm,
5021                        },
5022                        timer_id,
5023                        deadline,
5024                        interval,
5025                        notification,
5026                    )
5027                    .await
5028                {
5029                    SchedulerRpcResult::Continue(()) => R::RegisterPosixTimer(()),
5030                    SchedulerRpcResult::ThreadExited => R::ThreadExited,
5031                };
5032                self.sched.lock().unwrap().note_arming_hint(dtid, now);
5033                response
5034            }
5035            // AUTONOMOUS-BOT-IMPLEMENTED
5036            // TODO-HUMAN-REVIEW(#663)
5037            GlobalRequest::ResolveKillTargets(dpid) => {
5038                let scheduler = self.lock_rpc_scheduler(false).await;
5039                R::ResolveKillTargets(
5040                    scheduler.process_signal_targets(dpid),
5041                    scheduler.exited_process_is_zombie(dpid),
5042                )
5043            }
5044            GlobalRequest::NamesHermitTask(caller, tgid, tid) => {
5045                R::NamesHermitTask(hermit_task_named(caller, tgid, tid))
5046            }
5047            GlobalRequest::KernelRunTarget(target) => R::KernelRunTarget(
5048                self.lock_rpc_scheduler(false)
5049                    .await
5050                    .kernel_run_block(target),
5051            ),
5052            GlobalRequest::HoldTurnForGroupExit(dettid) => R::HoldTurnForGroupExit(
5053                self.lock_rpc_scheduler(false)
5054                    .await
5055                    .hold_turn_for_group_exit(dettid),
5056            ),
5057            GlobalRequest::NotifySignalPending(
5058                dettid,
5059                SigWrapper(signal),
5060                target_process,
5061                scope,
5062            ) => {
5063                let (fence, valve) = {
5064                    let mut scheduler = self.lock_rpc_scheduler(false).await;
5065                    // A5 step 3c.1e A: this handler is the commit point of a
5066                    // guest's cross-task send (it runs only after the injected
5067                    // send succeeded); while the target holds a temporary-mask
5068                    // publication, the send joins its committed-send window
5069                    // record, thread-directed in the private queue. Nothing
5070                    // with the in-guest delivery gate off.
5071                    scheduler.note_committed_send_in_window(
5072                        dettid,
5073                        signal,
5074                        scope == SignalScope::Thread,
5075                    );
5076                    scheduler.notify_signal_pending(dettid, SigWrapper(signal));
5077                    let dbt = self.cfg.backend.requires_thread_directed_process_signals;
5078                    // kill, rt_sigqueueinfo and a pidfd without
5079                    // PIDFD_SIGNAL_THREAD name a process; tgkill, tkill,
5080                    // rt_tgsigqueueinfo and a pidfd with PIDFD_SIGNAL_THREAD
5081                    // name a thread (the notice's SignalScope), whose SIGKILL ends its
5082                    // whole group only while that thread is alive
5083                    // (https://github.com/rrnewton/hermit/issues/4362; Claude
5084                    // review of https://github.com/rrnewton/hermit/pull/4439, R1).
5085                    let process_directed = scope == SignalScope::Process;
5086                    let target_process = if signal == libc::SIGKILL && dbt {
5087                        target_process.or_else(|| scheduler.registered_process(dettid))
5088                    } else {
5089                        target_process
5090                    };
5091                    let mut fence = None;
5092                    if signal == libc::SIGKILL {
5093                        scheduler.record_committed_sigkill(dettid, target_process);
5094                        if let Some(detpid) = target_process {
5095                            if dbt {
5096                                fence =
5097                                    fence_or_refuse(&scheduler, dettid, detpid, process_directed);
5098                            }
5099                            scheduler.note_process_sigkill(dettid, detpid, process_directed);
5100                        }
5101                    }
5102                    (fence, scheduler.sigkill_fence_valve)
5103                };
5104                // The sender's kill does not return until the victim process
5105                // is gone, as ptrace's SIGKILL fence makes it wait: a process
5106                // pidfd reports exit after the kernel has closed the victim's
5107                // files and sent its parent SIGCHLD. Only then can the sender,
5108                // which still holds the serial grant, observe the death, at a
5109                // point the schedule fixes (Claude review of
5110                // https://github.com/rrnewton/hermit/pull/4400, F1).
5111                if let Some((detpid, pidfd)) = fence
5112                    && let Err(reason) = wait_for_process_exit(pidfd, valve).await
5113                {
5114                    crate::scheduler::exit_on_scheduler_refusal(
5115                        &crate::scheduler::sigkill_retirement::UnfencedSigkill {
5116                            process: detpid,
5117                            reason,
5118                        },
5119                    );
5120                }
5121                R::NotifySignalPending(())
5122            }
5123            GlobalRequest::NotifyUnresolvedSigkill(group) => {
5124                self.lock_rpc_scheduler(false)
5125                    .await
5126                    .record_unresolved_sigkill(group);
5127                R::NotifyUnresolvedSigkill(())
5128            }
5129            GlobalRequest::ReserveSigkill(sender, target, uid) => {
5130                let mut scheduler = self.lock_rpc_scheduler(false).await;
5131                let reserved = scheduler
5132                    .registered_process(target)
5133                    .ok_or(crate::scheduler::sigkill_retirement::SigkillRefusal::NoTarget)
5134                    .and_then(|process| scheduler.reserve_sigkill(sender, process, uid));
5135                R::ReserveSigkill(reserved.ok())
5136            }
5137            GlobalRequest::ReserveSigkillGroup(sender, pgid, uid) => R::ReserveSigkill(
5138                self.lock_rpc_scheduler(false)
5139                    .await
5140                    .reserve_sigkill_group(sender, pgid, uid)
5141                    .ok(),
5142            ),
5143            GlobalRequest::CommitSigkill(token, sent) => {
5144                // A send that includes the sender is committed at its Exit
5145                // grant, before it is sent: its victims die only afterwards,
5146                // and its sender never returns, so it does not wait.
5147                let (victims, wait) = {
5148                    let mut sched = self.lock_rpc_scheduler(false).await;
5149                    let wait = sent && !sched.sigkill_reservation_includes_sender(token);
5150                    (sched.commit_sigkill(token, sent), wait)
5151                };
5152                // The sender's `kill` returns only once every victim's exit is
5153                // accounted and published to its parent. A parent that killed
5154                // its own child then finds the kernel's SIGCHLD pending when
5155                // `kill` returns, a point the schedule fixes, instead of
5156                // receiving it at a host-chosen moment in its own code. The
5157                // wait is bounded: past the valve the run is refused.
5158                let since = std::time::Instant::now();
5159                if wait {
5160                    loop {
5161                        {
5162                            let mut sched = self.lock_rpc_scheduler(false).await;
5163                            if sched.sigkill_settled(&victims) {
5164                                break;
5165                            }
5166                            if since.elapsed() >= sched.sigkill_fence_valve {
5167                                sched.refuse_unsettled_sigkill(since.elapsed());
5168                                break;
5169                            }
5170                        }
5171                        yield_once().await;
5172                    }
5173                }
5174                R::CommitSigkill(())
5175            }
5176            GlobalRequest::ThreadIsLive(dtid) => {
5177                R::ThreadIsLive(self.lock_rpc_scheduler(false).await.thread_is_live(dtid))
5178            }
5179            GlobalRequest::NamesARetiredExitedLeader(named) => R::NamesARetiredExitedLeader(
5180                self.lock_rpc_scheduler(false)
5181                    .await
5182                    .names_a_retired_exited_leader(named),
5183            ),
5184            GlobalRequest::ParentDeathSignal(arm) => {
5185                let mut sched = self.lock_rpc_scheduler(false).await;
5186                if let Some(signal) = arm {
5187                    sched.set_parent_death_signal(dtid, signal);
5188                }
5189                R::ParentDeathSignal(sched.parent_death_signal(dtid))
5190            }
5191            GlobalRequest::ThreadIdRefused(id) => {
5192                let (registered, zombie) = {
5193                    let sched = self.lock_rpc_scheduler(false).await;
5194                    (
5195                        sched.thread_was_registered(id),
5196                        sched.id_is_a_zombie_child_of(id, dtid),
5197                    )
5198                };
5199                let clocked = self.global_time.lock().unwrap().contains_thread(id);
5200                R::ThreadIdRefused((registered || clocked) && !zombie)
5201            }
5202            GlobalRequest::PendingRealTimeExpirations(detpid) => R::PendingRealTimeExpirations(
5203                self.lock_rpc_scheduler(false)
5204                    .await
5205                    .pending_real_time_expirations(detpid),
5206            ),
5207            GlobalRequest::RecordHandlerRestoreMask(detpid, unblocked) => {
5208                self.lock_rpc_scheduler(false)
5209                    .await
5210                    .record_handler_restore_unblocked(detpid, unblocked);
5211                R::RecordHandlerRestoreMask(())
5212            }
5213            GlobalRequest::RegisterSignalfd(detpid, mask) => {
5214                self.lock_rpc_scheduler(false)
5215                    .await
5216                    .register_signalfd(detpid, mask);
5217                R::RegisterSignalfd(())
5218            }
5219            GlobalRequest::DescriptorsReplaced(files, first, last) => {
5220                self.lock_rpc_scheduler(false)
5221                    .await
5222                    .note_descriptors_replaced(files, first, last);
5223                R::DescriptorsReplaced(())
5224            }
5225            GlobalRequest::DiscardPendingRealTimeExpirations(detpid, signals) => {
5226                self.lock_rpc_scheduler(false)
5227                    .await
5228                    .discard_pending_real_time_expirations(detpid, signals);
5229                R::DiscardPendingRealTimeExpirations(())
5230            }
5231            GlobalRequest::RecordHostTimedSignals(dtid, scope, signals) => R::HostTimedSignals(
5232                self.lock_rpc_scheduler(false)
5233                    .await
5234                    .record_host_timed_signals(dtid, scope, signals),
5235            ),
5236            GlobalRequest::HostTimedSignals(dtid) => R::HostTimedSignals(
5237                self.lock_rpc_scheduler(false)
5238                    .await
5239                    .host_timed_signals(dtid),
5240            ),
5241            GlobalRequest::HostTimedAndPrivateSigchld(dtid) => {
5242                let (host_timed, private) = self
5243                    .lock_rpc_scheduler(false)
5244                    .await
5245                    .host_timed_and_private_sigchld(dtid);
5246                R::HostTimedAndPrivateSigchld(host_timed, private)
5247            }
5248            GlobalRequest::ExactChildWaitState(parent, child) => R::ExactChildWaitState(
5249                self.lock_rpc_scheduler(false)
5250                    .await
5251                    .exact_child_wait_state(parent, child),
5252            ),
5253            GlobalRequest::ReadyChildWait(parent, selector) => {
5254                let sched = self.lock_rpc_scheduler(false).await;
5255                R::ReadyChildWait((
5256                    sched.ready_child_wait(parent, selector),
5257                    sched.has_child_wait_target(parent, selector),
5258                ))
5259            }
5260            GlobalRequest::ConsumeChildWait(parent, child) => R::ConsumeChildWait({
5261                let mut sched = self.lock_rpc_scheduler(false).await;
5262                if let Some(cpu) = work_span_cpu
5263                    && sched.thread_tree.parent_process(&child) == Some(parent)
5264                {
5265                    sched.work_span_note_reap(dtid, cpu, parent, child);
5266                }
5267                sched.consume_child_wait(parent, child)
5268            }),
5269            GlobalRequest::ProcessGroup(process) => R::ProcessGroup(
5270                self.lock_rpc_scheduler(false)
5271                    .await
5272                    .thread_tree
5273                    .process_group(process),
5274            ),
5275            GlobalRequest::SetProcessGroup(process, group) => R::SetProcessGroup(
5276                self.lock_rpc_scheduler(false)
5277                    .await
5278                    .thread_tree
5279                    .set_process_group(process, group),
5280            ),
5281            GlobalRequest::CreateSession(process) => R::CreateSession(
5282                self.lock_rpc_scheduler(false)
5283                    .await
5284                    .thread_tree
5285                    .create_session(process),
5286            ),
5287            GlobalRequest::RegisteredProcessOf(dettid) => R::RegisteredProcessOf(
5288                self.lock_rpc_scheduler(false)
5289                    .await
5290                    .thread_tree
5291                    .process_of(dettid),
5292            ),
5293            GlobalRequest::ForgetThreadTimerCopies(process, signo) => {
5294                self.lock_rpc_scheduler(false)
5295                    .await
5296                    .forget_thread_timer_copies(process, signo);
5297                R::ForgetThreadTimerCopies(())
5298            }
5299            GlobalRequest::ExecTimerFlush(process) => R::ExecTimerFlush(
5300                self.lock_rpc_scheduler(false)
5301                    .await
5302                    .take_exec_timer_flush(dtid, process),
5303            ),
5304            GlobalRequest::RetireExitedLeader => R::RetireExitedLeader(
5305                self.lock_rpc_scheduler(false)
5306                    .await
5307                    .retire_exited_leader(dtid, request_mm),
5308            ),
5309            GlobalRequest::GuestPid(caller, pid) => {
5310                let sched = self.lock_rpc_scheduler(false).await;
5311                R::GuestPid(sched.guest_pid(caller, pid))
5312            }
5313            GlobalRequest::ProcfsListing(process) => {
5314                R::ProcfsListing(self.lock_rpc_scheduler(false).await.procfs_listing(process))
5315            }
5316            GlobalRequest::ProcfsIds(to_kernel, ids) => R::ProcfsIds(
5317                self.lock_rpc_scheduler(false)
5318                    .await
5319                    .procfs_ids(to_kernel, &ids),
5320            ),
5321            GlobalRequest::UserNamespaceRelation(reader, task) => {
5322                let sched = self.lock_rpc_scheduler(false).await;
5323                let reader = sched.registered_process(reader).unwrap_or(reader);
5324                let task = sched.registered_process(task).unwrap_or(task);
5325                let record = &sched.thread_tree.user_namespaces;
5326                R::UserNamespaceRelation(
5327                    record.relation(reader, task),
5328                    record.owner_may_differ_below(reader, task),
5329                )
5330            }
5331            GlobalRequest::UserNamespaceOwnerMayDiffer(process) => {
5332                let mut sched = self.lock_rpc_scheduler(false).await;
5333                let process = sched.registered_process(process).unwrap_or(process);
5334                sched
5335                    .thread_tree
5336                    .user_namespaces
5337                    .note_owner_may_differ(process);
5338                R::UserNamespaceOwnerMayDiffer(())
5339            }
5340            GlobalRequest::ProcessImage(process) => {
5341                let sched = self.lock_rpc_scheduler(false).await;
5342                // `/proc/<tid>/exe` names a thread's process's image.
5343                let process = sched
5344                    .registered_process(DetTid::from_raw(process.as_raw()))
5345                    .unwrap_or(process);
5346                R::ProcessImage(
5347                    self.exec_images
5348                        .lock()
5349                        .unwrap()
5350                        .get(&process)
5351                        .cloned()
5352                        .map(Box::new),
5353                )
5354            }
5355            GlobalRequest::RecordProcessAuxv(bytes, cmdline) => {
5356                let sched = self.lock_rpc_scheduler(false).await;
5357                let process = sched.registered_process(dtid).unwrap_or(dtid);
5358                let mut table = self.process_auxv.lock().unwrap();
5359                table.record(process, bytes);
5360                table.record_cmdline(process, cmdline);
5361                R::RecordProcessAuxv(())
5362            }
5363            GlobalRequest::ProcessAuxv(process) => {
5364                let sched = self.lock_rpc_scheduler(false).await;
5365                // `/proc/<tid>/auxv` is a thread's process's.
5366                let process = sched
5367                    .registered_process(DetTid::from_raw(process.as_raw()))
5368                    .unwrap_or(process);
5369                R::ProcessAuxv(self.process_auxv.lock().unwrap().current(process))
5370            }
5371            GlobalRequest::ProcessCmdline(process) => {
5372                let sched = self.lock_rpc_scheduler(false).await;
5373                // `/proc/<tid>/cmdline` is a thread's process's.
5374                let process = sched
5375                    .registered_process(DetTid::from_raw(process.as_raw()))
5376                    .unwrap_or(process);
5377                let area = self.process_auxv.lock().unwrap().current_cmdline(process);
5378                R::ProcessCmdline(process, area)
5379            }
5380            GlobalRequest::MarkChangedRoot => {
5381                // The sending task's own filesystem group: a thread created
5382                // without CLONE_FS has its own root.
5383                let _sched = self.lock_rpc_scheduler(false).await;
5384                self.process_auxv.lock().unwrap().mark_changed_root(dtid);
5385                R::MarkChangedRoot(())
5386            }
5387            GlobalRequest::RootChanged(task) => {
5388                // The task the path names: a process's number names its
5389                // leader thread, whose root Linux's `/proc/<pid>` shows.
5390                let _sched = self.lock_rpc_scheduler(false).await;
5391                R::RootChanged(self.process_auxv.lock().unwrap().root_changed(task))
5392            }
5393            GlobalRequest::DescriptionLocks(open_file, locks, slots) => {
5394                let sched = self.lock_rpc_scheduler(false).await;
5395                let process = sched.registered_process(dtid).unwrap_or(dtid);
5396                let mut ledger = self.lock_ledger.lock().unwrap();
5397                match locks {
5398                    Ok(locks) => ledger.note_locks(process, open_file, locks, slots),
5399                    Err(reason) => ledger.note_unknown(process, open_file, reason, slots),
5400                }
5401                R::DescriptionLocks(())
5402            }
5403            GlobalRequest::DescriptionLocksTaken(open_file, locks, taken, slots, identity) => {
5404                let sched = self.lock_rpc_scheduler(false).await;
5405                let process = sched.registered_process(dtid).unwrap_or(dtid);
5406                self.lock_ledger
5407                    .lock()
5408                    .unwrap()
5409                    .note_locks_taken(process, open_file, locks, taken, slots, identity);
5410                R::DescriptionLocksTaken(())
5411            }
5412            GlobalRequest::SecondIdentityLocks(open_file, locks, slots) => {
5413                let sched = self.lock_rpc_scheduler(false).await;
5414                let process = sched.registered_process(dtid).unwrap_or(dtid);
5415                self.lock_ledger
5416                    .lock()
5417                    .unwrap()
5418                    .note_second_identity_locks(process, open_file, locks, slots);
5419                R::SecondIdentityLocks(())
5420            }
5421            GlobalRequest::LockOwners(open_file, locks) => {
5422                // A taker is shown while the container's `/proc` lists it (a
5423                // live process, or a zombie not yet reaped).
5424                let sched = self.lock_rpc_scheduler(false).await;
5425                let listed = sched.procfs_listing(None).processes;
5426                let ledger = self.lock_ledger.lock().unwrap();
5427                R::LockOwners(match ledger.record_lock_held_as_ofd(&locks) {
5428                    Some(reason) => Err(reason),
5429                    None => Ok(ledger.owners(open_file, &locks, &|process| {
5430                        listed.binary_search(&process.as_raw()).is_ok()
5431                    })),
5432                })
5433            }
5434            GlobalRequest::LedgerLockRows => {
5435                let sched = self.lock_rpc_scheduler(false).await;
5436                let listed = sched.procfs_listing(None).processes;
5437                // As for a kernel snapshot (`GuestLockRows`): a table a
5438                // process that exited used references nothing, and a
5439                // referenced description whose locks are unknown refuses the
5440                // snapshot.
5441                // A row that would show a guest's record lock the backend
5442                // holds as an OFD lock refuses it too.
5443                let mut ledger = self.lock_ledger.lock().unwrap();
5444                ledger.prune(|process| sched.process_is_alive(process));
5445                R::LedgerLockRows(match ledger.unknown() {
5446                    Some(reason) => Err(reason.to_string()),
5447                    None => {
5448                        let rows = ledger.rendered_rows(&|process| {
5449                            listed.binary_search(&process.as_raw()).is_ok()
5450                        });
5451                        match ledger.record_lock_held_as_ofd(rows.iter().map(|(lock, _)| lock)) {
5452                            Some(reason) => Err(reason),
5453                            None => Ok(rows),
5454                        }
5455                    }
5456                })
5457            }
5458            GlobalRequest::TableReleased(open_file, remaining) => {
5459                let sched = self.lock_rpc_scheduler(false).await;
5460                let process = sched.registered_process(dtid).unwrap_or(dtid);
5461                self.lock_ledger
5462                    .lock()
5463                    .unwrap()
5464                    .release(process, open_file, remaining);
5465                R::TableReleased(())
5466            }
5467            GlobalRequest::TableExec(surviving) => {
5468                let sched = self.lock_rpc_scheduler(false).await;
5469                let process = sched.registered_process(dtid).unwrap_or(dtid);
5470                self.lock_ledger
5471                    .lock()
5472                    .unwrap()
5473                    .exec_kept(process, &surviving);
5474                R::TableExec(())
5475            }
5476            GlobalRequest::AliasedInode(inode) => {
5477                let _sched = self.lock_rpc_scheduler(false).await;
5478                self.lock_ledger.lock().unwrap().alias_inode(inode);
5479                R::AliasedInode(())
5480            }
5481            GlobalRequest::LockLedgerRefused(reason) => {
5482                let _sched = self.lock_rpc_scheduler(false).await;
5483                self.lock_ledger.lock().unwrap().refuse(reason);
5484                R::LockLedgerRefused(())
5485            }
5486            GlobalRequest::RecordLocksHeldAsOfd(objects) => {
5487                let _sched = self.lock_rpc_scheduler(false).await;
5488                self.lock_ledger
5489                    .lock()
5490                    .unwrap()
5491                    .note_record_locks_held_as_ofd(objects);
5492                R::RecordLocksHeldAsOfd(())
5493            }
5494            GlobalRequest::GuestLockRows(rows) => {
5495                // A positive owner is the kernel's number for a process in the
5496                // container's PID namespace, as `/proc` spells it; it is the
5497                // guest's when it names a process `/proc` lists, in the
5498                // guest's numbering. Hermit's own init is not listed.
5499                let sched = self.lock_rpc_scheduler(false).await;
5500                let owners = rows
5501                    .iter()
5502                    .filter_map(|row| i32::try_from(row.owner).ok().filter(|owner| *owner > 0))
5503                    .collect::<BTreeSet<_>>()
5504                    .into_iter()
5505                    .collect::<Vec<_>>();
5506                let listed = sched.procfs_listing(None).processes;
5507                let guest_owners = owners
5508                    .iter()
5509                    .zip(sched.procfs_ids(false, &owners))
5510                    .filter(|(_, guest)| {
5511                        guest.is_some_and(|guest| listed.binary_search(&guest).is_ok())
5512                    })
5513                    .map(|(owner, _)| i64::from(*owner))
5514                    .collect::<BTreeSet<_>>();
5515                // A table a process that exited used, reaped or not,
5516                // references nothing (`LockLedger::prune`).
5517                let mut ledger = self.lock_ledger.lock().unwrap();
5518                ledger.prune(|process| sched.process_is_alive(process));
5519                // A referenced description whose locks are unknown could own
5520                // any OFD row (OFD rows are kept by field match against what a
5521                // description holds): the snapshot is refused
5522                // (`LockLedger::unknown`). Flock rows are kept by their taker's
5523                // pid alone, so for a description that only ever held flocks
5524                // this refusal is conservative.
5525                if let Some(reason) = ledger.unknown() {
5526                    R::GuestLockRows(Err(reason.to_string()))
5527                } else {
5528                    let held = ledger.held();
5529                    R::GuestLockRows(Ok(crate::procfs::locks::guest_lock_rows(
5530                        &rows,
5531                        |owner| guest_owners.contains(&owner),
5532                        &held,
5533                    )))
5534                }
5535            }
5536            GlobalRequest::AuxvFromKernel(why, cmdline) => {
5537                let sched = self.lock_rpc_scheduler(false).await;
5538                let process = sched.registered_process(dtid).unwrap_or(dtid);
5539                let mut table = self.process_auxv.lock().unwrap();
5540                table.forget(process);
5541                table.record_cmdline(process, cmdline);
5542                R::AuxvFromKernel(table.first_kernel_file(why))
5543            }
5544            GlobalRequest::GroupOrSession(pid, session) => R::GroupOrSession(
5545                self.lock_rpc_scheduler(false)
5546                    .await
5547                    .group_or_session_of(pid, session),
5548            ),
5549            GlobalRequest::SetsidDecision(process) => R::SetsidDecision(
5550                self.lock_rpc_scheduler(false)
5551                    .await
5552                    .setsid_decision(process)
5553                    .map_err(|errno| errno.into_raw()),
5554            ),
5555            GlobalRequest::SetpgidDecision(caller, pid, pgid) => R::SetpgidDecision(
5556                self.lock_rpc_scheduler(false)
5557                    .await
5558                    .setpgid_decision(caller, pid, pgid)
5559                    .map_err(|errno| errno.into_raw()),
5560            ),
5561            GlobalRequest::ProcfsTask(named, thread) => {
5562                let scheduler = self.lock_rpc_scheduler(false).await;
5563                let existence = scheduler.procfs_task(named, thread);
5564                let mut answer = self
5565                    .inodes
5566                    .lock()
5567                    .unwrap()
5568                    .procfs_task_answer(named, thread, existence);
5569                if existence.0 && !existence.1 {
5570                    answer.mm = scheduler.thread_mm(thread.unwrap_or(named));
5571                    answer.runtime_private_memory = answer
5572                        .mm
5573                        .and_then(|mm| scheduler.runtime_private_memory(mm));
5574                }
5575                R::ProcfsTask(answer)
5576            }
5577            GlobalRequest::TaskStatFacts(named, thread) => R::TaskStatFacts(
5578                self.lock_rpc_scheduler(false)
5579                    .await
5580                    .task_stat_facts(named, thread),
5581            ),
5582            GlobalRequest::SetTaskComm(tid, comm) => {
5583                self.lock_rpc_scheduler(false)
5584                    .await
5585                    .set_task_comm(tid, comm);
5586                R::SetTaskComm(())
5587            }
5588            GlobalRequest::RecordExternalParent(process, parent) => {
5589                self.lock_rpc_scheduler(false)
5590                    .await
5591                    .record_external_parent(process, parent);
5592                R::RecordExternalParent(())
5593            }
5594            GlobalRequest::SetRssLimit(process, limit) => {
5595                self.lock_rpc_scheduler(false)
5596                    .await
5597                    .set_rss_limit(process, limit);
5598                R::SetRssLimit(())
5599            }
5600            GlobalRequest::RegisterPlaceholder(directory, open_file) => {
5601                let _sched = self.lock_rpc_scheduler(false).await;
5602                R::RegisterPlaceholder(
5603                    self.inodes
5604                        .lock()
5605                        .unwrap()
5606                        .register_placeholder(directory, open_file),
5607                )
5608            }
5609            GlobalRequest::Placeholder(tag) => {
5610                let _sched = self.lock_rpc_scheduler(false).await;
5611                R::Placeholder(self.inodes.lock().unwrap().placeholder(tag))
5612            }
5613            GlobalRequest::PlaceholderFlock(request) => {
5614                let sched = self.lock_rpc_scheduler(false).await;
5615                let process = sched.registered_process(dtid).unwrap_or(dtid);
5616                let mut ledger = self.lock_ledger.lock().unwrap();
5617                // The tables of processes that have exited, reaped or not,
5618                // released their descriptions with them, and so the flocks
5619                // those held on placeholder directories, as Linux's exit
5620                // releases them (`procfs::locks::LockLedger::prune`).
5621                ledger.prune(|process| sched.process_is_alive(process));
5622                R::PlaceholderFlock(ledger.placeholder_flock(process, request))
5623            }
5624            GlobalRequest::PlaceholderWriteHint(key, hint) => {
5625                let _sched = self.lock_rpc_scheduler(false).await;
5626                R::PlaceholderWriteHint(
5627                    self.lock_ledger
5628                        .lock()
5629                        .unwrap()
5630                        .placeholder_write_hint(key, hint),
5631                )
5632            }
5633            GlobalRequest::PlaceholderCopies(change) => {
5634                let _sched = self.lock_rpc_scheduler(false).await;
5635                R::PlaceholderCopies(self.lock_ledger.lock().unwrap().placeholder_copies(change))
5636            }
5637            GlobalRequest::CpuAffinity(tid) => {
5638                R::CpuAffinity(self.lock_rpc_scheduler(false).await.cpu_affinity(tid))
5639            }
5640            GlobalRequest::SetCpuAffinity(tid, cpus) => R::CpuAffinity(
5641                self.lock_rpc_scheduler(false)
5642                    .await
5643                    .set_cpu_affinity(tid, cpus),
5644            ),
5645            GlobalRequest::RenderedFileState(inode, change) => R::RenderedFileState(
5646                self.inodes
5647                    .lock()
5648                    .unwrap()
5649                    .rendered_file_state(inode, change),
5650            ),
5651            GlobalRequest::LocalPid(caller, task) => R::LocalPid(
5652                self.lock_rpc_scheduler(false)
5653                    .await
5654                    .thread_tree
5655                    .pid_namespaces
5656                    .localize_for(caller, task),
5657            ),
5658            GlobalRequest::SignalTargetProcess(named) => R::SignalTargetProcess(
5659                self.lock_rpc_scheduler(false)
5660                    .await
5661                    .signal_target_process(named),
5662            ),
5663            GlobalRequest::PosixTimerThread(process, named) => R::PosixTimerThread(
5664                self.lock_rpc_scheduler(false)
5665                    .await
5666                    .posix_timer_thread(process, named),
5667            ),
5668            GlobalRequest::SetSigchldDisposition(process, ignored, nocldwait) => {
5669                R::SetSigchldDisposition(
5670                    self.lock_rpc_scheduler(false)
5671                        .await
5672                        .thread_tree
5673                        .set_sigchld_disposition(process, ignored, nocldwait),
5674                )
5675            }
5676            GlobalRequest::SetChildSubreaper(process, on) => R::SetChildSubreaper(
5677                self.lock_rpc_scheduler(false)
5678                    .await
5679                    .thread_tree
5680                    .set_child_subreaper(process, on),
5681            ),
5682            GlobalRequest::UnrecoverableShutdown => {
5683                self.force_shutdown_with_error();
5684                R::UnrecoverableShutdown(())
5685            }
5686            GlobalRequest::Network(request) => {
5687                let _sched = self.lock_rpc_scheduler(false).await;
5688                let mut engine = self
5689                    .network
5690                    .as_ref()
5691                    .expect("network request without a network trace mode")
5692                    .lock()
5693                    .unwrap();
5694                let now = self.global_time.lock().unwrap().as_nanos();
5695                R::Network(engine.apply(now, request))
5696            }
5697            GlobalRequest::RequestPort(open_file_id) => {
5698                let _sched = self.lock_rpc_scheduler(false).await;
5699                let mut mut_used_ports = self.used_ports.lock().unwrap();
5700                self.update_port_range();
5701                let total_available =
5702                    self.port_end_range.load(SeqCst) - self.port_start_range.load(SeqCst);
5703                let mut index = 0;
5704                while (*mut_used_ports).contains(&self.next_port.load(SeqCst))
5705                    && index < total_available
5706                {
5707                    self.next_port.fetch_add(1, SeqCst);
5708                    if self.next_port.load(SeqCst) > self.port_end_range.load(SeqCst) {
5709                        self.next_port
5710                            .store(self.port_start_range.load(SeqCst), SeqCst);
5711                    }
5712                    index += 1;
5713                }
5714                if index == total_available {
5715                    R::PortFull
5716                } else {
5717                    (*mut_used_ports).insert(self.next_port.load(SeqCst));
5718                    let mut open_file_to_port = self.open_file_to_port.lock().unwrap();
5719                    open_file_to_port.insert(open_file_id, self.next_port.load(SeqCst));
5720                    R::RequestPort(self.next_port.load(SeqCst))
5721                }
5722            }
5723            GlobalRequest::AddUsedPort(port, open_file_id) => {
5724                let _sched = self.lock_rpc_scheduler(false).await;
5725                let mut used_ports = self.used_ports.lock().unwrap();
5726                used_ports.insert(port);
5727                let mut open_file_to_port = self.open_file_to_port.lock().unwrap();
5728                open_file_to_port.insert(open_file_id, port);
5729                R::AddUsedPort
5730            }
5731            GlobalRequest::ReleasePort(open_file_id) => {
5732                let _sched = self.lock_rpc_scheduler(false).await;
5733                R::ReleasePort(self.release_port(open_file_id))
5734            }
5735        };
5736
5737        #[cfg(feature = "physical-mm-test-support")]
5738        if matches!(&resp, R::MarkPastFirstExecve(_)) {
5739            // Mark's state/input updates are complete and every match-arm
5740            // mutex guard is out of scope. The ordinary terminal recheck below
5741            // still follows this checkpoint before any reply can be returned.
5742            self.initial_mm_checkpoint(initial_mm_probe::Point::PostExec, dtid, request_mm)
5743                .await;
5744        }
5745
5746        // Awaited scheduler operations may have raced logical teardown. Never return their
5747        // operation-specific response after the sender acquired a permanent tombstone.
5748        // A SIGKILL committed meanwhile leaves the request unanswered, whatever
5749        // its operation-specific response, ThreadExited included.
5750        let (sender_became_terminal, sender_sigkill_retired) =
5751            if is_deregister || exec_reconnect.is_some() || is_exec_caller_after_local_mm_swap {
5752                (false, false)
5753            } else {
5754                let sched = self.lock_rpc_scheduler(consuming_cleanup).await;
5755                (
5756                    sched.thread_is_logically_killed(dtid)
5757                        || !sched.rpc_incarnation_matches(dtid, request_mm),
5758                    !answered_for_sigkill_victim && sched.is_unaccounted_sigkill_victim(dtid),
5759                )
5760            };
5761        if sender_sigkill_retired {
5762            return (None, R::Retired);
5763        }
5764        if resp == R::ThreadExited || sender_became_terminal {
5765            return (None, R::ThreadExited);
5766        }
5767
5768        let time_from_sched = self.global_time.lock().unwrap().threads_time(dtid);
5769        let time_update = match time_from_sched.cmp(&time_from_guest) {
5770            Ordering::Equal => None,
5771            Ordering::Less => {
5772                panic!(
5773                    "internal error: thread time should never go down, only monotonically up: time in sched {}, thread local time was {}",
5774                    time_from_sched, time_from_guest
5775                )
5776            }
5777            Ordering::Greater => Some(time_from_sched),
5778        };
5779        (time_update, resp)
5780    }
5781
5782    async fn recv_resources_with_origin(
5783        &self,
5784        from: Tid,
5785        detpid: DetPid,
5786        mut rs: Resources,
5787        request_mm: Option<MmId>,
5788        rpc: RpcOrigin,
5789        capability: ControlCapability,
5790    ) -> (
5791        SchedulerRpcResult<ResourceReply>,
5792        Option<LogicalTime>,
5793        MaybePrintStack,
5794    ) {
5795        let dettid = DetTid::from_raw(from.into()); // TODO(T78538674): FIXME
5796        let mut boundary_stack = None;
5797        let resp2 = {
5798            let mut sched = self.lock_rpc_scheduler(false).await;
5799            if sched.thread_is_logically_killed(dettid)
5800                || request_mm.is_some_and(|mm| !sched.rpc_incarnation_matches(dettid, mm))
5801            {
5802                return (SchedulerRpcResult::ThreadExited, None, None);
5803            }
5804            // A host-timed request (a background operation's rejoin) can pass
5805            // admission just before a SIGKILL commit retires its thread. The
5806            // handler's response exit turns this into an unanswered request.
5807            if sched.is_unaccounted_sigkill_victim(dettid) {
5808                return (SchedulerRpcResult::ThreadExited, None, None);
5809            }
5810            let Some(nextturn) = sched.next_turns.get(&dettid).cloned() else {
5811                panic!(
5812                    "Detcore internal error: no entry for dettid {} in next_turns during resource request.",
5813                    dettid
5814                );
5815            };
5816            trace!(
5817                "[detcore, dtid {}] ResourceRequest, filling request into {}",
5818                &dettid, &nextturn.req
5819            );
5820            if let Some(mm) = request_mm
5821                && let Err(error) = sched.install_resource_origin(
5822                    dettid,
5823                    ResourceOrigin {
5824                        rpc,
5825                        mm,
5826                        control: capability,
5827                    },
5828                )
5829            {
5830                sched.fail_parked(dettid, error);
5831                return (SchedulerRpcResult::ThreadExited, None, None);
5832            }
5833            if let Some(boundary) = rs.slice_boundary.take() {
5834                let mm = request_mm.unwrap_or_else(|| {
5835                    crate::scheduler::exit_on_scheduler_refusal(
5836                        &"slice-boundary protocol refusal: unauthenticated request",
5837                    )
5838                });
5839                if rs.tid != dettid {
5840                    crate::scheduler::exit_on_scheduler_refusal(
5841                        &"slice-boundary protocol refusal: resource owner differs from sender",
5842                    );
5843                }
5844                boundary_stack = sched
5845                    .accept_slice_boundary(dettid, mm, &boundary)
5846                    .unwrap_or_else(|error| crate::scheduler::exit_on_scheduler_refusal(&error));
5847                if boundary_stack.is_some() {
5848                    self.signal_requested_stacktrace(dettid);
5849                }
5850            }
5851            // The guest mask the request publishes (A5 step 3c.1b), recorded
5852            // before the scheduler can act on the request; a no-op with the
5853            // in-guest delivery gate off, and for a rejoin, whose filing is
5854            // host-timed (recorded when the thread is readmitted instead).
5855            sched.record_request_guest_mask(dettid, &rs);
5856            // Whether it is a kernel-run call (A5 step 3c.1c, D44), recorded
5857            // in the same ordered filing; a no-op with the gate off.
5858            sched.record_request_kernel_run(dettid, &rs);
5859            sched.request_put(&nextturn.req, rs.clone(), &self.global_time);
5860            nextturn.resp
5861        };
5862        trace!(
5863            "[detcore, dtid {}] waiting on {} for resources: {:?}",
5864            dettid, &resp2, rs
5865        );
5866        let answer = resp2.get().await; // Block on the scheduler allowing our guest to proceed.
5867        let (result, time) = self
5868            .finish_resource_response(from, detpid, rs, request_mm, answer)
5869            .await;
5870        if matches!(result, SchedulerRpcResult::ThreadExited) {
5871            (result, time, None)
5872        } else {
5873            (result, time, boundary_stack)
5874        }
5875    }
5876
5877    async fn finish_resource_response(
5878        &self,
5879        from: Tid,
5880        detpid: DetPid,
5881        rs: Resources,
5882        request_mm: Option<MmId>,
5883        answer: SchedResponse,
5884    ) -> (SchedulerRpcResult<ResourceReply>, Option<LogicalTime>) {
5885        let dettid = DetTid::from_raw(from.as_raw());
5886        let request_became_stale = {
5887            let sched = self.lock_rpc_scheduler(false).await;
5888            sched.thread_is_logically_killed(dettid)
5889                || request_mm.is_some_and(|mm| !sched.rpc_incarnation_matches(dettid, mm))
5890        };
5891        if request_became_stale {
5892            // `logically_kill_thread` wakes an already-pending request with a
5893            // signal response.  Treat that wake-up as terminal: otherwise a
5894            // caller that ignores `ResumeStatus::Signaled` can inject the
5895            // original syscall after the thread was logically removed.
5896            // TODO-HUMAN-REVIEW(PR-1023): Review pending SaBRe resource-request cancellation.
5897            trace!(
5898                "[detcore, dtid {}] terminating pending request after logical removal",
5899                dettid
5900            );
5901            return (SchedulerRpcResult::ThreadExited, None);
5902        }
5903        if matches!(answer, SchedResponse::InGuestFatalAlarm) {
5904            let mut sched = self.lock_rpc_scheduler(false).await;
5905            if !self.cfg.sequentialize_threads
5906                || !sched.in_guest_fatal_alarm_response_matches(dettid, detpid, request_mm, &rs)
5907            {
5908                sched.fail_parked(dettid, ProtocolFailure::UnexpectedControl);
5909                return (SchedulerRpcResult::ThreadExited, None);
5910            }
5911            return (
5912                SchedulerRpcResult::Continue(ResourceReply::Grant(ResumeStatus::InGuestFatalAlarm)),
5913                None,
5914            );
5915        }
5916        // A control spends only this response transport, not the resource
5917        // operation. It must precede exit-group or normal grant side effects.
5918        if let SchedResponse::ObserveSignal(control) = answer {
5919            return (
5920                SchedulerRpcResult::Continue(ResourceReply::ObserveSignal(control)),
5921                None,
5922            );
5923        }
5924        if let Some((true, process, mm)) = rs.exit_identity() {
5925            info!(
5926                "Scheduler authorized an exit-group scenario, from dettid {} / detpid {}",
5927                dettid, detpid
5928            );
5929            // Before allowing an `exit_group` to physically proceed, we
5930            // deregister the other threads in the thread group to reflect the
5931            // fact that they will not receive further logical turns.
5932            //
5933            // We trust the kernel to physically kill them irrespective of what they're
5934            // blocked on, including us having blocked them in the `futex_waiters` list.
5935            {
5936                let mut sched = self.lock_rpc_scheduler(false).await;
5937                if sched.thread_is_logically_killed(dettid)
5938                    || request_mm.is_some_and(|mm| !sched.rpc_incarnation_matches(dettid, mm))
5939                {
5940                    return (SchedulerRpcResult::ThreadExited, None);
5941                }
5942                sched.logically_kill_exit_group_siblings(dettid, process, mm);
5943            }
5944        }
5945
5946        match answer {
5947            SchedResponse::ObserveSignal(_) | SchedResponse::InGuestFatalAlarm => {
5948                self.sched
5949                    .lock()
5950                    .unwrap()
5951                    .fail_parked(dettid, ProtocolFailure::UnexpectedControl);
5952                (SchedulerRpcResult::ThreadExited, None)
5953            }
5954            // In this context, SchedValue
5955            SchedResponse::Go(Some(schedval)) => {
5956                trace!(
5957                    "[dtid {}] resources granted, resuming normally: {:?}",
5958                    dettid, rs
5959                );
5960
5961                let endtime_update = match schedval {
5962                    // Only syscalls timeout, and they don't need to update guest timeslice end.
5963                    // Nor does a futex requeue's or a PI futex command's answer,
5964                    // which a resource grant never carries.
5965                    SchedValue::TimeOut
5966                    | SchedValue::Signaled(_)
5967                    | SchedValue::Requeued { .. }
5968                    | SchedValue::PiFutex(_)
5969                    | SchedValue::PiTaken(_)
5970                    | SchedValue::PiWaiterReached { .. } => None,
5971                    SchedValue::Value(timeslice) => Some(LogicalTime::from_nanos(timeslice)),
5972                };
5973                (
5974                    SchedulerRpcResult::Continue(ResourceReply::Grant(ResumeStatus::Normal)),
5975                    endtime_update,
5976                )
5977            }
5978            SchedResponse::GoAlone(schedval) => {
5979                trace!(
5980                    "[dtid {}] resources granted, and nothing else can run",
5981                    dettid
5982                );
5983                let endtime_update = match schedval {
5984                    Some(SchedValue::Value(timeslice)) => Some(LogicalTime::from_nanos(timeslice)),
5985                    _ => None,
5986                };
5987                (
5988                    SchedulerRpcResult::Continue(ResourceReply::Grant(
5989                        ResumeStatus::NothingElseCanRun,
5990                    )),
5991                    endtime_update,
5992                )
5993            }
5994            SchedResponse::Go(None) => {
5995                trace!(
5996                    "[dtid {}] resources granted but no timeslice specified",
5997                    dettid,
5998                );
5999                (
6000                    SchedulerRpcResult::Continue(ResourceReply::Grant(ResumeStatus::Normal)),
6001                    None,
6002                )
6003            }
6004            SchedResponse::Signaled(signal) => {
6005                trace!(
6006                    "[dtid {}] resources granted but interrupted by signal",
6007                    dettid,
6008                );
6009                (
6010                    SchedulerRpcResult::Continue(ResourceReply::Grant(ResumeStatus::Signaled(
6011                        signal,
6012                    ))),
6013                    None,
6014                )
6015            }
6016        }
6017    }
6018
6019    async fn recv_release_resources(&self, from: Tid, rs: Resources) {
6020        // TODO(T78627377): add real resource-locking when we enable backgrounding actions.
6021        trace!("[detcore] Resources released to pid {}: {:?}", from, rs);
6022    }
6023
6024    async fn recv_release_all_resources(&self, from: Tid) {
6025        // TODO(T78627377): add real resource-locking when we enable backgrounding actions.
6026        trace!("[detcore] All resources held by pid {} released", from);
6027    }
6028
6029    /// Whether `dtid`'s clock component belongs to an earlier thread that had
6030    /// the same ID (`id_awaits_new_registration`), rather than to the new
6031    /// thread now being registered under it. Caller holds the scheduler,
6032    /// which nests outside the clock.
6033    fn clock_belongs_to_earlier_thread(&self, sched: &Scheduler, dtid: DetTid) -> bool {
6034        id_awaits_new_registration(sched, dtid)
6035            && self.global_time.lock().unwrap().contains_thread(dtid)
6036    }
6037
6038    /// Global portion of parent-forking-child protocol.  Called by the parent
6039    /// thread for an ordinary clone, or by the child itself for a vfork whose
6040    /// parent is blocked inside the kernel (`parent_is_kernel_blocked`).
6041    async fn recv_create_child_thread(
6042        &self,
6043        rpc_sender: Tid,
6044        request_mm: MmId,
6045        registration: ChildRegistration,
6046    ) -> SchedulerRpcResult<()> {
6047        let ChildRegistration {
6048            parent_dettid,
6049            parent_detpid,
6050            child_dettid,
6051            child_tid_addr: ctid,
6052            flags,
6053            exit_signal,
6054            physical_ids,
6055            maybe_priority,
6056            parent_is_kernel_blocked,
6057        } = registration;
6058        #[cfg(feature = "physical-mm-test-support")]
6059        if flags.is_none() && rpc_sender.as_raw() == child_dettid.as_raw() {
6060            self.initial_mm_checkpoint(
6061                initial_mm_probe::Point::BeforeRegistration,
6062                child_dettid,
6063                request_mm,
6064            )
6065            .await;
6066        }
6067        let initial_priority = if let Some(pr) = &self.preemptions_to_replay {
6068            assert!(maybe_priority.is_none());
6069            let prio = pr
6070                .thread_initial_priority(&child_dettid)
6071                .unwrap_or_else(|| {
6072                    warn!(
6073                        "Child thread {} not found in preemption history to replay",
6074                        child_dettid
6075                    );
6076                    DEFAULT_PRIORITY
6077                });
6078            if !is_ordinary_priority(prio) {
6079                panic!(
6080                    "Read a bad initial_prority from file: {}\nFull file: {}",
6081                    prio, pr,
6082                );
6083            }
6084            prio
6085        } else {
6086            let prio = maybe_priority.expect(
6087                "create_child_thread must take an initial priority unless replaying preemptions",
6088            );
6089            if !is_ordinary_priority(prio) {
6090                panic!(
6091                    "recv_create_child_thread received a bad prority argument : {}",
6092                    prio,
6093                );
6094            }
6095            prio
6096        };
6097
6098        {
6099            let mut sched = self.lock_rpc_scheduler(false).await;
6100            let sender = DetTid::from_raw(rpc_sender.into());
6101            if sched.thread_is_logically_killed(sender)
6102                || !sched.rpc_incarnation_matches(sender, request_mm)
6103                || sched.thread_is_logically_killed(child_dettid)
6104            {
6105                return SchedulerRpcResult::ThreadExited;
6106            }
6107            // A process the backend already reported physically gone must not
6108            // be admitted: the scheduler would wait for a request it can never
6109            // send. A forked child is its own process.
6110            let child_detpid =
6111                if flags.is_some_and(|flags| flags.contains(CloneFlags::CLONE_THREAD)) {
6112                    parent_detpid
6113                } else {
6114                    DetPid::from_raw(child_dettid.as_raw())
6115                };
6116            if self.cfg.backend.process_exits_complete_asynchronously
6117                && sched.process_physically_gone(child_detpid)
6118            {
6119                drop(sched);
6120                self.report_backend_failure(reverie::BackendFailure {
6121                    pid: reverie::Pid::from_raw(child_detpid.as_raw()),
6122                    tid: Tid::from_raw(child_dettid.as_raw()),
6123                    phase: "a process was already physically gone when it was registered",
6124                });
6125                return SchedulerRpcResult::ThreadExited;
6126            }
6127
6128            // A parent-authored child registration carries the parent's MM,
6129            // which naturally differs from the child's initial MM (including
6130            // when the parent has execed). It must retain the ordinary clone
6131            // transaction. Only an already registered, matching parent may
6132            // take that path; a malformed root self-request still reaches the
6133            // complete first-image checks below.
6134            let registered_parent_creation = sender == parent_dettid
6135                && parent_dettid != child_dettid
6136                && sched.thread_was_registered(sender);
6137            let initial_current_root = flags.is_none()
6138                && request_mm != MmId::initial(child_dettid)
6139                && !registered_parent_creation;
6140            if initial_current_root
6141                && (sender != child_dettid
6142                    || parent_dettid != child_dettid
6143                    || parent_detpid != child_dettid
6144                    || ctid != 0
6145                    || parent_is_kernel_blocked
6146                    || request_mm != MmId::initial(child_dettid).for_exec(child_dettid)
6147                    || self.root_dettid.get().is_some()
6148                    || self.initial_descriptor_image_complete.load(SeqCst)
6149                    || self.root_image_marked.load(SeqCst)
6150                    || self
6151                        .pending_exec_states
6152                        .lock()
6153                        .unwrap()
6154                        .contains_key(&child_dettid))
6155            {
6156                return SchedulerRpcResult::ThreadExited;
6157            }
6158            // The local Tool selected this MM synchronously under the genuine
6159            // initial-image lifecycle before this request began. Record both
6160            // identities at birth, not in a later Mark response/ack interval.
6161            if initial_current_root
6162                && !sched.record_initial_current_image_birth(child_dettid, request_mm)
6163            {
6164                return SchedulerRpcResult::ThreadExited;
6165            }
6166
6167            if parent_is_kernel_blocked && self.cfg.sequentialize_threads {
6168                sched.complete_vfork_registration(parent_dettid, child_dettid);
6169            }
6170
6171            let child_mm = if initial_current_root {
6172                request_mm
6173            } else {
6174                MmId::for_clone(
6175                    request_mm,
6176                    child_dettid,
6177                    flags.is_some_and(|flags| flags.contains(CloneFlags::CLONE_VM)),
6178                )
6179            };
6180            if let Some(owners) = &mut sched.boundary_owners {
6181                owners
6182                    .fresh(child_dettid, child_mm)
6183                    .unwrap_or_else(|error| crate::scheduler::exit_on_scheduler_refusal(&error));
6184            }
6185            if !initial_current_root {
6186                sched.register_reused_transferred_exec_tid(child_dettid, child_mm);
6187                sched.record_thread_mm(child_dettid, child_mm);
6188            }
6189            // A parent registers its child before the child's own first
6190            // message publishes a clock (an early start waits for this), so a
6191            // clock already under a reused ID is the earlier thread's. A vfork
6192            // child, which registers itself, retired it before publishing.
6193            if sender != child_dettid && self.clock_belongs_to_earlier_thread(&sched, child_dettid)
6194            {
6195                self.global_time.lock().unwrap().retire_thread(child_dettid);
6196            }
6197            // The guest's procfs paths name a task by its thread ID in the
6198            // guest's PID namespace, which is the raw value of its DetTid.
6199            self.inodes
6200                .lock()
6201                .unwrap()
6202                .count_incarnation(child_dettid.as_raw());
6203
6204            // A new thread on a reused tid has not exited, and has published
6205            // no mask.
6206            sched.forget_committed_exit(child_dettid);
6207            sched.published_masks.remove(&child_dettid);
6208            // Don't fill in the request, as the child will do it:
6209            let _entry = sched
6210                .next_turns
6211                .entry(child_dettid)
6212                .or_insert_with(|| ThreadNextTurn {
6213                    dettid: child_dettid,
6214                    child_tid_addr: ctid,
6215                    req: Ivar::new(),
6216                    resp: Ivar::new(),
6217                    protocol: Default::default(),
6218                });
6219            // Under the in-guest delivery gate, a new task starts with the
6220            // guest mask its creator published, as Linux's child starts with
6221            // its creator's `blocked` (A5 step 3c.1e A, fork-child seeding);
6222            // a no-op with the gate off, and for the root thread.
6223            if flags.is_some() {
6224                sched.seed_child_guest_sigmask(parent_dettid, child_dettid);
6225            }
6226
6227            {
6228                let is_group_leader = if let Some(f) = flags {
6229                    !f.contains(CloneFlags::CLONE_THREAD)
6230                } else {
6231                    true // root thread
6232                };
6233                sched.thread_tree.add_child_with_wait_metadata(
6234                    parent_dettid,
6235                    child_dettid,
6236                    is_group_leader,
6237                    flags.is_some_and(|flags| flags.contains(CloneFlags::CLONE_PARENT)),
6238                    exit_signal,
6239                );
6240                sched.work_span_note_spawn(parent_dettid, child_dettid);
6241                if is_group_leader {
6242                    sched.forget_previous_process_exit(child_dettid);
6243                    // The child's exit signal is posted to its parent at a
6244                    // host-timed moment: that pair stops being observable now,
6245                    // before the child can run (4262 design §3.4).
6246                    sched.child_ends_timer_observability(child_dettid);
6247                }
6248                let new_pid_namespace = is_group_leader
6249                    && flags.is_some_and(|flags| flags.contains(CloneFlags::CLONE_NEWPID));
6250                if new_pid_namespace {
6251                    sched.thread_tree.enter_new_pid_namespace(child_dettid);
6252                    sched.thread_tree.mark_pid_namespace_init(child_dettid);
6253                }
6254                // A new process made with CLONE_NEWUSER lives in a new user
6255                // namespace below its creator's, recorded in its creator's
6256                // turn (`scheduler::user_namespaces`).
6257                if is_group_leader
6258                    && flags.is_some_and(|flags| flags.contains(CloneFlags::CLONE_NEWUSER))
6259                {
6260                    sched.thread_tree.enter_new_user_namespace(child_dettid);
6261                }
6262                // A nested task's pid in each namespace is fixed now, in its
6263                // creator's turn, before either runs again
6264                // (https://github.com/rrnewton/hermit/issues/4155).
6265                let pid_namespaces = &mut sched.thread_tree.pid_namespaces;
6266                let mut chain = pid_namespaces.chain_of(parent_dettid);
6267                if new_pid_namespace {
6268                    chain.push(child_dettid);
6269                }
6270                if let Some(reason) = pid_namespaces.untranslatable(parent_dettid) {
6271                    let reason = format!("its creator {parent_dettid}: {reason}");
6272                    pid_namespaces.mark_untranslatable(child_dettid, reason);
6273                } else if chain.is_empty() {
6274                    pid_namespaces.forget(child_dettid);
6275                } else {
6276                    let recorded =
6277                        crate::scheduler::pid_namespaces::read_nspid(child_detpid, child_dettid)
6278                            .and_then(|nspid| {
6279                                pid_namespaces
6280                                    .record(child_dettid, &chain, &nspid)
6281                                    .map_err(|error| error.to_string())
6282                            });
6283                    if let Err(reason) = recorded {
6284                        error!(
6285                            "[detcore, dtid {}] cannot record its pids in its PID namespaces: {}",
6286                            child_dettid, reason
6287                        );
6288                        pid_namespaces.mark_untranslatable(child_dettid, reason);
6289                    } else if is_group_leader {
6290                        sched.note_member_of_ended_namespace(child_dettid);
6291                    }
6292                }
6293                // A new task's root: its creator's filesystem group with
6294                // CLONE_FS, a copy of it without.
6295                self.process_auxv.lock().unwrap().clone_root(
6296                    parent_dettid,
6297                    child_dettid,
6298                    flags.is_some_and(|flags| flags.contains(CloneFlags::CLONE_FS)),
6299                );
6300                if is_group_leader && flags.is_some() {
6301                    // A new process runs its creator's image until it execs.
6302                    let mut images = self.exec_images.lock().unwrap();
6303                    match images.get(&parent_detpid).cloned() {
6304                        Some(image) => images.insert(child_dettid, image),
6305                        None => images.remove(&child_dettid),
6306                    };
6307                    drop(images);
6308                    self.process_auxv
6309                        .lock()
6310                        .unwrap()
6311                        .inherit(parent_detpid, child_dettid);
6312                    sched.inherit_real_time_records(parent_detpid, child_dettid);
6313                    // Its descriptor table: its creator's with CLONE_FILES, a
6314                    // copy without. Recorded here, in the creator's turn and
6315                    // before the child can run, so a lock the child takes
6316                    // first is never overwritten by its own fork
6317                    // (`procfs::locks::LockLedger::fork`).
6318                    self.lock_ledger.lock().unwrap().fork(
6319                        parent_detpid,
6320                        child_dettid,
6321                        flags.is_some_and(|flags| flags.contains(CloneFlags::CLONE_FILES)),
6322                    );
6323                }
6324                // A thread with a table of its own: the ledger counts its
6325                // calls as its process's, so that process's tables can no
6326                // longer be told apart
6327                // (`procfs::locks::LockLedger::mark_private_tables`).
6328                if !is_group_leader
6329                    && flags.is_some_and(|flags| !flags.contains(CloneFlags::CLONE_FILES))
6330                {
6331                    self.lock_ledger
6332                        .lock()
6333                        .unwrap()
6334                        .mark_private_tables(parent_detpid);
6335                }
6336            }
6337
6338            if let Some((physical_pid, physical_tid)) = physical_ids {
6339                let child_is_thread =
6340                    flags.is_some_and(|flags| flags.contains(CloneFlags::CLONE_THREAD));
6341                let child_detpid = if child_is_thread {
6342                    parent_detpid
6343                } else {
6344                    child_dettid
6345                };
6346                if let Err(open_error) = sched.register_physical_thread(
6347                    child_dettid,
6348                    child_mm,
6349                    physical_pid,
6350                    physical_tid,
6351                ) {
6352                    error!(
6353                        "[detcore, dtid {}] cannot bind host process {} thread {} during child registration: {}",
6354                        child_dettid, physical_pid, physical_tid, open_error,
6355                    );
6356                    sched.logically_kill_thread(&child_dettid, &child_detpid, child_mm);
6357                    return SchedulerRpcResult::ThreadExited;
6358                }
6359            }
6360
6361            // Bind only the existing root self-registration, after its
6362            // registration has succeeded. An arbitrary descendant's first
6363            // completion cannot claim a launcher-created input endpoint.
6364            if flags.is_none()
6365                && parent_dettid == child_dettid
6366                && parent_detpid == child_dettid
6367                && let Some(input) = self.inherited_input.lock().unwrap().as_mut()
6368                && input.root.is_none()
6369            {
6370                input.root = Some((child_dettid, child_mm));
6371            }
6372            // The same root self-registration names the thread whose first
6373            // image `Config::launch_image` describes.
6374            if flags.is_none() && parent_dettid == child_dettid && parent_detpid == child_dettid {
6375                let _ = self.root_dettid.set(child_dettid);
6376            }
6377
6378            // Record this thread in deterministic creation order so a
6379            // happens-before anchor addressed by `spawn_ordinal` resolves to it.
6380            sched.hb_note_spawn(child_dettid);
6381
6382            if self.cfg.replay_schedule_from.is_none() {
6383                // Give the thread an initial priority
6384                let old_prio = sched.priorities.insert(child_dettid, initial_priority);
6385                assert!(old_prio.is_none());
6386            } else {
6387                // In replay mode the schedule decides the thread's priority
6388                // (`Scheduler::replay_registration_priority`).
6389                // A child registers before the schedule switches to it
6390                // whenever its parent runs on after the clone, as recorded
6391                // schedules usually have it, so this is a sign that the
6392                // replay lost its guide only once it has left the record.
6393                if sched.replay_registration_priority(child_dettid, initial_priority) {
6394                    if sched.replay_left_its_record() {
6395                        warn!(
6396                            "[detcore, dtid {}] registered before the replayed schedule switched to it, created by process {}; it takes its starting priority {}",
6397                            child_dettid, parent_detpid, initial_priority
6398                        );
6399                    } else {
6400                        debug!(
6401                            "[detcore, dtid {}] registered before the replayed schedule switched to it, created by process {}; it takes its starting priority {}",
6402                            child_dettid, parent_detpid, initial_priority
6403                        );
6404                    }
6405                }
6406            }
6407
6408            sched.register_preemption_thread(
6409                child_dettid,
6410                initial_priority,
6411                parent_is_kernel_blocked,
6412            );
6413
6414            // Describe *how* the admission side is chosen, but do not resolve it
6415            // (and in particular do not draw the post-fork PRNG) here: this
6416            // handler runs on whichever backend worker fielded the RPC, so on an
6417            // asynchronous backend (e.g. DBT, where the child self-registers
6418            // outside a scheduler turn) resolving the side now would consume the
6419            // PRNG draw in host RPC order. `admit_to_run_queue` resolves the
6420            // intent at the step2 drain -- which under ptrace is post-commit, in
6421            // schedule order, unchanged. Read its "What this does and does not
6422            // make deterministic" section before relying on that word: the drain
6423            // is a deterministic *point* and resolution within one drain is
6424            // `DetTid`-ordered, but on an asynchronous backend which drain a
6425            // given admission lands in is not itself schedule-determined unless
6426            // that admission is anchored (ordinary clone is, via the parent's
6427            // `ParentContinue`; vfork is, via `step2a`'s barrier). When threads
6428            // are not sequentialized, or the
6429            // parent is already kernel-blocked (vfork), the child takes the tail
6430            // and no PRNG is consumed.
6431            let intent = if self.cfg.sequentialize_threads && !parent_is_kernel_blocked {
6432                AdmitIntent::PostFork(self.cfg.runs_post_fork)
6433            } else {
6434                AdmitIntent::Fixed(AdmitSide::Back)
6435            };
6436            sched.admit_to_run_queue(child_dettid, intent);
6437            debug!(
6438                "[detcore] CreateChildThread with dtid {}: admit child via {:?}.",
6439                child_dettid, intent,
6440            );
6441            sched.started_up.try_put(());
6442        }
6443        #[cfg(feature = "physical-mm-test-support")]
6444        if flags.is_none() && rpc_sender.as_raw() == child_dettid.as_raw() {
6445            // No scheduler mutex lives across the test rendezvous. The real
6446            // ParentContinue call below is unchanged and has not been issued.
6447            self.initial_mm_checkpoint(
6448                initial_mm_probe::Point::RegisteredBeforeGrant,
6449                child_dettid,
6450                request_mm,
6451            )
6452            .await;
6453        }
6454        // The child queue position above determines which equal-priority side
6455        // gets the first turn when the parent requests ParentContinue.
6456        // A vfork parent is already blocked by the kernel and is not in the run
6457        // queue, so it must not issue a ParentContinue request here.
6458        if self.cfg.sequentialize_threads && !parent_is_kernel_blocked {
6459            let mut rs = Resources::new(parent_detpid);
6460            rs.insert(
6461                ResourceID::ParentContinue {
6462                    parent: parent_dettid,
6463                    child: child_dettid,
6464                },
6465                Permission::W,
6466            );
6467            if matches!(
6468                self.recv_grant_resources(
6469                    rpc_sender,
6470                    parent_detpid,
6471                    rs,
6472                    Some(request_mm),
6473                    RpcOrigin::ParentContinue
6474                )
6475                .await
6476                .0,
6477                SchedulerRpcResult::ThreadExited
6478            ) {
6479                return SchedulerRpcResult::ThreadExited;
6480            }
6481        }
6482        SchedulerRpcResult::Continue(())
6483    }
6484
6485    /// Called by the child thread upon startup.
6486    /// Returns a thread-preemption history for the new guest thread (if --replay-preemptions-from
6487    /// is used).
6488    async fn recv_start_new_thread(
6489        &self,
6490        from: Tid,
6491        dettid: DetTid,
6492        detpid: DetPid,
6493        request_mm: MmId,
6494        physical_ids: Option<(i32, i32)>,
6495        signal_identity: Option<reverie::SignalTaskIdentity>,
6496    ) -> SchedulerRpcResult<Option<ThreadHistory>> {
6497        #[cfg(feature = "physical-mm-test-support")]
6498        self.initial_mm_checkpoint(initial_mm_probe::Point::ThreadStartup, dettid, request_mm)
6499            .await;
6500        let mut tries: u64 = 0;
6501        // TODO: eliminate this loop. Could instead signal with an ivar.
6502        let response_ivar = loop {
6503            yield_once().await;
6504            let mut sched = self.lock_rpc_scheduler(false).await;
6505            if sched.thread_is_logically_killed(dettid)
6506                || !sched.rpc_incarnation_matches(dettid, request_mm)
6507            {
6508                return SchedulerRpcResult::ThreadExited;
6509            }
6510            if self.cfg.backend.process_exits_complete_asynchronously
6511                && sched.process_physically_gone(detpid)
6512            {
6513                drop(sched);
6514                self.report_backend_failure(reverie::BackendFailure {
6515                    pid: reverie::Pid::from_raw(detpid.as_raw()),
6516                    tid: Tid::from_raw(dettid.as_raw()),
6517                    phase: "a process was already physically gone when it started",
6518                });
6519                return SchedulerRpcResult::ThreadExited;
6520            }
6521            if self.cfg.backend.requires_thread_directed_process_signals && physical_ids.is_none() {
6522                error!(
6523                    "[detcore, dtid {}] backend requires a host thread ID at StartNewThread",
6524                    dettid,
6525                );
6526                sched.logically_kill_thread(&dettid, &detpid, request_mm);
6527                return SchedulerRpcResult::ThreadExited;
6528            }
6529            // The resources that must be held for the fresh thread to run:
6530            let rsrcs = {
6531                let mut s = HashMap::new();
6532                s.insert(ResourceID::MemAddrSpace(detpid), Permission::RW); // TODO(T78055411): track mem aliasing.
6533                Resources {
6534                    tid: dettid,
6535                    resources: s,
6536                    poll_attempt: 0,
6537                    fyi: String::new(),
6538                    signal_interrupt_errno: None,
6539                    backend_runtime_bootstrap: false,
6540                    blocked_signal_mask: None,
6541                    blocked_signal_mask_unknown: false,
6542                    replay_served_from_log: false,
6543                    reads_signalfd: false,
6544                    read_fd: None,
6545                    polls_guest_state_only: false,
6546                    polls_a_named_fifo: false,
6547                    relies_on_guest_held_marks: false,
6548                    passes_descriptors_out: false,
6549                    waits_until_nothing_else_can_run: false,
6550                    previous_attempt_made_progress: false,
6551                    upgraded_from_attempt: 0,
6552                    polls_channel: None,
6553                    poll_deadline: None,
6554                    in_guest_sleep_sigalrm: None,
6555                    signal_state_task: None,
6556                    decides_signal_interruption_itself: false,
6557                    continues_sigreturn: false,
6558                    private_sigchld_decides: false,
6559                    slice_boundary: None,
6560                    guest_signal_mask: None,
6561                    callback_checkpoint: None,
6562                    priority_row: None,
6563                    slice_expired: false,
6564                    zero_length_sleep: false,
6565                }
6566            };
6567            let nextturn = match sched.next_turns.entry(dettid) {
6568                Entry::Vacant(_entry) => {
6569                    // CreateChildThread on the parent hasn't run yet.
6570
6571                    // TODO: We could try to populate the entry since we get here
6572                    // first, but currently we lack the information right here to
6573                    // populate the child_tid_addr field.
6574                    if tries == 0 {
6575                        trace!(
6576                            "[detcore, dtid {}] thread showed up early, no queue entry yet.  Waiting...",
6577                            dettid
6578                        );
6579                    }
6580                    tries += 1;
6581                    continue;
6582                }
6583                Entry::Occupied(entry) => {
6584                    trace!(
6585                        "[detcore, dtid {}] handling StartNewThread rpc.  Found next_turns entry (after {} tries)",
6586                        from, tries
6587                    );
6588                    entry.get().clone()
6589                }
6590            };
6591            if let Some((physical_pid, physical_tid)) = physical_ids
6592                && let Err(open_error) =
6593                    sched.register_physical_thread(dettid, request_mm, physical_pid, physical_tid)
6594            {
6595                error!(
6596                    "[detcore, dtid {}] cannot bind host process {} thread {} for exact signal delivery: {}",
6597                    dettid, physical_pid, physical_tid, open_error,
6598                );
6599                sched.logically_kill_thread(&dettid, &detpid, request_mm);
6600                return SchedulerRpcResult::ThreadExited;
6601            }
6602            if sched.signal_control_installed() {
6603                let binding = signal_identity
6604                    .ok_or(TimerFailure::Identity)
6605                    .and_then(|identity| {
6606                        if from.as_raw() != dettid.as_raw()
6607                            || sched.registered_process(dettid) != Some(detpid)
6608                        {
6609                            return Err(TimerFailure::Identity);
6610                        }
6611                        sched.real_timers.bind(detpid, dettid, request_mm, identity)
6612                    });
6613                if let Err(error) = binding {
6614                    sched.fail_parked(dettid, ProtocolFailure::Timer(error));
6615                    return SchedulerRpcResult::ThreadExited;
6616                }
6617            }
6618            if let Err(error) = sched.install_resource_origin(
6619                dettid,
6620                ResourceOrigin {
6621                    rpc: RpcOrigin::ThreadStart,
6622                    mm: request_mm,
6623                    control: ControlCapability::None,
6624                },
6625            ) {
6626                sched.fail_parked(dettid, error);
6627                return SchedulerRpcResult::ThreadExited;
6628            }
6629            sched.request_put(&nextturn.req, rsrcs, &self.global_time);
6630            break nextturn.resp;
6631        };
6632        debug!(
6633            "[detcore, dtid {}] New thread will now wait for response on {}...",
6634            &dettid, &response_ivar
6635        );
6636        let answer = response_ivar.get().await;
6637        if matches!(
6638            answer,
6639            SchedResponse::ObserveSignal(_) | SchedResponse::InGuestFatalAlarm
6640        ) {
6641            self.sched
6642                .lock()
6643                .unwrap()
6644                .fail_parked(dettid, ProtocolFailure::UnexpectedControl);
6645            return SchedulerRpcResult::ThreadExited;
6646        }
6647        let request_became_stale = {
6648            let sched = self.lock_rpc_scheduler(false).await;
6649            sched.thread_is_logically_killed(dettid)
6650                || !sched.rpc_incarnation_matches(dettid, request_mm)
6651        };
6652        if request_became_stale {
6653            return SchedulerRpcResult::ThreadExited;
6654        }
6655        info!(
6656            "[detcore, dtid {}] New thread given go-ahead to proceed via {}",
6657            &dettid, &response_ivar
6658        );
6659        if let Some(pr) = &self.preemptions_to_replay {
6660            let (history, old_prio) = {
6661                let mut sched = self.lock_rpc_scheduler(false).await;
6662                if sched.thread_is_logically_killed(dettid)
6663                    || !sched.rpc_incarnation_matches(dettid, request_mm)
6664                {
6665                    return SchedulerRpcResult::ThreadExited;
6666                }
6667                let history = pr.extract_thread_record(&dettid).unwrap_or_else(|| {
6668                    warn!(
6669                        "Replaying preemptions, but no record found for thread {}",
6670                        dettid
6671                    );
6672                    ThreadHistory::new()
6673                });
6674                let old_prio = sched.priorities.insert(dettid, history.initial_priority());
6675                (history, old_prio)
6676            };
6677            debug!(
6678                "[replay-preemption] Enqueing new thread at priority {:?} (changed from {:?})",
6679                history.initial_priority(),
6680                old_prio,
6681            );
6682            SchedulerRpcResult::Continue(Some(history))
6683        } else {
6684            SchedulerRpcResult::Continue(None)
6685        }
6686    }
6687
6688    /// Warning: this happens completely asynchronously, whenever the guest exit hook fires.
6689    /// Its timing is not coordinated by the scheduler.
6690    async fn recv_deregister_thread(&self, _from: Tid, mut deregistration: ThreadDeregistration) {
6691        // Keep the same scheduler -> pending-exec lock order as reconnect.
6692        let mut sched = self.sched.lock().unwrap();
6693        let ThreadDeregistration {
6694            dettid, detpid, mm, ..
6695        } = &deregistration;
6696        // A fatal signal can tear down the caller after its local state has advanced to the
6697        // candidate exec image but before the successful reconnect (or failed-exec cancel).
6698        // Retire that one-shot preparation before scheduler-incarnation admission rejects the
6699        // candidate image's final cleanup RPC.
6700        let mut pending = self.pending_exec_states.lock().unwrap();
6701        if pending.get(detpid).is_some_and(|state| {
6702            state.caller == *dettid && (*mm == state.mm || *mm == state.mm.for_exec(state.process))
6703        }) {
6704            let prepared = pending.remove(detpid).unwrap();
6705            // Unlike a recoverable failure (CancelExec), the caller's old image
6706            // deregistered before any reconnect: its group is dying, and Linux
6707            // walks every robust list of the group, in the exec or in each
6708            // thread's exit. On DBT this takes a fatal signal during the exec.
6709            // An exec whose target the kernel cannot load does not get here:
6710            // DynamoRIO's own exec succeeds, the new image reconnects and its
6711            // wakes are queued there, and the run then hangs on that image's
6712            // unretired death (https://github.com/rrnewton/hermit/issues/4362).
6713            // Hold turns until the kernel has ended the group, as an
6714            // exit_group's grant does, and wake the prepared waiters now, while
6715            // the caller's outstanding exec request still withholds every turn;
6716            // they run at the drain after the hold. A group that outlives this
6717            // is ended by the exited-thread valve with a named HERMIT_DEADLOCK
6718            // rather than losing the wakes (Codex review of
6719            // https://github.com/rrnewton/hermit/pull/4352, P2).
6720            if !prepared.fd_blocking.robust_wakes.is_empty() {
6721                sched.hold_turns_until_the_group_ends(*detpid);
6722                wake_exec_robust_waiters(
6723                    &mut sched,
6724                    &prepared.fd_blocking.robust_wakes,
6725                    "terminal exec",
6726                );
6727            }
6728            sched.finish_exec_teardown(prepared.caller, *detpid, prepared.mm, false);
6729            // The local caller can have installed its candidate exec MmId
6730            // before cancellation. It still owns the original registration;
6731            // consume that exact prepared incarnation rather than discarding
6732            // cleanup as a stale request after removing the preparation.
6733            deregistration.mm = prepared.mm;
6734        }
6735        drop(pending);
6736
6737        // Invariant: will only be called when sequentialize-threads is on.
6738        assert!(self.cfg.sequentialize_threads);
6739        self.account_deregistered_thread(&mut sched, deregistration);
6740    }
6741
6742    /// Consume final statistics and registration while holding admission closed.
6743    /// Authenticated exec cleanup uses this same once-only accounting path.
6744    fn account_deregistered_thread(
6745        &self,
6746        sched: &mut Scheduler,
6747        deregistration: ThreadDeregistration,
6748    ) {
6749        let ThreadDeregistration {
6750            dettid,
6751            detpid,
6752            mm,
6753            thread_start_entered: _,
6754            vfork_origin: _,
6755            timeslice_stats,
6756            syscall_count,
6757            syscall_histogram,
6758            chaos_epochs,
6759            killed_by_group_signal,
6760            passed_descriptors_out,
6761        } = deregistration;
6762        if !sched.rpc_incarnation_matches(dettid, mm) {
6763            debug!(
6764                "[detcore, dtid {}] ignoring deregistration from retired exec incarnation {:?}",
6765                dettid, mm,
6766            );
6767            return;
6768        }
6769        self.post_exec_fd_blocking.lock().unwrap().remove(&dettid);
6770        // A SIGKILL victim's cleanup is accounted exactly once
6771        // (`scheduler::sigkill_retirement`), also on a backend that otherwise
6772        // accepts a repeated callback.
6773        if !sched.note_deregistration_accounted(dettid)
6774            || sched.sigkill_receipt_already_counted(dettid)
6775        {
6776            trace!(
6777                "[detcore, dtid {}] acknowledging already-accounted deregistration",
6778                dettid
6779            );
6780            return;
6781        }
6782        // This consuming owner may have been cancelled before any request
6783        // published its export. Keep the final fact behind incarnation and
6784        // receipt admission, but before either SIGKILL routing or retirement.
6785        if passed_descriptors_out {
6786            sched.revoke_guest_held_marks(dettid);
6787        }
6788        if let Some(writer) = &mut sched.preemption_writer {
6789            for transition in chaos_epochs {
6790                writer.insert_chaos_epoch(dettid, transition);
6791            }
6792        }
6793        sched.record_timeslice_stats(dettid, timeslice_stats);
6794        sched.record_syscall_count(dettid, syscall_count, syscall_histogram);
6795        // A SIGKILL victim is retired at its sender's turn, not here
6796        // (`scheduler::sigkill_retirement`): its receipt is accounted above
6797        // and only counted.
6798        if sched.route_sigkill_receipt(dettid)
6799            != crate::scheduler::sigkill_retirement::SigkillReceipt::Ordinary
6800        {
6801            return;
6802        }
6803        // The process as the scheduler registered the thread, as the retire
6804        // path reads it (`logically_kill_thread`), not the backend's report.
6805        let process = sched.registered_process(dettid).unwrap_or(detpid);
6806        // A leader retired at its own exit (`retire_exited_leader`) reports
6807        // here a death already accounted: whatever killed its group, a guest's
6808        // SIGKILL above all, is accounted where it happened, not at this
6809        // host-timed moment.
6810        if sched.retire_deregistered_thread(dettid, detpid, mm) && killed_by_group_signal {
6811            sched.note_signal_killed_group(dettid, process);
6812        }
6813        trace!(
6814            "[detcore, dtid {}] thread deregistered, removed from sched structures.",
6815            dettid
6816        );
6817    }
6818
6819    async fn recv_futex_action(
6820        &self,
6821        caller: RpcIncarnation,
6822        action: FutexAction,
6823        futexid: FutexID,
6824        init_read: i32,
6825        mask: u32,
6826        passed_descriptors_out: bool,
6827    ) -> Option<SchedValue> {
6828        let RpcIncarnation { dettid, mm } = caller;
6829        trace!("[detcore, dtid {}] Futex action: {:?}", &dettid, action);
6830        let response_iv = {
6831            let mut sched = self.lock_rpc_scheduler(false).await;
6832            if sched.thread_is_logically_killed(dettid)
6833                || !sched.rpc_incarnation_matches(dettid, mm)
6834            {
6835                return Some(SchedValue::Value(nix::errno::Errno::EINTR as u64));
6836            }
6837            let Some(resp_iv) = sched
6838                .next_turns
6839                .get(&dettid)
6840                .map(|nextturn| nextturn.resp.clone())
6841            else {
6842                // AUTONOMOUS-BOT-IMPLEMENTED
6843                // TODO-HUMAN-REVIEW(PR-845): Review late RPCs from exit-group siblings.
6844                trace!(
6845                    "[detcore, dtid {}] ignoring futex action after logical thread removal",
6846                    dettid
6847                );
6848                return Some(SchedValue::Value(nix::errno::Errno::EINTR as u64));
6849            };
6850            // A direct futex wait has no Resources request to carry this
6851            // fact. Only a live, admitted owner may publish it, before that
6852            // owner's wait can leave other threads as the only pollers.
6853            if passed_descriptors_out {
6854                sched.revoke_guest_held_marks(dettid);
6855            }
6856            match action {
6857                FutexAction::WaitRequest(maybe_timeout, signal_watch) => {
6858                    if sched.child_tid_was_cleared(futexid, init_read) {
6859                        trace!(
6860                            "[detcore, dtid {}] late wait on cleared child-TID futex {:?}",
6861                            dettid, futexid
6862                        );
6863                        return Some(SchedValue::Value(0));
6864                    }
6865                    if let Err(error) = sched.install_resource_origin(
6866                        dettid,
6867                        ResourceOrigin {
6868                            rpc: RpcOrigin::FutexAction,
6869                            mm,
6870                            control: ControlCapability::None,
6871                        },
6872                    ) {
6873                        sched.fail_parked(dettid, error);
6874                        return Some(SchedValue::Value(nix::errno::Errno::EINTR as u64));
6875                    }
6876                    sched.sleep_futex_waiter(&dettid, futexid, maybe_timeout, mask, signal_watch);
6877                    // block on ivar, below
6878                }
6879                FutexAction::WaitFinished => {
6880                    sched.leave_pi_futex_queue(dettid);
6881                    return None;
6882                }
6883                FutexAction::WakeRequest(num_threads) => {
6884                    let (num, met_pi) =
6885                        sched.wake_futex_waiters_until_pi(dettid, futexid, num_threads, mask);
6886                    if self.cfg.yields_after_futex_wake(num) {
6887                        sched.requeue_futex_waker(dettid);
6888                    }
6889                    return Some(if met_pi {
6890                        SchedValue::PiWaiterReached { woken: num }
6891                    } else {
6892                        SchedValue::Value(num)
6893                    });
6894                }
6895                FutexAction::WakeFinished(_num_threads) => {
6896                    return None;
6897                }
6898                FutexAction::RequeueRequest(to, nr_wake, nr_requeue) => {
6899                    let (woken, moved, met_pi) = sched.requeue_futex_waiters(
6900                        dettid,
6901                        futexid,
6902                        to,
6903                        nr_wake as usize,
6904                        nr_requeue as usize,
6905                    );
6906                    if self.cfg.yields_after_futex_wake(woken) {
6907                        sched.requeue_futex_waker(dettid);
6908                    }
6909                    return Some(if met_pi {
6910                        SchedValue::PiWaiterReached { woken }
6911                    } else {
6912                        SchedValue::Requeued { woken, moved }
6913                    });
6914                }
6915                FutexAction::PiState(named) => {
6916                    return Some(SchedValue::PiFutex(
6917                        sched.pi_futex_state(futexid, named, dettid),
6918                    ));
6919                }
6920                FutexAction::PiTake => {
6921                    return Some(SchedValue::PiTaken(
6922                        sched.take_handed_pi_futex(futexid, dettid),
6923                    ));
6924                }
6925                FutexAction::PiHandOff => {
6926                    let woken = u64::from(sched.hand_off_pi_futex(futexid).is_some());
6927                    if self.cfg.yields_after_futex_wake(woken) {
6928                        sched.requeue_futex_waker(dettid);
6929                    }
6930                    return Some(SchedValue::Value(woken));
6931                }
6932                FutexAction::PiLockWait(maybe_timeout, signal_watch, owner) => {
6933                    if let Err(error) = sched.install_resource_origin(
6934                        dettid,
6935                        ResourceOrigin {
6936                            rpc: RpcOrigin::FutexAction,
6937                            mm,
6938                            control: ControlCapability::None,
6939                        },
6940                    ) {
6941                        sched.fail_parked(dettid, error);
6942                        return Some(SchedValue::Value(nix::errno::Errno::EINTR as u64));
6943                    }
6944                    sched.sleep_pi_futex_waiter(
6945                        dettid,
6946                        futexid,
6947                        maybe_timeout,
6948                        signal_watch,
6949                        owner,
6950                    );
6951                    // block on ivar, below
6952                }
6953            }
6954            // Blocking on the FUTEX_WAIT here, remove ourselves:
6955            assert!(sched.run_queue.remove_tid(dettid));
6956            resp_iv
6957        };
6958        // Wait for wake+scheduler response.
6959        match response_iv.get().await {
6960            // A futex wait never asks for `GoAlone`.
6961            SchedResponse::Go(answer) | SchedResponse::GoAlone(answer) => {
6962                trace!(
6963                    "[detcore, dtid {}] Unblocked from futex_wait! ({})",
6964                    &dettid, &response_iv
6965                );
6966                answer
6967            }
6968            SchedResponse::Signaled(signals) => Some(SchedValue::Signaled(
6969                signals.iter().flatten().fold(0, |set, signal| {
6970                    set | crate::scheduler::kernel_signal_bit(signal.0)
6971                }),
6972            )),
6973            SchedResponse::ObserveSignal(_) | SchedResponse::InGuestFatalAlarm => {
6974                self.sched
6975                    .lock()
6976                    .unwrap()
6977                    .fail_parked(dettid, ProtocolFailure::UnexpectedControl);
6978                self.wait_for_backend_failure().await;
6979                futures::future::pending().await
6980            }
6981        }
6982    }
6983
6984    fn recv_robust_list_wakes(&self, from: DetTid, wakes: Vec<(DetTid, FutexID)>) -> Vec<u64> {
6985        let mut sched = self.sched.lock().unwrap();
6986        if sched.retain_sigkill_victim_wakes(from, &wakes) {
6987            // A SIGKILL victim's batch runs with its cohort; the empty answer
6988            // has no counts, so the victim logs nothing for it.
6989            return Vec::new();
6990        }
6991        sched.wake_futex_waiters_after_exit(&wakes)
6992    }
6993
6994    /// The epoch as a logical time: the virtual mtime of a file first seen
6995    /// with a non-canonical host mtime.
6996    fn epoch_logical_time(&self) -> LogicalTime {
6997        let nanos = self
6998            .cfg
6999            .epoch
7000            .timestamp_nanos_opt()
7001            .expect("epoch cannot be represented in a timestamp with nanosecond precision")
7002            as u64;
7003        LogicalTime::from_nanos(nanos)
7004    }
7005
7006    async fn recv_determinize_inode(
7007        &self,
7008        from: Tid,
7009        ino: RawInode,
7010        procfs: Option<ProcfsName>,
7011        observed: ObservedMtime,
7012        sighting: InodeSighting,
7013    ) -> (DetInode, LogicalTime) {
7014        let _sched = self.lock_rpc_scheduler(false).await;
7015        // Here we establish a policy that when we first see a file its mtime is
7016        // the epoch, unless its host mtime is one of the canonical values in
7017        // `CANONICAL_FILE_MTIME_SECONDS`, which it keeps.
7018        let epoch = self.epoch_logical_time();
7019        let mut pool = self.inodes.lock().unwrap();
7020        if let Some(name) = procfs {
7021            let path = pool.procfs_path_key(&name);
7022            pool.procfs_key(ino, path);
7023        }
7024        let (dino, ns) = pool.add_sighted_inode(ino, observed, epoch, sighting);
7025        drop(pool);
7026        trace!(
7027            "[detcore, dtid {}] resolved (raw) inode {:?} to {:?}, mtime {}",
7028            from, ino, dino, ns
7029        );
7030        (dino, ns)
7031    }
7032
7033    async fn recv_determinize_mapping_inodes(
7034        &self,
7035        from: Tid,
7036        inodes: Vec<RawInode>,
7037    ) -> Vec<DetInode> {
7038        let _sched = self.lock_rpc_scheduler(false).await;
7039        let epoch = self.epoch_logical_time();
7040        let mut pool = self.inodes.lock().unwrap();
7041        let dinos: Vec<DetInode> = inodes
7042            .iter()
7043            .map(|ino| pool.add_inode(*ino, ObservedMtime::Unobserved, epoch).0)
7044            .collect();
7045        trace!(
7046            "[detcore, dtid {}] resolved mapping (raw) inodes {:?} to {:?}",
7047            from, inodes, dinos
7048        );
7049        dinos
7050    }
7051
7052    // AUTONOMOUS-BOT-IMPLEMENTED
7053    // TODO-HUMAN-REVIEW(PR-1056): Deterministic st_dev remapping RPC.
7054    async fn recv_determinize_device(&self, from: Tid, raw_device: u64) -> u64 {
7055        let _sched = self.lock_rpc_scheduler(false).await;
7056        let det_device = self.devices.lock().unwrap().determinize(raw_device);
7057        trace!(
7058            "[detcore, dtid {}] resolved (raw) device {} to {}",
7059            from, raw_device, det_device
7060        );
7061        det_device
7062    }
7063
7064    async fn recv_determinize_mount_id(
7065        &self,
7066        from: Tid,
7067        raw_mount_id: u64,
7068        mountinfo_order: Option<&[u64]>,
7069    ) -> Option<u64> {
7070        let _sched = self.lock_rpc_scheduler(false).await;
7071        let virtual_mount_id = self
7072            .mount_ids
7073            .lock()
7074            .unwrap()
7075            .determinize(raw_mount_id, mountinfo_order);
7076        trace!(
7077            "[detcore, dtid {}] resolved fdinfo mount ID {} to {:?}",
7078            from, raw_mount_id, virtual_mount_id
7079        );
7080        virtual_mount_id
7081    }
7082
7083    async fn recv_resolve_mountinfo_ids(
7084        &self,
7085        from: Tid,
7086        mountinfo_order: &[u64],
7087        observation_order: &[u64],
7088    ) -> Option<Vec<u64>> {
7089        let _sched = self.lock_rpc_scheduler(false).await;
7090        let virtual_mount_ids = self
7091            .mount_ids
7092            .lock()
7093            .unwrap()
7094            .resolve_mountinfo(mountinfo_order, observation_order);
7095        trace!(
7096            "[detcore, dtid {}] resolved mountinfo observation order {:?} (canonical {:?}) to {:?}",
7097            from, observation_order, mountinfo_order, virtual_mount_ids
7098        );
7099        virtual_mount_ids
7100    }
7101
7102    async fn recv_retire_inode(&self, from: Tid, ino: RawInode) {
7103        let _sched = self.lock_rpc_scheduler(false).await;
7104        trace!("[detcore, dtid {}] retire (raw) inode {:?}", from, ino);
7105        self.inodes.lock().unwrap().retire(ino);
7106    }
7107
7108    async fn recv_forget_retired_inode(
7109        &self,
7110        from: Tid,
7111        ino: RawInode,
7112        procfs: Option<ProcfsName>,
7113    ) {
7114        let _sched = self.lock_rpc_scheduler(false).await;
7115        trace!(
7116            "[detcore, dtid {}] forget retired (raw) inode {:?}",
7117            from, ino
7118        );
7119        let mut pool = self.inodes.lock().unwrap();
7120        if let Some(name) = procfs {
7121            let path = pool.procfs_path_key(&name);
7122            pool.procfs_key(ino, path);
7123        }
7124        pool.forget_retired(ino);
7125    }
7126
7127    async fn recv_touch_file(&self, from: Tid, ino: RawInode) {
7128        let _sched = self.lock_rpc_scheduler(false).await;
7129        let mtime = if self.cfg.virtualize_time {
7130            self.global_time.lock().unwrap().as_nanos()
7131        } else {
7132            // In this scenario, virtualize_metadata is set and virtualize_time isn't.
7133            // We virtualize initial mtimes, but update using realtime.
7134            let dt: DateTime<Utc> = Utc::now();
7135            let nanos = dt.timestamp_nanos_opt().expect(
7136                "current time cannot be represented in a timestamp with nanosecond precision",
7137            ) as u64;
7138            LogicalTime::from_nanos(nanos)
7139        };
7140        trace!(
7141            "[dtid {}] bumping mtime on file (rawinode {:?}) to {}",
7142            from, ino, mtime,
7143        );
7144        self.set_inode_mtime(ino, mtime);
7145    }
7146
7147    /// Set the virtual mtime to the time the guest asked for with utimensat
7148    /// (or utime/utimes, which are routed through it). Without this, a file's
7149    /// mtime is the time of its last write, so `tar` extraction, `cp -p` and
7150    /// `touch -r` could not restore one, and `make` saw files ordered by when
7151    /// they were unpacked.
7152    async fn recv_set_file_mtime(&self, from: Tid, ino: RawInode, mtime: LogicalTime) {
7153        let _sched = self.lock_rpc_scheduler(false).await;
7154        trace!(
7155            "[dtid {}] setting mtime on file (rawinode {:?}) to {}",
7156            from, ino, mtime,
7157        );
7158        self.set_inode_mtime(ino, mtime);
7159    }
7160
7161    fn set_inode_mtime(&self, ino: RawInode, mtime: LogicalTime) {
7162        self.inodes.lock().unwrap().set_mtime(ino, mtime);
7163    }
7164
7165    fn with_boundary_stack(response: GlobalResponse, stack: MaybePrintStack) -> GlobalResponse {
7166        match stack {
7167            Some(path) => GlobalResponse::WithSliceBoundaryStack(path, Box::new(response)),
7168            None => response,
7169        }
7170    }
7171
7172    // The caller holds the same scheduler lock as the existing event path.
7173    fn signal_requested_stacktrace(&self, dettid: DetTid) {
7174        if let Some(sig) = &self.cfg.stacktrace_signal {
7175            trace!(
7176                "[dtid {}] signaling thread with {} at the point of stack trace printing.",
7177                dettid, sig.0
7178            );
7179            let tid = Pid::from_raw(dettid.as_raw());
7180            // TODO(T78538674): virtualize pid/tid:
7181            // We send a raw signal here and let the guest pick it up WHENEVER it resumes.
7182            // We don't use the "signal_guest" method because we don't necessarily respect that
7183            // protocol here.
7184            // Alarm/timer signals are guest-chosen and may be realtime, which
7185            // `nix` cannot name; fall through to the raw syscall for those.
7186            match sig.signal() {
7187                Some(named) => signal::kill(tid, named).unwrap(),
7188                None => {
7189                    // SAFETY: `tid` is a live thread this scheduler owns and
7190                    // `sig.raw()` is a signal number the guest already supplied.
7191                    let rc = unsafe { libc::kill(tid.as_raw(), sig.raw()) };
7192                    assert_eq!(rc, 0, "raw kill of signal {} failed", sig.raw());
7193                }
7194            }
7195        }
7196    }
7197
7198    async fn recv_trace_schedevent(
7199        &self,
7200        ev: SchedEvent,
7201        detpid: DetPid,
7202        request_mm: MmId,
7203        command_bootstrap: bool,
7204        access: Option<Box<crate::access_trace::AccessPayload>>,
7205    ) -> SchedulerRpcResult<TraceSchedEventResponse> {
7206        let ev = {
7207            let mut sched = self.lock_rpc_scheduler(false).await;
7208            if !sched.rpc_incarnation_matches(ev.dettid, request_mm) {
7209                return SchedulerRpcResult::ThreadExited;
7210            }
7211            if sched.callback_history_active {
7212                sched
7213                    .callback_progress
7214                    .entry(ev.dettid)
7215                    .or_default()
7216                    .observe_event(&ev);
7217            }
7218            // TODO(T124316762): debug address randomization in the tracer and get rid of this hack:
7219            let ev = {
7220                if self.past_first_execve.load(SeqCst) {
7221                    ev
7222                } else {
7223                    info!(
7224                        "Warning: erasing rip of pre-execve sched event! {:?}",
7225                        SchedEventForLog {
7226                            event: &ev,
7227                            command_bootstrap
7228                        }
7229                    );
7230                    SchedEvent {
7231                        end_rip: None,
7232                        start_rip: None,
7233                        ..ev
7234                    }
7235                }
7236            };
7237            // Future trace_schedevent calls will retain their rip values.
7238            if ev.op == Op::Syscall(Sysno::execve, SyscallPhase::Prehook) {
7239                self.past_first_execve.store(true, SeqCst);
7240            }
7241            ev
7242        };
7243
7244        // Yield this guest thread if needed to follow schedule.
7245        let result = if self.cfg.replay_schedule_from.is_some() {
7246            let (consumed, print_stack2) = {
7247                let mut sched = self.lock_rpc_scheduler(false).await;
7248                if sched.thread_is_logically_killed(ev.dettid)
7249                    || !sched.rpc_incarnation_matches(ev.dettid, request_mm)
7250                {
7251                    return SchedulerRpcResult::ThreadExited;
7252                }
7253                let consumed = sched.consume_schedevent(&ev);
7254                let print_stack2 = if self.cfg.record_preemptions {
7255                    let stack = sched.record_event(&ev);
7256                    sched.record_access(&ev, access);
7257                    stack
7258                } else {
7259                    None
7260                };
7261                (consumed, print_stack2)
7262            };
7263            let ConsumeResult {
7264                keep_running,
7265                print_stack,
7266                event_ix: _,
7267                timeslice_remaining: mut end_of_timeslice,
7268            } = consumed;
7269            trace!(
7270                "keep_running :{}, end_of_timeslice: {:?}",
7271                keep_running, end_of_timeslice
7272            );
7273
7274            if !keep_running {
7275                trace!(
7276                    "[detcore, dtid {}] Thread yielding to follow replay schedule",
7277                    &ev.dettid,
7278                );
7279                let tid = reverie::Tid::from(ev.dettid.as_raw()); // TODO(T78538674): virtualize pid/tid:
7280                let mut rsrcs = Resources::new(ev.dettid);
7281                rsrcs.insert(ResourceID::TraceReplay, Permission::RW);
7282                let (response, timeslice) = self
7283                    .recv_grant_resources(
7284                        tid,
7285                        detpid,
7286                        rsrcs,
7287                        Some(request_mm),
7288                        RpcOrigin::TraceSchedEvent,
7289                    )
7290                    .await;
7291                if response == SchedulerRpcResult::ThreadExited {
7292                    return SchedulerRpcResult::ThreadExited;
7293                }
7294                end_of_timeslice = timeslice;
7295                trace!(
7296                    "[detcore, dtid {}] Thread reactivated after yielding for replay schedule",
7297                    &ev.dettid,
7298                );
7299            }
7300
7301            // Read after any wait for the schedule: a release another thread's
7302            // event made meanwhile, in an earlier turn, counts too, and so does
7303            // the end of the schedule. On a release the scheduler drops its
7304            // copy of this thread's callback checkpoints here, and the thread
7305            // drops its own when this reply reaches it.
7306            let (schedule_released, schedule_over) = self
7307                .lock_rpc_scheduler(false)
7308                .await
7309                .schedule_replay_reply(ev.dettid);
7310            TraceSchedEventResponse {
7311                print_stack_strace: print_stack.or(print_stack2),
7312                timeslice: end_of_timeslice,
7313                schedule_released,
7314                schedule_over,
7315            }
7316        } else {
7317            let print_stack_strace = {
7318                let mut sched = self.lock_rpc_scheduler(false).await;
7319                if sched.thread_is_logically_killed(ev.dettid)
7320                    || !sched.rpc_incarnation_matches(ev.dettid, request_mm)
7321                {
7322                    return SchedulerRpcResult::ThreadExited;
7323                }
7324                if self.cfg.record_preemptions {
7325                    let stack = sched.record_event(&ev);
7326                    sched.record_access(&ev, access);
7327                    stack
7328                } else {
7329                    None
7330                }
7331            };
7332            TraceSchedEventResponse {
7333                print_stack_strace,
7334                timeslice: None,
7335                schedule_released: false,
7336                schedule_over: false,
7337            }
7338        };
7339
7340        if result.print_stack_strace.is_some() && self.cfg.stacktrace_signal.is_some() {
7341            let _sched = self.lock_rpc_scheduler(false).await;
7342            self.signal_requested_stacktrace(ev.dettid);
7343        }
7344
7345        SchedulerRpcResult::Continue(result)
7346    }
7347
7348    // Ephemeral port range is in file /proc/sys/net/ipv4/ip_local_port_range"
7349    // This function reads from the file and returns the range
7350    // Start of range is at index 0, end of range is at index 1.
7351    fn read_port_range() -> Vec<u16> {
7352        let contents = fs::read_to_string("/proc/sys/net/ipv4/ip_local_port_range")
7353            .expect("File should be present");
7354        let range: Vec<u16> = contents
7355            .split_whitespace()
7356            .filter_map(|number| number.parse().ok())
7357            .collect();
7358        range
7359    }
7360
7361    // Reflect ephemeral port range updated outside of the tracer program internally.
7362    fn update_port_range(&self) {
7363        let range = Self::read_port_range();
7364        self.port_start_range.store(range[0], SeqCst);
7365        self.port_end_range.store(range[1], SeqCst);
7366    }
7367
7368    // AUTONOMOUS-BOT-IMPLEMENTED
7369    // TODO-HUMAN-REVIEW(#663)
7370    // TODO-HUMAN-REVIEW(#869)
7371    /// Register an alarm (delayed signal delivery) with the global scheduler.
7372    async fn recv_register_alarm(
7373        &self,
7374        detpid: DetPid,
7375        caller: RpcIncarnation,
7376        now: LogicalTime,
7377        duration: LogicalTime,
7378        interval: LogicalTime,
7379        sig: SigWrapper,
7380    ) -> SchedulerRpcResult<(LogicalTime, LogicalTime)> {
7381        let RpcIncarnation { dettid, mm } = caller;
7382        let mut sched = self.lock_rpc_scheduler(false).await;
7383        if sched.thread_is_logically_killed(dettid) || !sched.rpc_incarnation_matches(dettid, mm) {
7384            return SchedulerRpcResult::ThreadExited;
7385        }
7386        match sched.replace_real_timer(detpid, dettid, now, duration, interval, alarm_signal(sig)) {
7387            Ok(old) => SchedulerRpcResult::Continue(old),
7388            Err(error) => {
7389                sched.fail_parked(dettid, ProtocolFailure::Timer(error));
7390                SchedulerRpcResult::ThreadExited
7391            }
7392        }
7393    }
7394
7395    // AUTONOMOUS-BOT-IMPLEMENTED
7396    // TODO-HUMAN-REVIEW(#869)
7397    /// Register, re-arm, or disarm a POSIX timer in the global scheduler.
7398    /// `notification` is the timer's signal and, for a `SIGEV_THREAD_ID`
7399    /// timer, the thread it notifies.
7400    async fn recv_register_posix_timer(
7401        &self,
7402        detpid: DetPid,
7403        caller: RpcIncarnation,
7404        timer_id: i32,
7405        deadline: Option<LogicalTime>,
7406        interval: LogicalTime,
7407        notification: PosixTimerNotification,
7408    ) -> SchedulerRpcResult<()> {
7409        let PosixTimerNotification {
7410            sig,
7411            thread,
7412            value,
7413            deleted,
7414        } = notification;
7415        let RpcIncarnation { dettid, mm } = caller;
7416        let mut sched = self.lock_rpc_scheduler(false).await;
7417        if sched.thread_is_logically_killed(dettid) || !sched.rpc_incarnation_matches(dettid, mm) {
7418            return SchedulerRpcResult::ThreadExited;
7419        }
7420        // A process-directed timer prefers the thread that armed it.
7421        let target = match thread {
7422            Some(thread) => SignalTarget::Thread(thread),
7423            None => SignalTarget::Process(dettid),
7424        };
7425        sched.note_posix_timer_notification(detpid, timer_id, sig.raw(), value, deleted);
7426        sched.register_posix_timer(
7427            detpid,
7428            target,
7429            timer_id,
7430            deadline,
7431            interval,
7432            match sig.signal() {
7433                Some(signal) => signal.into(),
7434                None => crate::scheduler::timed_waiters::TimerSignal::RefuseRealTime(sig.raw()),
7435            },
7436        );
7437        SchedulerRpcResult::Continue(())
7438    }
7439}
7440
7441/// What a POSIX timer notifies (`GlobalRequest::RegisterPosixTimer`).
7442#[derive(PartialEq, Debug, Eq, Clone, Copy, Serialize, Deserialize)]
7443pub struct PosixTimerNotification {
7444    /// The signal.
7445    pub sig: SigWrapper,
7446    /// The thread a `SIGEV_THREAD_ID` timer notifies, or `None` for a
7447    /// process-directed one.
7448    pub thread: Option<DetTid>,
7449    /// The `si_value` of its signal.
7450    pub value: u64,
7451    /// The timer is being deleted (`timer_delete`), not set.
7452    pub deleted: bool,
7453}
7454
7455/// Identity and final accounting for an asynchronous scheduler deregistration.
7456#[derive(PartialEq, Debug, Eq, Clone, Serialize, Deserialize)]
7457pub struct ThreadDeregistration {
7458    pub(crate) dettid: DetTid,
7459    pub(crate) detpid: DetPid,
7460    pub(crate) mm: MmId,
7461    /// Carried by the consuming ThreadState owner, independently of detpid's
7462    /// delayed initialization and the parent's scheduler registration RPC.
7463    pub(crate) thread_start_entered: bool,
7464    /// The parent's `BlockingVfork` operation if a `CLONE_VFORK` call created
7465    /// this thread, carried from the owner's `ThreadState::vfork_origin`.
7466    pub(crate) vfork_origin: Option<ExternalOpId>,
7467    pub(crate) timeslice_stats: TimesliceStats,
7468    pub(crate) syscall_count: u64,
7469    /// The thread's syscall entries and outcomes, its nonzero slots only; its
7470    /// total is `syscall_count`.
7471    pub(crate) syscall_histogram: SparseSyscallHistogram,
7472    pub(crate) chaos_epochs: Vec<ChaosEpochTransition>,
7473    /// The thread died of a signal that ends its whole thread group
7474    /// (`signal_ends_thread_group`, `Scheduler::note_signal_killed_group`).
7475    pub(crate) killed_by_group_signal: bool,
7476    /// A descriptor may have escaped before cancellation prevented its
7477    /// owner's next request from publishing the fact.
7478    pub(crate) passed_descriptors_out: bool,
7479}
7480
7481/// A question about, or a record in, the scheduler's ledger of guest-handled
7482/// SIGALRMs (`Scheduler::sigalrm_control`), sent by a guest's Detcore in the
7483/// calling thread's turn before the call reaches the kernel. Signal phase 1
7484/// refuses each of these with EPERM while it could reach a process that
7485/// handles SIGALRM.
7486#[derive(PartialEq, Debug, Eq, Clone, Copy, Serialize, Deserialize)]
7487pub enum SigalrmControl {
7488    /// A send of SIGALRM to the process of this thread.
7489    SendTo(DetTid),
7490    /// A send of SIGALRM to a process the caller cannot name to Detcore: a
7491    /// pidfd with no recorded target (`pidfd_open` records one).
7492    SendToUnnamed,
7493    /// An arming of a kernel producer of SIGALRM that Detcore does not
7494    /// emulate through its alarm path (a POSIX timer, `F_SETSIG` or
7495    /// `PR_SET_PDEATHSIG`), or of a file lease, which makes another process's
7496    /// open wait for the lease break with no scheduler admission. Recorded
7497    /// when allowed, so a later handler installation can be refused.
7498    ArmProducer,
7499    /// An arming of a recurring `ITIMER_REAL` (nonzero interval), which phase 1
7500    /// does not model for a handling process.
7501    ArmRecurringTimer,
7502    /// The caller, in a process that handles SIGALRM, is about to run
7503    /// synchronous I/O on inherited stdio, which may sleep with no scheduler
7504    /// admission. Never refused: a SIGALRM already due is recorded as a
7505    /// determinism loss (design closure 1).
7506    UnadmittedStdioIo,
7507    /// Before a guest SIGALRM handler is installed in the caller's process
7508    /// (phase 1 step I3, design section 6): refused once any guest armed a
7509    /// kernel producer of SIGALRM, anywhere in the run.
7510    InstallHandler,
7511    /// Before the caller's process changes its SIGALRM handler to `SIG_DFL`:
7512    /// refused while the process has a pending entry, which would take the
7513    /// default action inside the callback (design section 6).
7514    HandlerToDefault,
7515    /// Whether the caller's process has a pending entry, for `rt_sigpending`
7516    /// ("refused" is true when it has one). Never changes anything.
7517    PendingEntry,
7518    /// Records the caller's process's SIGALRM disposition and the caller's
7519    /// "SIGALRM virtually blocked" bit, after its runtime changed them inside
7520    /// this turn. Never refused.
7521    Publish {
7522        /// Whether the process's SIGALRM disposition is a guest handler.
7523        handled: bool,
7524        /// Whether the caller's virtual mask blocks SIGALRM.
7525        blocked: bool,
7526    },
7527    /// Records the caller's "SIGALRM virtually blocked" bit, after its runtime
7528    /// changed its mask inside this turn. Never refused.
7529    PublishBlocked(bool),
7530    /// Whether the caller's process has an entry that is due for the caller
7531    /// (its SIGALRM not virtually blocked; "refused" is true when it has one).
7532    /// Never changes anything.
7533    DueEntry,
7534    /// Takes the caller's process's entry for delivery to the caller, if it
7535    /// is still pending, handled and due ("refused" is true when taken), in
7536    /// the turn of the delivery's `InboundSignal` request (step I4, design
7537    /// section 4, step 3).
7538    TakeEntry,
7539    /// Records a determinism loss for the caller's process, here in the
7540    /// coordinator, where the verifier reads it: the runtime could not
7541    /// deliver an entry the caller took (or could not tell the caller's
7542    /// virtual mask). The caller then stops the process. Never refused.
7543    DeliveryLost,
7544    /// Records a determinism loss for the caller's process, here in the
7545    /// coordinator: the caller delivers an entry before a syscall that has
7546    /// not run (the I4 addendum's point c), whose handler cannot be shown the
7547    /// registers Linux would show it (Reverie restores rcx and r11 as the
7548    /// `syscall` instruction already wrote them). The delivery goes ahead and
7549    /// the run continues; verification refuses to compare it. Never refused.
7550    EntryDeliveryUnfaithful,
7551}
7552
7553/// Messages to the global object.
7554///
7555/// Whether a signal send named a process (kill, rt_sigqueueinfo, a pidfd
7556/// without `PIDFD_SIGNAL_THREAD`) or one thread (tkill, tgkill,
7557/// rt_tgsigqueueinfo, a pidfd with `PIDFD_SIGNAL_THREAD`). A SIGKILL ends its
7558/// group either way while the named thread is alive; for an exited thread only
7559/// a process-directed one does (Claude re-check of
7560/// <https://github.com/rrnewton/hermit/pull/4439>, N1).
7561#[derive(PartialEq, Debug, Eq, Clone, Copy, Serialize, Deserialize)]
7562pub enum SignalScope {
7563    /// The send named a process.
7564    Process,
7565    /// The send named one thread.
7566    Thread,
7567}
7568
7569/// The revision of the global RPC, [`GlobalRequest`] and [`GlobalResponse`],
7570/// which [`crate::config_wire_fingerprint`] includes: a runtime embedding
7571/// Detcore and a coordinator built at different revisions refuse each other
7572/// before either decodes the other's messages. A change to either enum
7573/// appends a revision (`config::tests::the_global_rpc_revision_matches_its_definitions`
7574/// fails until it does). That test digests the two enums' bodies and
7575/// `Resources`: a change to another payload type defined elsewhere
7576/// (`CommittedSendWindow`, ...) changes what is on the wire without changing
7577/// the digest, and needs a revision appended by hand.
7578pub(crate) const GLOBAL_RPC_REVISION: u32 = 21;
7579
7580/// A guest thread's descriptor table, which `GlobalRequest::HoldTable`
7581/// carries to the global state, for the terminal receipt of a death by a
7582/// fatal signal to let go of (`GlobalState::release_tables_at_death`). The
7583/// request carries the table itself.
7584///
7585/// The assumption, written down: this works only where the tool runs in
7586/// Hermit's own process and the backend hands each request to the global
7587/// state in memory. No Reverie capability states that. It holds for KVM, the
7588/// one backend whose process signal control is `ToolControlled` (the only
7589/// mode that sends such receipts and names tables,
7590/// `Detcore::name_table_for_death`), whose transports call `receive_rpc` in
7591/// memory. A table never crosses a process boundary: a transport that encodes
7592/// its requests encodes it as nothing and decodes it as no table (`None`),
7593/// never a copy that nothing shares, and the global state answers that
7594/// request with its reason (`GlobalState::hold_table`), by which the sender
7595/// stops the run with the policy-refusal exit at the thread's first naming,
7596/// rather than panic in the transport or let a death go undecided. Two are
7597/// equal when they are the same table, or both carry none.
7598#[derive(Clone)]
7599pub struct HeldTable(Option<Arc<Mutex<crate::tool_local::FileMetadata>>>);
7600
7601impl HeldTable {
7602    /// `table`, to hold.
7603    pub(crate) fn new(table: Arc<Mutex<crate::tool_local::FileMetadata>>) -> Self {
7604        Self(Some(table))
7605    }
7606}
7607
7608impl std::fmt::Debug for HeldTable {
7609    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
7610        f.write_str("HeldTable")
7611    }
7612}
7613
7614impl PartialEq for HeldTable {
7615    fn eq(&self, other: &Self) -> bool {
7616        match (&self.0, &other.0) {
7617            (Some(table), Some(other)) => Arc::ptr_eq(table, other),
7618            (None, None) => true,
7619            _ => false,
7620        }
7621    }
7622}
7623
7624impl Eq for HeldTable {}
7625
7626/// Why a [`HeldTable`] carries no table across a process boundary.
7627const HELD_TABLE_IS_IN_PROCESS: &str = "a guest thread's descriptor table (GlobalRequest::HoldTable) \
7628     is handed over in memory and does not cross a process boundary";
7629
7630/// Encoded as nothing: the table stays in its process.
7631impl Serialize for HeldTable {
7632    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
7633        serializer.serialize_unit()
7634    }
7635}
7636
7637/// Decoded as no table, which the global state refuses by name
7638/// (`GlobalState::hold_table`).
7639impl<'de> Deserialize<'de> for HeldTable {
7640    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
7641        <()>::deserialize(deserializer).map(|()| Self(None))
7642    }
7643}
7644
7645/// This is public only so it can be used in the `GlobalTool` trait.
7646/// It should NOT be used by any client outside of this file.
7647#[derive(PartialEq, Debug, Eq, Clone, Serialize, Deserialize)]
7648#[allow(clippy::enum_variant_names)]
7649pub enum GlobalRequest {
7650    /// `request`, sent with the number of records this guest thread produced
7651    /// for forwarding since its previous counted request, so the coordinator
7652    /// can check they all arrived (`detlog::check_forwarded_count`). Sent only
7653    /// by a thread that has produced some (`counted_request`).
7654    WithForwardedCount(u64, Box<GlobalRequest>),
7655    /// Lock the resources
7656    /// Also contains the `DetPid` of the process containing the thread requesting resources.
7657    RequestResources(Resources, DetPid),
7658    /// Request resources with the actual backend parked-callback capability.
7659    ParkedRequest(Resources, DetPid, ControlCapability),
7660    /// Resume a previously parked request at its actual current callback site.
7661    ResumeParkedRequest {
7662        /// Scheduler-issued one-use continuation ticket.
7663        ticket: ResumeTicket,
7664        /// Actual callback site supplied by the backend on resumption.
7665        current_site: reverie::CallbackSignalSite,
7666    },
7667    /// Finish the observation belonging to one scheduler-owned parked wait.
7668    FinishParkedObservation {
7669        /// Actual pending wait identity.
7670        wait: ContinuationId,
7671        /// Backend observation lease belonging to that wait.
7672        lease: reverie::ParkedObservationLease,
7673        /// Actual callback site at the observation boundary.
7674        site: reverie::CallbackSignalSite,
7675        /// Result of the observation, including an explicit failure.
7676        finish: ObservationFinish,
7677    },
7678    /// Report a terminal violation of the parked continuation protocol.
7679    ParkedProtocolFailure(ProtocolFailure),
7680    /// Report an actual signal removed from this process's pending set.
7681    SignalDequeued {
7682        /// Deterministic process whose signal was dequeued.
7683        detpid: DetPid,
7684        /// Backend identity of the actual signal task.
7685        identity: reverie::SignalTaskIdentity,
7686        /// Exact signal dequeue observation.
7687        dequeue: reverie::SignalDequeue,
7688    },
7689    /// Release the locks
7690    ReleaseResources(Resources),
7691    /// For convenience, release all the resources held by the current TID.
7692    ReleaseAllResources,
7693
7694    // TODO-HUMAN-REVIEW(PR-643): Review this new Detcore global RPC request.
7695    /// Add a syscall to the run-wide unsupported-use summary.
7696    ReportUnsupportedSyscall(String),
7697
7698    /// A guest asked for a disposition of this signal that Detcore answered
7699    /// with success but did not install. The coordinator announces it once per
7700    /// signal per run.
7701    AnnounceSignalHandlerNotInstalled(i32),
7702
7703    /// The fail-closed policy is ending the run, for this reason. Sent just
7704    /// before the Tool image exits with `HERMIT_POLICY_REFUSAL_EXIT`, which on
7705    /// an in-guest backend ends only the guest process: the launcher reads the
7706    /// reason here to report the refusal (`GlobalState::policy_refusal`).
7707    PolicyRefusal(String),
7708
7709    // AUTONOMOUS-BOT-IMPLEMENTED
7710    // TODO-HUMAN-REVIEW(PR-1154): Review the SaBRe exec descriptor-status handoff.
7711    /// Save the caller, address-space identity, and logically blocking descriptors before exec.
7712    /// A successful backend may reload the Tool or preserve and transfer its thread state.
7713    /// Boxed, as is `GlobalResponse::MarkPastFirstExecve`'s: inline, the
7714    /// largest payload sizes every RPC future, and each of the hundreds of
7715    /// handler futures `handle_syscall_event` awaits holds some, which in a
7716    /// debug build overflowed the 2 MiB test thread that runs ptrace record
7717    /// through the library (`record_replay`'s `public_record_*` tests).
7718    PrepareExec(DetPid, MmId, Box<ExecFdState>),
7719
7720    /// Clear the saved transition after an exec attempt returns with an error.
7721    CancelExec(DetPid),
7722
7723    /// Authenticate the preserved exec caller taking over its process leader's TID.
7724    ReconnectExec {
7725        /// Preserved caller before the kernel leader-identity transition.
7726        former: DetTid,
7727        /// Continuing process leader identity.
7728        process: DetPid,
7729    },
7730    /// Acquire the replacement's first turn without newborn state initialization.
7731    ResumeExec(DetPid),
7732    /// Consume a transferred owner if genuine cancellation interrupts local
7733    /// rebinding. The Tool copies `signaled` from the backend's exit status;
7734    /// ordinary exit requires a separately recorded backend failure.
7735    RetireExec {
7736        /// Final transferred thread state and accounting.
7737        thread: ThreadDeregistration,
7738        /// Whether the backend observed signal termination.
7739        signaled: bool,
7740    },
7741
7742    /// Complete a successful image transition, including the initial image, and
7743    /// delete the process's POSIX timer deadlines before the new image runs.
7744    /// The last field is the device and inode of the file the kernel's
7745    /// `/proc/<pid>/exe` link names for the new image, where the tool can
7746    /// read it, which the image's record is checked against
7747    /// (`crate::exec_record`). Before it, in a run that declares a launcher,
7748    /// the record the new image resolved from the path its exec was given,
7749    /// for an exec Detcore did not prepare.
7750    MarkPastFirstExecve(
7751        DetPid,
7752        Option<reverie::SignalTaskIdentity>,
7753        Option<crate::exec_record::ExecRecord>,
7754        Option<(u64, u64)>,
7755    ),
7756
7757    /// The parent is adding a child-thread to the round-robin pool.  Contains the dettid
7758    /// of the new child and it's starting scheduler priority IF it is available to the caller.
7759    /// The only scenario where the Priority will be missing is when we're replaying preemptions.
7760    /// In that case it is the global state that holds the information regarding the new thread's
7761    /// initial priority.
7762    CreateChildThread(
7763        DetTid,
7764        DetPid,
7765        usize,
7766        Option<CloneFlags>,
7767        libc::c_int,
7768        Option<(i32, i32)>,
7769        Option<Priority>,
7770    ),
7771
7772    /// A vfork child registering itself while its parent is blocked inside the
7773    /// kernel. Contains the (real) parent dettid and detpid, the child dettid,
7774    /// the child TID address, the clone flags, and the starting priority (absent
7775    /// only when replaying preemptions).
7776    CreateVforkChildThread(
7777        DetTid,
7778        DetPid,
7779        DetTid,
7780        usize,
7781        CloneFlags,
7782        libc::c_int,
7783        Option<Priority>,
7784    ),
7785
7786    /// New thread is alive and waiting to run its first instruction.  Contains the dettid
7787    /// and detpid of the new child.
7788    StartNewThread(
7789        DetTid,
7790        DetPid,
7791        Option<(i32, i32)>,
7792        Option<reverie::SignalTaskIdentity>,
7793    ),
7794
7795    /// Remove a thread from scheduler data structures, guaranteeing that it will
7796    /// consume no further turns. Carries its final timeslice distribution and any
7797    /// chaos-epoch transitions not yet flushed by a priority-change commit.
7798    DeregisterThread(ThreadDeregistration),
7799
7800    /// Replace the address cleared and woken when the calling thread exits.
7801    /// A zero address disables the exit-time store and wake.
7802    SetChildTidAddress(usize),
7803
7804    /// Record (`Some`) or clear (`None`) the calling thread's own signal mask
7805    /// while Detcore holds it under a private blocking mask, so the scheduler's
7806    /// child-exit classification reads the guest's mask, not Detcore's.
7807    SetSavedGuestSigmask(Option<u64>),
7808
7809    /// The guest's own signal mask that a thread published while Detcore
7810    /// holds it under a private blocking mask, if any
7811    /// (`Scheduler::published_guest_sigmask`), for its `/proc` status.
7812    PublishedGuestSigmask(DetTid),
7813
7814    /// Record (`Some`) or clear (`None`) the set the calling thread waits for
7815    /// while it polls `rt_sigtimedwait`.
7816    SetSigtimedwaitSet(Option<u64>),
7817
7818    /// The inputs to a process-directed signal the calling thread sends to its
7819    /// own process (`Scheduler::own_process_signal_inputs`).
7820    OwnProcessSignalInputs,
7821
7822    /// The thread of a process that takes the process-directed signal the
7823    /// calling thread sends naming a task of it
7824    /// (`Scheduler::process_signal_receiver`).
7825    ProcessSignalReceiver(DetPid, DetTid, i32),
7826
7827    /// The calling thread's forwarded process-directed send to this process,
7828    /// whose receiver the scheduler chose, succeeded
7829    /// (`Scheduler::process_signal_sent`).
7830    ProcessSignalSent(DetPid),
7831
7832    /// The calling thread is about to forward a send of this signal to its own
7833    /// process, naming this task, that Linux gives to it
7834    /// (`Scheduler::own_process_signal_choice`).
7835    OwnProcessSignalChoice(DetTid, i32),
7836
7837    /// The calling thread sends SIGSTKFLT (`Scheduler::record_guest_marker_signal`).
7838    RecordGuestMarkerSignal,
7839
7840    /// Notify scheduler before/after futex action.
7841    /// The final arguments are the initial memory word, the mask, and
7842    /// whether the caller may have passed descriptors outside the guest.
7843    FutexAction(DetTid, FutexAction, FutexID, i32, u32, bool),
7844
7845    /// Translate nondeterministic to deterministic inode, and report what the
7846    /// caller observed of the host mtime (see `ObservedMtime`), how it
7847    /// reached the file (see `InodeSighting`), and, for a procfs entry, its
7848    /// name (see `procfs_inode::ProcfsName` and `InodePool::procfs_key`).
7849    DeterminizeInode(RawInode, ObservedMtime, InodeSighting, Option<ProcfsName>),
7850
7851    /// Translate the inode of every file-backed line of one `/proc/*/maps`
7852    /// read, in line order, repeats included: one numbering request per line.
7853    DeterminizeMappingInodes(Vec<RawInode>),
7854
7855    // AUTONOMOUS-BOT-IMPLEMENTED
7856    // TODO-HUMAN-REVIEW(PR-1056): Deterministic st_dev remapping RPC.
7857    /// Translate a nondeterministic (host-assigned) device number to a
7858    /// deterministic one.
7859    DeterminizeDevice(u64),
7860
7861    /// Translate a host-assigned fdinfo mount ID to a run-local identity. The
7862    /// fallback order is populated only by low-level callers without captured
7863    /// namespace provenance.
7864    DeterminizeMountId(u64, Option<Vec<u64>>),
7865
7866    /// Seed or validate the snapshot's canonical mountinfo identity order
7867    /// (first field), then return the run-local identity of each entry of the
7868    /// observation order (second field: the same raw IDs, rows then parents),
7869    /// numbering unseen entries in that observation order.
7870    ResolveMountinfoIds(Vec<u64>, Vec<u64>),
7871
7872    /// The file behind this host inode lost its last name (see
7873    /// `InodePool::retire`).
7874    RetireInode(RawInode),
7875
7876    /// An `open` found this host inode linked or freshly created (see
7877    /// `InodePool::forget_retired`); for a procfs entry, also its name (see
7878    /// `procfs_inode::ProcfsName` and `InodePool::procfs_key`).
7879    ForgetRetiredInode(RawInode, Option<ProcfsName>),
7880
7881    /// The inode pool's state, for a directory snapshot (see `SnapshotMark`).
7882    DirectorySnapshotMark,
7883
7884    /// `--pipe-wake-latency`: an action of the calling thread readied the
7885    /// pollers of a channel that want this (`Scheduler::ready_channel`).
7886    ReadyChannel(crate::pipe_wake::ChannelId, crate::pipe_wake::Wants),
7887    /// `--pipe-wake-latency`: the calling thread's hand-off is due
7888    /// (`Scheduler::hand_off_turn`).
7889    HandOffTurn,
7890    /// The calling thread is delivering a signal whose default action ends
7891    /// its process (`Scheduler::commit_fatal_delivery`), and whether the
7892    /// kernel carries that action out when it resumes the thread, so the
7893    /// process's other threads may be retired now.
7894    CommitFatalDelivery(bool),
7895
7896    /// Bump mtime
7897    TouchFile(RawInode),
7898
7899    /// Set mtime to a guest-requested time (utimensat and friends).
7900    SetFileMtime(RawInode, LogicalTime),
7901
7902    /// Retrieve global time.
7903    GlobalTimeLowerBound,
7904
7905    /// Record scheduling event in a total order. The last field is what the
7906    /// event adds to the access trace (`--record-access-trace-to`); `None`
7907    /// without one.
7908    // Logging provenance only; never serialized into a schedule artifact.
7909    TraceSchedEvent(
7910        SchedEvent,
7911        DetPid,
7912        bool,
7913        Option<Box<crate::access_trace::AccessPayload>>,
7914    ),
7915
7916    // AUTONOMOUS-BOT-IMPLEMENTED
7917    // TODO-HUMAN-REVIEW(#663)
7918    // TODO-HUMAN-REVIEW(#869)
7919    /// Basically performs an alarm syscall, takes a logical duration.
7920    RegisterAlarm(DetPid, DetTid, LogicalTime, LogicalTime, SigWrapper),
7921
7922    // AUTONOMOUS-BOT-IMPLEMENTED
7923    // TODO-HUMAN-REVIEW(#869)
7924    /// Register, re-arm, or disarm one POSIX timer, or delete it.
7925    RegisterPosixTimer(
7926        DetPid,
7927        DetTid,
7928        i32,
7929        Option<LogicalTime>,
7930        LogicalTime,
7931        PosixTimerNotification,
7932    ),
7933
7934    // AUTONOMOUS-BOT-IMPLEMENTED
7935    // TODO-HUMAN-REVIEW(PR-841): Review logical alarm query RPC.
7936    /// Return the logical time remaining on a process's one-shot alarm.
7937    AlarmRemaining(DetPid),
7938    /// Reconcile the process's `Inactive` ITIMER_REAL after a call that may
7939    /// have dequeued its SIGALRM (`Scheduler::reconcile_alarm`).
7940    ReconcileAlarm(DetPid),
7941    /// A guest's committed SIGALRM send (rule 4, `Scheduler::note_guest_sigalrm`).
7942    NoteGuestSigalrm(crate::scheduler::itimer_coalesce::GuestSigalrmTarget),
7943    /// The caller changed SIGALRM's disposition (rule 5).
7944    NoteSigalrmDisposition,
7945
7946    // AUTONOMOUS-BOT-IMPLEMENTED
7947    // TODO-HUMAN-REVIEW(#663)
7948    /// Deterministically select a logically exited matching child.
7949    ReadyChildWait(DetPid, ChildWaitSpec),
7950    /// Retire a consumed terminal child wait status.
7951    ConsumeChildWait(DetPid, DetPid),
7952    /// Query scheduler-owned process-group membership.
7953    ProcessGroup(DetPid),
7954    /// Apply a successful setpgid transition.
7955    SetProcessGroup(DetPid, DetPid),
7956    /// Apply a successful setsid transition.
7957    CreateSession(DetPid),
7958    /// Query live threads before translating process-directed signal delivery,
7959    /// and the process's exit state (`Scheduler::exited_process_is_zombie`).
7960    ResolveKillTargets(DetPid),
7961    /// Whether a pid or tid that a guest task named (with the thread group it
7962    /// named, if any) is one of Hermit's own tasks; see `hermit_task_named`.
7963    NamesHermitTask(DetTid, Option<i32>, i32),
7964    /// A successful kill(2) queued a physical signal for this sole target,
7965    /// sent with the given scope.
7966    NotifySignalPending(DetTid, SigWrapper, Option<DetPid>, SignalScope),
7967    /// The sender, holding the serial grant in its signal-delivery turn,
7968    /// decided that a default action ends its thread group: its next request,
7969    /// the group `Exit`, is granted before any other thread runs
7970    /// (`Scheduler::hold_turn_for_group_exit`).
7971    HoldTurnForGroupExit(DetTid),
7972    /// A successful SIGKILL whose victims the scheduler is not told one by
7973    /// one: a process-group kill (`Some(group)`), or a pidfd whose process
7974    /// Detcore did not record (`None`).
7975    NotifyUnresolvedSigkill(Option<DetPid>),
7976    /// Liveness of one tid, leader or not; see `thread_is_live`.
7977    ThreadIsLive(DetTid),
7978    /// The pending real-time timer expirations of a process; see
7979    /// `pending_real_time_expirations`.
7980    PendingRealTimeExpirations(DetPid),
7981    /// Discard pending real-time timer expirations of a process (a kernel
7982    /// sigset) whose disposition became `SIG_IGN`.
7983    DiscardPendingRealTimeExpirations(DetPid, u64),
7984    /// A process created a signalfd for these signals (a kernel sigset).
7985    RegisterSignalfd(DetPid, u64),
7986    /// A handler's return in a process restores a mask leaving these
7987    /// real-time signals (a kernel sigset) unblocked.
7988    RecordHandlerRestoreMask(DetPid, u64),
7989    /// Scheduler-owned lifecycle state for a direct child process.
7990    ExactChildWaitState(DetPid, DetPid),
7991
7992    /// The container is shutting down.  Exit the scheduler "thread".
7993    UnrecoverableShutdown,
7994
7995    /// Request a port for an open file description.
7996    RequestPort(OpenFileId),
7997
7998    /// Add a port to the used-port list for an open file description.
7999    AddUsedPort(u16, OpenFileId),
8000
8001    /// Release the port when the last alias of its open file description closes.
8002    ReleasePort(OpenFileId),
8003
8004    /// Deliver robust-futex wakes collected before exit after the backend has
8005    /// confirmed that Linux's physical task cleanup completed.
8006    RobustListWakes(Vec<(DetTid, FutexID)>),
8007    /// One external network record or replay operation.
8008    Network(NetworkRequest),
8009
8010    /// Record that the sending thread, in its syscall number `syscall`, opened
8011    /// `path` and found the host file `identity` (see
8012    /// `detcore_model::host_input`). An observation for `hermit run --verify`
8013    /// only: answered before any clock or scheduler accounting. Appended at
8014    /// the end, so that adding it left every earlier variant's encoded tag
8015    /// unchanged.
8016    RecordHostInput {
8017        /// Guest pathname observed by the original syscall.
8018        path: String,
8019        /// Number of the syscall that observed the input.
8020        syscall: u64,
8021        /// Captured actual host file identity.
8022        identity: HostFileIdentity,
8023        /// Where the tool found the file at the open (the kernel's path for
8024        /// the guest's descriptor, or for an exec's image), for the run's
8025        /// end to read its content (`crate::host_inputs::content_at_end`):
8026        /// present for a regular file or a directory the tool could name.
8027        source: Option<String>,
8028        /// Whether the file is a regular file.
8029        regular: bool,
8030        /// Whether the kernel opened the file for an exec
8031        /// (`detcore_model::host_input::HostInputRecord::exec`).
8032        exec: bool,
8033        /// Whether the descriptor's own path is a kernel object
8034        /// (`detcore_model::host_input::HostInputRecord::kernel_object`).
8035        kernel_object: bool,
8036    },
8037
8038    /// Record that the sending thread rebound `path` in the guest's file
8039    /// namespace (see `detcore_model::host_input::HostMutationRecord`). An
8040    /// observation for `hermit run --verify` only, answered like
8041    /// [`Self::RecordHostInput`]. Appended at the end, so that adding it left
8042    /// every earlier variant's encoded tag unchanged.
8043    RecordHostMutation {
8044        /// Guest pathname whose namespace binding changed.
8045        path: String,
8046    },
8047    /// This thread armed, in its turn, a host-timed source of these signals;
8048    /// see `record_host_timed_signals`. Appended after `RecordHostMutation`,
8049    /// so that adding it left every earlier variant's encoded tag unchanged.
8050    RecordHostTimedSignals(DetTid, HostTimedSignalScope, u64),
8051    /// The signals no gated wait of this thread ends for; see
8052    /// `host_timed_signals`. Appended after `RecordHostTimedSignals`, for the
8053    /// same reason.
8054    HostTimedSignals(DetTid),
8055    /// A control message about a shared open file description, answered
8056    /// before any clock or scheduler accounting (see
8057    /// `crate::shared_open_files`). Appended after `HostTimedSignals`, for the
8058    /// same reason.
8059    SharedOpenFile(OpenFileControl),
8060    /// A SIGALRM ledger control message, answered before any clock or
8061    /// scheduler accounting (see `sigalrm_refuses`). Appended after
8062    /// `SharedOpenFile`, for the same reason.
8063    Sigalrm(SigalrmControl),
8064    /// A child-exit `SIGCHLD` control message
8065    /// (`scheduler::child_exit_sigchld`).
8066    ChildExitSigchld(crate::scheduler::child_exit_sigchld::ChildExitSigchldControl),
8067    /// A determinism loss to record for the run (see
8068    /// `crate::detlog::write_loss_notice`), from a guest-side handler, which
8069    /// may run in a process whose own record verification never reads.
8070    /// Answered before any clock or scheduler accounting. Appended after
8071    /// `ChildExitSigchld`, for the same reason.
8072    RecordDeterminismLoss(String),
8073    /// Mirror a successful `prctl(PR_SET_CHILD_SUBREAPER)` by this process,
8074    /// in the caller's turn (`set_child_subreaper`). Appended after
8075    /// `RecordDeterminismLoss`, for the same reason.
8076    SetChildSubreaper(DetPid, bool),
8077    /// The process Detcore registered `DetTid` in: its own thread group's
8078    /// leader in the thread tree. Unlike the backend's `Guest::pid`, it names
8079    /// a raw clone child with a non-SIGCHLD exit signal as its own process.
8080    /// Appended after `SetChildSubreaper`, for the same reason.
8081    RegisteredProcessOf(DetTid),
8082    /// Mirror a successful `rt_sigaction(SIGCHLD, ...)` by this process:
8083    /// whether the action is `SIG_IGN`, and whether it has `SA_NOCLDWAIT`
8084    /// (`set_sigchld_disposition`). Appended after `RegisteredProcessOf`, for
8085    /// the same reason.
8086    SetSigchldDisposition(DetPid, bool, bool),
8087    /// The process a signal naming this id goes to
8088    /// (`Scheduler::signal_target_process`). Appended after
8089    /// `SetSigchldDisposition`, for the same reason.
8090    SignalTargetProcess(DetPid),
8091    /// Reserve the victims of a SIGKILL the sender is about to send to the
8092    /// process of the given thread, before the physical send
8093    /// (`scheduler::sigkill_retirement`). Appended after
8094    /// `RegisteredProcessOf`, for the same reason.
8095    ReserveSigkill(DetTid, DetTid, u32),
8096    /// Commit (true: the send succeeded) or unwind a SIGKILL reservation.
8097    /// Appended after `ReserveSigkill`, for the same reason.
8098    CommitSigkill(u64, bool),
8099    /// Reserve the victims of a `kill(-pgid, SIGKILL)`: every live process in
8100    /// the recorded process group. Appended after `CommitSigkill`, for the
8101    /// same reason.
8102    ReserveSigkillGroup(DetTid, DetPid, u32),
8103    /// The thread a `SIGEV_THREAD_ID` timer of this process names by this id
8104    /// (`Scheduler::posix_timer_thread`). Appended after
8105    /// `ReserveSigkillGroup`, for the same reason.
8106    PosixTimerThread(DetPid, i32),
8107    /// A disposition change in this process flushed this signal from every
8108    /// thread (`Scheduler::forget_thread_timer_copies`). Appended after
8109    /// `PosixTimerThread`, for the same reason.
8110    ForgetThreadTimerCopies(DetPid, i32),
8111    /// What a positive pid the given nested thread names, in its own PID
8112    /// namespace, names (`PidNamespaces::resolve_for`). Appended after
8113    /// `ReserveSigkillGroup`, for the same reason.
8114    GuestPid(DetTid, i32),
8115    /// The pid the given nested thread sees for a task
8116    /// (`PidNamespaces::localize_for`). Appended after `GuestPid`, for the
8117    /// same reason.
8118    LocalPid(DetTid, DetTid),
8119    /// The POSIX timer copies a successful exec of this process flushes
8120    /// (`Scheduler::take_exec_timer_flush`), asked by its surviving thread.
8121    /// Appended after `LocalPid`, for the same reason.
8122    ExecTimerFlush(DetPid),
8123    /// The calling thread-group leader is about to complete its own `exit`
8124    /// while its group lives on (`Scheduler::retire_exited_leader`).
8125    /// Appended after `ExecTimerFlush`, for the same reason.
8126    RetireExitedLeader,
8127    /// A discarded-at-send record message (`scheduler::discarded_at_send`),
8128    /// answered before any clock or scheduler accounting. Appended after
8129    /// `RetireExitedLeader`, for the same reason.
8130    DiscardedSignal(crate::scheduler::discarded_at_send::DiscardedSignalControl),
8131    /// A `close`, `close_range`, `dup2` or `dup3` released or replaced the
8132    /// descriptors `first..=last` of the caller's table, named by its identity
8133    /// (`Scheduler::note_descriptors_replaced`). Appended after
8134    /// `DiscardedSignal`, for the same reason.
8135    DescriptorsReplaced(detcore_model::fd::FilesId, u32, u32),
8136    /// Whether a task of this run had this id and it is not a zombie's pid
8137    /// (`thread_id_refused`). Appended after `DescriptorsReplaced`, for the
8138    /// same reason.
8139    ThreadIdRefused(DetTid),
8140    /// The processes `/proc` lists, and the threads the `task` directory of
8141    /// the given process lists (`Scheduler::procfs_listing`). Appended after
8142    /// `ThreadIdRefused`, for the same reason.
8143    ProcfsListing(Option<DetPid>),
8144    /// Arm (`Some`, 0 disarms) or read (`None`) the sender's parent-death
8145    /// signal that Detcore sends itself (`parent_death_signal`). Appended
8146    /// after `ProcfsListing`, for the same reason.
8147    ParentDeathSignal(Option<i32>),
8148
8149    /// Neutral controls appended without changing any existing wire tag.
8150    PrepareNativeExec(native_exec::PrepareNativeExec),
8151    /// Seal the exact versioned runtime bytes before one-use descriptor provision.
8152    SealNativeExecStats {
8153        /// Retained prepared transaction ticket.
8154        ticket: native_exec::ExecTicket,
8155        /// SHA-256 of the exact runtime envelope bytes.
8156        runtime_digest: [u8; 32],
8157        /// Bounded actual runtime envelope, not host-decoded Tool state.
8158        runtime: Vec<u8>,
8159    },
8160    /// Last old-image control after the sealed empty forwarding cut.
8161    IssueNativeExec {
8162        /// Successfully provisioned retained transaction ticket.
8163        ticket: native_exec::ExecTicket,
8164        /// Independently computed composite Tool/runtime digest.
8165        state_digest: [u8; 32],
8166    },
8167    /// Invalidate a pre-redemption ticket; ordinary CancelExec still follows.
8168    CancelNativeExec {
8169        /// Prepared or Issued ticket whose physical exec did not succeed.
8170        ticket: native_exec::ExecTicket,
8171    },
8172    /// Acknowledge the real new-mm lifecycle after MarkPastFirstExecve.
8173    CompleteNativeExec {
8174        /// Consumed successor ticket after real lifecycle and frontend checks.
8175        ticket: native_exec::ExecTicket,
8176    },
8177    /// Whether a tid names a thread-group leader retired at its own exit
8178    /// while its group lives on (`names_a_retired_exited_leader`). Appended
8179    /// last, for the same reason as the controls above.
8180    NamesARetiredExitedLeader(DetTid),
8181    /// The kernel's numbers for guest process or thread numbers (`true`), or
8182    /// the guest's for kernel numbers (`false`), as `/proc` spells them
8183    /// (`Scheduler::procfs_ids`). Appended last, for the same reason.
8184    ProcfsIds(bool, Vec<i32>),
8185    /// The process group (`false`) or session (`true`) of the task with an id,
8186    /// as `getpgid` and `getsid` answer it (`Scheduler::group_or_session_of`).
8187    /// Appended last, for the same reason.
8188    GroupOrSession(DetPid, bool),
8189    /// Linux's answer to `setsid()` by a process
8190    /// (`Scheduler::setsid_decision`), deciding nothing yet. Appended last.
8191    SetsidDecision(DetPid),
8192    /// Linux's answer to `setpgid(pid, pgid)` by a process
8193    /// (`Scheduler::setpgid_decision`), deciding nothing yet. Appended last.
8194    SetpgidDecision(DetPid, i32, i32),
8195    /// The image a guest process executes, as Detcore recorded it at the
8196    /// process's exec (`crate::exec_record`), for its `/proc/<pid>/exe`
8197    /// link. Appended last.
8198    ProcessImage(DetPid),
8199    /// Whether the task a per-process `/proc` file names exists, whether it
8200    /// has exited unreaped, and the incarnation of its numbers
8201    /// (`Scheduler::procfs_task`). Appended last.
8202    ProcfsTask(DetTid, Option<DetTid>),
8203    /// Change or read the state Detcore keeps on a rendered per-process
8204    /// `/proc` file's inode (`InodePool::rendered_file_state`). Appended last.
8205    RenderedFileState(u64, crate::procfs::listing::RenderedFileChange),
8206    /// The sender's process's signal dispositions as Detcore's copy holds
8207    /// them, or `None` to withdraw them, with the publication's generation
8208    /// (`Scheduler::publish_signal_dispositions`). Answered before any clock
8209    /// or scheduler accounting. Appended last.
8210    PublishSignalDispositions(
8211        u64,
8212        Option<crate::syscalls::signal_shadow::CopiedDispositions>,
8213    ),
8214    /// A Detcore warning or error from inside the guest, which the coordinator
8215    /// emits at the run's own log level (`crate::guest_log`), or none, and how
8216    /// many of the sender's process could not be sent before it. Answered
8217    /// before forwarded records are drained and before any clock or scheduler
8218    /// accounting. Appended last.
8219    GuestLog(Option<crate::guest_log::GuestLogRecord>, u64),
8220    /// The sender's mask as Detcore's copy holds it, or `None` to withdraw
8221    /// it (`Scheduler::publish_signal_mask`). Answered before any clock or
8222    /// scheduler accounting. Appended last.
8223    PublishSignalMask(Option<u64>),
8224    /// The auxiliary vector the sender's process's new image found on its
8225    /// initial stack (`ProcessAuxvTable::record`), and the image's argument
8226    /// and environment areas there, if Detcore found them
8227    /// (`ProcessAuxvTable::record_cmdline`). Appended last.
8228    RecordProcessAuxv(Vec<u8>, Option<crate::procfs::CmdlineArea>),
8229    /// A process's image's auxiliary vector (`ProcessAuxvTable::current`), for
8230    /// `/proc/<pid>/auxv`. Appended last.
8231    ProcessAuxv(DetPid),
8232    /// The sender's process's new image's `/proc/<pid>/auxv` is left to the
8233    /// kernel beneath (`ProcessAuxvTable::forget`), for the reason given.
8234    /// Answers whether this is the run's first such image for that reason,
8235    /// which the sender warns of. Also the image's argument and environment
8236    /// areas on its initial stack, if Detcore found them
8237    /// (`ProcessAuxvTable::record_cmdline`). Appended last.
8238    AuxvFromKernel(AuxvFromKernel, Option<crate::procfs::CmdlineArea>),
8239    /// The signals whose new action an in-guest runtime refused, bit `n - 1`
8240    /// for signal `n`, and whether it refused an alternate signal stack
8241    /// (`crate::refused_signal_controls`). Answered before the forwarded-record
8242    /// drain and any clock or scheduler accounting. Appended last.
8243    AnnounceRefusedSignalControls(u64, bool),
8244    /// A fixed reply from an external controller while the sender retains its
8245    /// serial grant. The runtime fact and the scheduler jointly establish that
8246    /// no guest can observe the temporary procfs descriptor. Appended last.
8247    CheckBrkMappings(
8248        crate::syscalls::memory::BrkMappingCheck,
8249        reverie::ControllerCaptureFact,
8250    ),
8251    /// Current image's native ownership facts, authenticated by the RPC MmId.
8252    /// Answered without granting another task or advancing logical time.
8253    PublishRuntimePrivateMemory(reverie::RuntimePrivateMemory),
8254    /// The sender's process changed its root with `chroot`
8255    /// (`ProcessAuxvTable::mark_changed_root`). Appended last.
8256    MarkChangedRoot,
8257    /// Whether the process a number names has changed its root
8258    /// (`ProcessAuxvTable::root_changed`). Appended last.
8259    RootChanged(DetPid),
8260    /// The OFD and flock locks this open file description holds, as its
8261    /// fdinfo showed them after a call through one of the sender's
8262    /// descriptors, and how many slots of the sender's descriptor table name
8263    /// it (`procfs::locks::LockLedger::note_locks`), or why that fdinfo
8264    /// could not be read (`procfs::locks::LockLedger::note_unknown`).
8265    /// Appended last.
8266    DescriptionLocks(
8267        OpenFileId,
8268        Result<Vec<crate::procfs::locks::HeldLock>, String>,
8269        u32,
8270    ),
8271    /// How many slots of the sender's descriptor table still name this
8272    /// description after a close or replacement there
8273    /// (`procfs::locks::LockLedger::release`). Appended last.
8274    TableReleased(OpenFileId, u32),
8275    /// Which rows of a `/proc/locks` snapshot the guest owns
8276    /// (`procfs::locks::guest_lock_rows`). Appended last.
8277    GuestLockRows(Vec<crate::procfs::locks::LockRowFacts>),
8278    /// The descriptions whose locks Detcore read that the sender's process's
8279    /// table still names after its successful exec, with their slot counts
8280    /// (`procfs::locks::LockLedger::exec_kept`). Appended last.
8281    TableExec(Vec<(OpenFileId, u32)>),
8282    /// A lock or unlock went through a descriptor whose description the
8283    /// ledger may know under another identity, on the file with this inode
8284    /// number, or on an unknown one
8285    /// (`procfs::locks::LockLedger::alias_inode`). Appended last.
8286    AliasedInode(Option<String>),
8287    /// The calling thread's host-timed signals and the `SIGCHLD` the schedule
8288    /// recorded in its private queue (`Scheduler::host_timed_and_private_sigchld`).
8289    /// Appended last.
8290    HostTimedAndPrivateSigchld(DetTid),
8291    /// A lock or unlock through a descriptor whose description the ledger
8292    /// may know under another identity, whose fdinfo could not be read for
8293    /// this reason (`procfs::locks::LockLedger::refuse`). Appended last.
8294    LockLedgerRefused(String),
8295    /// Contained mapping capture; answered without changing the serial grant.
8296    MappingCapture(mapping_capture::Request),
8297    /// The in-guest delivery protocol cannot go on in this run, for this
8298    /// reason: the whole refusal line, conventionally `hermit refused to
8299    /// continue the run (NAME): ...` (A5 design D49). The coordinator ends the
8300    /// whole run on it (`scheduler::exit_on_scheduler_refusal`): it writes the
8301    /// line and exits with `HERMIT_POLICY_REFUSAL_EXIT`, and where the
8302    /// scheduler runs as the init of the guest's PID namespace, the kernel
8303    /// then kills every guest process. Unlike `PolicyRefusal`, which records a
8304    /// reason while only the refusing Tool image exits, no guest process runs
8305    /// on after it, and it is never answered. Filed only under
8306    /// `Config::in_guest_signal_delivery`, whose gate requires that
8307    /// ownership. Appended last.
8308    StoppingRefusal(String),
8309    /// The process a `/proc/<pid>/cmdline` names (a thread's number names its
8310    /// process), and its argument and environment areas
8311    /// (`ProcessAuxvTable::current_cmdline`). Appended last.
8312    ProcessCmdline(DetPid),
8313    /// What the scheduler holds for this thread if its latest committed
8314    /// request is a kernel-run call whose continuation turn has not been
8315    /// granted (A5 design D44, `Scheduler::kernel_run_block`), asked by a
8316    /// sender in its turn under the in-guest delivery gate. Appended last.
8317    KernelRunTarget(DetTid),
8318    /// Error-only notice, not a resource or a scheduling turn. Appended last.
8319    CallbackCheckpointMismatch {
8320        /// The first unconsumed checkpoint, as sent on thread start.
8321        checkpoint: crate::preemptions::callback_history::Checkpoint,
8322        /// A semantic endpoint/phase mismatch rather than time/RCB drift.
8323        hard: bool,
8324        /// Independently observed actual callback coordinates.
8325        actual: crate::preemptions::callback_history::Origin,
8326    },
8327    /// What the process table says about the task a per-process `stat` file
8328    /// names (`Scheduler::task_stat_facts`): the top-level number and the
8329    /// thread below `task/`. Appended last.
8330    TaskStatFacts(DetTid, Option<DetTid>),
8331    /// Record a task's name (`Scheduler::set_task_comm`): the name a new
8332    /// image's `AT_EXECFN` gives it at its exec, or the one `PR_SET_NAME`
8333    /// set, in the task's own turn. Appended last.
8334    SetTaskComm(DetTid, crate::procfs::task_stat::TaskComm),
8335    /// Record the guest-visible parent of a process whose real parent is
8336    /// outside the guest's tree, its own `getppid` answer when it first
8337    /// opens or lists its rendered `stat`
8338    /// (`Scheduler::record_external_parent`). Appended last.
8339    RecordExternalParent(DetPid, i32),
8340    /// Record a process's virtual soft `RLIMIT_RSS` after a `setrlimit` or
8341    /// `prlimit64` changed it (`Scheduler::set_rss_limit`). Appended last.
8342    SetRssLimit(DetPid, u64),
8343    /// A5 step 3c.1e A, IR-23(a): the sender's masked wait publishes its
8344    /// temporary mask with D37's held masks before its first expired-timerfd
8345    /// publication, or withdraws that publication as a call that filed no
8346    /// later request returns (`Scheduler::temporary_mask_control`). Answered
8347    /// in the sender's own turn with no scheduling decision; filed only under
8348    /// `Config::in_guest_signal_delivery`. Appended last.
8349    PublishTemporaryMask(crate::scheduler::guest_mask_publication::TemporaryMaskControl),
8350    /// A5 step 3c.1e A, IR-15: whether this thread's process is the init of a
8351    /// PID namespace it created with `CLONE_NEWPID`
8352    /// (`ThreadTree::is_pid_namespace_init`), asked under
8353    /// `Config::in_guest_signal_delivery` before a two-phase delivery's
8354    /// announcement and a predicted death. Appended last.
8355    InPidNamespaceInit(DetTid),
8356    /// The virtual cookie for each host inotify rename cookie of one read, in
8357    /// order (`crate::syscalls::inotify`). Answered in the sender's own turn
8358    /// with no scheduling decision. Appended last.
8359    VirtualInotifyCookies(Vec<u32>),
8360    /// Register a placeholder directory a guest opened, and the open file
8361    /// description it opened it in (`InodePool::register_placeholder`).
8362    /// Appended last.
8363    RegisterPlaceholder(
8364        crate::procfs::fd_directory::PlaceholderDirectory,
8365        OpenFileId,
8366    ),
8367    /// The placeholder directory a kernel position tags
8368    /// (`InodePool::placeholder`). Appended last.
8369    Placeholder(u64),
8370    /// A `flock` through a placeholder descriptor
8371    /// (`procfs::locks::LockLedger::placeholder_flock`). Appended last.
8372    PlaceholderFlock(crate::procfs::locks::PlaceholderFlock),
8373    /// Set (`Some`) or read the write hint of a placeholder directory's
8374    /// inode (`procfs::locks::LockLedger::placeholder_write_hint`). Appended
8375    /// last.
8376    PlaceholderWriteHint(crate::procfs::fd_directory::PlaceholderLockKey, Option<u64>),
8377    /// A change to the copies of placeholder descriptions in flight over
8378    /// `SCM_RIGHTS` (`procfs::locks::LockLedger::placeholder_copies`).
8379    /// Appended last.
8380    PlaceholderCopies(crate::procfs::locks::PlaceholderCopies),
8381    /// A live task's CPU affinity under `--virtual-cpus`
8382    /// (`Scheduler::cpu_affinity`). Appended last.
8383    CpuAffinity(DetTid),
8384    /// Record a live task's CPU affinity under `--virtual-cpus`, in the turn
8385    /// of the `sched_setaffinity` that set it (`Scheduler::set_cpu_affinity`).
8386    /// Appended last.
8387    SetCpuAffinity(DetTid, crate::cpu_set::CpuSet),
8388    /// aarch64: a report of the exclusive pairs in executable mappings, or
8389    /// the root guest task's first image, where the run's store-exclusive
8390    /// counters start (`crate::store_exclusive`). It carries no time and changes no scheduler
8391    /// state, so it is answered before any grant or clock accounting.
8392    /// Appended last.
8393    #[cfg(target_arch = "aarch64")]
8394    StoreExclusive(crate::store_exclusive::Report),
8395    /// The threads the send of SIGKILL reservation `token` kills, asked by its
8396    /// sender before the send so it can record their robust lists
8397    /// (`Scheduler::sigkill_dying_threads`). Appended last.
8398    SigkillDyingThreads(u64),
8399    /// The robust lists the sender of SIGKILL reservation `token` recorded
8400    /// before its send, for the threads it kills
8401    /// (`Scheduler::record_sigkill_robust_lists`). Appended last.
8402    RecordSigkillRobustLists(
8403        u64,
8404        Vec<(
8405            DetTid,
8406            MmId,
8407            crate::syscalls::robust_list::RobustListSnapshot,
8408        )>,
8409    ),
8410    /// A thread a SIGKILL ended takes the robust list its sender recorded for
8411    /// it, in its exit callback (`Scheduler::take_sigkill_robust_list`).
8412    /// Answered before clock accounting. Appended last.
8413    TakeSigkillRobustList,
8414    /// Take the ID for the task a clone-family call is about to create, when
8415    /// Detcore chooses the IDs (`GuestIds::Chosen`), in the caller's turn.
8416    /// Appended last.
8417    NextGuestId,
8418    /// Report a host-ID outcome or a conservative pre-vfork attempt while
8419    /// Detcore chooses IDs (`GlobalState::host_id_tasks`). Appended last.
8420    GaveHostId,
8421    /// A faithful thread could not file its next change point. The receiver
8422    /// derives identity/source/output coordinates, acknowledges within the
8423    /// existing grant, and does not consume time or grant state. Appended last.
8424    ReplayDeparture {
8425        /// The previous change point filed by this thread.
8426        previous: LogicalTime,
8427        /// The attempted time, at or before the previous filing.
8428        observed: LogicalTime,
8429        /// The thread's actual slice counter at detection.
8430        slice: u64,
8431    },
8432    /// As `DescriptionLocks`, with the locks known, after a call of the
8433    /// sender's that can take a lock through the description, and the locks
8434    /// the call proved it took, which are the sender's process's, and what
8435    /// else names its open file description: possibly another identity (one
8436    /// recorded on reception as a description of its own), or the other
8437    /// standard streams of its alias group, with their slots in the sender's
8438    /// table (`procfs::locks::LedgerIdentity`,
8439    /// `procfs::locks::LockLedger::note_locks_taken`). Appended last.
8440    DescriptionLocksTaken(
8441        OpenFileId,
8442        Vec<crate::procfs::locks::HeldLock>,
8443        Vec<crate::procfs::locks::HeldLock>,
8444        u32,
8445        crate::procfs::locks::LedgerIdentity,
8446    ),
8447    /// The owner each of these locks, held through this description, shows
8448    /// on a lock line of a rendered fdinfo
8449    /// (`procfs::locks::LockLedger::owners`). Appended last.
8450    LockOwners(OpenFileId, Vec<crate::procfs::locks::HeldLock>),
8451    /// The `/proc/locks` rows Detcore renders from the ledger where the
8452    /// guest's procfs is not the kernel's
8453    /// (`procfs::locks::LockLedger::rendered_rows`). Appended last.
8454    LedgerLockRows,
8455    /// As `DescriptionLocks`, with the locks known, for a description of the
8456    /// sender's table that may be a second identity of another, read after a
8457    /// call of its own that took no lock: one the sender recorded on reception
8458    /// as a description of its own
8459    /// (`procfs::locks::LockLedger::note_second_identity_locks`). Appended
8460    /// last.
8461    SecondIdentityLocks(OpenFileId, Vec<crate::procfs::locks::HeldLock>, u32),
8462    /// The descriptor table the sender holds, at its start or after an exec
8463    /// gave it a new one, for the terminal receipt of a death by a fatal
8464    /// signal to let go of (`GlobalState::held_tables`,
8465    /// `Detcore::name_table_for_death`). Sent only where the tool runs in this
8466    /// process (`HeldTable`). Answered at once, outside the scheduler.
8467    /// Appended last.
8468    HoldTable(HeldTable),
8469    /// A guest's record-lock request (`F_SETLK`, `F_SETLKW`) succeeded, where
8470    /// the ledger takes the backend's fact, and the backend holds the lock it
8471    /// took as an OFD lock, on these files (`MAJOR:MINOR:INODE`), or on a file
8472    /// the fact could not name (`None`)
8473    /// (`procfs::locks::LockLedger::note_record_locks_held_as_ofd`). Appended
8474    /// last.
8475    RecordLocksHeldAsOfd(Option<Vec<String>>),
8476    /// The host's `/proc/diskstats` as the run first read it, which every
8477    /// `/proc/diskstats` Detcore renders starts from
8478    /// (`GlobalState::host_diskstats`). A fact of the host, not of the
8479    /// guest's state: answered before any clock accounting, with no time.
8480    /// Appended last.
8481    HostDiskstats,
8482    /// Where the user namespace of the process of task `.1` is from that of
8483    /// the process of thread `.0`, and whether the namespace whose owner
8484    /// Linux's rule asks about there may be owned by an effective user ID
8485    /// other than the real user ID every guest task has
8486    /// (`scheduler::user_namespaces`). Appended last.
8487    UserNamespaceRelation(DetTid, DetTid),
8488    /// The clone that made process `.0`, which made a user namespace, was a
8489    /// creator's whose effective user ID was not its real user ID, read as
8490    /// the overflow ID, or could not be read; sent once the clone succeeded,
8491    /// in the turn that registered the process
8492    /// (`UserNamespaces::note_owner_may_differ`). Appended last.
8493    UserNamespaceOwnerMayDiffer(DetTid),
8494    /// The record sequence of the open file description `.0`, a per-process
8495    /// file whose description a fork copied where the tool runs in each guest
8496    /// process, as the last read, positioned read or seek of it, in whichever
8497    /// process, left it (`Detcore::take_procfs_sequence`); none before the
8498    /// first. Appended last.
8499    ProcfsSequence(OpenFileId),
8500    /// Keep `.1` as the record sequence of the open file description `.0`
8501    /// after a read, positioned read or seek of it
8502    /// (`Detcore::store_procfs_sequence`). Appended last.
8503    StoreProcfsSequence(OpenFileId, crate::procfs::ProcfsSequence),
8504}
8505
8506/// Responses from the global object
8507#[allow(missing_docs, clippy::unit_arg)]
8508#[derive(PartialEq, Debug, Eq, Clone, Serialize, Deserialize)]
8509pub enum GlobalResponse {
8510    /// The scheduler permanently removed this raw TID. Guest-side RPC handling retires
8511    /// this task without cancelling a live group-exit issuer or resuming the operation.
8512    ThreadExited,
8513    /// A reply, with the virtual time that remained before the earliest
8514    /// pending deadline when the thread was granted the turn it resumes, or
8515    /// armed a timer (`Scheduler::deadline_remaining_from`). Unwrapped by
8516    /// `send_and_update_time`, which caps the thread's slice there.
8517    WithDeadline(Duration, Box<GlobalResponse>),
8518    /// A reply to a request while which the thread was granted a turn
8519    /// (`Scheduler::take_grant`): other threads may have run since it was
8520    /// sent. Unwrapped by `send_and_update_time`, which notes the hand-off
8521    /// (`ThreadState::note_scheduler_handoff`).
8522    Granted(Box<GlobalResponse>),
8523    RequestResources(ResumeStatus),
8524    ParkedRequest(ResourceReply),
8525    ResumeParkedRequest(ResourceReply),
8526    FinishParkedObservation(Result<FinishAck, ProtocolFailure>),
8527    SignalDequeued {
8528        ack: Result<DequeueAck, TimerFailure>,
8529        terminal: bool,
8530    },
8531    ReleaseResources(()),
8532    ReleaseAllResources(()),
8533    // TODO-HUMAN-REVIEW(PR-643): Review this new Detcore global RPC response.
8534    ReportUnsupportedSyscall(()),
8535    AnnounceSignalHandlerNotInstalled(()),
8536    PolicyRefusal(()),
8537    PrepareExec(()),
8538    CancelExec(()),
8539    ReconnectExec(bool),
8540    ResumeExec(ResumeStatus, Option<LogicalTime>),
8541    RetireExec(bool),
8542    MarkPastFirstExecve(Box<ExecFdState>),
8543    CreateChildThread(Option<MmId>),
8544    /// Includes optional preemption points for the new thread.
8545    StartNewThread(Option<ThreadHistory>),
8546    DeregisterThread(()),
8547    SetChildTidAddress(()),
8548    SetSavedGuestSigmask(()),
8549    PublishedGuestSigmask(Option<u64>),
8550    SetSigtimedwaitSet(()),
8551    OwnProcessSignalInputs(OwnProcessSignalInputs),
8552    ProcessSignalReceiver(ProcessSignalReceiver),
8553    ProcessSignalSent(()),
8554    OwnProcessSignalChoice(()),
8555    RecordGuestMarkerSignal(()),
8556    FutexAction(Option<SchedValue>),
8557    /// Return the mtime as well:
8558    DeterminizeInode((DetInode, LogicalTime)),
8559    DeterminizeMappingInodes(Vec<DetInode>),
8560    // AUTONOMOUS-BOT-IMPLEMENTED
8561    // TODO-HUMAN-REVIEW(PR-1056): Deterministic st_dev remapping RPC.
8562    DeterminizeDevice(u64),
8563    DeterminizeMountId(Option<u64>),
8564    ResolveMountinfoIds(Option<Vec<u64>>),
8565    RetireInode(()),
8566    ForgetRetiredInode(()),
8567    DirectorySnapshotMark(SnapshotMark),
8568    ReadyChannel(crate::pipe_wake::ReadyOutcome),
8569    HandOffTurn(bool),
8570    /// Whether the process's other threads were retired.
8571    CommitFatalDelivery(bool),
8572    TouchFile(()),
8573    SetFileMtime(()),
8574    GlobalTimeLowerBound(LogicalTime),
8575    TraceSchedEvent(TraceSchedEventResponse),
8576    // AUTONOMOUS-BOT-IMPLEMENTED
8577    // TODO-HUMAN-REVIEW(#663)
8578    // TODO-HUMAN-REVIEW(#869)
8579    RegisterAlarm((LogicalTime, LogicalTime)),
8580    // AUTONOMOUS-BOT-IMPLEMENTED
8581    // TODO-HUMAN-REVIEW(#869)
8582    RegisterPosixTimer(()),
8583    // AUTONOMOUS-BOT-IMPLEMENTED
8584    // TODO-HUMAN-REVIEW(PR-841): Review logical alarm query RPC.
8585    ReadyChildWait((Option<DetPid>, bool)),
8586    ConsumeChildWait(bool),
8587    ProcessGroup(Option<DetPid>),
8588    SetProcessGroup(bool),
8589    CreateSession(bool),
8590    AlarmRemaining(ItimerSnapshot),
8591    ReconcileAlarm,
8592    NoteGuestSigalrm,
8593    // AUTONOMOUS-BOT-IMPLEMENTED
8594    // TODO-HUMAN-REVIEW(#663)
8595    ResolveKillTargets(Vec<DetTid>, Option<bool>),
8596    NamesHermitTask(bool),
8597    NotifySignalPending(()),
8598    HoldTurnForGroupExit(bool),
8599    NotifyUnresolvedSigkill(()),
8600    ThreadIsLive(bool),
8601    PendingRealTimeExpirations(u64),
8602    DiscardPendingRealTimeExpirations(()),
8603    RegisterSignalfd(()),
8604    RecordHandlerRestoreMask(()),
8605    ExactChildWaitState(ExactChildWaitState),
8606    // TODO: use void_send_rpc, and remove this bogus response:
8607    UnrecoverableShutdown(()),
8608
8609    RequestPort(u16),
8610    AddUsedPort,
8611    ReleasePort(Option<u16>),
8612    PortFull,
8613    RobustListWakes(Vec<u64>),
8614    Network(Result<NetworkReply, NetworkEngineError>),
8615    /// Appended at the end, so that adding it left every earlier variant's
8616    /// tag unchanged.
8617    RecordHostInput(()),
8618    /// Appended at the end, so that adding it left every earlier variant's
8619    /// tag unchanged.
8620    RecordHostMutation(()),
8621    /// Appended after `RecordHostMutation`, so that adding it left every
8622    /// earlier variant's tag unchanged.
8623    HostTimedSignals(u64),
8624    /// Appended after `HostTimedSignals`, for the same reason.
8625    SharedOpenFile(Result<OpenFileControlReply, SharedOpenFileError>),
8626    /// Whether the SIGALRM ledger refused the control message. Appended after
8627    /// `SharedOpenFile`, for the same reason.
8628    Sigalrm(bool),
8629    ChildExitSigchld(crate::scheduler::child_exit_sigchld::ChildExitSigchldAnswer),
8630    /// Appended after `ChildExitSigchld`, for the same reason.
8631    RecordDeterminismLoss(()),
8632    /// Appended after `RecordDeterminismLoss`, for the same reason.
8633    SetChildSubreaper(()),
8634    /// Appended after `SetChildSubreaper`, for the same reason.
8635    RegisteredProcessOf(Option<DetPid>),
8636    /// Appended after `RegisteredProcessOf`, for the same reason.
8637    SetSigchldDisposition(()),
8638    /// Appended after `SetSigchldDisposition`, for the same reason.
8639    SignalTargetProcess(Option<DetPid>),
8640    /// The reservation token, or none when the send keeps the ordinary path.
8641    /// Appended after `RegisteredProcessOf`, for the same reason.
8642    ReserveSigkill(Option<crate::scheduler::sigkill_retirement::SigkillReservation>),
8643    /// Appended after `ReserveSigkill`, for the same reason.
8644    CommitSigkill(()),
8645    /// The sender is a committed SIGKILL victim
8646    /// (`Scheduler::is_unaccounted_sigkill_victim`): its request is never
8647    /// answered, and the client waits for the kernel to end it. Unlike
8648    /// `ThreadExited`, it never tail-injects an exit. Appended after
8649    /// `CommitSigkill`, for the same reason.
8650    Retired,
8651    /// Appended after `Retired`, for the same reason.
8652    PosixTimerThread(PosixTimerThread),
8653    /// Appended after `PosixTimerThread`, for the same reason.
8654    ForgetThreadTimerCopies(()),
8655    /// Appended after `Retired`, for the same reason.
8656    GuestPid(crate::scheduler::pid_namespaces::GuestPidAnswer),
8657    /// Appended after `GuestPid`, for the same reason.
8658    LocalPid(Result<Option<i32>, String>),
8659    /// Appended after `LocalPid`, for the same reason.
8660    ExecTimerFlush(Result<crate::scheduler::ExecTimerFlush, String>),
8661    /// Appended after `ExecTimerFlush`, for the same reason.
8662    RetireExitedLeader(bool),
8663    /// Appended after `RetireExitedLeader`, for the same reason.
8664    DiscardedSignal(crate::scheduler::discarded_at_send::DiscardedSignalAnswer),
8665    /// Appended after `DiscardedSignal`, for the same reason.
8666    DescriptorsReplaced(()),
8667    /// Appended after `DescriptorsReplaced`, for the same reason.
8668    ThreadIdRefused(bool),
8669    /// Appended after `ThreadIdRefused`, for the same reason.
8670    ProcfsListing(crate::procfs::listing::ListingFacts),
8671    /// Appended after `ProcfsListing`, for the same reason.
8672    ParentDeathSignal(i32),
8673
8674    /// Exact native responses, appended without changing existing wire tags.
8675    PrepareNativeExec(Result<native_exec::ExecTicket, native_exec::ExecControlError>),
8676    SealNativeExecStats(Result<[u8; 32], native_exec::ExecControlError>),
8677    IssueNativeExec(Result<(), native_exec::ExecControlError>),
8678    CancelNativeExec(Result<(), native_exec::ExecControlError>),
8679    CompleteNativeExec(Result<(), native_exec::ExecControlError>),
8680    /// Appended last, for the same reason.
8681    NamesARetiredExitedLeader(bool),
8682    /// Appended last, for the same reason.
8683    ProcfsIds(Vec<Option<i32>>),
8684    /// Appended last, for the same reason.
8685    GroupOrSession(Option<DetPid>),
8686    /// Appended last; the error is an errno.
8687    SetsidDecision(Result<(), i32>),
8688    /// The process to move and its new group; the error is an errno. Appended
8689    /// last.
8690    SetpgidDecision(Result<(DetPid, DetPid), i32>),
8691    /// Boxed, to keep the response small. Appended last.
8692    ProcessImage(Option<Box<crate::exec_record::ExecRecord>>),
8693    /// Appended last.
8694    ProcfsTask(crate::procfs::listing::ProcfsTaskAnswer),
8695    /// Appended last.
8696    RenderedFileState(crate::procfs::listing::RenderedFileState),
8697    /// The generation the scheduler holds for the sender's process afterwards
8698    /// and whether this publication is what it holds
8699    /// (`Scheduler::publish_signal_dispositions`), or `None` when it took
8700    /// nothing from this sender. Appended last.
8701    PublishSignalDispositions(Option<crate::syscalls::signal_shadow::PublicationAnswer>),
8702    /// Appended last.
8703    GuestLog(()),
8704    /// Whether the scheduler took the mask (it takes nothing from a thread it
8705    /// does not know). Appended last.
8706    PublishSignalMask(bool),
8707    /// Appended last.
8708    RecordProcessAuxv(()),
8709    /// Appended last.
8710    ProcessAuxv(Option<ProcessAuxv>),
8711    /// Whether this is the run's first. Appended last.
8712    AuxvFromKernel(bool),
8713    /// Appended last.
8714    AnnounceRefusedSignalControls(()),
8715    /// None means the capture authority or mappings were unavailable.
8716    /// Appended last; this response owns no allocated storage.
8717    CheckBrkMappings(Option<bool>),
8718    /// Current image ownership publication was accepted. Appended last.
8719    PublishRuntimePrivateMemory(bool),
8720    /// Appended last.
8721    MarkChangedRoot(()),
8722    /// Whether the root changed. Appended last.
8723    RootChanged(bool),
8724    /// Appended last.
8725    DescriptionLocks(()),
8726    /// Appended last.
8727    TableReleased(()),
8728    /// One answer per row, in row order, or, while a referenced description's
8729    /// locks are unknown, why. Appended last.
8730    GuestLockRows(Result<Vec<bool>, String>),
8731    /// Appended last.
8732    TableExec(()),
8733    /// Appended last.
8734    AliasedInode(()),
8735    /// The host-timed signals, then the recorded private `SIGCHLD`. Appended
8736    /// last.
8737    HostTimedAndPrivateSigchld(u64, u64),
8738    /// Appended last.
8739    LockLedgerRefused(()),
8740    /// Appended mapping capture response.
8741    MappingCapture(mapping_capture::Reply),
8742    /// Typed expectation delivered by the existing grant, appended without
8743    /// renumbering any legacy wire variant. Not a new resource request.
8744    WithSliceBoundaryBudget(crate::slice_boundary::grant::Budget, Box<GlobalResponse>),
8745    /// Actual lifecycle identity on the existing grant, not an expectation
8746    /// from replay. Appended without renumbering existing wire variants.
8747    WithSliceBoundaryOwner(
8748        detcore_model::schedule::boundary::Owner,
8749        Box<GlobalResponse>,
8750    ),
8751    /// Stored-index stack request returned on the annotated operation's own
8752    /// response, without a mailbox another concurrent RPC could consume.
8753    WithSliceBoundaryStack(Option<PathBuf>, Box<GlobalResponse>),
8754    /// The process, and its areas if Detcore recorded them. Appended last.
8755    ProcessCmdline(DetPid, Option<crate::procfs::CmdlineArea>),
8756    /// The answer to `GlobalRequest::KernelRunTarget`. Appended last.
8757    KernelRunTarget(Option<crate::scheduler::KernelRunBlock>),
8758    /// Legacy callback metadata on the original thread-start response. Appended
8759    /// without changing existing wire tags or ThreadHistory's positional layout.
8760    WithCallbackHistory(
8761        crate::preemptions::callback_history::Startup,
8762        Box<GlobalResponse>,
8763    ),
8764    /// A mismatch report acknowledges no scheduler grant and carries no time.
8765    CallbackCheckpointMismatch(()),
8766    /// The answer to `GlobalRequest::TaskStatFacts`. Appended last.
8767    TaskStatFacts(crate::procfs::task_stat::TaskStatFacts),
8768    /// Appended last.
8769    SetTaskComm(()),
8770    /// Appended last.
8771    RecordExternalParent(()),
8772    /// Appended last.
8773    SetRssLimit(()),
8774    /// The answer to `GlobalRequest::PublishedGuestSigmask` under
8775    /// `Config::in_guest_signal_delivery` (A5 step 3c.1e A): the published
8776    /// mask, tri-state (IR-18), and D37's held masks of a temporary-mask
8777    /// publication in force (`Scheduler::status_guest_mask`). With the gate
8778    /// off the query is answered with `PublishedGuestSigmask`, as before.
8779    /// Appended last.
8780    GuestSigmaskForStatus(crate::scheduler::guest_mask_publication::StatusGuestMask),
8781    /// The answer to `GlobalRequest::PublishTemporaryMask`: for a withdrawal
8782    /// of a publication in force, the sends committed to the thread while it
8783    /// was (the committed-send window record); `None` otherwise. Appended
8784    /// last.
8785    PublishTemporaryMask(Option<crate::scheduler::guest_mask_publication::CommittedSendWindow>),
8786    /// The answer to `GlobalRequest::InPidNamespaceInit`. Appended last.
8787    InPidNamespaceInit(bool),
8788    /// The answer to `GlobalRequest::VirtualInotifyCookies`: one virtual
8789    /// cookie per host cookie, in order. Appended last.
8790    VirtualInotifyCookies(Vec<u32>),
8791    /// The answer to `GlobalRequest::RegisterPlaceholder`: the tag, if one
8792    /// was left. Appended last.
8793    RegisterPlaceholder(Option<u64>),
8794    /// The answer to `GlobalRequest::Placeholder`: the directory and the
8795    /// open file description it was opened in. Appended last.
8796    Placeholder(
8797        Option<(
8798            crate::procfs::fd_directory::PlaceholderDirectory,
8799            OpenFileId,
8800        )>,
8801    ),
8802    /// The answer to `GlobalRequest::PlaceholderFlock`. Appended last.
8803    PlaceholderFlock(crate::procfs::locks::PlaceholderFlockOutcome),
8804    /// The answer to `GlobalRequest::PlaceholderWriteHint`: the inode's write
8805    /// hint after the request. Appended last.
8806    PlaceholderWriteHint(u64),
8807    /// The answer to `GlobalRequest::PlaceholderCopies`: a send's locked
8808    /// description, if any. Appended last.
8809    PlaceholderCopies(Result<(), OpenFileId>),
8810    /// The answer to `GlobalRequest::CpuAffinity` and
8811    /// `GlobalRequest::SetCpuAffinity`. Appended last.
8812    CpuAffinity(crate::cpu_set::TaskAffinity),
8813    /// A granted reply whose grant found the thread the only one able to run
8814    /// before it next enters Detcore (`Scheduler::take_lone_grant`).
8815    /// Outermost; unwrapped first by `send_and_update_time`, which records it
8816    /// in `ThreadState::lone_grant`.
8817    /// Appended last.
8818    WithLoneGrant(Box<GlobalResponse>),
8819    /// The answer to `GlobalRequest::StoreExclusive`. Appended last.
8820    #[cfg(target_arch = "aarch64")]
8821    StoreExclusive(()),
8822    /// The answer to `GlobalRequest::SigkillDyingThreads`: each thread and its
8823    /// address space, in tid order. Appended last.
8824    SigkillDyingThreads(Vec<(DetTid, MmId)>),
8825    /// Appended last.
8826    RecordSigkillRobustLists(()),
8827    /// The answer to `GlobalRequest::TakeSigkillRobustList`. Appended last.
8828    TakeSigkillRobustList(Option<crate::syscalls::robust_list::RobustListSnapshot>),
8829    /// The answer to `GlobalRequest::NextGuestId`: the ID taken, or `None`
8830    /// when the kernel numbers the guest or no positive ID remains. Appended last.
8831    NextGuestId(Option<i32>),
8832    /// The answer to `GlobalRequest::GaveHostId`. Appended last.
8833    GaveHostId(()),
8834    /// An accepted finding or named refusal. Never carries a clock or grant.
8835    /// Appended last; this is not a forwarding-health certificate.
8836    ReplayDeparture(Result<(), replay_departure::Refusal>),
8837    /// Appended last.
8838    DescriptionLocksTaken(()),
8839    /// One owner per lock asked about, in order, or why the locks cannot be
8840    /// shown (`procfs::locks::LockLedger::record_lock_held_as_ofd`). Appended
8841    /// last.
8842    LockOwners(Result<Vec<i64>, String>),
8843    /// The rows and their owners, or, while a referenced description's locks
8844    /// are unknown, why no snapshot can be decided. Appended last.
8845    LedgerLockRows(Result<Vec<(crate::procfs::locks::HeldLock, i64)>, String>),
8846    /// Appended last.
8847    SecondIdentityLocks(()),
8848    /// The table is held, or why it cannot be: it reached the global state
8849    /// through a transport that encodes requests, which carries no table
8850    /// (`HeldTable`). Appended last.
8851    HoldTable(Result<(), String>),
8852    /// Appended last.
8853    RecordLocksHeldAsOfd(()),
8854    /// The answer to `GlobalRequest::HostDiskstats`: the file's bytes, or
8855    /// the errno of the coordinator's read of it. Appended last.
8856    HostDiskstats(Result<Vec<u8>, i32>),
8857    /// The answer to `GlobalRequest::UserNamespaceRelation`. Appended last.
8858    UserNamespaceRelation(
8859        crate::scheduler::user_namespaces::UserNamespaceRelation,
8860        bool,
8861    ),
8862    /// The answer to `GlobalRequest::UserNamespaceOwnerMayDiffer`. Appended
8863    /// last.
8864    UserNamespaceOwnerMayDiffer(()),
8865    /// The answer to `GlobalRequest::ProcfsSequence`. Appended last.
8866    ProcfsSequence(Option<crate::procfs::ProcfsSequence>),
8867    /// The answer to `GlobalRequest::StoreProcfsSequence`. Appended last.
8868    StoreProcfsSequence(()),
8869}
8870
8871/// Read only the mapping predicate through a backend's synchronous external
8872/// channel. No deadline wrapper, log accounting, or async future may allocate
8873/// or free guest-mm storage after the controller captures the mappings.
8874pub(crate) fn check_brk_mappings<G, T>(
8875    guest: &G,
8876    check: crate::syscalls::memory::BrkMappingCheck,
8877) -> Option<bool>
8878where
8879    G: Guest<Detcore<T>>,
8880    T: RecordOrReplay,
8881{
8882    let fact = guest.controller_capture_fact()?;
8883    let state = guest.thread_state();
8884    let request = (
8885        state.thread_logical_time.clone(),
8886        state.mm_id,
8887        GlobalRequest::CheckBrkMappings(check, fact),
8888    );
8889    match guest.try_send_rpc_sync(request).ok()? {
8890        (None, GlobalResponse::CheckBrkMappings(answer)) => answer,
8891        _ => None,
8892    }
8893}
8894
8895/// `request`, carrying the number of records this guest thread produced for
8896/// forwarding since its previous counted request when there are any
8897/// (`GlobalRequest::WithForwardedCount`). A thread of a backend whose Tool does
8898/// not forward records never has any, so its requests are unchanged.
8899pub(crate) fn counted_request(request: GlobalRequest) -> GlobalRequest {
8900    match crate::detlog::take_forwarded_since_request() {
8901        0 => request,
8902        produced => GlobalRequest::WithForwardedCount(produced, Box::new(request)),
8903    }
8904}
8905
8906/// The global request that carries one shared open file control message. It
8907/// is answered before any clock accounting, so its clock and address-space
8908/// fields are placeholders. The sender is the thread the backend's transport
8909/// names for the request; for in-guest LiteInst that is the runtime's own
8910/// envelope, checked against the connecting process at admission, on a
8911/// connection the guest cannot write to.
8912pub fn shared_open_file_request(control: OpenFileControl) -> (DetTime, MmId, GlobalRequest) {
8913    (
8914        DetTime::default(),
8915        MmId::initial(DetTid::from_raw(0)),
8916        GlobalRequest::SharedOpenFile(control),
8917    )
8918}
8919
8920/// The request [`crate::guest_log`] sends through a runtime's
8921/// [`crate::guest_log::CoordinatorCall`]: like [`shared_open_file_request`], it
8922/// carries no clock.
8923pub(crate) fn guest_log_request(
8924    record: Option<crate::guest_log::GuestLogRecord>,
8925    unsent_before: u64,
8926) -> (DetTime, MmId, GlobalRequest) {
8927    (
8928        DetTime::default(),
8929        MmId::initial(DetTid::from_raw(0)),
8930        GlobalRequest::GuestLog(record, unsent_before),
8931    )
8932}
8933
8934/// The answer to a [`guest_log_request`].
8935pub(crate) fn guest_log_reply(
8936    response: (Option<LogicalTime>, GlobalResponse),
8937) -> std::io::Result<()> {
8938    match response {
8939        (None, GlobalResponse::GuestLog(())) => Ok(()),
8940        other => Err(std::io::Error::other(format!(
8941            "unexpected answer to a guest log record: {other:?}"
8942        ))),
8943    }
8944}
8945
8946/// A store-exclusive report (`crate::store_exclusive`). Like a guest log
8947/// record, it carries no time.
8948#[cfg(target_arch = "aarch64")]
8949pub(crate) fn store_exclusive_request(
8950    report: crate::store_exclusive::Report,
8951) -> (DetTime, MmId, GlobalRequest) {
8952    (
8953        DetTime::default(),
8954        MmId::initial(DetTid::from_raw(0)),
8955        GlobalRequest::StoreExclusive(report),
8956    )
8957}
8958
8959/// Check the answer to a [`store_exclusive_request`]. A report is
8960/// diagnostic, so another answer (a run already failing) is only logged.
8961#[cfg(target_arch = "aarch64")]
8962pub(crate) fn store_exclusive_reply(response: (Option<LogicalTime>, GlobalResponse)) {
8963    if response != (None, GlobalResponse::StoreExclusive(())) {
8964        debug!("unexpected answer to a store-exclusive report: {response:?}");
8965    }
8966}
8967
8968/// The answer to a [`shared_open_file_request`].
8969pub fn shared_open_file_reply(
8970    response: (Option<LogicalTime>, GlobalResponse),
8971) -> Result<OpenFileControlReply, SharedOpenFileError> {
8972    match response {
8973        (None, GlobalResponse::SharedOpenFile(reply)) => reply,
8974        other => Err(SharedOpenFileError(format!(
8975            "unexpected answer to a shared open file control message: {other:?}"
8976        ))),
8977    }
8978}
8979
8980// AUTONOMOUS-BOT-IMPLEMENTED
8981// TODO-HUMAN-REVIEW(PR-644): Review the shared warning formatter API.
8982/// Formats one deterministic warning for a set of unsupported syscall names.
8983pub fn format_unsupported_syscall_warning(syscalls: &BTreeSet<String>) -> Option<String> {
8984    if syscalls.is_empty() {
8985        None
8986    } else {
8987        Some(format!(
8988            "syscalls {} used but not yet supported",
8989            syscalls.iter().cloned().collect::<Vec<_>>().join(",")
8990        ))
8991    }
8992}
8993
8994// AUTONOMOUS-BOT-IMPLEMENTED
8995// TODO-HUMAN-REVIEW(PR-1154): Review the SaBRe exec descriptor-status handoff.
8996/// Notifies the coordinator that `guest` is about to `execve`, recording the
8997/// pre-exec address space `mm` and any file-descriptor blocking overrides. A
8998/// backend that handles `execve` outside Detcore's syscall handler must call
8999/// this before the native syscall so the next image reconnects to the existing
9000/// scheduler identity and logical clock.
9001///
9002/// `robust_wakes` are the owner-death wakes a successful exec makes Linux
9003/// deliver, from [`Detcore::exec_robust_list_wakes`], where the backend runs
9004/// the native execve outside Detcore's handler; the coordinator delivers them
9005/// at the new image's reconnect, or when the caller's thread ends without
9006/// one, and drops them at [`cancel_exec`]. A backend whose execve Detcore handles stages
9007/// them in its own thread state instead and passes none.
9008///
9009/// `exe` is the image the exec runs, where the caller resolved it
9010/// ([`crate::exec_record::ExecRecord::resolve`]); the new image's
9011/// `/proc/<pid>/exe` link answers from it.
9012pub async fn prepare_exec<G, T>(
9013    guest: &mut G,
9014    mm: MmId,
9015    fd_blocking: ExecFdBlockingOverrides,
9016    robust_wakes: Vec<(DetTid, FutexID)>,
9017    exe: Option<crate::exec_record::ExecRecord>,
9018) where
9019    G: Guest<Detcore<T>>,
9020    T: RecordOrReplay,
9021{
9022    // An exec replaces the image holding this process's unsent warning count
9023    // (`crate::guest_log`); every exec path, DBT's native one included, comes
9024    // here first.
9025    crate::guest_log::flush_unsent();
9026    let detpid = guest.thread_state().detpid.expect("detpid unset");
9027    let fd_state = ExecFdState {
9028        blocking: fd_blocking,
9029        inherited_reads: guest.thread_state().inherited_reads_for_exec(),
9030        initialize_inherited_stdio: false,
9031        initial_input: None,
9032        robust_wakes,
9033        open_files: guest
9034            .thread_state()
9035            .shared_open_files_for_exec()
9036            .expect("prepared exec descriptors are no longer live"),
9037        maps: if guest.config().backend.rediscovers_descriptors_after_exec {
9038            guest
9039                .thread_state()
9040                .maps_for_exec(guest.runtime_private_memory_regions().as_ref())
9041                .expect("prepared maps descriptors are no longer live")
9042        } else {
9043            None
9044        },
9045        // A per-process file that follows its record's sequence, with its
9046        // open file's identity and sequence, and the table's numbering,
9047        // carried where the backend rebuilds the table after exec.
9048        procfs_records: if guest.config().backend.rediscovers_descriptors_after_exec {
9049            guest
9050                .thread_state()
9051                .procfs_records_for_exec()
9052                .expect("prepared per-process descriptors are no longer live")
9053        } else {
9054            None
9055        },
9056        channels: Default::default(),
9057        open_file_namespace: None,
9058        random_device_sequence: guest.thread_state().random_device_sequence_for_exec(),
9059        exe,
9060    };
9061    // `--pipe-wake-latency` on a backend that rediscovers its descriptors
9062    // after exec: carry the channels, and reserve the old numbering at every
9063    // exec (`crate::pipe_wake`).
9064    let fd_state = if guest.config().pipe_wake_latency_in_effect().is_some()
9065        && guest.config().backend.rediscovers_descriptors_after_exec
9066    {
9067        ExecFdState {
9068            channels: guest.thread_state().pipe_channels_for_exec(),
9069            open_file_namespace: Some(guest.thread_state().open_file_namespace_for_exec()),
9070            ..fd_state
9071        }
9072    } else {
9073        fd_state
9074    };
9075    let (_, response) = send_and_update_time(
9076        guest,
9077        GlobalRequest::PrepareExec(detpid, mm, Box::new(fd_state)),
9078    )
9079    .await;
9080    assert_eq!(response, GlobalResponse::PrepareExec(()));
9081}
9082
9083/// Cancels a matching [`prepare_exec`] after a native exec fails. Call this
9084/// before returning to the old image, preserving its thread state and address
9085/// space identity so frozen siblings can resume without an exec transition.
9086pub async fn cancel_exec<G, T>(guest: &mut G)
9087where
9088    G: Guest<Detcore<T>>,
9089    T: RecordOrReplay,
9090{
9091    let detpid = guest.thread_state().detpid.expect("detpid unset");
9092    let (_, response) = send_and_update_time(guest, GlobalRequest::CancelExec(detpid)).await;
9093    assert_eq!(response, GlobalResponse::CancelExec(()));
9094}
9095
9096pub async fn mark_past_first_execve<G, T>(
9097    guest: &mut G,
9098    unprepared_exe: Option<crate::exec_record::ExecRecord>,
9099) where
9100    G: Guest<Detcore<T>>,
9101    T: RecordOrReplay,
9102{
9103    let controlled =
9104        guest.signal_control_mode() == reverie::BackendSignalControlMode::ToolControlled;
9105    let signal_identity = controlled.then(|| guest.signal_task_identity()).flatten();
9106    let detpid = guest.thread_state().detpid.expect("detpid unset");
9107    // The device and inode of the file the kernel's `exe` link names for the
9108    // new image, which the coordinator checks the exec's record against.
9109    let kernel_image = crate::syscalls::guest_task_procfs(
9110        &guest.config().backend,
9111        guest.tid().as_raw(),
9112        guest
9113            .thread_state()
9114            .changed_root
9115            .load(std::sync::atomic::Ordering::SeqCst),
9116    )
9117    .and_then(|task| std::fs::metadata(task.join("exe")).ok())
9118    .map(|meta| {
9119        use std::os::unix::fs::MetadataExt;
9120        (meta.dev(), meta.ino())
9121    });
9122    let (_, response) = send_and_update_time(
9123        guest,
9124        GlobalRequest::MarkPastFirstExecve(detpid, signal_identity, unprepared_exe, kernel_image),
9125    )
9126    .await;
9127    let mut overrides = match response {
9128        GlobalResponse::MarkPastFirstExecve(overrides) => *overrides,
9129        _ => unreachable!(),
9130    };
9131    if controlled {
9132        guest.thread_state_mut().signal_task_identity = signal_identity;
9133    }
9134    // The new image's `/proc/<pid>/exe` link answers from its record, or
9135    // without one is the kernel's.
9136    guest.thread_state_mut().exe = overrides.exe.take().map(Arc::new);
9137    if !overrides.is_empty() {
9138        if let Some(creator) = overrides.open_file_creator() {
9139            // Every carried description continues the old namespace, including
9140            // maps-only handoffs and the pipe-wake-latency channel namespace.
9141            guest.thread_state_mut().set_open_file_creator(creator);
9142        }
9143        let dettid = guest.thread_state().dettid;
9144        let metadata = Arc::clone(&guest.thread_state().file_metadata);
9145        metadata
9146            .lock()
9147            .unwrap()
9148            .apply_exec_fd_state(
9149                dettid,
9150                overrides,
9151                guest.config().backend.tool_shares_guest_descriptor_table,
9152            )
9153            .expect("prepared exec descriptor provenance no longer matches the guest table");
9154    }
9155}
9156
9157/// Report to the global state that this thread, in its current syscall,
9158/// opened `path` and found the host file `identity` (`regular` for a regular
9159/// file), which the tool found at `source` for the run's end to read, for
9160/// `hermit run --verify` and the run's host-input set (see
9161/// `detcore_model::host_input`). The answer carries no time.
9162pub(crate) async fn record_host_input<G, T>(
9163    guest: &mut G,
9164    path: String,
9165    identity: HostFileIdentity,
9166    source: Option<String>,
9167    regular: bool,
9168    exec: bool,
9169    kernel_object: bool,
9170) where
9171    G: Guest<Detcore<T>>,
9172    T: RecordOrReplay,
9173{
9174    let syscall = guest.thread_state().stats.syscall_count;
9175    let (time, response) = send_and_update_time(
9176        guest,
9177        GlobalRequest::RecordHostInput {
9178            path,
9179            syscall,
9180            identity,
9181            source,
9182            regular,
9183            exec,
9184            kernel_object,
9185        },
9186    )
9187    .await;
9188    assert_eq!(
9189        (time, response),
9190        (None, GlobalResponse::RecordHostInput(()))
9191    );
9192}
9193
9194/// Print `change` on Hermit's own stderr, never the guest's output or the
9195/// log `--verify` compares, without panicking where the guest made the shared
9196/// stderr nonblocking (`crate::util::RetryingStderr`). The caller holds no
9197/// lock.
9198fn report_host_input_change(change: &detcore_model::host_input::HostInputChange) {
9199    let line = format!(
9200        "{} {change}\n",
9201        detcore_model::host_input::HOST_INPUT_CHANGED_SINCE_RECORDING_MARKER
9202    );
9203    let _ = if crate::nonwaiting_write::diagnostics_must_not_wait() {
9204        std::io::Write::write_all(&mut crate::util::DroppingStderr, line.as_bytes())
9205    } else {
9206        std::io::Write::write_all(&mut crate::util::RetryingStderr, line.as_bytes())
9207    };
9208}
9209
9210/// Report to the global state that this thread rebound `path` in the guest's
9211/// file namespace, for `hermit run --verify` (see
9212/// `detcore_model::host_input::HostMutationRecord`). The answer carries no
9213/// time.
9214pub(crate) async fn record_host_mutation<G, T>(guest: &mut G, path: String)
9215where
9216    G: Guest<Detcore<T>>,
9217    T: RecordOrReplay,
9218{
9219    let (time, response) =
9220        send_and_update_time(guest, GlobalRequest::RecordHostMutation { path }).await;
9221    assert_eq!(
9222        (time, response),
9223        (None, GlobalResponse::RecordHostMutation(()))
9224    );
9225}
9226
9227/// Record a determinism loss for the run in the global state's process,
9228/// whose losses verification reads (see `crate::detlog::determinism_loss`),
9229/// whichever process this handler runs in. The answer carries no time.
9230pub(crate) async fn report_determinism_loss<G, T>(guest: &mut G, reason: String)
9231where
9232    G: Guest<Detcore<T>>,
9233    T: RecordOrReplay,
9234{
9235    let (time, response) =
9236        send_and_update_time(guest, GlobalRequest::RecordDeterminismLoss(reason)).await;
9237    assert_eq!(
9238        (time, response),
9239        (None, GlobalResponse::RecordDeterminismLoss(()))
9240    );
9241}
9242
9243// TODO-HUMAN-REVIEW(PR-643): Review the guest-to-global unsupported-syscall report path.
9244pub async fn report_unsupported_syscall<G, T>(guest: &mut G, sysno: Sysno)
9245where
9246    G: Guest<Detcore<T>>,
9247    T: RecordOrReplay,
9248{
9249    let (_, response) = send_and_update_time(
9250        guest,
9251        GlobalRequest::ReportUnsupportedSyscall(sysno.to_string()),
9252    )
9253    .await;
9254    assert_eq!(response, GlobalResponse::ReportUnsupportedSyscall(()));
9255}
9256
9257/// The once-per-run WARN for a signal disposition a guest asked for and
9258/// Detcore answered with success but did not install.
9259pub(crate) fn signal_handler_not_installed_warning(signum: i32) -> String {
9260    format!(
9261        "HERMIT_SIGNAL_HANDLER_NOT_INSTALLED signum={signum} (SIGSTKFLT): the guest set a handler \
9262         or SIG_IGN for it and rt_sigaction reported success, but Hermit did not install it, \
9263         because the backend reserves SIGSTKFLT (Reverie's PMU preemption timer, SaBRe's \
9264         controlled exit). A SIGSTKFLT the guest receives takes the default action. Hermit \
9265         answers success on purpose so that programs that register it routinely, such as the \
9266         Go runtime, still start."
9267    )
9268}
9269
9270/// Tells the coordinator the signal controls an in-guest runtime refused
9271/// (`GlobalRequest::AnnounceRefusedSignalControls`). Like the publications of
9272/// Detcore's signal-state copy, it is sent directly, not through
9273/// `send_and_update_time`, and carries no logical time either way.
9274pub(crate) async fn announce_refused_signal_controls<G, T>(
9275    guest: &mut G,
9276    actions: u64,
9277    alternate_stack: bool,
9278) where
9279    G: Guest<Detcore<T>>,
9280    T: RecordOrReplay,
9281{
9282    // Answered before any clock accounting, so the clock and address-space
9283    // fields are placeholders, as `shared_open_file_request`'s are.
9284    let (_, response) = guest
9285        .send_rpc((
9286            DetTime::default(),
9287            MmId::initial(DetTid::from_raw(0)),
9288            GlobalRequest::AnnounceRefusedSignalControls(actions, alternate_stack),
9289        ))
9290        .await;
9291    debug_assert_eq!(response, GlobalResponse::AnnounceRefusedSignalControls(()));
9292}
9293
9294/// The host's `/proc/diskstats` as this run first read it
9295/// (`GlobalRequest::HostDiskstats`), or the errno of that read. The
9296/// coordinator reads it, in its own process, so no guest descriptor is
9297/// needed, and every process of the run is answered the same bytes.
9298/// Answered before any clock accounting, as
9299/// [`announce_refused_signal_controls`] is.
9300pub(crate) async fn host_diskstats<G, T>(guest: &mut G) -> Result<Vec<u8>, crate::Errno>
9301where
9302    G: Guest<Detcore<T>>,
9303    T: RecordOrReplay,
9304{
9305    let (_, response) = guest
9306        .send_rpc((
9307            DetTime::default(),
9308            MmId::initial(DetTid::from_raw(0)),
9309            GlobalRequest::HostDiskstats,
9310        ))
9311        .await;
9312    match response {
9313        GlobalResponse::HostDiskstats(answer) => answer.map_err(crate::Errno::new),
9314        other => panic!("unexpected response to HostDiskstats: {other:?}"),
9315    }
9316}
9317
9318/// The record sequence of the open file description `id`, as the last read,
9319/// positioned read or seek of it, in whichever process, left it
9320/// (`GlobalRequest::ProcfsSequence`); none before the first.
9321pub(crate) async fn procfs_sequence<G, T>(
9322    guest: &mut G,
9323    id: OpenFileId,
9324) -> Option<crate::procfs::ProcfsSequence>
9325where
9326    G: Guest<Detcore<T>>,
9327    T: RecordOrReplay,
9328{
9329    let (_, response) = guest
9330        .send_rpc((
9331            DetTime::default(),
9332            MmId::initial(DetTid::from_raw(0)),
9333            GlobalRequest::ProcfsSequence(id),
9334        ))
9335        .await;
9336    match response {
9337        GlobalResponse::ProcfsSequence(sequence) => sequence,
9338        other => panic!("unexpected response to ProcfsSequence: {other:?}"),
9339    }
9340}
9341
9342/// Keep `sequence` as the record sequence of the open file description `id`
9343/// (`GlobalRequest::StoreProcfsSequence`).
9344pub(crate) async fn store_procfs_sequence<G, T>(
9345    guest: &mut G,
9346    id: OpenFileId,
9347    sequence: crate::procfs::ProcfsSequence,
9348) where
9349    G: Guest<Detcore<T>>,
9350    T: RecordOrReplay,
9351{
9352    let (_, response) = guest
9353        .send_rpc((
9354            DetTime::default(),
9355            MmId::initial(DetTid::from_raw(0)),
9356            GlobalRequest::StoreProcfsSequence(id, sequence),
9357        ))
9358        .await;
9359    match response {
9360        GlobalResponse::StoreProcfsSequence(()) => {}
9361        other => panic!("unexpected response to StoreProcfsSequence: {other:?}"),
9362    }
9363}
9364
9365/// Tells the coordinator a guest's disposition for `signum` was answered with
9366/// success but not installed (`GlobalRequest::AnnounceSignalHandlerNotInstalled`).
9367pub(crate) async fn announce_signal_handler_not_installed<G, T>(guest: &mut G, signum: i32)
9368where
9369    G: Guest<Detcore<T>>,
9370    T: RecordOrReplay,
9371{
9372    let (_, response) = send_and_update_time(
9373        guest,
9374        GlobalRequest::AnnounceSignalHandlerNotInstalled(signum),
9375    )
9376    .await;
9377    assert_eq!(
9378        response,
9379        GlobalResponse::AnnounceSignalHandlerNotInstalled(())
9380    );
9381}
9382
9383/// Tells the coordinator why the fail-closed policy is ending the run, before
9384/// this Tool image exits (`GlobalRequest::PolicyRefusal`).
9385pub(crate) async fn report_policy_refusal<G, T>(guest: &mut G, reason: String)
9386where
9387    G: Guest<Detcore<T>>,
9388    T: RecordOrReplay,
9389{
9390    let (_, response) = send_and_update_time(guest, GlobalRequest::PolicyRefusal(reason)).await;
9391    assert!(
9392        matches!(
9393            response,
9394            GlobalResponse::PolicyRefusal(()) | GlobalResponse::ThreadExited
9395        ),
9396        "unexpected response to PolicyRefusal: {response:?}"
9397    );
9398}
9399
9400/// What the scheduler holds for `target` if it is blocked in a kernel-run
9401/// call whose continuation turn has not been granted (A5 design D44,
9402/// `GlobalRequest::KernelRunTarget`).
9403pub(crate) async fn kernel_run_target<G, T>(
9404    guest: &mut G,
9405    target: DetTid,
9406) -> Option<crate::scheduler::KernelRunBlock>
9407where
9408    G: Guest<Detcore<T>>,
9409    T: RecordOrReplay,
9410{
9411    let (_, response) = send_and_update_time(guest, GlobalRequest::KernelRunTarget(target)).await;
9412    match response {
9413        GlobalResponse::KernelRunTarget(block) => block,
9414        GlobalResponse::ThreadExited => None,
9415        response => panic!("unexpected response to KernelRunTarget: {response:?}"),
9416    }
9417}
9418
9419/// File the run-wide stopping refusal `reason` (A5 design D49,
9420/// `GlobalRequest::StoppingRefusal`). Under the in-guest delivery gate the
9421/// coordinator exits on it, taking every guest with it, and this never
9422/// returns; with the gate off (IR-16) the coordinator records it as a policy
9423/// refusal and answers, and this returns.
9424pub(crate) async fn stopping_refusal<G, T>(guest: &mut G, reason: String)
9425where
9426    G: Guest<Detcore<T>>,
9427    T: RecordOrReplay,
9428{
9429    let (_, response) = send_and_update_time(guest, GlobalRequest::StoppingRefusal(reason)).await;
9430    assert!(
9431        matches!(
9432            response,
9433            GlobalResponse::PolicyRefusal(()) | GlobalResponse::ThreadExited
9434        ),
9435        "unexpected response to StoppingRefusal: {response:?}"
9436    );
9437}
9438
9439/// Mirrors a successful `set_tid_address(2)` into scheduler-owned exit state.
9440/// Tells the scheduler the calling thread's own signal mask while Detcore holds
9441/// it under a private blocking mask (`Some`), or that the guest's mask is back
9442/// (`None`).
9443pub(crate) async fn set_saved_guest_sigmask<G, T>(guest: &mut G, mask: Option<u64>)
9444where
9445    G: Guest<Detcore<T>>,
9446    T: RecordOrReplay,
9447{
9448    let (_, response) =
9449        send_and_update_time(guest, GlobalRequest::SetSavedGuestSigmask(mask)).await;
9450    assert!(
9451        matches!(
9452            response,
9453            GlobalResponse::SetSavedGuestSigmask(()) | GlobalResponse::ThreadExited
9454        ),
9455        "unexpected response to SetSavedGuestSigmask: {response:?}"
9456    );
9457}
9458
9459/// The guest's own signal mask that `thread` published while Detcore holds it
9460/// under a private blocking mask, if any (`Scheduler::published_guest_sigmask`):
9461/// the `SigBlk` its `/proc` status reports in place of the kernel's
9462/// (https://github.com/rrnewton/hermit/issues/4273, R2b-2 stage 2).
9463pub(crate) async fn published_guest_sigmask<G, T>(guest: &mut G, thread: DetTid) -> Option<u64>
9464where
9465    G: Guest<Detcore<T>>,
9466    T: RecordOrReplay,
9467{
9468    let (_, response) =
9469        send_and_update_time(guest, GlobalRequest::PublishedGuestSigmask(thread)).await;
9470    match response {
9471        GlobalResponse::PublishedGuestSigmask(mask) => mask,
9472        GlobalResponse::ThreadExited => None,
9473        other => panic!("unexpected response to PublishedGuestSigmask: {other:?}"),
9474    }
9475}
9476
9477/// A5 step 3c.1e A: what the `/proc` status reader needs of `thread` under
9478/// the in-guest delivery gate (`Scheduler::status_guest_mask`): its published
9479/// mask, tri-state (IR-18), and D37's held masks of a temporary-mask
9480/// publication in force. Asked through the same query as
9481/// [`published_guest_sigmask`], which the coordinator answers with the
9482/// appended `GuestSigmaskForStatus` under the gate.
9483pub(crate) async fn status_guest_mask<G, T>(
9484    guest: &mut G,
9485    thread: DetTid,
9486) -> crate::scheduler::guest_mask_publication::StatusGuestMask
9487where
9488    G: Guest<Detcore<T>>,
9489    T: RecordOrReplay,
9490{
9491    use crate::scheduler::guest_mask_publication::PublishedGuestMask;
9492    use crate::scheduler::guest_mask_publication::StatusGuestMask;
9493    let (_, response) =
9494        send_and_update_time(guest, GlobalRequest::PublishedGuestSigmask(thread)).await;
9495    match response {
9496        GlobalResponse::GuestSigmaskForStatus(status) => status,
9497        GlobalResponse::PublishedGuestSigmask(mask) => StatusGuestMask {
9498            mask: mask.map(PublishedGuestMask::Known),
9499            ..Default::default()
9500        },
9501        GlobalResponse::ThreadExited => StatusGuestMask::default(),
9502        other => panic!("unexpected response to PublishedGuestSigmask: {other:?}"),
9503    }
9504}
9505
9506/// A5 step 3c.1e A, IR-23(a): file `control`, the sender's temporary-mask
9507/// publication or its withdrawal (`GlobalRequest::PublishTemporaryMask`),
9508/// answered in its own turn with no scheduling decision. A withdrawal of a
9509/// publication in force answers the committed-send window record.
9510pub(crate) async fn publish_temporary_mask<G, T>(
9511    guest: &mut G,
9512    control: crate::scheduler::guest_mask_publication::TemporaryMaskControl,
9513) -> Option<crate::scheduler::guest_mask_publication::CommittedSendWindow>
9514where
9515    G: Guest<Detcore<T>>,
9516    T: RecordOrReplay,
9517{
9518    let (_, response) =
9519        send_and_update_time(guest, GlobalRequest::PublishTemporaryMask(control)).await;
9520    match response {
9521        GlobalResponse::PublishTemporaryMask(window) => window,
9522        GlobalResponse::ThreadExited => None,
9523        other => panic!("unexpected response to PublishTemporaryMask: {other:?}"),
9524    }
9525}
9526
9527/// A5 step 3c.1e A, IR-15: whether `thread`'s process is the init of a PID
9528/// namespace it created with `CLONE_NEWPID` (`GlobalRequest::InPidNamespaceInit`).
9529pub(crate) async fn in_pid_namespace_init<G, T>(guest: &mut G, thread: DetTid) -> bool
9530where
9531    G: Guest<Detcore<T>>,
9532    T: RecordOrReplay,
9533{
9534    let (_, response) =
9535        send_and_update_time(guest, GlobalRequest::InPidNamespaceInit(thread)).await;
9536    match response {
9537        GlobalResponse::InPidNamespaceInit(init) => init,
9538        GlobalResponse::ThreadExited => false,
9539        other => panic!("unexpected response to InPidNamespaceInit: {other:?}"),
9540    }
9541}
9542
9543/// The run-wide virtual cookie for each host inotify rename cookie of one
9544/// read, in order (`GlobalRequest::VirtualInotifyCookies`). The
9545/// `ThreadExited` arm, kept as in its siblings, is never taken:
9546/// `send_and_update_time` retires an exited thread instead of returning.
9547pub(crate) async fn virtual_inotify_cookies<G, T>(guest: &mut G, host: Vec<u32>) -> Vec<u32>
9548where
9549    G: Guest<Detcore<T>>,
9550    T: RecordOrReplay,
9551{
9552    let (_, response) =
9553        send_and_update_time(guest, GlobalRequest::VirtualInotifyCookies(host.clone())).await;
9554    match response {
9555        GlobalResponse::VirtualInotifyCookies(cookies) => cookies,
9556        GlobalResponse::ThreadExited => host,
9557        other => panic!("unexpected response to VirtualInotifyCookies: {other:?}"),
9558    }
9559}
9560
9561/// Tells the scheduler the set the calling thread waits for while it polls
9562/// `rt_sigtimedwait` (`Some`), or that it polls no more (`None`).
9563pub(crate) async fn set_sigtimedwait_set<G, T>(guest: &mut G, set: Option<u64>)
9564where
9565    G: Guest<Detcore<T>>,
9566    T: RecordOrReplay,
9567{
9568    let (_, response) = send_and_update_time(guest, GlobalRequest::SetSigtimedwaitSet(set)).await;
9569    assert!(
9570        matches!(
9571            response,
9572            GlobalResponse::SetSigtimedwaitSet(()) | GlobalResponse::ThreadExited
9573        ),
9574        "unexpected response to SetSigtimedwaitSet: {response:?}"
9575    );
9576}
9577
9578/// Tells the scheduler the calling thread sends SIGSTKFLT, in its own turn
9579/// (`Scheduler::record_guest_marker_signal`).
9580pub(crate) async fn record_guest_marker_signal<G, T>(guest: &mut G)
9581where
9582    G: Guest<Detcore<T>>,
9583    T: RecordOrReplay,
9584{
9585    let (_, response) = send_and_update_time(guest, GlobalRequest::RecordGuestMarkerSignal).await;
9586    assert!(
9587        matches!(response, GlobalResponse::RecordGuestMarkerSignal(())),
9588        "unexpected response to RecordGuestMarkerSignal: {response:?}"
9589    );
9590}
9591
9592/// The inputs to a process-directed signal the calling thread sends to its own
9593/// process, read in its turn (`Scheduler::own_process_signal_inputs`).
9594pub(crate) async fn own_process_signal_inputs<G, T>(guest: &mut G) -> OwnProcessSignalInputs
9595where
9596    G: Guest<Detcore<T>>,
9597    T: RecordOrReplay,
9598{
9599    match send_and_update_time(guest, GlobalRequest::OwnProcessSignalInputs)
9600        .await
9601        .1
9602    {
9603        GlobalResponse::OwnProcessSignalInputs(inputs) => inputs,
9604        response => unreachable!("unexpected response to OwnProcessSignalInputs: {response:?}"),
9605    }
9606}
9607
9608/// The thread of `process` that takes the process-directed signal `signo`
9609/// the calling thread sends naming `named`, decided in its turn
9610/// (`Scheduler::process_signal_receiver`).
9611pub(crate) async fn process_signal_receiver<G, T>(
9612    guest: &mut G,
9613    process: DetPid,
9614    named: DetTid,
9615    signo: i32,
9616) -> ProcessSignalReceiver
9617where
9618    G: Guest<Detcore<T>>,
9619    T: RecordOrReplay,
9620{
9621    match send_and_update_time(
9622        guest,
9623        GlobalRequest::ProcessSignalReceiver(process, named, signo),
9624    )
9625    .await
9626    .1
9627    {
9628        GlobalResponse::ProcessSignalReceiver(receiver) => receiver,
9629        response => unreachable!("unexpected response to ProcessSignalReceiver: {response:?}"),
9630    }
9631}
9632
9633/// The calling thread's forwarded process-directed send to `process`, whose
9634/// receiver the scheduler chose, succeeded (`Scheduler::process_signal_sent`).
9635pub(crate) async fn process_signal_sent<G, T>(guest: &mut G, process: DetPid)
9636where
9637    G: Guest<Detcore<T>>,
9638    T: RecordOrReplay,
9639{
9640    match send_and_update_time(guest, GlobalRequest::ProcessSignalSent(process))
9641        .await
9642        .1
9643    {
9644        GlobalResponse::ProcessSignalSent(()) => {}
9645        response => unreachable!("unexpected response to ProcessSignalSent: {response:?}"),
9646    }
9647}
9648
9649/// The calling thread is about to forward `signo` to its own process, naming
9650/// `named`, as Linux gives it to the caller (`Scheduler::own_process_signal_choice`).
9651pub(crate) async fn own_process_signal_choice<G, T>(guest: &mut G, named: DetTid, signo: i32)
9652where
9653    G: Guest<Detcore<T>>,
9654    T: RecordOrReplay,
9655{
9656    match send_and_update_time(guest, GlobalRequest::OwnProcessSignalChoice(named, signo))
9657        .await
9658        .1
9659    {
9660        GlobalResponse::OwnProcessSignalChoice(()) => {}
9661        response => unreachable!("unexpected response to OwnProcessSignalChoice: {response:?}"),
9662    }
9663}
9664
9665pub(crate) async fn set_child_tid_address<G, T>(guest: &mut G, address: usize)
9666where
9667    G: Guest<Detcore<T>>,
9668    T: RecordOrReplay,
9669{
9670    let (_, response) =
9671        send_and_update_time(guest, GlobalRequest::SetChildTidAddress(address)).await;
9672    assert_eq!(response, GlobalResponse::SetChildTidAddress(()));
9673}
9674
9675/// Publishes `dispositions`, publication `generation`, as the calling
9676/// thread's process's (`Scheduler::publish_signal_dispositions`; `None`
9677/// withdraws them), and returns the scheduler's answer: the generation it
9678/// holds afterwards and whether this publication is what it holds, or `None`
9679/// when it took nothing from this thread. Like the zero-time control messages it carries no logical time and
9680/// its answer carries none back. It is sent from `send_and_update_time`, not
9681/// through it, so it neither consumes a grant's deadline nor counts forwarded
9682/// records.
9683pub(crate) async fn publish_signal_dispositions<G, T>(
9684    guest: &mut G,
9685    generation: u64,
9686    dispositions: Option<crate::syscalls::signal_shadow::CopiedDispositions>,
9687) -> Option<crate::syscalls::signal_shadow::PublicationAnswer>
9688where
9689    G: Guest<Detcore<T>>,
9690    T: RecordOrReplay,
9691{
9692    let state = guest.thread_state();
9693    let message = (
9694        state.thread_logical_time.clone(),
9695        state.mm_id,
9696        GlobalRequest::PublishSignalDispositions(generation, dispositions),
9697    );
9698    match guest.send_rpc(message).await.1 {
9699        GlobalResponse::PublishSignalDispositions(answer) => answer,
9700        _ => None,
9701    }
9702}
9703
9704/// Whether the scheduler took `mask` as the calling thread's
9705/// (`Scheduler::publish_signal_mask`; `None` withdraws it). Sent like
9706/// [`publish_signal_dispositions`].
9707pub(crate) async fn publish_signal_mask<G, T>(guest: &mut G, mask: Option<u64>) -> bool
9708where
9709    G: Guest<Detcore<T>>,
9710    T: RecordOrReplay,
9711{
9712    let state = guest.thread_state();
9713    let message = (
9714        state.thread_logical_time.clone(),
9715        state.mm_id,
9716        GlobalRequest::PublishSignalMask(mask),
9717    );
9718    matches!(
9719        guest.send_rpc(message).await.1,
9720        GlobalResponse::PublishSignalMask(true)
9721    )
9722}
9723
9724async fn publish_runtime_private_memory<G, T>(guest: &mut G)
9725where
9726    G: Guest<Detcore<T>>,
9727    T: RecordOrReplay,
9728{
9729    let Some(facts) = guest.runtime_private_memory_regions() else {
9730        return;
9731    };
9732    let state = guest.thread_state();
9733    let mm = state.mm_id;
9734    if state.published_runtime_private_memory.as_ref() == Some(&(mm, facts.clone())) {
9735        return;
9736    }
9737    let message = (
9738        state.thread_logical_time.clone(),
9739        mm,
9740        GlobalRequest::PublishRuntimePrivateMemory(facts.clone()),
9741    );
9742    if guest.send_rpc(message).await.1 == GlobalResponse::PublishRuntimePrivateMemory(true) {
9743        guest.thread_state_mut().published_runtime_private_memory = Some((mm, facts));
9744    }
9745}
9746
9747pub async fn send_and_update_time<G, T>(
9748    guest: &mut G,
9749    request: GlobalRequest,
9750) -> (Option<LogicalTime>, GlobalResponse)
9751where
9752    G: Guest<Detcore<T>>,
9753    T: RecordOrReplay,
9754{
9755    // The successful exec callback must bind a surviving worker to its new
9756    // backend TID before issuing ordinary replacement-image requests. In
9757    // particular, a missed reconnect must not turn ThreadExited into exit(0).
9758    // Consuming cancellation and the reconnect itself use their authenticated
9759    // direct RPC paths instead of this ordinary-request helper.
9760    assert_eq!(
9761        DetTid::from_raw(guest.tid().as_raw()),
9762        guest.thread_state().dettid,
9763        "replacement image must reconnect before ordinary RPCs"
9764    );
9765    // Any request may let another thread run, which may then decide from
9766    // this thread's process's dispositions or this thread's mask.
9767    crate::syscalls::signal_shadow::publish_dispositions(guest).await;
9768    crate::syscalls::signal_shadow::publish_mask(guest).await;
9769    if !matches!(request, GlobalRequest::StartNewThread(..)) {
9770        publish_runtime_private_memory(guest).await;
9771    }
9772    let mytime = guest.thread_state().thread_logical_time.clone();
9773    let mm = guest.thread_state().mm_id;
9774    let continues_sigreturn = request_continues_sigreturn(&request);
9775    let starts_thread = matches!(request, GlobalRequest::StartNewThread(..));
9776    let mut resp = guest.send_rpc((mytime, mm, counted_request(request))).await;
9777    let lone_grant = if let GlobalResponse::WithLoneGrant(response) = resp.1 {
9778        resp.1 = *response;
9779        true
9780    } else {
9781        false
9782    };
9783    guest.thread_state_mut().lone_grant = lone_grant;
9784    if guest.thread_state().replaying_lone_slice_end.is_some() {
9785        // A grant while a skipped slice end is processed must find the
9786        // thread alone again: nothing but its own requests could have
9787        // changed that (`Detcore::catch_up_lone_slice_ends` reports it).
9788        if !lone_grant && matches!(resp.1, GlobalResponse::Granted(_)) {
9789            guest.thread_state_mut().lone_replay_unsound = true;
9790        }
9791    } else if !lone_grant {
9792        // Another thread may run before this one next enters Detcore: its
9793        // pending slice end needs its stop again, whatever this callback
9794        // does next.
9795        crate::stop_skipping_lone_slice_end(guest);
9796    }
9797    // A grant while the request was pending is a hand-off to the scheduler,
9798    // the one place every hand-off is noted (`ThreadState::note_scheduler_handoff`).
9799    if let GlobalResponse::Granted(response) = resp.1 {
9800        resp.1 = *response;
9801        guest
9802            .thread_state_mut()
9803            .note_scheduler_handoff(continues_sigreturn);
9804    }
9805    if let GlobalResponse::WithSliceBoundaryOwner(owner, response) = resp.1 {
9806        resp.1 = *response;
9807        crate::slice_boundary::adopt_owner(guest.thread_state_mut(), owner, starts_thread)
9808            .unwrap_or_else(|error| crate::scheduler::exit_on_scheduler_refusal(&error));
9809    }
9810    let boundary_budget = match resp.1 {
9811        GlobalResponse::WithSliceBoundaryBudget(budget, response) => {
9812            resp.1 = *response;
9813            Some(budget)
9814        }
9815        _ => None,
9816    };
9817    let deadline = match resp.1 {
9818        GlobalResponse::WithDeadline(remaining, response) => {
9819            resp.1 = *response;
9820            Some(remaining)
9821        }
9822        _ => None,
9823    };
9824    if let GlobalResponse::WithSliceBoundaryStack(path, response) = resp.1 {
9825        resp.1 = *response;
9826        write_backtrace(guest, path.as_ref());
9827    }
9828    if let GlobalResponse::WithCallbackHistory(checkpoints, response) = resp.1 {
9829        resp.1 = *response;
9830        guest
9831            .thread_state_mut()
9832            .callback_history
9833            .install(checkpoints);
9834    } else if matches!(resp.1, GlobalResponse::StartNewThread(_)) {
9835        assert!(
9836            !guest.thread_state().callback_history.is_active(),
9837            "preserved callback activation matches coordinator startup"
9838        );
9839    }
9840    guest.thread_state_mut().last_grant_deadline = deadline;
9841    cap_timeslice_at_deadline(guest, deadline);
9842    if resp.1 == GlobalResponse::Retired {
9843        // A committed SIGKILL victim (`Scheduler::is_unaccounted_sigkill_victim`):
9844        // this callback never resumes. No error, errno or injection reaches
9845        // the backend; its terminal owner consumes the real death.
9846        trace!(
9847            "[detcore, dtid {}] waiting for the kernel to end a SIGKILLed thread",
9848            guest.thread_state().dettid
9849        );
9850        return std::future::pending().await;
9851    }
9852    if resp.1 == GlobalResponse::ThreadExited {
9853        let dettid = guest.thread_state().dettid;
9854        // A thread an exit_group killed, on DBT where the exit_group's handler
9855        // walks the group's robust lists after the grant: its own native exit
9856        // would let Linux mark and clear the same words before the walk reads
9857        // them, so it retires only after the walk
9858        // (https://github.com/rrnewton/hermit/issues/4302).
9859        guest.thread_state().cancel_exit_group_filing();
9860        if !guest
9861            .thread_state()
9862            .wait_for_exit_group_robust_walk(crate::tool_local::EXIT_GROUP_ROBUST_WALK_WAIT)
9863        {
9864            // Retiring now would let this thread's native exit race a walk
9865            // that may still be running, so the run fails instead.
9866            panic!(
9867                "[detcore, dtid {}] EXIT_GROUP_ROBUST_WALK_TIMEOUT: the exit_group's robust-list walk neither finished nor was cancelled within {:?}",
9868                dettid,
9869                crate::tool_local::EXIT_GROUP_ROBUST_WALK_WAIT,
9870            );
9871        }
9872        trace!(
9873            "[detcore, dtid {}] exiting after terminal scheduler cancellation",
9874            dettid
9875        );
9876        // This exact task must never resume, but a sibling may still own the
9877        // winning exit-group commit. Retire only this task; explicit backend
9878        // cancellation could cancel that issuer or lose its later status.
9879        guest.retire_current_thread().await
9880    }
9881    // AUTONOMOUS-BOT-IMPLEMENTED
9882    // TODO-HUMAN-REVIEW(PR-845): Review applying the coordinator clock after exec reload.
9883    if let Some(time) = resp.0 {
9884        guest
9885            .thread_state_mut()
9886            .thread_logical_time
9887            .advance_to(time);
9888    }
9889    if let Some(budget) = boundary_budget {
9890        let installed = if starts_thread {
9891            crate::slice_boundary::grant::stage_start(guest.thread_state_mut(), budget)
9892        } else {
9893            crate::slice_boundary::grant::install(guest.thread_state_mut(), budget)
9894        };
9895        installed.unwrap_or_else(|error| crate::scheduler::exit_on_scheduler_refusal(&error));
9896    }
9897    resp
9898}
9899
9900/// When the thread resumes after a potentially-blocking scheduler request, is it a normal
9901/// continuation of execution, or is it because the thread will now execute a signal handler.
9902/// If the latter, that interrupts logically blocking syscalls that were in progress.
9903#[derive(PartialEq, Debug, Eq, Clone, Serialize, Deserialize)]
9904pub enum ResumeStatus {
9905    Normal,
9906    Signaled(Option<Vec<SigWrapper>>),
9907    /// A validated one-shot SIGALRM wake of this exact in-guest sleep.
9908    InGuestFatalAlarm,
9909    /// Granted, and nothing else can run (`SchedResponse::GoAlone`).
9910    NothingElseCanRun,
9911}
9912
9913/// Internal result of a scheduler operation. Terminal results become
9914/// [`GlobalResponse::ThreadExited`] before returning to the guest-side RPC helper.
9915#[derive(PartialEq, Debug, Eq, Clone)]
9916enum SchedulerRpcResult<T> {
9917    Continue(T),
9918    ThreadExited,
9919}
9920
9921/// Wake one waiter per word an exec's owner death marked, for the next
9922/// deterministic drain (https://github.com/rrnewton/hermit/issues/4268).
9923/// The INFO record is not logged on DBT, whose coordinator has no tracing
9924/// subscriber, so there an L2 run compares the guest's output only (Claude
9925/// review of https://github.com/rrnewton/hermit/pull/4352, F2).
9926fn wake_exec_robust_waiters(sched: &mut Scheduler, wakes: &[(DetTid, FutexID)], at: &str) {
9927    if wakes.is_empty() {
9928        return;
9929    }
9930    let counts = sched.wake_futex_waiters_after_exit(wakes);
9931    for ((owner, futex), count) in wakes.iter().zip(counts) {
9932        info!(
9933            "[detcore, dtid {}] robust-list owner death woke {} waiter(s) on futex {:?} at {}",
9934            owner, count, futex, at,
9935        );
9936    }
9937}
9938
9939/// A process pidfd for the host process `pid` (`pidfd_open` with no flags),
9940/// which reports exit only once the whole thread group has exited.
9941pub(crate) fn open_process_pidfd(pid: i32) -> std::io::Result<std::os::fd::OwnedFd> {
9942    // SAFETY: pidfd_open takes no pointers.
9943    let raw = unsafe { libc::syscall(libc::SYS_pidfd_open, pid, 0) };
9944    if raw < 0 {
9945        return Err(std::io::Error::last_os_error());
9946    }
9947    // SAFETY: pidfd_open returned a new descriptor owned by this process.
9948    Ok(unsafe { std::os::fd::OwnedFd::from_raw_fd(raw as libc::c_int) })
9949}
9950
9951/// The pidfd that holds a SIGKILL's sender until victim `detpid` is gone, or
9952/// none when there is nothing left to wait for. The sender is never left
9953/// unfenced (Codex design review of
9954/// https://github.com/rrnewton/hermit/issues/4362 part 2, P2-3):
9955/// - for a kill that named the process (`process_directed`: kill or a pidfd),
9956///   a zombie whose exit the schedule granted: nothing to wait for
9957///   (https://github.com/rrnewton/hermit/issues/4411);
9958/// - otherwise the victim's host process is the one registered for `named`,
9959///   the thread the kill named, or else, for a kill that named the process,
9960///   for the victim's lowest registered thread, as when a pidfd names a leader
9961///   that has exited while its siblings run on
9962///   (https://github.com/rrnewton/hermit/issues/4411). A thread-directed
9963///   SIGKILL to an exited thread gets no fallback, because Linux does not end
9964///   the group for it (Claude review of
9965///   https://github.com/rrnewton/hermit/pull/4439, R1). With no such
9966///   thread, the run is refused: whether the victim still has a live thread
9967///   cannot decide it, because the kill may already have reached the kernel
9968///   (Claude re-check of https://github.com/rrnewton/hermit/pull/4423, N1);
9969/// - opening the process pidfd fails with ESRCH (or EINVAL, which some older
9970///   kernels gave for a reaped process whose pid number lived on as a process
9971///   group or session id): the process is gone only if that thread's own
9972///   `PIDFD_THREAD` pidfd, held since it registered, reports that it exited;
9973///   otherwise the run is refused;
9974/// - any other open failure, such as EMFILE: the run is refused.
9975fn sigkill_fence_for(
9976    scheduler: &Scheduler,
9977    named: DetTid,
9978    detpid: DetPid,
9979    process_directed: bool,
9980    open: impl Fn(i32) -> std::io::Result<std::os::fd::OwnedFd>,
9981) -> Result<Option<std::os::fd::OwnedFd>, crate::scheduler::sigkill_retirement::UnfencedSigkill> {
9982    let unfenced = |reason: String| crate::scheduler::sigkill_retirement::UnfencedSigkill {
9983        process: detpid,
9984        reason,
9985    };
9986    if process_directed && scheduler.process_exit_granted(detpid) {
9987        return Ok(None);
9988    }
9989    let Some(via) = scheduler.sigkill_victim_registered_thread(named, detpid, process_directed)
9990    else {
9991        return Err(unfenced(if process_directed {
9992            format!(
9993                "neither the thread it named (dettid {named}) nor any other thread of it has a registered host process"
9994            )
9995        } else {
9996            format!(
9997                "the thread it named (dettid {named}) has no registered host process, and a SIGKILL to that thread alone does not end its group"
9998            )
9999        }));
10000    };
10001    let host = scheduler
10002        .physical_process_of(via)
10003        .expect("a registered thread has a host process");
10004    match open(host) {
10005        Ok(pidfd) => Ok(Some(pidfd)),
10006        Err(error)
10007            if matches!(error.raw_os_error(), Some(libc::ESRCH | libc::EINVAL))
10008                && scheduler.registered_thread_has_exited(via) == Some(true) =>
10009        {
10010            Ok(None)
10011        }
10012        Err(error) => Err(unfenced(format!(
10013            "pidfd_open of its host process failed: {error}"
10014        ))),
10015    }
10016}
10017
10018/// `sigkill_fence_for`, ending the run on its refusal before the sender is
10019/// answered.
10020fn fence_or_refuse(
10021    scheduler: &Scheduler,
10022    named: DetTid,
10023    detpid: DetPid,
10024    process_directed: bool,
10025) -> Option<(DetPid, std::os::fd::OwnedFd)> {
10026    match sigkill_fence_for(
10027        scheduler,
10028        named,
10029        detpid,
10030        process_directed,
10031        open_process_pidfd,
10032    ) {
10033        Ok(fence) => fence.map(|pidfd| (detpid, pidfd)),
10034        Err(refusal) => crate::scheduler::exit_on_scheduler_refusal(&refusal),
10035    }
10036}
10037
10038/// Wait until the process behind `pidfd` has exited, for at most `valve`;
10039/// why not, otherwise.
10040async fn wait_for_process_exit(pidfd: std::os::fd::OwnedFd, valve: Duration) -> Result<(), String> {
10041    let fd = tokio::io::unix::AsyncFd::with_interest(pidfd, tokio::io::Interest::READABLE)
10042        .map_err(|error| format!("its pidfd cannot be awaited: {error}"))?;
10043    match tokio::time::timeout(valve, fd.readable()).await {
10044        Ok(Ok(_)) => Ok(()),
10045        Ok(Err(error)) => Err(format!("waiting on its pidfd failed: {error}")),
10046        Err(_) => Err(format!("it was not gone within {valve:?}")),
10047    }
10048}
10049
10050/// Whether the reply to `request` can resume its thread from a scheduler grant
10051/// (`Scheduler::unblock_guest`), or answers a timer it armed: the requests
10052/// whose replies carry a deadline hint
10053/// (<https://github.com/rrnewton/hermit/issues/4121>). Every handler that
10054/// awaits a grant is reached from one of these.
10055/// Whether `request` is a signal delivery that continues the thread's previous
10056/// one (`Resources::continues_sigreturn`), as an ordinary or a parked request.
10057fn request_continues_sigreturn(request: &GlobalRequest) -> bool {
10058    match request {
10059        GlobalRequest::WithForwardedCount(_, request) => request_continues_sigreturn(request),
10060        GlobalRequest::RequestResources(resources, _)
10061        | GlobalRequest::ParkedRequest(resources, ..) => resources.continues_sigreturn,
10062        _ => false,
10063    }
10064}
10065
10066// Only the actual sender's registration is pre-admission. A parent's
10067// continuation after registering a different child remains an ordinary grant.
10068fn registers_own_image(sender: DetTid, request: &GlobalRequest) -> bool {
10069    match request {
10070        GlobalRequest::WithForwardedCount(_, request) => registers_own_image(sender, request),
10071        GlobalRequest::CreateChildThread(child, ..) => *child == sender,
10072        GlobalRequest::CreateVforkChildThread(_, _, child, ..) => *child == sender,
10073        _ => false,
10074    }
10075}
10076
10077fn resumes_from_grant(request: &GlobalRequest) -> bool {
10078    match request {
10079        GlobalRequest::WithForwardedCount(_, request) => resumes_from_grant(request),
10080        GlobalRequest::RequestResources(..)
10081        | GlobalRequest::ParkedRequest(..)
10082        | GlobalRequest::ResumeParkedRequest { .. }
10083        | GlobalRequest::ResumeExec(..)
10084        | GlobalRequest::CreateChildThread(..)
10085        | GlobalRequest::CreateVforkChildThread(..)
10086        | GlobalRequest::StartNewThread(..)
10087        | GlobalRequest::FutexAction(..)
10088        | GlobalRequest::TraceSchedEvent(..)
10089        | GlobalRequest::RegisterAlarm(..)
10090        | GlobalRequest::AlarmRemaining(..)
10091        | GlobalRequest::ReconcileAlarm(..)
10092        | GlobalRequest::RegisterPosixTimer(..) => true,
10093        _ => false,
10094    }
10095}
10096
10097/// End this thread's slice no later than the earliest deadline that was
10098/// pending when it was granted the turn it resumes
10099/// (<https://github.com/rrnewton/hermit/issues/4121>).
10100///
10101/// A deadline is acted on only at a scheduler turn, and a thread running a
10102/// long slice of cheap syscalls takes none, so without this a sleeper's
10103/// wake-up waits for the whole slice.
10104pub(crate) fn cap_timeslice_at_deadline<G, T>(guest: &mut G, deadline: Option<Duration>)
10105where
10106    G: Guest<Detcore<T>>,
10107    T: RecordOrReplay,
10108{
10109    let Some(remaining) = deadline else {
10110        return;
10111    };
10112    let clock_multiplier = guest.config().clock_multiplier.unwrap_or(1.0);
10113    let schedule_replay = guest
10114        .thread_state()
10115        .follows_replayed_schedule(guest.config());
10116    // A configuration with an invalid turn cost is refused before any guest
10117    // runs; zero here only drops the floor.
10118    let min_slice = Duration::from_nanos(guest.config().scheduler_turn_charge().unwrap_or(0));
10119    guest.thread_state_mut().cap_timeslice_at_deadline(
10120        remaining,
10121        clock_multiplier,
10122        min_slice,
10123        schedule_replay,
10124    );
10125}
10126
10127/// The guest mask a request publishes under the in-guest delivery gate
10128/// (`Resources::guest_signal_mask`; A5 step 3c.1b, design 18.16.1 section 6,
10129/// <https://github.com/rrnewton/hermit/issues/4485>), read in the thread's own
10130/// turn just before the request leaves it, or `None`:
10131/// - `None` with the gate (`Config::in_guest_signal_delivery`) off, so a run
10132///   without it sends exactly what it sent before;
10133/// - `None` when the call's temporary mask could not be read
10134///   (`Resources::blocked_signal_mask_unknown`): nothing is claimed;
10135/// - a call's temporary mask (`Resources::blocked_signal_mask`: the mask
10136///   argument of `rt_sigsuspend` or a masked `pselect6`, read in this turn),
10137///   which governs the thread while it sleeps in, or is readmitted from, that
10138///   call, as Linux's `blocked` holds it then;
10139/// - otherwise the backend's record of the mask the guest resumes under
10140///   (`Guest::resume_signal_mask`), `None` from a backend that keeps no guest
10141///   mask apart. In a process whose SIGALRM is a signal-phase-1 guest handler
10142///   (`BackendCapabilities::virtualizes_guest_sigalrm` and
10143///   `ThreadState::sigalrm_handled`), its SIGALRM bit is the runtime's virtual
10144///   one, as this thread last published it to the scheduler's ledger
10145///   (`ThreadState::sigalrm_blocked_published`, synced at the callback's
10146///   entry before any request), not the physical one. Phase 1 refuses every
10147///   temporary-mask call in such a process, so a temporary mask is never
10148///   adjusted.
10149pub(crate) fn publish_request_guest_mask<G, T>(guest: &G, r: &Resources) -> Option<u64>
10150where
10151    G: Guest<Detcore<T>>,
10152    T: RecordOrReplay,
10153{
10154    if !guest.config().in_guest_signal_delivery || r.blocked_signal_mask_unknown {
10155        return None;
10156    }
10157    if let Some(temporary) = r.blocked_signal_mask {
10158        return Some(temporary);
10159    }
10160    let own = guest.resume_signal_mask()?;
10161    let thread = guest.thread_state();
10162    if guest.config().backend.virtualizes_guest_sigalrm && thread.sigalrm_handled {
10163        let sigalrm = 1_u64 << (libc::SIGALRM - 1);
10164        return Some(if thread.sigalrm_blocked_published {
10165            own | sigalrm
10166        } else {
10167            own & !sigalrm
10168        });
10169    }
10170    Some(own)
10171}
10172
10173/// Global method RPC to request to control a resource.
10174///
10175/// Blocking: future returns only when resources are fully acquired.
10176pub async fn resource_request<G, T>(guest: &mut G, mut r: Resources) -> ResumeStatus
10177where
10178    G: Guest<Detcore<T>>,
10179    T: RecordOrReplay,
10180{
10181    // A request made while handling a syscall whose cost the bootstrap window
10182    // withholds carries that fact to the scheduler, which then withholds the
10183    // turn's scheduler time too (see `Scheduler::bump_global_time`). Only a
10184    // backend whose `is_backend_runtime_bootstrap` can return true ever sets it.
10185    r.backend_runtime_bootstrap |=
10186        guest.thread_state().in_uncharged_bootstrap_syscall && guest.is_backend_runtime_bootstrap();
10187    // A descriptor this thread may have passed out of the guest reaches the
10188    // scheduler with its next request, before it makes another decision
10189    // (`Resources::passes_descriptors_out`). Keep the local fact: publication
10190    // can suspend before the request reaches the scheduler, and cancellation
10191    // must leave it available to the consuming exit hook.
10192    r.passes_descriptors_out |= guest.thread_state().passed_descriptors_out_unreported;
10193    let carried_export = r.passes_descriptors_out;
10194    // Any turn of its own is a hand-off under `--pipe-wake-latency`: the
10195    // thread goes back behind the pollers it readied (`crate::pipe_wake`).
10196    guest.thread_state_mut().pipe_handoff_due = None;
10197    if guest.config().sequentialize_threads {
10198        // Native input authority follows this actual hand-off, not the fact
10199        // that this thread once received a grant. Observation-only requests
10200        // below cannot create replacement execution authority.
10201        crate::reserved_input::invalidate(guest);
10202        if let Some(lease) = guest.signal_observation_lease() {
10203            if let Some(site) = guest.parked_signal_site() {
10204                return parked::capable_resource_request(
10205                    guest,
10206                    r,
10207                    ControlCapability::PublishOnly { lease, site },
10208                )
10209                .await;
10210            }
10211            parked::terminate_protocol(guest, ProtocolFailure::Identity).await;
10212        }
10213        let dettid = guest.thread_state().dettid;
10214        let detpid = guest.thread_state().detpid.expect("detpid unset");
10215        trace!(
10216            "[detcore, dtid {}] BLOCKING on resource_request rpc... {:?}",
10217            &dettid, r
10218        );
10219        r.guest_signal_mask = publish_request_guest_mask(guest, &r);
10220        let resp =
10221            send_and_update_time(guest, GlobalRequest::RequestResources(r.clone(), detpid)).await;
10222        if carried_export {
10223            // The scheduler has been told: consume the local fact now that
10224            // publication completed (a future dropped before this point
10225            // retains it for the exit hook), and do not report it again.
10226            guest.thread_state_mut().passed_descriptors_out_unreported = false;
10227            guest.thread_state_mut().passed_descriptors_out_reported = true;
10228        }
10229        match resp.1 {
10230            GlobalResponse::RequestResources(x) => {
10231                use reverie::reserved_input::InputGrantKind;
10232                let pending = r.resources.keys().find_map(|resource| match resource {
10233                    ResourceID::BlockingVfork(op) => Some(op.sequence),
10234                    _ => None,
10235                });
10236                let background_or_terminal = r.resources.keys().any(|resource| {
10237                    matches!(
10238                        resource,
10239                        ResourceID::BlockingExternalIO(_)
10240                            | ResourceID::BlockingRtSigsuspend(_)
10241                            | ResourceID::Exit { .. }
10242                            | ResourceID::InGuestFatalAlarm
10243                    )
10244                });
10245                if !background_or_terminal && !matches!(x, ResumeStatus::InGuestFatalAlarm) {
10246                    crate::reserved_input::granted(
10247                        guest,
10248                        pending.map_or(InputGrantKind::Ordinary, |logical_operation| {
10249                            InputGrantKind::PendingVfork { logical_operation }
10250                        }),
10251                    );
10252                }
10253                trace!(
10254                    "[detcore, dtid {}] UNBLOCKED, acquired resources: {:?}",
10255                    &dettid, r
10256                );
10257                x
10258            }
10259            _ => unreachable!(),
10260        }
10261    } else {
10262        ResumeStatus::Normal
10263    }
10264}
10265
10266/// Global method RPC to release all held resources.
10267///
10268/// Nonblocking: future may return immediately before the central global object has
10269/// processed the resource release.
10270pub async fn resource_release_all<G, T>(guest: &mut G)
10271where
10272    G: Guest<Detcore<T>>,
10273    T: RecordOrReplay,
10274{
10275    if guest.config().sequentialize_threads {
10276        // This bookkeeping RPC does not release the scheduler turn:
10277        // recv_release_all_resources only records the notification. Preserve
10278        // its existing input grant; resource_request revokes it at a real
10279        // hand-off. A reply here must neither erase nor mint that authority.
10280        let resp = send_and_update_time(guest, GlobalRequest::ReleaseAllResources).await;
10281        match resp.1 {
10282            GlobalResponse::ReleaseAllResources(x) => x,
10283            _ => unreachable!(),
10284        }
10285    }
10286}
10287
10288/// Global method RPC to allow a new thread to begin execution, called from the child thread.
10289///
10290/// Blocking: future returns only when the thread execution is truly ready to proceed.
10291///
10292/// Returns: a history of the thread preemptions, for it to play back when --replay-preemptions-from
10293/// is used.
10294pub async fn thread_start_request<G, T>(
10295    cfg: &Config,
10296    guest: &mut G,
10297    detpid: DetPid,
10298) -> Option<ThreadHistory>
10299where
10300    G: Guest<Detcore<T>>,
10301    T: RecordOrReplay,
10302{
10303    let dettid = guest.thread_state().dettid;
10304    if cfg.sequentialize_threads {
10305        trace!("[detcore, dtid {}] new thread BLOCKING on rpc...", &dettid);
10306        let physical_ids = guest
10307            .thread_state()
10308            .physical_tid
10309            .map(|tid| (guest.pid().as_raw(), tid));
10310        let signal_identity = guest.signal_task_identity();
10311        guest.thread_state_mut().signal_task_identity = signal_identity;
10312        let resp = send_and_update_time(
10313            guest,
10314            GlobalRequest::StartNewThread(dettid, detpid, physical_ids, signal_identity),
10315        )
10316        .await;
10317        match resp.1 {
10318            GlobalResponse::StartNewThread(preempts) => {
10319                // Publish only after the deterministic start grant. Before
10320                // it, parent registration and this RPC can race in host time.
10321                publish_runtime_private_memory(guest).await;
10322                crate::reserved_input::granted(
10323                    guest,
10324                    reverie::reserved_input::InputGrantKind::Ordinary,
10325                );
10326                trace!("[detcore, dtid {}] new thread UNBLOCKED (post-rpc)", dettid);
10327                preempts
10328            }
10329            _ => unreachable!(),
10330        }
10331    } else {
10332        None
10333    }
10334}
10335
10336/// Complete the existing startup reset after its grant. In particular, a
10337/// boundary budget arrives before this reset and is installed only after it;
10338/// the legacy preemption-history and deadline-cap ordering is unchanged.
10339pub(crate) fn finish_thread_start<G, T>(cfg: &Config, guest: &mut G, history: Option<ThreadHistory>)
10340where
10341    G: Guest<Detcore<T>>,
10342    T: RecordOrReplay,
10343{
10344    let reset = {
10345        let thread = guest.thread_state_mut();
10346        thread.preemption_points = history.map(|history| history.into_iter());
10347        crate::slice_boundary::grant::reset_start(thread, cfg)
10348            .unwrap_or_else(|error| crate::scheduler::exit_on_scheduler_refusal(&error))
10349    };
10350    let deadline = guest.thread_state_mut().last_grant_deadline.take();
10351    cap_timeslice_at_deadline(guest, deadline);
10352    crate::slice_boundary::grant::finish_start(guest.thread_state_mut(), reset)
10353        .unwrap_or_else(|error| crate::scheduler::exit_on_scheduler_refusal(&error));
10354}
10355
10356/// Keep only the clone pointer that requests an exit-time clear and wake.
10357/// SETTID alone requests a birth-time store and must not register an exit wake.
10358pub(crate) fn child_tid_clear_address(flags: CloneFlags, address: usize) -> usize {
10359    if flags.contains(CloneFlags::CLONE_CHILD_CLEARTID) {
10360        address
10361    } else {
10362        0
10363    }
10364}
10365
10366/// Global method RPC for the parent to add a child-thread to the round-robin pool.
10367///
10368/// Nonblocking: future returning does not guarantee anything about the central scheduler,
10369/// except that it will eventually give a slot to the child.  Then the protocol is that
10370/// child will subsequently make a `thread_start_request` to gate the start of its execution.
10371pub async fn create_child_thread<G, T>(
10372    guest: &mut G,
10373    child_dettid: DetTid,
10374    ctid: usize,
10375    flags: Option<CloneFlags>,
10376    exit_signal: libc::c_int,
10377    physical_ids: Option<(i32, i32)>,
10378) -> Option<MmId>
10379where
10380    G: Guest<Detcore<T>>,
10381    T: RecordOrReplay,
10382{
10383    // Random (or replayed) starting priority in chaos mode, constant priority otherwise.
10384    let starting_priority = if guest.config().replay_preemptions_from.is_some() {
10385        // In preemption replay mode, the initial priority is set on the other
10386        // side of the rpc, in recv_create_child_thread.
10387        None
10388    } else if guest.config().replay_schedule_from.is_some() {
10389        // FIXME!  Find a cleaner way to make the root thread start off high-priority:
10390        if child_dettid <= DetTid::from_raw(3) {
10391            Some(REPLAY_FOREGROUND_PRIORITY)
10392        } else {
10393            Some(REPLAY_DEFERRED_PRIORITY)
10394        }
10395    } else if guest.config().chaos {
10396        let entropy = guest
10397            .thread_state_mut()
10398            .chaos_prng_next_u64("child_priority");
10399        if guest.config().chaos_target_races {
10400            // Targeted chaos: bias a freshly created child to an extreme priority
10401            // so it either runs before the parent resumes or strictly after it,
10402            // instead of landing at a uniformly random priority. This maximizes
10403            // parent/child ordering divergence to surface fork/exec races.
10404            // Reproducible under `--fuzz-seed`/`--sched-seed`.
10405            if entropy.is_multiple_of(2) {
10406                Some(FIRST_PRIORITY)
10407            } else {
10408                Some(LAST_PRIORITY)
10409            }
10410        } else {
10411            Some(entropy_to_priority(entropy))
10412        }
10413    } else {
10414        Some(DEFAULT_PRIORITY)
10415    };
10416
10417    let detpid = guest.thread_state().detpid.expect("detpid unset");
10418
10419    // Under the in-guest delivery gate (A5 step 3c.1e A), the creator
10420    // publishes its current guest mask before the child is registered and
10421    // seeded from it (`Scheduler::seed_child_guest_sigmask`), as Linux's
10422    // child starts with its creator's `blocked` at the clone
10423    // (`arch_dup_task_struct`, kernel/fork.c:1086-1091 at v6.12). An ordinary
10424    // clone files no request, so without this a mask changed since the
10425    // creator's last request (an `rt_sigprocmask` just before the `fork`)
10426    // would seed the child with the older one. The mask is the one a request
10427    // without a temporary mask publishes (`publish_request_guest_mask`: the
10428    // backend's record of the guest's mask, with phase 1's virtual SIGALRM
10429    // bit; a clone never runs inside a masked call), nothing on a backend
10430    // that keeps no guest mask apart, so readers take the kernel's `SigBlk`.
10431    // Non-yielding; nothing is sent with the gate off. A vfork's own request
10432    // already publishes the mask before its child registers.
10433    if flags.is_some() && guest.config().in_guest_signal_delivery {
10434        let current =
10435            publish_request_guest_mask(guest, &Resources::new(guest.thread_state().dettid));
10436        set_saved_guest_sigmask(guest, current).await;
10437    }
10438
10439    let resp = send_and_update_time(
10440        guest,
10441        GlobalRequest::CreateChildThread(
10442            child_dettid,
10443            detpid,
10444            ctid,
10445            flags,
10446            exit_signal,
10447            physical_ids,
10448            starting_priority,
10449        ),
10450    )
10451    .await;
10452    match resp.1 {
10453        GlobalResponse::CreateChildThread(x) => x,
10454        _ => unreachable!(),
10455    }
10456}
10457
10458/// Register a vfork child while its parent is blocked inside `clone(2)`.
10459///
10460/// Unlike an ordinary clone, the parent cannot perform this registration
10461/// because the kernel has blocked it until the child execs or exits. The child
10462/// therefore registers itself, carrying the inherited parent identity, flags,
10463/// and starting priority. The starting priority is derived the same way as an
10464/// ordinary clone so that chaos and replay scheduling stay deterministic.
10465pub async fn create_vfork_child_thread<G, T>(
10466    guest: &mut G,
10467    child_dettid: DetTid,
10468    vfork: crate::tool_local::PendingVfork,
10469) where
10470    G: Guest<Detcore<T>>,
10471    T: RecordOrReplay,
10472{
10473    let starting_priority = if guest.config().replay_preemptions_from.is_some() {
10474        None
10475    } else if guest.config().replay_schedule_from.is_some() {
10476        Some(if child_dettid <= DetTid::from_raw(3) {
10477            REPLAY_FOREGROUND_PRIORITY
10478        } else {
10479            REPLAY_DEFERRED_PRIORITY
10480        })
10481    } else if guest.config().chaos {
10482        Some(entropy_to_priority(vfork.child_priority_entropy.expect(
10483            "vfork child priority entropy missing in chaos mode",
10484        )))
10485    } else {
10486        // POSIX vfork suspends the parent until the child execs or _exits. Give
10487        // the vfork child a strictly higher priority (lower number) than the
10488        // parent's DEFAULT_PRIORITY so the deterministic scheduler always runs
10489        // the child first, rather than round-robining the parent and child at
10490        // equal priority (which leaves fork/exec ordering nondeterministic).
10491        Some(DEFAULT_PRIORITY - 1)
10492    };
10493
10494    let resp = send_and_update_time(
10495        guest,
10496        GlobalRequest::CreateVforkChildThread(
10497            vfork.parent_dettid,
10498            vfork.parent_detpid,
10499            child_dettid,
10500            vfork.child_tid_addr,
10501            vfork.flags,
10502            vfork.exit_signal,
10503            starting_priority,
10504        ),
10505    )
10506    .await;
10507    match resp.1 {
10508        GlobalResponse::CreateChildThread(_) => (),
10509        _ => unreachable!(),
10510    }
10511}
10512
10513/// Whether `dtid` names a thread the scheduler registered and has since
10514/// retired, with no new registration under it yet. Linux gives an ID to a new
10515/// task only once its last holder is gone, and without a PID namespace IDs
10516/// wrap at the host's `pid_max` (32768 on many devices), so a run that keeps
10517/// starting threads gets IDs it used before. Registrations are never removed;
10518/// retirement removes a thread's turn state and a new registration inserts
10519/// it. A message from such an ID therefore comes from a new thread that is
10520/// not registered yet (a vfork child's own registration is one); a live
10521/// thread has turn state.
10522fn id_awaits_new_registration(sched: &Scheduler, dtid: DetTid) -> bool {
10523    sched.thread_was_registered(dtid) && !sched.next_turns.contains_key(&dtid)
10524}
10525
10526#[cfg(test)]
10527mod reserved_input_registration_tests {
10528    use reverie::GlobalTool;
10529    use reverie::reserved_input::ReservedInputControl;
10530
10531    use super::GlobalState;
10532    use crate::Config;
10533    use crate::types::DetTid;
10534    use crate::types::MmId;
10535
10536    #[test]
10537    fn input_registration_binds_explicit_tool_identity_without_a_physical_ids_rpc() {
10538        let mut global = GlobalState::for_tests(&Config::default());
10539        let control = ReservedInputControl::new_backend().unwrap();
10540        global
10541            .install_reserved_input_control(control.clone())
10542            .unwrap();
10543        let native = control.backend_register_root(123, 123).unwrap();
10544        assert!(global.on_reserved_input_task(native).is_err());
10545        let bound = control
10546            .backend_bind_tool_identity(native.task, 5000, 5000)
10547            .unwrap();
10548        global.on_reserved_input_task(bound).unwrap();
10549        // Production initial-state admission seeds this native table before preinit
10550        // or a child can run. This identity fixture models that bookkeeping only.
10551        crate::reserved_input::register_initial_history(&control, native.task).unwrap();
10552        let logical = DetTid::from_raw(5000);
10553        {
10554            let mut scheduler = global.sched.lock().unwrap();
10555            assert!(!scheduler.reserved_input_tasks.contains_key(&logical));
10556            scheduler.record_thread_mm(logical, MmId::initial(logical));
10557            assert_eq!(scheduler.reserved_input_tasks[&logical], native.task);
10558            assert_eq!(scheduler.physical_process_of(logical), None);
10559            assert!(
10560                !scheduler
10561                    .reserved_input_tasks
10562                    .contains_key(&DetTid::from_raw(123))
10563            );
10564        }
10565        let child = control
10566            .backend_register_child(native.task, 124, 123, Some(true), Some(true))
10567            .unwrap();
10568        let child = control
10569            .backend_bind_tool_identity(child.task, 5001, 5000)
10570            .unwrap();
10571        global.on_reserved_input_task(child).unwrap();
10572        {
10573            let mut scheduler = global.sched.lock().unwrap();
10574            let child_tid = DetTid::from_raw(5001);
10575            scheduler.record_thread_mm(child_tid, MmId::initial(logical));
10576            assert_eq!(scheduler.reserved_input_tasks[&child_tid], child.task);
10577        }
10578        let replacement = control
10579            .backend_exec_replacement(native.task, child.task)
10580            .unwrap();
10581        let replacement = control
10582            .backend_bind_tool_identity(replacement.task, 5000, 5000)
10583            .unwrap();
10584        global.on_reserved_input_task(replacement).unwrap();
10585        let scheduler = global.sched.lock().unwrap();
10586        assert_eq!(
10587            scheduler.reserved_input_tool_tasks[&logical],
10588            replacement.task
10589        );
10590        // The original logical exec fence still names both destroyed images.
10591        assert_eq!(scheduler.reserved_input_tasks[&logical], native.task);
10592        assert_eq!(
10593            scheduler.reserved_input_tasks[&DetTid::from_raw(5001)],
10594            child.task
10595        );
10596    }
10597
10598    #[test]
10599    fn input_registration_rejects_a_tool_identity_not_bound_by_the_native_owner() {
10600        let mut global = GlobalState::for_tests(&Config::default());
10601        let control = ReservedInputControl::new_backend().unwrap();
10602        global
10603            .install_reserved_input_control(control.clone())
10604            .unwrap();
10605        let native = control.backend_register_root(123, 123).unwrap();
10606        let mut forged = native;
10607        forged.tool_tid = Some(5000);
10608        forged.tool_pid = Some(5000);
10609        assert!(global.on_reserved_input_task(forged).is_err());
10610        assert!(
10611            global
10612                .sched
10613                .lock()
10614                .unwrap()
10615                .reserved_input_tool_tasks
10616                .is_empty()
10617        );
10618    }
10619}
10620
10621/// Remove the thread from the scheduler.
10622///
10623/// Nonblocking: the future may return immediately, not guaranteeing the changes to the
10624/// scheduler have been completed.
10625pub(crate) async fn deregister_thread<R>(
10626    threads_time: DetTime,
10627    cfg: &Config,
10628    reverie: &R,
10629    thread: ThreadDeregistration,
10630) where
10631    // Note, this is called from a context where we DON'T have a full, operable `Guest`.
10632    R: GlobalRPC<GlobalState>,
10633{
10634    if cfg.sequentialize_threads {
10635        let mm = thread.mm;
10636        // TODO: void_send_rpc
10637        let resp = reverie
10638            .send_rpc((threads_time, mm, {
10639                // The thread's last count; its slot is free for a later thread.
10640                let request = counted_request(GlobalRequest::DeregisterThread(thread));
10641                crate::detlog::release_forwarded_count();
10642                request
10643            }))
10644            .await;
10645        // We can't update the thread time here.  But it's dead anyway!
10646        match resp.1 {
10647            GlobalResponse::DeregisterThread(x) => x,
10648            _ => unreachable!(),
10649        }
10650    }
10651}
10652
10653/// Account this physical-exit callback's own clock before it joins a staged
10654/// robust-list batch. The empty wake request has no scheduler effects, but its
10655/// ordinary RPC header is admitted and accounted before it is acknowledged.
10656/// A retired incarnation must not contribute to the completed-exit barrier.
10657pub(crate) async fn acknowledge_robust_list_exit_time<R>(
10658    threads_time: DetTime,
10659    reverie: &R,
10660    mm: MmId,
10661) -> bool
10662where
10663    R: GlobalRPC<GlobalState>,
10664{
10665    let response = reverie
10666        .send_rpc((threads_time, mm, GlobalRequest::RobustListWakes(Vec::new())))
10667        .await;
10668    match response.1 {
10669        GlobalResponse::RobustListWakes(counts) => {
10670            assert!(
10671                counts.is_empty(),
10672                "an empty exit-clock acknowledgement woke a waiter"
10673            );
10674            true
10675        }
10676        GlobalResponse::ThreadExited => false,
10677        _ => unreachable!(),
10678    }
10679}
10680
10681/// Deliver owner-death wakes from an exit callback that no longer has guest
10682/// memory access. The callback runs only after ptrace has observed physical
10683/// exit, so Linux's atomic robust-list word update is already complete.
10684pub(crate) async fn robust_list_wakes_after_exit<R>(
10685    threads_time: DetTime,
10686    reverie: &R,
10687    mm: MmId,
10688    wakes: Vec<(DetTid, RobustListWake)>,
10689) -> Vec<u64>
10690where
10691    R: GlobalRPC<GlobalState>,
10692{
10693    if wakes.is_empty() {
10694        return Vec::new();
10695    }
10696    let response = reverie
10697        .send_rpc((
10698            threads_time,
10699            mm,
10700            GlobalRequest::RobustListWakes(
10701                wakes
10702                    .into_iter()
10703                    .map(|(owner, wake)| (owner, wake.futex))
10704                    .collect(),
10705            ),
10706        ))
10707        .await;
10708    match response.1 {
10709        GlobalResponse::RobustListWakes(counts) => counts,
10710        _ => unreachable!(),
10711    }
10712}
10713
10714/// Deliver the owner-death wakes staged at a successful exec's entry. The
10715/// exec has returned, so Linux has already walked every robust list in the old
10716/// thread group and changed each owner word atomically.
10717pub(crate) async fn robust_list_wakes_after_exec<G, T>(
10718    guest: &mut G,
10719    wakes: Vec<(DetTid, RobustListWake)>,
10720) -> Vec<u64>
10721where
10722    G: Guest<Detcore<T>>,
10723    T: RecordOrReplay,
10724{
10725    if wakes.is_empty() {
10726        return Vec::new();
10727    }
10728    let (_, response) = send_and_update_time(
10729        guest,
10730        GlobalRequest::RobustListWakes(
10731            wakes
10732                .into_iter()
10733                .map(|(owner, wake)| (owner, wake.futex))
10734                .collect(),
10735        ),
10736    )
10737    .await;
10738    match response {
10739        GlobalResponse::RobustListWakes(counts) => counts,
10740        _ => unreachable!(),
10741    }
10742}
10743
10744/// Which actions we can take before/after a futex system call.
10745#[derive(PartialEq, Debug, Eq, Clone, Copy, Serialize, Deserialize)]
10746pub enum FutexAction {
10747    /// Check in before a FUTEX_WAIT, including an optional timeout and, on a backend
10748    /// that reports the kernel's signal state, the waiter's mask and where the
10749    /// scheduler reads its dispositions when it commits a wake. With `None`, as
10750    /// before, a scheduler-sent signal wakes the waiter and a cross-task signal
10751    /// does not.
10752    WaitRequest(Option<LogicalTime>, Option<FutexSignalWatch>),
10753    /// Check in after a FUTEX_WAIT
10754    WaitFinished,
10755    /// Check in before a FUTEX_WAKE, parameterized by the number of threads woken.
10756    WakeRequest(i32),
10757    /// Check in after a FUTEX_WAKE, parameterized by the number of threads woken.
10758    WakeFinished(i32),
10759    /// Check in for a plain FUTEX_REQUEUE or FUTEX_CMP_REQUEUE of the request's
10760    /// futex onto the given one, waking up to the first count and moving up to
10761    /// the second (`Scheduler::requeue_futex_waiters`). Both counts are already
10762    /// checked to be non-negative, and the futex word already compared for
10763    /// FUTEX_CMP_REQUEUE. Answers with the number woken plus the number moved,
10764    /// and is followed by a `WakeFinished`, as a wake is.
10765    RequeueRequest(FutexID, u32, u32),
10766    /// Ask what the scheduler holds for a priority-inheritance futex whose word
10767    /// names the given TID, and whether the caller queueing would deadlock
10768    /// (`Scheduler::pi_futex_state`). Changes nothing the guest can observe: it
10769    /// answers in the caller's turn.
10770    PiState(u32),
10771    /// Park as a FUTEX_LOCK_PI waiter behind the given owner, found in this
10772    /// turn by `PiState`, with an optional deadline and signal watch as
10773    /// `WaitRequest` has (`Scheduler::sleep_pi_futex_waiter`).
10774    PiLockWait(Option<LogicalTime>, Option<FutexSignalWatch>, DetTid),
10775    /// A woken FUTEX_LOCK_PI waiter takes the lock if it was handed it, and
10776    /// learns how (`Scheduler::take_handed_pi_futex`).
10777    PiTake,
10778    /// FUTEX_UNLOCK_PI hands the futex to its longest PI waiter
10779    /// (`Scheduler::hand_off_pi_futex`); answers with the number woken.
10780    PiHandOff,
10781}
10782
10783/// Ask scheduler for permission to proceed before/after futex operation.
10784/// Returns true if the operation completed normally, and false if it timed out.
10785pub async fn futex_action<G, T>(
10786    guest: &mut G,
10787    futex_action: FutexAction,
10788    futexid: &FutexID,
10789    init_read: i32,
10790    mask: u32,
10791) -> Option<SchedValue>
10792where
10793    G: Guest<Detcore<T>>,
10794    T: RecordOrReplay,
10795{
10796    assert!(guest.config().sequentialize_threads);
10797    let dettid = guest.thread_state().dettid;
10798    let req = GlobalRequest::FutexAction(
10799        dettid,
10800        futex_action,
10801        *futexid,
10802        init_read,
10803        mask,
10804        guest.thread_state().passed_descriptors_out_unreported,
10805    );
10806    trace!(
10807        "BLOCKING on futex_action: sending request to scheduler: {:?}",
10808        req
10809    );
10810    // Update local time from potentially blocking operation:
10811    let resp = send_and_update_time(guest, req.clone()).await;
10812    match resp.1 {
10813        GlobalResponse::FutexAction(answer) => {
10814            trace!("UNBLOCKING after futex_action. Request was: {:?}", req);
10815            answer
10816        }
10817        _ => unreachable!(),
10818    }
10819}
10820
10821/// track a (possibly new) inode, by returning a deterministic inode.
10822/// Also return the logical mtime for the inode, though this is only
10823/// used if `virtualize_metadata` is set.
10824///
10825/// For callers that have not read the file's metadata. A stat must use
10826/// [`determinize_inode_observing_mtime`] so the first-seen mtime policy sees
10827/// the host mtime.
10828pub async fn determinize_inode<G, T>(guest: &mut G, inode: RawInode) -> (DetInode, LogicalTime)
10829where
10830    G: Guest<Detcore<T>>,
10831    T: RecordOrReplay,
10832{
10833    determinize_inode_observing_mtime(
10834        guest,
10835        inode,
10836        None,
10837        ObservedMtime::Unobserved,
10838        InodeSighting::Descriptor,
10839    )
10840    .await
10841}
10842
10843/// [`determinize_inode`] for a directory entry, which names a linked file
10844/// (see [`InodeSighting::Name`]). `procfs` names a procfs entry (see
10845/// `procfs_inode::ProcfsName`).
10846pub async fn determinize_named_inode<G, T>(
10847    guest: &mut G,
10848    inode: RawInode,
10849    procfs: Option<ProcfsName>,
10850) -> (DetInode, LogicalTime)
10851where
10852    G: Guest<Detcore<T>>,
10853    T: RecordOrReplay,
10854{
10855    determinize_inode_observing_mtime(
10856        guest,
10857        inode,
10858        procfs,
10859        ObservedMtime::Unobserved,
10860        InodeSighting::Name,
10861    )
10862    .await
10863}
10864
10865/// Like [`determinize_inode`], for a caller that observed the file's host
10866/// mtime. If the file's virtual mtime is still unresolved, `observed` resolves
10867/// it: a canonical mtime (see [`CANONICAL_FILE_MTIME_SECONDS`]) is kept, and any
10868/// other becomes the epoch. `procfs` names a procfs entry (see
10869/// `procfs_inode::ProcfsName`).
10870pub async fn determinize_inode_observing_mtime<G, T>(
10871    guest: &mut G,
10872    inode: RawInode,
10873    procfs: Option<ProcfsName>,
10874    observed: ObservedMtime,
10875    sighting: InodeSighting,
10876) -> (DetInode, LogicalTime)
10877where
10878    G: Guest<Detcore<T>>,
10879    T: RecordOrReplay,
10880{
10881    let resp = send_and_update_time(
10882        guest,
10883        GlobalRequest::DeterminizeInode(inode, observed, sighting, procfs),
10884    )
10885    .await;
10886    match resp.1 {
10887        GlobalResponse::DeterminizeInode(x) => x,
10888        _ => unreachable!(),
10889    }
10890}
10891
10892/// Determinize the inode of every file-backed line of one `/proc/*/maps` read.
10893/// `inodes` holds one entry per line, in line order, with repeats: each entry
10894/// is one numbering request, so how many requests a read makes depends on the
10895/// guest's mappings alone, never on whether two lines name the same host file
10896/// (<https://github.com/rrnewton/hermit/issues/2897>). The result is in the
10897/// same order.
10898pub async fn determinize_mapping_inodes<G, T>(guest: &mut G, inodes: Vec<RawInode>) -> Vec<DetInode>
10899where
10900    G: Guest<Detcore<T>>,
10901    T: RecordOrReplay,
10902{
10903    let resp = send_and_update_time(guest, GlobalRequest::DeterminizeMappingInodes(inodes)).await;
10904    match resp.1 {
10905        GlobalResponse::DeterminizeMappingInodes(x) => x,
10906        _ => unreachable!(),
10907    }
10908}
10909
10910// AUTONOMOUS-BOT-IMPLEMENTED
10911// TODO-HUMAN-REVIEW(PR-1056): Deterministic st_dev remapping RPC.
10912/// Translate a host-assigned device number (`st_dev`) to a deterministic one.
10913pub async fn determinize_device<G, T>(guest: &mut G, raw_device: u64) -> u64
10914where
10915    G: Guest<Detcore<T>>,
10916    T: RecordOrReplay,
10917{
10918    let resp = send_and_update_time(guest, GlobalRequest::DeterminizeDevice(raw_device)).await;
10919    match resp.1 {
10920        GlobalResponse::DeterminizeDevice(x) => x,
10921        _ => unreachable!(),
10922    }
10923}
10924
10925/// Translate an observed fdinfo mount ID to the shared run-local identity.
10926pub async fn determinize_mount_id<G, T>(
10927    guest: &mut G,
10928    raw_mount_id: u64,
10929    mountinfo_order: Option<Vec<u64>>,
10930) -> Option<u64>
10931where
10932    G: Guest<Detcore<T>>,
10933    T: RecordOrReplay,
10934{
10935    let resp = send_and_update_time(
10936        guest,
10937        GlobalRequest::DeterminizeMountId(raw_mount_id, mountinfo_order),
10938    )
10939    .await;
10940    match resp.1 {
10941        GlobalResponse::DeterminizeMountId(value) => value,
10942        _ => unreachable!(),
10943    }
10944}
10945
10946/// Seed or validate the run-global mount-ID pool against one mountinfo
10947/// snapshot's canonical order, and return the run-local identity of each entry
10948/// of `observation_order`, numbering unseen entries in that order. `None`
10949/// means validation failed or the two orders name different raw IDs.
10950pub async fn resolve_mountinfo_identities<G, T>(
10951    guest: &mut G,
10952    mountinfo_order: Vec<u64>,
10953    observation_order: Vec<u64>,
10954) -> Option<Vec<u64>>
10955where
10956    G: Guest<Detcore<T>>,
10957    T: RecordOrReplay,
10958{
10959    let resp = send_and_update_time(
10960        guest,
10961        GlobalRequest::ResolveMountinfoIds(mountinfo_order, observation_order),
10962    )
10963    .await;
10964    match resp.1 {
10965        GlobalResponse::ResolveMountinfoIds(value) => value,
10966        _ => unreachable!(),
10967    }
10968}
10969
10970/// The file behind `inode` lost its last name (see `InodePool::retire`).
10971pub async fn retire_inode<G, T>(guest: &mut G, inode: RawInode)
10972where
10973    G: Guest<Detcore<T>>,
10974    T: RecordOrReplay,
10975{
10976    let resp = send_and_update_time(guest, GlobalRequest::RetireInode(inode)).await;
10977    match resp.1 {
10978        GlobalResponse::RetireInode(x) => x,
10979        _ => unreachable!(),
10980    }
10981}
10982
10983/// The inode pool's state for a directory snapshot about to be taken (see
10984/// [`SnapshotMark`]).
10985pub async fn directory_snapshot_mark<G, T>(guest: &mut G) -> SnapshotMark
10986where
10987    G: Guest<Detcore<T>>,
10988    T: RecordOrReplay,
10989{
10990    let resp = send_and_update_time(guest, GlobalRequest::DirectorySnapshotMark).await;
10991    match resp.1 {
10992        GlobalResponse::DirectorySnapshotMark(mark) => mark,
10993        _ => unreachable!(),
10994    }
10995}
10996
10997/// `--pipe-wake-latency`: report that the calling thread's action readied the
10998/// pollers of `channel` that want `wants` (`Scheduler::ready_channel`).
10999pub async fn ready_channel<G, T>(
11000    guest: &mut G,
11001    channel: crate::pipe_wake::ChannelId,
11002    wants: crate::pipe_wake::Wants,
11003) -> crate::pipe_wake::ReadyOutcome
11004where
11005    G: Guest<Detcore<T>>,
11006    T: RecordOrReplay,
11007{
11008    let resp = send_and_update_time(guest, GlobalRequest::ReadyChannel(channel, wants)).await;
11009    match resp.1 {
11010        GlobalResponse::ReadyChannel(outcome) => outcome,
11011        _ => unreachable!(),
11012    }
11013}
11014
11015/// `--pipe-wake-latency`: the calling thread's hand-off is due. Returns
11016/// whether the scheduler moved it behind the pollers it readied, so that it
11017/// must now yield (`Scheduler::hand_off_turn`).
11018pub async fn hand_off_turn<G, T>(guest: &mut G) -> bool
11019where
11020    G: Guest<Detcore<T>>,
11021    T: RecordOrReplay,
11022{
11023    let resp = send_and_update_time(guest, GlobalRequest::HandOffTurn).await;
11024    match resp.1 {
11025        GlobalResponse::HandOffTurn(yields) => yields,
11026        _ => false,
11027    }
11028}
11029
11030/// Commit that the calling thread is delivering a signal whose default action
11031/// ends its process (`Scheduler::commit_fatal_delivery`), retiring the
11032/// process's other threads when `retire_others`. Returns whether any was
11033/// retired.
11034pub async fn commit_fatal_delivery<G, T>(guest: &mut G, retire_others: bool) -> bool
11035where
11036    G: Guest<Detcore<T>>,
11037    T: RecordOrReplay,
11038{
11039    matches!(
11040        send_and_update_time(guest, GlobalRequest::CommitFatalDelivery(retire_others))
11041            .await
11042            .1,
11043        GlobalResponse::CommitFatalDelivery(true)
11044    )
11045}
11046
11047/// [`determinize_inode`] for an entry of a directory snapshot taken at
11048/// `mark` (see [`SnapshotMark`]). `procfs` names a procfs entry (see
11049/// `procfs_inode::ProcfsName`).
11050pub async fn determinize_listed_inode<G, T>(
11051    guest: &mut G,
11052    inode: RawInode,
11053    procfs: Option<ProcfsName>,
11054    mark: SnapshotMark,
11055) -> (DetInode, LogicalTime)
11056where
11057    G: Guest<Detcore<T>>,
11058    T: RecordOrReplay,
11059{
11060    determinize_inode_observing_mtime(
11061        guest,
11062        inode,
11063        procfs.map(|name| name.in_snapshot(mark.threads_counted)),
11064        ObservedMtime::Unobserved,
11065        InodeSighting::Listed(mark.retirements),
11066    )
11067    .await
11068}
11069
11070/// An `open` found `inode` linked or freshly created (see
11071/// `InodePool::forget_retired`). `procfs` names a procfs entry (see
11072/// `procfs_inode::ProcfsName`), so that writes through the new descriptor
11073/// update the entry that stats number.
11074pub async fn forget_retired_inode<G, T>(guest: &mut G, inode: RawInode, procfs: Option<ProcfsName>)
11075where
11076    G: Guest<Detcore<T>>,
11077    T: RecordOrReplay,
11078{
11079    let resp = send_and_update_time(guest, GlobalRequest::ForgetRetiredInode(inode, procfs)).await;
11080    match resp.1 {
11081        GlobalResponse::ForgetRetiredInode(x) => x,
11082        _ => unreachable!(),
11083    }
11084}
11085
11086/// Update the modification time for a file, using its inode.
11087/// This will set the mtime to a coherent global-time value.
11088pub async fn touch_file<G, T>(guest: &mut G, inode: RawInode)
11089where
11090    G: Guest<Detcore<T>>,
11091    T: RecordOrReplay,
11092{
11093    let resp = send_and_update_time(guest, GlobalRequest::TouchFile(inode)).await;
11094    match resp.1 {
11095        GlobalResponse::TouchFile(x) => x,
11096        _ => unreachable!(),
11097    }
11098}
11099
11100/// Set a file's virtual mtime to a guest-requested time.
11101pub async fn set_file_mtime<G, T>(guest: &mut G, inode: RawInode, mtime: LogicalTime)
11102where
11103    G: Guest<Detcore<T>>,
11104    T: RecordOrReplay,
11105{
11106    let resp = send_and_update_time(guest, GlobalRequest::SetFileMtime(inode, mtime)).await;
11107    match resp.1 {
11108        GlobalResponse::SetFileMtime(x) => x,
11109        _ => unreachable!(),
11110    }
11111}
11112
11113/// Read the global clock, or at least a deterministic lower bound on it.
11114pub async fn global_time_lower_bound<G, T>(guest: &mut G) -> LogicalTime
11115where
11116    G: Guest<Detcore<T>>,
11117    T: RecordOrReplay,
11118{
11119    let resp = send_and_update_time(guest, GlobalRequest::GlobalTimeLowerBound).await;
11120    match resp.1 {
11121        GlobalResponse::GlobalTimeLowerBound(x) => x,
11122        _ => unreachable!(),
11123    }
11124}
11125
11126/// Take a time observation from the current thread. This extra indirection
11127/// helps abstract over whether or not we need to use local or global
11128/// information for this.
11129pub async fn thread_observe_time<G, T>(guest: &mut G) -> LogicalTime
11130where
11131    G: Guest<Detcore<T>>,
11132    T: RecordOrReplay,
11133{
11134    global_time_lower_bound(guest).await
11135}
11136
11137/// Writes a structured json backtrace to a given file
11138fn write_backtrace<G, T>(guest: &mut G, m_path: Option<&PathBuf>)
11139where
11140    G: Guest<Detcore<T>>,
11141    T: RecordOrReplay,
11142{
11143    if let Some(backtrace) = guest.backtrace() {
11144        if let Some(path) = m_path {
11145            let file = File::create(path).expect("Failed to open preemption stacktrace log file");
11146            serde_json::to_writer(file, &backtrace.force_pretty()).unwrap();
11147        } else {
11148            crate::util::capped_eprintln!("{}", backtrace.force_pretty());
11149        }
11150    } else {
11151        warn!("Could not read backtrace!");
11152    }
11153}
11154
11155/// Additional instructions to a guest after shed events is consumed by global tool
11156#[derive(PartialEq, Debug, Eq, Clone, Serialize, Deserialize)]
11157pub struct TraceSchedEventResponse {
11158    print_stack_strace: MaybePrintStack,
11159    timeslice: Option<LogicalTime>,
11160    /// Whether the replay has dropped the rest of its schedule
11161    /// (`Scheduler::schedule_released`).
11162    schedule_released: bool,
11163    /// Whether the replayed schedule is over: it ran out or was released
11164    /// (`Scheduler::schedule_replay_over`).
11165    schedule_over: bool,
11166}
11167
11168struct SchedEventForLog<'a> {
11169    event: &'a SchedEvent,
11170    command_bootstrap: bool,
11171}
11172
11173struct CommandBootstrapInstructionPointer(NonZeroUsize);
11174
11175impl std::fmt::Debug for CommandBootstrapInstructionPointer {
11176    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
11177        write!(f, "{}", crate::logdiff::host_addr(self.0.get()))
11178    }
11179}
11180
11181impl std::fmt::Debug for SchedEventForLog<'_> {
11182    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
11183        if !self.command_bootstrap {
11184            return std::fmt::Debug::fmt(self.event, f);
11185        }
11186        f.debug_struct("SchedEvent")
11187            .field("dettid", &self.event.dettid)
11188            .field("op", &self.event.op)
11189            .field("count", &self.event.count)
11190            .field(
11191                "start_rip",
11192                &self.event.start_rip.map(CommandBootstrapInstructionPointer),
11193            )
11194            .field(
11195                "end_rip",
11196                &self.event.end_rip.map(CommandBootstrapInstructionPointer),
11197            )
11198            .field("end_time", &self.event.end_time)
11199            .finish()
11200    }
11201}
11202
11203/// Record an event in the schedule trace, OR check the event on replay.
11204/// This also prints the backtrace of the schedevent, if indicated.
11205///
11206/// Arguments:
11207/// - tag_end_rip: read the current guest registers to fill in the `end_rip` on the event with the
11208///   current instruction pointer.
11209pub async fn trace_schedevent<G, T>(guest: &mut G, ev: SchedEvent, tag_end_rip: bool)
11210where
11211    G: Guest<Detcore<T>>,
11212    T: RecordOrReplay,
11213{
11214    trace_schedevent_with_access(guest, ev, tag_end_rip, None).await
11215}
11216
11217/// [`trace_schedevent`], carrying what the event adds to the access trace
11218/// (`crate::access_trace`). The payload rides on the same request: no
11219/// request is added, and an RPC's clock charge does not depend on its size.
11220pub async fn trace_schedevent_with_access<G, T>(
11221    guest: &mut G,
11222    ev: SchedEvent,
11223    tag_end_rip: bool,
11224    access: Option<crate::access_trace::AccessPayload>,
11225) where
11226    G: Guest<Detcore<T>>,
11227    T: RecordOrReplay,
11228{
11229    assert!(guest.config().sequentialize_threads);
11230
11231    // trace_schedevent is called AFTER the event is complete, and the rip is resting just after it.
11232    let ev = if tag_end_rip {
11233        let end_rip = if let Some(r) = ev.end_rip {
11234            r
11235        } else {
11236            // While a SIGALRM delivery's signal pipeline runs at a syscall's
11237            // completion, the registers are its completed projection.
11238            let regs = match guest.thread_state().signal_register_projection {
11239                Some(projection) => projection,
11240                None => guest.regs().await,
11241            };
11242            NonZeroUsize::new(crate::arch::instruction_pointer(&regs).try_into().unwrap()).unwrap()
11243        };
11244        SchedEvent {
11245            end_rip: Some(end_rip),
11246            ..ev
11247        }
11248    } else {
11249        ev
11250    };
11251
11252    if tracing::enabled!(tracing::Level::TRACE)
11253        && let Some(rip) = ev.end_rip
11254    {
11255        let rip_addr = AddrMut::<u16>::from_raw(rip.into()).unwrap();
11256        // These bytes are diagnostic, not part of the recorded event. A guest
11257        // may legitimately make its memory unreadable (PR_SET_DUMPABLE), so
11258        // preserve that observation without unwinding the live run owner.
11259        let rip_contents = guest.memory().read_value(rip_addr);
11260        trace!(
11261            "Tracing sched event, after which rip is {}, next two instruction bytes {:?}",
11262            rip, rip_contents
11263        );
11264    }
11265
11266    let detpid = guest.thread_state().detpid.expect("detpid unset");
11267    let command_bootstrap = guest.is_command_bootstrap();
11268    if crate::preemptions::callback_history::recording_enabled(guest.config())
11269        || guest.thread_state().callback_history.is_active()
11270    {
11271        guest.thread_state_mut().callback_history.observe_event(&ev);
11272    }
11273    let resp = send_and_update_time(
11274        guest,
11275        GlobalRequest::TraceSchedEvent(ev, detpid, command_bootstrap, access.map(Box::new)),
11276    )
11277    .await;
11278
11279    trace!("trace_schedevent result: {:?}", resp);
11280    match resp {
11281        (
11282            _,
11283            GlobalResponse::TraceSchedEvent(TraceSchedEventResponse {
11284                print_stack_strace,
11285                timeslice,
11286                schedule_released,
11287                schedule_over,
11288            }),
11289        ) => {
11290            if schedule_released {
11291                guest.thread_state_mut().callback_history.release();
11292            }
11293            if schedule_over {
11294                guest.thread_state_mut().schedule_replay_over = true;
11295            }
11296            if let Some(m_path) = print_stack_strace {
11297                trace!("[trace_schedevent] writing stacktrace via Reverie...");
11298                write_backtrace(guest, m_path.as_ref());
11299            }
11300
11301            if let Some(timeslice) = timeslice
11302                && guest.thread_state().past_global_first_execve
11303            {
11304                let end_of_timeslice =
11305                    guest.thread_state().thread_logical_time.as_nanos() + timeslice;
11306                trace!(
11307                    "[detcore][dettid {}] setting end_of_timeslice to {:?} as instructed by replayer",
11308                    guest.thread_state().dettid,
11309                    end_of_timeslice
11310                );
11311                guest.thread_state_mut().end_of_timeslice = Some(end_of_timeslice);
11312                if guest.config().max_timeslice.is_some() {
11313                    guest.thread_state_mut().max_timeslice_end = Some(end_of_timeslice);
11314                }
11315            }
11316        }
11317        _ => {
11318            unreachable!()
11319        }
11320    }
11321}
11322
11323// AUTONOMOUS-BOT-IMPLEMENTED
11324// TODO-HUMAN-REVIEW(#663)
11325// TODO-HUMAN-REVIEW(#869)
11326/// Register an alarm (delayed signal delivery) with the global scheduler.
11327/// Returns the logical duration remaining until any previously scheduled alarm.
11328pub async fn register_alarm<G, T>(
11329    guest: &mut G,
11330    duration: LogicalTime,
11331    interval: LogicalTime,
11332    sig: Signal,
11333) -> (LogicalTime, LogicalTime)
11334where
11335    G: Guest<Detcore<T>>,
11336    T: RecordOrReplay,
11337{
11338    let dettid = guest.thread_state().dettid;
11339    let detpid = guest.thread_state().detpid.expect("detpid unset");
11340    let resp = send_and_update_time(
11341        guest,
11342        GlobalRequest::RegisterAlarm(detpid, dettid, duration, interval, SigWrapper::from(sig)),
11343    )
11344    .await;
11345    match resp.1 {
11346        GlobalResponse::RegisterAlarm(x) => x,
11347        _ => unreachable!(),
11348    }
11349}
11350
11351// AUTONOMOUS-BOT-IMPLEMENTED
11352// TODO-HUMAN-REVIEW(PR-841): Review logical alarm query API.
11353/// Return the logical duration remaining on the process's one-shot alarm.
11354pub async fn alarm_remaining<G, T>(guest: &mut G) -> ItimerSnapshot
11355where
11356    G: Guest<Detcore<T>>,
11357    T: RecordOrReplay,
11358{
11359    let detpid = guest.thread_state().detpid.expect("detpid unset");
11360    let resp = send_and_update_time(guest, GlobalRequest::AlarmRemaining(detpid)).await;
11361    match resp.1 {
11362        GlobalResponse::AlarmRemaining(remaining) => remaining,
11363        _ => unreachable!(),
11364    }
11365}
11366
11367/// Rule 4/5 notifications for the coalescing ITIMER_REAL
11368/// (<https://github.com/rrnewton/hermit/issues/4139>): a committed guest
11369/// SIGALRM send, or a change of SIGALRM's disposition.
11370pub async fn note_sigalrm_event<G, T>(guest: &mut G, request: GlobalRequest)
11371where
11372    G: Guest<Detcore<T>>,
11373    T: RecordOrReplay,
11374{
11375    let resp = send_and_update_time(guest, request).await;
11376    match resp.1 {
11377        GlobalResponse::NoteGuestSigalrm => {}
11378        _ => unreachable!(),
11379    }
11380}
11381
11382/// O2: after a call that may have dequeued a SIGALRM from this process's
11383/// shared queue (a signalfd read, an `rt_sigtimedwait` completion), let the
11384/// scheduler restart an `Inactive` ITIMER_REAL at this point in the turn
11385/// (<https://github.com/rrnewton/hermit/issues/4139>).
11386pub async fn reconcile_alarm<G, T>(guest: &mut G)
11387where
11388    G: Guest<Detcore<T>>,
11389    T: RecordOrReplay,
11390{
11391    let detpid = guest.thread_state().detpid.expect("detpid unset");
11392    let resp = send_and_update_time(guest, GlobalRequest::ReconcileAlarm(detpid)).await;
11393    match resp.1 {
11394        GlobalResponse::ReconcileAlarm => {}
11395        _ => unreachable!(),
11396    }
11397}
11398
11399// AUTONOMOUS-BOT-IMPLEMENTED
11400// TODO-HUMAN-REVIEW(#869)
11401/// Register, re-arm, or disarm a POSIX timer with the global scheduler, or
11402/// delete it.
11403pub async fn register_posix_timer<G, T>(
11404    guest: &mut G,
11405    timer_id: i32,
11406    deadline: Option<LogicalTime>,
11407    interval: LogicalTime,
11408    notification: PosixTimerNotification,
11409) where
11410    G: Guest<Detcore<T>>,
11411    T: RecordOrReplay,
11412{
11413    let dettid = guest.thread_state().dettid;
11414    let detpid = guest.thread_state().detpid.expect("detpid unset");
11415    let resp = send_and_update_time(
11416        guest,
11417        GlobalRequest::RegisterPosixTimer(
11418            detpid,
11419            dettid,
11420            timer_id,
11421            deadline,
11422            interval,
11423            notification,
11424        ),
11425    )
11426    .await;
11427    match resp.1 {
11428        GlobalResponse::RegisterPosixTimer(()) => {}
11429        _ => unreachable!(),
11430    }
11431}
11432
11433// AUTONOMOUS-BOT-IMPLEMENTED
11434// TODO-HUMAN-REVIEW(#663)
11435/// Return the scheduler's live threads for a positive process ID.
11436/// Does a live thread with this tid exist, leader or not?
11437///
11438/// Distinct from [`resolve_kill_targets`], which models `kill(2)` and therefore
11439/// only recognises thread-group leaders. Syscalls that resolve a task through
11440/// `find_task_by_vpid` -- `sched_setattr` among them -- must use this instead,
11441/// or a non-leader thread reports ESRCH while it is plainly running.
11442pub async fn thread_is_live<G, T>(guest: &mut G, dettid: DetTid) -> bool
11443where
11444    G: Guest<Detcore<T>>,
11445    T: RecordOrReplay,
11446{
11447    let response = send_and_update_time(guest, GlobalRequest::ThreadIsLive(dettid)).await;
11448    match response.1 {
11449        GlobalResponse::ThreadIsLive(live) => live,
11450        _ => unreachable!(),
11451    }
11452}
11453
11454/// The ID the task that the caller's clone-family call is about to create
11455/// gets, when Detcore chooses the IDs (`GuestIds::Chosen`); `None` when the
11456/// kernel numbers the guest or no positive ID remains (`GlobalState::take_guest_id`).
11457pub(crate) async fn next_guest_id<G, T>(guest: &mut G) -> Option<i32>
11458where
11459    G: Guest<Detcore<T>>,
11460    T: RecordOrReplay,
11461{
11462    let response = send_and_update_time(guest, GlobalRequest::NextGuestId).await;
11463    match response.1 {
11464        GlobalResponse::NextGuestId(id) => id,
11465        _ => unreachable!(),
11466    }
11467}
11468
11469/// Report a host-ID outcome or a conservative pre-vfork attempt while
11470/// Detcore chooses IDs (`GlobalState::host_id_tasks`).
11471pub(crate) async fn note_host_id_task<G, T>(guest: &mut G)
11472where
11473    G: Guest<Detcore<T>>,
11474    T: RecordOrReplay,
11475{
11476    let response = send_and_update_time(guest, GlobalRequest::GaveHostId).await;
11477    assert_eq!(response.1, GlobalResponse::GaveHostId(()));
11478}
11479
11480/// Whether a `clone3` `set_tid` naming `dettid` is refused: a task of this
11481/// run had that id, and it is not the pid of a zombie child of the caller's
11482/// process (`Scheduler::id_is_a_zombie_child_of`). "Had" is a registration with
11483/// the scheduler, which is never removed, or a clock component in the global
11484/// time (which exec reassignment can move away from an id that stays
11485/// registered). Such a zombie's pid is left to the kernel, which answers
11486/// EEXIST, and only the caller's own process can release it. Any other used id is
11487/// refused, a live task's included: whether a live thread is still alive when
11488/// the kernel runs the call can depend on host timing (a thread killed
11489/// without a granted exit deregisters when the host says), and a freed id
11490/// given to a new task would merge two tasks' state, keyed by that id, in
11491/// Detcore (`Detcore::clone3_set_tid_refusal`,
11492/// https://github.com/rrnewton/hermit/issues/4370).
11493pub(crate) async fn thread_id_refused<G, T>(guest: &mut G, dettid: DetTid) -> bool
11494where
11495    G: Guest<Detcore<T>>,
11496    T: RecordOrReplay,
11497{
11498    let response = send_and_update_time(guest, GlobalRequest::ThreadIdRefused(dettid)).await;
11499    match response.1 {
11500        GlobalResponse::ThreadIdRefused(refused) => refused,
11501        _ => unreachable!(),
11502    }
11503}
11504
11505/// Whether `named` is a thread-group leader the scheduler retired at its own
11506/// exit while its group lives on, so the kernel shows it a zombie
11507/// (`Scheduler::names_a_retired_exited_leader`,
11508/// https://github.com/rrnewton/hermit/issues/4445).
11509pub(crate) async fn names_a_retired_exited_leader<G, T>(guest: &mut G, named: DetTid) -> bool
11510where
11511    G: Guest<Detcore<T>>,
11512    T: RecordOrReplay,
11513{
11514    let response =
11515        send_and_update_time(guest, GlobalRequest::NamesARetiredExitedLeader(named)).await;
11516    match response.1 {
11517        GlobalResponse::NamesARetiredExitedLeader(retired) => retired,
11518        _ => unreachable!(),
11519    }
11520}
11521
11522/// Arm (`Some(signal)`, 0 disarms) or read (`None`) the calling task's
11523/// parent-death signal that Detcore sends itself when its parent thread
11524/// leaves (`Scheduler::deliver_parent_death_signals`,
11525/// https://github.com/rrnewton/hermit/issues/4440). Returns the value held
11526/// after the call.
11527pub(crate) async fn parent_death_signal<G, T>(guest: &mut G, arm: Option<i32>) -> i32
11528where
11529    G: Guest<Detcore<T>>,
11530    T: RecordOrReplay,
11531{
11532    let response = send_and_update_time(guest, GlobalRequest::ParentDeathSignal(arm)).await;
11533    match response.1 {
11534        GlobalResponse::ParentDeathSignal(signal) => signal,
11535        _ => unreachable!(),
11536    }
11537}
11538
11539/// The real-time signals (a kernel sigset) of `detpid`'s POSIX timers that
11540/// expired while every thread of the process blocked them, and that are still
11541/// pending (https://github.com/rrnewton/hermit/issues/4089). Asked in the
11542/// calling thread's turn, while no timed event can pop, so the answer is a
11543/// function of the schedule.
11544pub(crate) async fn pending_real_time_expirations<G, T>(guest: &mut G, detpid: DetPid) -> u64
11545where
11546    G: Guest<Detcore<T>>,
11547    T: RecordOrReplay,
11548{
11549    let response =
11550        send_and_update_time(guest, GlobalRequest::PendingRealTimeExpirations(detpid)).await;
11551    match response.1 {
11552        GlobalResponse::PendingRealTimeExpirations(pending) => pending,
11553        GlobalResponse::ThreadExited => 0,
11554        other => unreachable!("unexpected response to PendingRealTimeExpirations: {other:?}"),
11555    }
11556}
11557
11558/// Record that a handler's return in `detpid` restores a mask leaving
11559/// `unblocked` (real-time signals) unblocked; see
11560/// `Scheduler::record_handler_restore_unblocked`.
11561pub(crate) async fn record_handler_restore_mask<G, T>(guest: &mut G, detpid: DetPid, unblocked: u64)
11562where
11563    G: Guest<Detcore<T>>,
11564    T: RecordOrReplay,
11565{
11566    let response = send_and_update_time(
11567        guest,
11568        GlobalRequest::RecordHandlerRestoreMask(detpid, unblocked),
11569    )
11570    .await;
11571    assert!(
11572        matches!(
11573            response.1,
11574            GlobalResponse::RecordHandlerRestoreMask(()) | GlobalResponse::ThreadExited
11575        ),
11576        "unexpected response to RecordHandlerRestoreMask: {:?}",
11577        response.1
11578    );
11579}
11580
11581/// Record that `detpid` created a signalfd for `mask` (a kernel sigset with a
11582/// real-time signal); see `Scheduler::register_signalfd`.
11583pub(crate) async fn register_signalfd<G, T>(guest: &mut G, detpid: DetPid, mask: u64)
11584where
11585    G: Guest<Detcore<T>>,
11586    T: RecordOrReplay,
11587{
11588    let response = send_and_update_time(guest, GlobalRequest::RegisterSignalfd(detpid, mask)).await;
11589    assert!(
11590        matches!(
11591            response.1,
11592            GlobalResponse::RegisterSignalfd(()) | GlobalResponse::ThreadExited
11593        ),
11594        "unexpected response to RegisterSignalfd: {:?}",
11595        response.1
11596    );
11597}
11598
11599/// Record, under record and replay, that the calling thread's `close`,
11600/// `close_range`, `dup2` or `dup3` released or replaced its descriptors
11601/// `first..=last`; see `Scheduler::note_descriptors_replaced`.
11602pub(crate) async fn descriptors_replaced<G, T>(guest: &mut G, first: u32, last: u32)
11603where
11604    G: Guest<Detcore<T>>,
11605    T: RecordOrReplay,
11606{
11607    if !guest.config().recordreplay_modes {
11608        return;
11609    }
11610    let files = guest.thread_state().files_id();
11611    let response = send_and_update_time(
11612        guest,
11613        GlobalRequest::DescriptorsReplaced(files, first, last),
11614    )
11615    .await;
11616    assert!(
11617        matches!(
11618            response.1,
11619            GlobalResponse::DescriptorsReplaced(()) | GlobalResponse::ThreadExited
11620        ),
11621        "unexpected response to DescriptorsReplaced: {:?}",
11622        response.1
11623    );
11624}
11625
11626/// Discard `signals` from `detpid`'s pending real-time timer expirations, as
11627/// Linux flushes a pending signal whose disposition becomes `SIG_IGN`.
11628pub(crate) async fn discard_pending_real_time_expirations<G, T>(
11629    guest: &mut G,
11630    detpid: DetPid,
11631    signals: u64,
11632) where
11633    G: Guest<Detcore<T>>,
11634    T: RecordOrReplay,
11635{
11636    let response = send_and_update_time(
11637        guest,
11638        GlobalRequest::DiscardPendingRealTimeExpirations(detpid, signals),
11639    )
11640    .await;
11641    assert!(
11642        matches!(
11643            response.1,
11644            GlobalResponse::DiscardPendingRealTimeExpirations(()) | GlobalResponse::ThreadExited
11645        ),
11646        "unexpected response to DiscardPendingRealTimeExpirations: {:?}",
11647        response.1
11648    );
11649}
11650
11651/// Records, in the calling thread's turn, that it armed a host-timed source of
11652/// `signals` (a kernel sigset) reaching the processes in `scope`, before the
11653/// arming call reaches the kernel. Returns the signals the caller's gated waits
11654/// now hold (`Scheduler::record_host_timed_signals`).
11655pub async fn record_host_timed_signals<G, T>(
11656    guest: &mut G,
11657    scope: HostTimedSignalScope,
11658    signals: u64,
11659) -> u64
11660where
11661    G: Guest<Detcore<T>>,
11662    T: RecordOrReplay,
11663{
11664    let dettid = guest.thread_state().dettid;
11665    let response = send_and_update_time(
11666        guest,
11667        GlobalRequest::RecordHostTimedSignals(dettid, scope, signals),
11668    )
11669    .await;
11670    match response.1 {
11671        GlobalResponse::HostTimedSignals(signals) => signals,
11672        _ => unreachable!(),
11673    }
11674}
11675
11676/// The signals (a kernel sigset) that no gated wait of the calling thread ends
11677/// for, because a host-timed source a guest armed can post them to its process
11678/// (`Scheduler::host_timed_signals`). Read it in the waiting thread's turn.
11679pub async fn host_timed_signals<G, T>(guest: &mut G) -> u64
11680where
11681    G: Guest<Detcore<T>>,
11682    T: RecordOrReplay,
11683{
11684    let dettid = guest.thread_state().dettid;
11685    let response = send_and_update_time(guest, GlobalRequest::HostTimedSignals(dettid)).await;
11686    match response.1 {
11687        GlobalResponse::HostTimedSignals(signals) => signals,
11688        _ => unreachable!(),
11689    }
11690}
11691
11692/// The calling thread's host-timed signals (`host_timed_signals`) and the
11693/// `SIGCHLD` a guest sent it that the schedule recorded in its private queue,
11694/// in one round trip (`Scheduler::host_timed_and_private_sigchld`). Read it in
11695/// the waiting thread's turn, before any injection of that turn.
11696pub(crate) async fn host_timed_and_private_sigchld<G, T>(guest: &mut G) -> (u64, u64)
11697where
11698    G: Guest<Detcore<T>>,
11699    T: RecordOrReplay,
11700{
11701    let dettid = guest.thread_state().dettid;
11702    let response =
11703        send_and_update_time(guest, GlobalRequest::HostTimedAndPrivateSigchld(dettid)).await;
11704    match response.1 {
11705        GlobalResponse::HostTimedAndPrivateSigchld(host_timed, private) => (host_timed, private),
11706        _ => unreachable!(),
11707    }
11708}
11709
11710/// The scheduler's answer to a child-exit `SIGCHLD` control message from the
11711/// calling thread (`Scheduler::child_exit_sigchld_control`). It carries no
11712/// logical time either way.
11713pub(crate) async fn child_exit_sigchld<G, T>(
11714    guest: &mut G,
11715    control: crate::scheduler::child_exit_sigchld::ChildExitSigchldControl,
11716) -> crate::scheduler::child_exit_sigchld::ChildExitSigchldAnswer
11717where
11718    G: Guest<Detcore<T>>,
11719    T: RecordOrReplay,
11720{
11721    let response = send_and_update_time(guest, GlobalRequest::ChildExitSigchld(control)).await;
11722    match response.1 {
11723        GlobalResponse::ChildExitSigchld(answer) => answer,
11724        _ => unreachable!(),
11725    }
11726}
11727
11728/// The scheduler's answer to a discarded-at-send record message from the
11729/// calling thread (`Scheduler::discarded_signal_control`). It carries no
11730/// logical time either way.
11731pub(crate) async fn discarded_signal<G, T>(
11732    guest: &mut G,
11733    control: crate::scheduler::discarded_at_send::DiscardedSignalControl,
11734) -> crate::scheduler::discarded_at_send::DiscardedSignalAnswer
11735where
11736    G: Guest<Detcore<T>>,
11737    T: RecordOrReplay,
11738{
11739    let response = send_and_update_time(guest, GlobalRequest::DiscardedSignal(control)).await;
11740    match response.1 {
11741        GlobalResponse::DiscardedSignal(answer) => answer,
11742        _ => unreachable!(),
11743    }
11744}
11745
11746/// Whether the scheduler's SIGALRM ledger refuses `control` for the calling
11747/// thread (`Scheduler::sigalrm_control`). Asked in the thread's own turn; it
11748/// carries no logical time either way.
11749pub async fn sigalrm_refuses<G, T>(guest: &mut G, control: SigalrmControl) -> bool
11750where
11751    G: Guest<Detcore<T>>,
11752    T: RecordOrReplay,
11753{
11754    let response = send_and_update_time(guest, GlobalRequest::Sigalrm(control)).await;
11755    match response.1 {
11756        GlobalResponse::Sigalrm(refused) => refused,
11757        _ => unreachable!(),
11758    }
11759}
11760
11761/// EPERM when the scheduler's SIGALRM ledger refuses `control`
11762/// ([`sigalrm_refuses`]).
11763pub async fn refuse_sigalrm<G, T>(
11764    guest: &mut G,
11765    control: SigalrmControl,
11766) -> Result<(), nix::errno::Errno>
11767where
11768    G: Guest<Detcore<T>>,
11769    T: RecordOrReplay,
11770{
11771    if sigalrm_refuses(guest, control).await {
11772        Err(nix::errno::Errno::EPERM)
11773    } else {
11774        Ok(())
11775    }
11776}
11777
11778/// Return scheduler-owned lifecycle state for an exact child-process wait.
11779pub async fn exact_child_wait_state<G, T>(guest: &mut G, child: DetPid) -> ExactChildWaitState
11780where
11781    G: Guest<Detcore<T>>,
11782    T: RecordOrReplay,
11783{
11784    let parent = guest.thread_state().detpid.expect("detpid unset");
11785    let response =
11786        send_and_update_time(guest, GlobalRequest::ExactChildWaitState(parent, child)).await;
11787    match response.1 {
11788        GlobalResponse::ExactChildWaitState(state) => state,
11789        _ => unreachable!(),
11790    }
11791}
11792
11793/// Wait without requesting a scheduler turn for a backend's physical-exit report.
11794pub async fn await_exact_child_physical_exit<G, T>(
11795    guest: &mut G,
11796    child: DetPid,
11797) -> ExactChildWaitState
11798where
11799    G: Guest<Detcore<T>>,
11800    T: RecordOrReplay,
11801{
11802    let mut state = exact_child_wait_state(guest, child).await;
11803    if matches!(
11804        state,
11805        ExactChildWaitState::PhysicalExitPending | ExactChildWaitState::PhysicallyExited
11806    ) {
11807        let dettid = guest.thread_state().dettid;
11808        let mut resources = Resources::new(dettid);
11809        resources.insert(ResourceID::WaitPhysicalChild(child), Permission::R);
11810        resources.fyi("wait-child-physical-exit");
11811        let _ = resource_request(guest, resources).await;
11812        state = exact_child_wait_state(guest, child).await;
11813    }
11814    state
11815}
11816
11817/// Park until an exact or any-child process wait has a logical exit to reap.
11818pub async fn wait_for_child_lifecycle<G, T>(guest: &mut G, spec: ChildWaitSpec) -> ResumeStatus
11819where
11820    G: Guest<Detcore<T>>,
11821    T: RecordOrReplay,
11822{
11823    let dettid = guest.thread_state().dettid;
11824    let parent = guest.thread_state().detpid.expect("detpid unset");
11825    let mut resources = Resources::new(dettid);
11826    resources.insert(ResourceID::WaitChild { parent, spec }, Permission::R);
11827    resources.fyi("wait-child-lifecycle");
11828    resource_request(guest, resources).await
11829}
11830
11831pub async fn ready_child_wait<G, T>(guest: &mut G, spec: ChildWaitSpec) -> (Option<DetPid>, bool)
11832where
11833    G: Guest<Detcore<T>>,
11834    T: RecordOrReplay,
11835{
11836    let parent = guest.thread_state().detpid.expect("detpid unset");
11837    let response = send_and_update_time(guest, GlobalRequest::ReadyChildWait(parent, spec)).await;
11838    match response.1 {
11839        GlobalResponse::ReadyChildWait(snapshot) => snapshot,
11840        _ => unreachable!(),
11841    }
11842}
11843
11844pub async fn process_group<G, T>(guest: &mut G, process: DetPid) -> Option<DetPid>
11845where
11846    G: Guest<Detcore<T>>,
11847    T: RecordOrReplay,
11848{
11849    let response = send_and_update_time(guest, GlobalRequest::ProcessGroup(process)).await;
11850    match response.1 {
11851        GlobalResponse::ProcessGroup(group) => group,
11852        _ => unreachable!(),
11853    }
11854}
11855
11856/// Linux's answer to `setsid()` by `process`, from Detcore's model
11857/// (`Scheduler::setsid_decision`).
11858pub async fn setsid_decision<G, T>(guest: &mut G, process: DetPid) -> Result<(), reverie::Errno>
11859where
11860    G: Guest<Detcore<T>>,
11861    T: RecordOrReplay,
11862{
11863    let response = send_and_update_time(guest, GlobalRequest::SetsidDecision(process)).await;
11864    match response.1 {
11865        GlobalResponse::SetsidDecision(decision) => decision.map_err(reverie::Errno::new),
11866        _ => unreachable!(),
11867    }
11868}
11869
11870/// Linux's answer to `setpgid(pid, pgid)` by `caller`, from Detcore's model
11871/// (`Scheduler::setpgid_decision`): the process to move and its group.
11872pub async fn setpgid_decision<G, T>(
11873    guest: &mut G,
11874    caller: DetPid,
11875    pid: i32,
11876    pgid: i32,
11877) -> Result<(DetPid, DetPid), reverie::Errno>
11878where
11879    G: Guest<Detcore<T>>,
11880    T: RecordOrReplay,
11881{
11882    let response =
11883        send_and_update_time(guest, GlobalRequest::SetpgidDecision(caller, pid, pgid)).await;
11884    match response.1 {
11885        GlobalResponse::SetpgidDecision(decision) => decision.map_err(reverie::Errno::new),
11886        _ => unreachable!(),
11887    }
11888}
11889
11890/// The process group (`session` false) or session (`session` true) of the
11891/// task with id `pid` in Detcore's model, as `getpgid` and `getsid` answer it
11892/// (`Scheduler::group_or_session_of`); `None` where Linux has no such task.
11893pub async fn group_or_session_of<G, T>(guest: &mut G, pid: DetPid, session: bool) -> Option<DetPid>
11894where
11895    G: Guest<Detcore<T>>,
11896    T: RecordOrReplay,
11897{
11898    let response = send_and_update_time(guest, GlobalRequest::GroupOrSession(pid, session)).await;
11899    match response.1 {
11900        GlobalResponse::GroupOrSession(answer) => answer,
11901        _ => unreachable!(),
11902    }
11903}
11904
11905/// The image guest process `process` executes, as Detcore recorded it at its
11906/// exec (`crate::exec_record`); `None` without a record, and for a process
11907/// that has exited.
11908pub(crate) async fn process_image<G, T>(
11909    guest: &mut G,
11910    process: DetPid,
11911) -> Option<crate::exec_record::ExecRecord>
11912where
11913    G: Guest<Detcore<T>>,
11914    T: RecordOrReplay,
11915{
11916    match send_and_update_time(guest, GlobalRequest::ProcessImage(process))
11917        .await
11918        .1
11919    {
11920        GlobalResponse::ProcessImage(image) => image.map(|image| *image),
11921        _ => unreachable!(),
11922    }
11923}
11924
11925pub async fn set_process_group<G, T>(guest: &mut G, process: DetPid, group: DetPid) -> bool
11926where
11927    G: Guest<Detcore<T>>,
11928    T: RecordOrReplay,
11929{
11930    let response =
11931        send_and_update_time(guest, GlobalRequest::SetProcessGroup(process, group)).await;
11932    match response.1 {
11933        GlobalResponse::SetProcessGroup(updated) => updated,
11934        _ => unreachable!(),
11935    }
11936}
11937
11938/// The process Detcore registered `dettid` in (`ThreadTree::process_of`), in
11939/// the calling thread's turn.
11940pub async fn registered_process_of<G, T>(guest: &mut G, dettid: DetTid) -> Option<DetPid>
11941where
11942    G: Guest<Detcore<T>>,
11943    T: RecordOrReplay,
11944{
11945    let response = send_and_update_time(guest, GlobalRequest::RegisteredProcessOf(dettid)).await;
11946    match response.1 {
11947        GlobalResponse::RegisteredProcessOf(process) => process,
11948        _ => unreachable!(),
11949    }
11950}
11951
11952/// Reserve the victims of the SIGKILL the calling thread is about to send to
11953/// `target`'s process (`scheduler::sigkill_retirement`). `None` keeps the
11954/// ordinary path.
11955pub async fn reserve_sigkill<G, T>(
11956    guest: &mut G,
11957    target: DetTid,
11958) -> Option<crate::scheduler::sigkill_retirement::SigkillReservation>
11959where
11960    G: Guest<Detcore<T>>,
11961    T: RecordOrReplay,
11962{
11963    // Backends that cannot retire at the kill turn send no extra RPC.
11964    if !crate::scheduler::sigkill_retirement::retires_sigkill_at_the_kill_turn(
11965        &guest.config().backend,
11966    ) {
11967        return None;
11968    }
11969    let sender = guest.thread_state().dettid;
11970    // Read before the send, while the target certainly exists.
11971    let uid =
11972        crate::syscalls::real_uid(target.as_raw()).unwrap_or_else(|| unsafe { libc::getuid() });
11973    let response =
11974        send_and_update_time(guest, GlobalRequest::ReserveSigkill(sender, target, uid)).await;
11975    match response.1 {
11976        GlobalResponse::ReserveSigkill(token) => token,
11977        _ => unreachable!(),
11978    }
11979}
11980
11981/// Mirror a successful `prctl(PR_SET_CHILD_SUBREAPER, on)` by `process` in
11982/// the scheduler's thread tree, in the calling thread's turn.
11983pub async fn set_child_subreaper<G, T>(guest: &mut G, process: DetPid, on: bool)
11984where
11985    G: Guest<Detcore<T>>,
11986    T: RecordOrReplay,
11987{
11988    let response = send_and_update_time(guest, GlobalRequest::SetChildSubreaper(process, on)).await;
11989    match response.1 {
11990        GlobalResponse::SetChildSubreaper(()) => {}
11991        _ => unreachable!(),
11992    }
11993}
11994
11995/// What the `sigev_notify_thread_id` `named` of a `timer_create` by the
11996/// calling thread names (`Scheduler::posix_timer_thread`).
11997pub async fn posix_timer_thread<G, T>(guest: &mut G, named: i32) -> PosixTimerThread
11998where
11999    G: Guest<Detcore<T>>,
12000    T: RecordOrReplay,
12001{
12002    let process = guest.thread_state().detpid.expect("detpid unset");
12003    let response =
12004        send_and_update_time(guest, GlobalRequest::PosixTimerThread(process, named)).await;
12005    match response.1 {
12006        GlobalResponse::PosixTimerThread(answer) => answer,
12007        _ => unreachable!(),
12008    }
12009}
12010
12011/// A disposition change in the calling thread's process flushed `signo` from
12012/// every thread (`Scheduler::forget_thread_timer_copies`).
12013pub async fn forget_thread_timer_copies<G, T>(guest: &mut G, signo: i32)
12014where
12015    G: Guest<Detcore<T>>,
12016    T: RecordOrReplay,
12017{
12018    let process = guest.thread_state().detpid.expect("detpid unset");
12019    let response = send_and_update_time(
12020        guest,
12021        GlobalRequest::ForgetThreadTimerCopies(process, signo),
12022    )
12023    .await;
12024    match response.1 {
12025        GlobalResponse::ForgetThreadTimerCopies(()) => {}
12026        _ => unreachable!(),
12027    }
12028}
12029
12030/// The POSIX timer copies the calling thread's successful exec flushes
12031/// (`Scheduler::take_exec_timer_flush`). Asked in the exec's turn, only in a
12032/// run that has recorded a timer copy or kept a real-time expiration pending.
12033pub async fn exec_timer_flush<G, T>(
12034    guest: &mut G,
12035) -> Result<crate::scheduler::ExecTimerFlush, String>
12036where
12037    G: Guest<Detcore<T>>,
12038    T: RecordOrReplay,
12039{
12040    let process = guest.thread_state().detpid.expect("detpid unset");
12041    let response = send_and_update_time(guest, GlobalRequest::ExecTimerFlush(process)).await;
12042    match response.1 {
12043        GlobalResponse::ExecTimerFlush(answer) => answer,
12044        _ => unreachable!(),
12045    }
12046}
12047
12048/// Retire the calling thread-group leader, which has made its last request
12049/// before completing its own `exit`, if its group lives on
12050/// (`Scheduler::retire_exited_leader`). Returns whether it was retired.
12051pub async fn retire_exited_leader<G, T>(guest: &mut G) -> bool
12052where
12053    G: Guest<Detcore<T>>,
12054    T: RecordOrReplay,
12055{
12056    let response = send_and_update_time(guest, GlobalRequest::RetireExitedLeader).await;
12057    match response.1 {
12058        GlobalResponse::RetireExitedLeader(retired) => retired,
12059        _ => unreachable!(),
12060    }
12061}
12062
12063/// What the positive pid `pid`, named by the calling thread in its nested PID
12064/// namespace, names (`PidNamespaces::resolve_for`). Asked only by a thread
12065/// below the container's namespace (`ThreadState::nested_pid_namespace`).
12066pub async fn guest_pid<G, T>(
12067    guest: &mut G,
12068    pid: i32,
12069) -> crate::scheduler::pid_namespaces::GuestPidAnswer
12070where
12071    G: Guest<Detcore<T>>,
12072    T: RecordOrReplay,
12073{
12074    let caller = guest.thread_state().dettid;
12075    let response = send_and_update_time(guest, GlobalRequest::GuestPid(caller, pid)).await;
12076    match response.1 {
12077        GlobalResponse::GuestPid(answer) => answer,
12078        _ => unreachable!(),
12079    }
12080}
12081
12082/// The processes `/proc` lists and, for `process`, the threads its `task`
12083/// directory lists, as a thread in the container's PID namespace numbers
12084/// them (`Scheduler::procfs_listing`).
12085pub async fn procfs_listing<G, T>(
12086    guest: &mut G,
12087    process: Option<DetPid>,
12088) -> crate::procfs::listing::ListingFacts
12089where
12090    G: Guest<Detcore<T>>,
12091    T: RecordOrReplay,
12092{
12093    let response = send_and_update_time(guest, GlobalRequest::ProcfsListing(process)).await;
12094    match response.1 {
12095        GlobalResponse::ProcfsListing(facts) => facts,
12096        _ => unreachable!(),
12097    }
12098}
12099
12100/// What Detcore's process table says of the task a per-process `/proc` file
12101/// names (`Scheduler::procfs_task`), with the incarnation of its numbers.
12102pub async fn procfs_task<G, T>(
12103    guest: &mut G,
12104    task: crate::procfs::BoundTask,
12105) -> crate::procfs::listing::ProcfsTaskAnswer
12106where
12107    G: Guest<Detcore<T>>,
12108    T: RecordOrReplay,
12109{
12110    let request = GlobalRequest::ProcfsTask(
12111        DetTid::from_raw(task.process),
12112        task.thread.map(DetTid::from_raw),
12113    );
12114    let response = send_and_update_time(guest, request).await;
12115    match response.1 {
12116        GlobalResponse::ProcfsTask(answer) => answer,
12117        _ => unreachable!(),
12118    }
12119}
12120
12121/// What the process table says about the task a per-process `stat` file
12122/// names (`Scheduler::task_stat_facts`).
12123pub(crate) async fn task_stat_facts<G, T>(
12124    guest: &mut G,
12125    task: crate::procfs::BoundTask,
12126) -> crate::procfs::task_stat::TaskStatFacts
12127where
12128    G: Guest<Detcore<T>>,
12129    T: RecordOrReplay,
12130{
12131    let request = GlobalRequest::TaskStatFacts(
12132        DetTid::from_raw(task.process),
12133        task.thread.map(DetTid::from_raw),
12134    );
12135    match send_and_update_time(guest, request).await.1 {
12136        GlobalResponse::TaskStatFacts(facts) => facts,
12137        _ => unreachable!(),
12138    }
12139}
12140
12141/// Record `comm` as task `tid`'s name (`Scheduler::set_task_comm`).
12142pub(crate) async fn set_task_comm<G, T>(
12143    guest: &mut G,
12144    tid: DetTid,
12145    comm: crate::procfs::task_stat::TaskComm,
12146) where
12147    G: Guest<Detcore<T>>,
12148    T: RecordOrReplay,
12149{
12150    let response = send_and_update_time(guest, GlobalRequest::SetTaskComm(tid, comm))
12151        .await
12152        .1;
12153    assert_eq!(response, GlobalResponse::SetTaskComm(()));
12154}
12155
12156/// Record `parent` as the guest-visible parent of `process`, whose real
12157/// parent is outside the guest's tree (`Scheduler::record_external_parent`).
12158pub(crate) async fn record_external_parent<G, T>(guest: &mut G, process: DetPid, parent: i32)
12159where
12160    G: Guest<Detcore<T>>,
12161    T: RecordOrReplay,
12162{
12163    let response =
12164        send_and_update_time(guest, GlobalRequest::RecordExternalParent(process, parent))
12165            .await
12166            .1;
12167    assert_eq!(response, GlobalResponse::RecordExternalParent(()));
12168}
12169
12170/// Register the placeholder directory `directory` that the calling guest
12171/// thread opened, and return the kernel position that tags its descriptor
12172/// (`InodePool::register_placeholder`).
12173pub(crate) async fn register_placeholder<G, T>(
12174    guest: &mut G,
12175    directory: crate::procfs::fd_directory::PlaceholderDirectory,
12176    open_file: OpenFileId,
12177) -> Option<u64>
12178where
12179    G: Guest<Detcore<T>>,
12180    T: RecordOrReplay,
12181{
12182    match send_and_update_time(
12183        guest,
12184        GlobalRequest::RegisterPlaceholder(directory, open_file),
12185    )
12186    .await
12187    .1
12188    {
12189        GlobalResponse::RegisterPlaceholder(tag) => tag,
12190        _ => unreachable!(),
12191    }
12192}
12193
12194/// The placeholder directory the kernel position `tag` tags, if any
12195/// (`InodePool::placeholder`).
12196pub(crate) async fn placeholder<G, T>(
12197    guest: &mut G,
12198    tag: u64,
12199) -> Option<(
12200    crate::procfs::fd_directory::PlaceholderDirectory,
12201    OpenFileId,
12202)>
12203where
12204    G: Guest<Detcore<T>>,
12205    T: RecordOrReplay,
12206{
12207    match send_and_update_time(guest, GlobalRequest::Placeholder(tag))
12208        .await
12209        .1
12210    {
12211        GlobalResponse::Placeholder(directory) => directory,
12212        _ => unreachable!(),
12213    }
12214}
12215
12216/// A `flock` through a placeholder descriptor of the calling thread's table
12217/// (`procfs::locks::LockLedger::placeholder_flock`).
12218pub(crate) async fn placeholder_flock<G, T>(
12219    guest: &mut G,
12220    request: crate::procfs::locks::PlaceholderFlock,
12221) -> crate::procfs::locks::PlaceholderFlockOutcome
12222where
12223    G: Guest<Detcore<T>>,
12224    T: RecordOrReplay,
12225{
12226    match send_and_update_time(guest, GlobalRequest::PlaceholderFlock(request))
12227        .await
12228        .1
12229    {
12230        GlobalResponse::PlaceholderFlock(outcome) => outcome,
12231        _ => unreachable!(),
12232    }
12233}
12234
12235/// A change to the copies of placeholder descriptions in flight over
12236/// `SCM_RIGHTS` (`procfs::locks::LockLedger::placeholder_copies`): for a
12237/// send, the description it would send that holds a placeholder `flock`, if
12238/// any (the send is then refused by name and nothing counts).
12239pub(crate) async fn placeholder_copies<G, T>(
12240    guest: &mut G,
12241    change: crate::procfs::locks::PlaceholderCopies,
12242) -> Result<(), OpenFileId>
12243where
12244    G: Guest<Detcore<T>>,
12245    T: RecordOrReplay,
12246{
12247    match send_and_update_time(guest, GlobalRequest::PlaceholderCopies(change))
12248        .await
12249        .1
12250    {
12251        GlobalResponse::PlaceholderCopies(result) => result,
12252        _ => unreachable!(),
12253    }
12254}
12255
12256/// Name `table` as the descriptor table the calling thread holds
12257/// (`GlobalRequest::HoldTable`, `GlobalState::held_tables`). Sent as it is,
12258/// outside the scheduler, like a mapping capture: it publishes nothing,
12259/// carries no forwarded-record count, and its answer moves no clock. Where the
12260/// table cannot reach the global state (the backend's transport encodes its
12261/// requests, `HeldTable`), the run stops by name with the policy-refusal exit.
12262pub(crate) async fn hold_table<G, T>(
12263    guest: &mut G,
12264    table: Arc<Mutex<crate::tool_local::FileMetadata>>,
12265) where
12266    G: Guest<Detcore<T>>,
12267    T: RecordOrReplay,
12268{
12269    let state = guest.thread_state();
12270    let request = (
12271        state.thread_logical_time.clone(),
12272        state.mm_id,
12273        GlobalRequest::HoldTable(HeldTable::new(table)),
12274    );
12275    match guest.send_rpc(request).await {
12276        (None, GlobalResponse::HoldTable(Ok(()))) => {}
12277        (None, GlobalResponse::HoldTable(Err(reason))) => {
12278            error!("[detcore] {reason}");
12279            report_policy_refusal(guest, reason).await;
12280            unrecoverable_shutdown(guest, detcore_model::HERMIT_POLICY_REFUSAL_EXIT).await
12281        }
12282        other => panic!("unexpected answer to a held table: {other:?}"),
12283    }
12284}
12285
12286/// Set the write hint of the inode of the placeholder directory `key` names
12287/// to `hint`, or read it with `None`, as `F_SET_RW_HINT` and `F_GET_RW_HINT`
12288/// do (`procfs::locks::LockLedger::placeholder_write_hint`): the hint after
12289/// the request.
12290pub(crate) async fn placeholder_write_hint<G, T>(
12291    guest: &mut G,
12292    key: crate::procfs::fd_directory::PlaceholderLockKey,
12293    hint: Option<u64>,
12294) -> u64
12295where
12296    G: Guest<Detcore<T>>,
12297    T: RecordOrReplay,
12298{
12299    match send_and_update_time(guest, GlobalRequest::PlaceholderWriteHint(key, hint))
12300        .await
12301        .1
12302    {
12303        GlobalResponse::PlaceholderWriteHint(hint) => hint,
12304        _ => unreachable!(),
12305    }
12306}
12307
12308/// The CPU affinity of task `tid` under `--virtual-cpus`
12309/// (`Scheduler::cpu_affinity`).
12310pub(crate) async fn cpu_affinity<G, T>(guest: &mut G, tid: DetTid) -> crate::cpu_set::TaskAffinity
12311where
12312    G: Guest<Detcore<T>>,
12313    T: RecordOrReplay,
12314{
12315    match send_and_update_time(guest, GlobalRequest::CpuAffinity(tid))
12316        .await
12317        .1
12318    {
12319        GlobalResponse::CpuAffinity(affinity) => affinity,
12320        _ => unreachable!(),
12321    }
12322}
12323
12324/// Record `cpus` as the CPU affinity of task `tid` under `--virtual-cpus`
12325/// (`Scheduler::set_cpu_affinity`).
12326pub(crate) async fn set_cpu_affinity<G, T>(
12327    guest: &mut G,
12328    tid: DetTid,
12329    cpus: crate::cpu_set::CpuSet,
12330) -> crate::cpu_set::TaskAffinity
12331where
12332    G: Guest<Detcore<T>>,
12333    T: RecordOrReplay,
12334{
12335    match send_and_update_time(guest, GlobalRequest::SetCpuAffinity(tid, cpus))
12336        .await
12337        .1
12338    {
12339        GlobalResponse::CpuAffinity(affinity) => affinity,
12340        _ => unreachable!(),
12341    }
12342}
12343
12344/// Record `limit` as `process`'s virtual soft `RLIMIT_RSS`
12345/// (`Scheduler::set_rss_limit`).
12346pub(crate) async fn set_rss_limit<G, T>(guest: &mut G, process: DetPid, limit: u64)
12347where
12348    G: Guest<Detcore<T>>,
12349    T: RecordOrReplay,
12350{
12351    let response = send_and_update_time(guest, GlobalRequest::SetRssLimit(process, limit))
12352        .await
12353        .1;
12354    assert_eq!(response, GlobalResponse::SetRssLimit(()));
12355}
12356
12357/// Record the auxiliary vector the sender's process's new image found on
12358/// its initial stack (`ProcessAuxvTable::record`), and the image's argument
12359/// and environment areas there, if Detcore found them
12360/// (`ProcessAuxvTable::record_cmdline`).
12361pub(crate) async fn record_process_auxv<G, T>(
12362    guest: &mut G,
12363    bytes: Vec<u8>,
12364    cmdline: Option<crate::procfs::CmdlineArea>,
12365) where
12366    G: Guest<Detcore<T>>,
12367    T: RecordOrReplay,
12368{
12369    let response = send_and_update_time(guest, GlobalRequest::RecordProcessAuxv(bytes, cmdline))
12370        .await
12371        .1;
12372    assert_eq!(response, GlobalResponse::RecordProcessAuxv(()));
12373}
12374
12375/// The process `/proc/<process>/cmdline` names (a thread's number names its
12376/// process), and the argument and environment areas of its image, if
12377/// Detcore recorded them (`ProcessAuxvTable::current_cmdline`).
12378pub(crate) async fn process_cmdline<G, T>(
12379    guest: &mut G,
12380    process: DetPid,
12381) -> (DetPid, Option<crate::procfs::CmdlineArea>)
12382where
12383    G: Guest<Detcore<T>>,
12384    T: RecordOrReplay,
12385{
12386    match send_and_update_time(guest, GlobalRequest::ProcessCmdline(process))
12387        .await
12388        .1
12389    {
12390        GlobalResponse::ProcessCmdline(process, area) => (process, area),
12391        _ => unreachable!(),
12392    }
12393}
12394
12395/// The process whose image `/proc/<process>/auxv` describes (a thread's
12396/// number names its process), and that image's auxiliary vector
12397/// (`ProcessAuxvTable::current`).
12398pub(crate) async fn process_auxv<G, T>(guest: &mut G, process: DetPid) -> Option<ProcessAuxv>
12399where
12400    G: Guest<Detcore<T>>,
12401    T: RecordOrReplay,
12402{
12403    match send_and_update_time(guest, GlobalRequest::ProcessAuxv(process))
12404        .await
12405        .1
12406    {
12407        GlobalResponse::ProcessAuxv(answer) => answer,
12408        _ => unreachable!(),
12409    }
12410}
12411
12412/// Record that the sender's process changed its root with `chroot`
12413/// (`ProcessAuxvTable::mark_changed_root`).
12414pub(crate) async fn mark_changed_root<G, T>(guest: &mut G)
12415where
12416    G: Guest<Detcore<T>>,
12417    T: RecordOrReplay,
12418{
12419    let response = send_and_update_time(guest, GlobalRequest::MarkChangedRoot)
12420        .await
12421        .1;
12422    assert_eq!(response, GlobalResponse::MarkChangedRoot(()));
12423}
12424
12425/// Whether the process `task` names has changed its root
12426/// (`ProcessAuxvTable::root_changed`).
12427pub(crate) async fn root_changed<G, T>(guest: &mut G, task: DetPid) -> bool
12428where
12429    G: Guest<Detcore<T>>,
12430    T: RecordOrReplay,
12431{
12432    match send_and_update_time(guest, GlobalRequest::RootChanged(task))
12433        .await
12434        .1
12435    {
12436        GlobalResponse::RootChanged(changed) => changed,
12437        _ => unreachable!(),
12438    }
12439}
12440
12441/// Record the OFD and flock locks `open_file` holds, as its fdinfo showed
12442/// them after a call through one of the sender's descriptors, and that
12443/// `slots` slots of the sender's table name it
12444/// (`procfs::locks::LockLedger::note_locks`), or why that fdinfo could not be
12445/// read (`procfs::locks::LockLedger::note_unknown`).
12446pub(crate) async fn note_description_locks<G, T>(
12447    guest: &mut G,
12448    open_file: OpenFileId,
12449    locks: Result<Vec<crate::procfs::locks::HeldLock>, String>,
12450    slots: u32,
12451) where
12452    G: Guest<Detcore<T>>,
12453    T: RecordOrReplay,
12454{
12455    let response = send_and_update_time(
12456        guest,
12457        GlobalRequest::DescriptionLocks(open_file, locks, slots),
12458    )
12459    .await
12460    .1;
12461    assert_eq!(response, GlobalResponse::DescriptionLocks(()));
12462}
12463
12464/// Record the locks `open_file` holds after a call of the sender's that can
12465/// take a lock through it, the locks `taken` that the call proved it took,
12466/// that `slots` slots of the sender's table name it, and what else names its
12467/// open file description (`identity`)
12468/// (`procfs::locks::LockLedger::note_locks_taken`).
12469pub(crate) async fn note_description_locks_taken<G, T>(
12470    guest: &mut G,
12471    open_file: OpenFileId,
12472    locks: Vec<crate::procfs::locks::HeldLock>,
12473    taken: Vec<crate::procfs::locks::HeldLock>,
12474    slots: u32,
12475    identity: crate::procfs::locks::LedgerIdentity,
12476) where
12477    G: Guest<Detcore<T>>,
12478    T: RecordOrReplay,
12479{
12480    let response = send_and_update_time(
12481        guest,
12482        GlobalRequest::DescriptionLocksTaken(open_file, locks, taken, slots, identity),
12483    )
12484    .await
12485    .1;
12486    assert_eq!(response, GlobalResponse::DescriptionLocksTaken(()));
12487}
12488
12489/// Record the locks `open_file`, a description of the sender's table that may
12490/// be a second identity of another, holds after a call of its own that took
12491/// no lock, and that `slots` slots of the sender's table name it
12492/// (`procfs::locks::LockLedger::note_second_identity_locks`).
12493pub(crate) async fn note_second_identity_locks<G, T>(
12494    guest: &mut G,
12495    open_file: OpenFileId,
12496    locks: Vec<crate::procfs::locks::HeldLock>,
12497    slots: u32,
12498) where
12499    G: Guest<Detcore<T>>,
12500    T: RecordOrReplay,
12501{
12502    let response = send_and_update_time(
12503        guest,
12504        GlobalRequest::SecondIdentityLocks(open_file, locks, slots),
12505    )
12506    .await
12507    .1;
12508    assert_eq!(response, GlobalResponse::SecondIdentityLocks(()));
12509}
12510
12511/// The owner each of `locks`, held through `open_file`, shows on a lock line
12512/// of a rendered fdinfo (`procfs::locks::LockLedger::owners`), in order, or
12513/// why the lines cannot be shown: one is an OFD lock on a file on which the
12514/// backend held a guest's record lock as an OFD lock
12515/// (`procfs::locks::LockLedger::record_lock_held_as_ofd`).
12516pub(crate) async fn lock_owners<G, T>(
12517    guest: &mut G,
12518    open_file: OpenFileId,
12519    locks: Vec<crate::procfs::locks::HeldLock>,
12520) -> Result<Vec<i64>, String>
12521where
12522    G: Guest<Detcore<T>>,
12523    T: RecordOrReplay,
12524{
12525    match send_and_update_time(guest, GlobalRequest::LockOwners(open_file, locks))
12526        .await
12527        .1
12528    {
12529        GlobalResponse::LockOwners(owners) => owners,
12530        _ => unreachable!(),
12531    }
12532}
12533
12534/// The `/proc/locks` rows Detcore renders from the ledger, with their owners
12535/// (`procfs::locks::LockLedger::rendered_rows`), or why no snapshot can be
12536/// decided (`procfs::locks::LockLedger::unknown`).
12537pub(crate) async fn ledger_lock_rows<G, T>(
12538    guest: &mut G,
12539) -> Result<Vec<(crate::procfs::locks::HeldLock, i64)>, String>
12540where
12541    G: Guest<Detcore<T>>,
12542    T: RecordOrReplay,
12543{
12544    match send_and_update_time(guest, GlobalRequest::LedgerLockRows)
12545        .await
12546        .1
12547    {
12548        GlobalResponse::LedgerLockRows(rows) => rows,
12549        _ => unreachable!(),
12550    }
12551}
12552
12553/// Record that `remaining` slots of the sender's table still name
12554/// `open_file` (`procfs::locks::LockLedger::release`).
12555pub(crate) async fn note_table_released<G, T>(guest: &mut G, open_file: OpenFileId, remaining: u32)
12556where
12557    G: Guest<Detcore<T>>,
12558    T: RecordOrReplay,
12559{
12560    let response = send_and_update_time(guest, GlobalRequest::TableReleased(open_file, remaining))
12561        .await
12562        .1;
12563    assert_eq!(response, GlobalResponse::TableReleased(()));
12564}
12565
12566/// Record which descriptions whose locks Detcore read the sender's table
12567/// still names after its successful exec
12568/// (`procfs::locks::LockLedger::exec_kept`).
12569pub(crate) async fn note_table_exec<G, T>(guest: &mut G, surviving: Vec<(OpenFileId, u32)>)
12570where
12571    G: Guest<Detcore<T>>,
12572    T: RecordOrReplay,
12573{
12574    let response = send_and_update_time(guest, GlobalRequest::TableExec(surviving))
12575        .await
12576        .1;
12577    assert_eq!(response, GlobalResponse::TableExec(()));
12578}
12579
12580/// Record that a lock or unlock went through a descriptor whose description
12581/// the ledger may know under another identity, on the file with inode number
12582/// `inode` (`None`: unknown) (`procfs::locks::LockLedger::alias_inode`).
12583pub(crate) async fn note_aliased_inode<G, T>(guest: &mut G, inode: Option<String>)
12584where
12585    G: Guest<Detcore<T>>,
12586    T: RecordOrReplay,
12587{
12588    let response = send_and_update_time(guest, GlobalRequest::AliasedInode(inode))
12589        .await
12590        .1;
12591    assert_eq!(response, GlobalResponse::AliasedInode(()));
12592}
12593
12594/// Record that every later `/proc/locks` snapshot is refused, for `reason`
12595/// (`procfs::locks::LockLedger::refuse`).
12596pub(crate) async fn note_lock_ledger_refused<G, T>(guest: &mut G, reason: String)
12597where
12598    G: Guest<Detcore<T>>,
12599    T: RecordOrReplay,
12600{
12601    let response = send_and_update_time(guest, GlobalRequest::LockLedgerRefused(reason))
12602        .await
12603        .1;
12604    assert_eq!(response, GlobalResponse::LockLedgerRefused(()));
12605}
12606
12607/// Record that the backend holds a guest's record lock as an OFD lock on the
12608/// files `objects` names, or on a file the fact could not name (`None`)
12609/// (`procfs::locks::LockLedger::note_record_locks_held_as_ofd`).
12610pub(crate) async fn note_record_locks_held_as_ofd<G, T>(guest: &mut G, objects: Option<Vec<String>>)
12611where
12612    G: Guest<Detcore<T>>,
12613    T: RecordOrReplay,
12614{
12615    let response = send_and_update_time(guest, GlobalRequest::RecordLocksHeldAsOfd(objects))
12616        .await
12617        .1;
12618    assert_eq!(response, GlobalResponse::RecordLocksHeldAsOfd(()));
12619}
12620
12621/// Which of the rows of a `/proc/locks` snapshot the guest owns, in row order
12622/// (`procfs::locks::guest_lock_rows`), or why no snapshot can be decided
12623/// (`procfs::locks::LockLedger::unknown`).
12624pub(crate) async fn guest_lock_rows<G, T>(
12625    guest: &mut G,
12626    rows: Vec<crate::procfs::locks::LockRowFacts>,
12627) -> Result<Vec<bool>, String>
12628where
12629    G: Guest<Detcore<T>>,
12630    T: RecordOrReplay,
12631{
12632    match send_and_update_time(guest, GlobalRequest::GuestLockRows(rows))
12633        .await
12634        .1
12635    {
12636        GlobalResponse::GuestLockRows(kept) => kept,
12637        _ => unreachable!(),
12638    }
12639}
12640
12641/// Report that the sender's process's new image's `/proc/<pid>/auxv` is left
12642/// to the kernel beneath for `why`, and learn whether it is the run's first
12643/// for that reason, which the caller warns of: the guest's tool prints where
12644/// the run's output shows it, which the global tool's log does not on DBT.
12645/// `cmdline` is the image's argument and environment areas, if Detcore found
12646/// them on its initial stack (`ProcessAuxvTable::record_cmdline`).
12647pub(crate) async fn auxv_from_kernel<G, T>(
12648    guest: &mut G,
12649    why: AuxvFromKernel,
12650    cmdline: Option<crate::procfs::CmdlineArea>,
12651) -> bool
12652where
12653    G: Guest<Detcore<T>>,
12654    T: RecordOrReplay,
12655{
12656    match send_and_update_time(guest, GlobalRequest::AuxvFromKernel(why, cmdline))
12657        .await
12658        .1
12659    {
12660        GlobalResponse::AuxvFromKernel(first) => first,
12661        _ => unreachable!(),
12662    }
12663}
12664
12665/// Change or read the state Detcore keeps on the rendered per-process `/proc`
12666/// file numbered `inode` (`InodePool::rendered_file_state`).
12667pub async fn rendered_file_state<G, T>(
12668    guest: &mut G,
12669    inode: u64,
12670    change: crate::procfs::listing::RenderedFileChange,
12671) -> crate::procfs::listing::RenderedFileState
12672where
12673    G: Guest<Detcore<T>>,
12674    T: RecordOrReplay,
12675{
12676    let response =
12677        send_and_update_time(guest, GlobalRequest::RenderedFileState(inode, change)).await;
12678    match response.1 {
12679        GlobalResponse::RenderedFileState(state) => state,
12680        _ => unreachable!(),
12681    }
12682}
12683
12684/// Where the user namespace of `task`'s process is from the caller's
12685/// process's, and whether the namespace whose owner Linux's rule asks about
12686/// there may be owned by an effective user ID other than the real user ID
12687/// every guest task has (`scheduler::user_namespaces`).
12688pub(crate) async fn user_namespace_relation<G, T>(
12689    guest: &mut G,
12690    task: DetTid,
12691) -> (
12692    crate::scheduler::user_namespaces::UserNamespaceRelation,
12693    bool,
12694)
12695where
12696    G: Guest<Detcore<T>>,
12697    T: RecordOrReplay,
12698{
12699    let reader = guest.thread_state().dettid;
12700    let response =
12701        send_and_update_time(guest, GlobalRequest::UserNamespaceRelation(reader, task)).await;
12702    match response.1 {
12703        GlobalResponse::UserNamespaceRelation(relation, owner_may_differ) => {
12704            (relation, owner_may_differ)
12705        }
12706        _ => unreachable!(),
12707    }
12708}
12709
12710/// Record that the user namespace `process`'s clone made was made by a
12711/// creator whose effective user ID was not its real user ID, read as the
12712/// overflow ID, or could not be read, once that clone has succeeded
12713/// (`UserNamespaces::note_owner_may_differ`).
12714pub(crate) async fn note_user_namespace_owner_may_differ<G, T>(guest: &mut G, process: DetTid)
12715where
12716    G: Guest<Detcore<T>>,
12717    T: RecordOrReplay,
12718{
12719    let response =
12720        send_and_update_time(guest, GlobalRequest::UserNamespaceOwnerMayDiffer(process)).await;
12721    match response.1 {
12722        GlobalResponse::UserNamespaceOwnerMayDiffer(()) => {}
12723        _ => unreachable!(),
12724    }
12725}
12726
12727/// The kernel's number for each guest process or thread number in `ids`
12728/// (`to_kernel`), or the guest's for each kernel number, as `/proc` spells
12729/// them (`Scheduler::procfs_ids`).
12730pub async fn procfs_ids<G, T>(guest: &mut G, to_kernel: bool, ids: Vec<i32>) -> Vec<Option<i32>>
12731where
12732    G: Guest<Detcore<T>>,
12733    T: RecordOrReplay,
12734{
12735    let response = send_and_update_time(guest, GlobalRequest::ProcfsIds(to_kernel, ids)).await;
12736    match response.1 {
12737        GlobalResponse::ProcfsIds(ids) => ids,
12738        _ => unreachable!(),
12739    }
12740}
12741
12742/// The pid the calling thread, in its nested PID namespace, sees for `task`
12743/// (`PidNamespaces::localize_for`): `Ok(None)` when `task` is not visible
12744/// there.
12745pub async fn local_pid<G, T>(guest: &mut G, task: DetTid) -> Result<Option<i32>, String>
12746where
12747    G: Guest<Detcore<T>>,
12748    T: RecordOrReplay,
12749{
12750    let caller = guest.thread_state().dettid;
12751    let response = send_and_update_time(guest, GlobalRequest::LocalPid(caller, task)).await;
12752    match response.1 {
12753        GlobalResponse::LocalPid(answer) => answer,
12754        _ => unreachable!(),
12755    }
12756}
12757
12758/// The process a process-directed signal naming `named` goes to: a live
12759/// non-leader thread's id names its thread group, as Linux's kill_pid_info
12760/// does (`Scheduler::signal_target_process`).
12761pub async fn signal_target_process<G, T>(guest: &mut G, named: DetPid) -> Option<DetPid>
12762where
12763    G: Guest<Detcore<T>>,
12764    T: RecordOrReplay,
12765{
12766    let response = send_and_update_time(guest, GlobalRequest::SignalTargetProcess(named)).await;
12767    match response.1 {
12768        GlobalResponse::SignalTargetProcess(process) => process,
12769        _ => unreachable!(),
12770    }
12771}
12772
12773/// Mirror a successful `rt_sigaction(SIGCHLD, ...)` by `process` in the
12774/// scheduler's thread tree, in the calling thread's turn.
12775pub async fn set_sigchld_disposition<G, T>(
12776    guest: &mut G,
12777    process: DetPid,
12778    ignored: bool,
12779    nocldwait: bool,
12780) where
12781    G: Guest<Detcore<T>>,
12782    T: RecordOrReplay,
12783{
12784    let response = send_and_update_time(
12785        guest,
12786        GlobalRequest::SetSigchldDisposition(process, ignored, nocldwait),
12787    )
12788    .await;
12789    match response.1 {
12790        GlobalResponse::SetSigchldDisposition(()) => {}
12791        _ => unreachable!(),
12792    }
12793}
12794
12795/// Reserve the victims of the `kill(-pgid, SIGKILL)` the calling thread is
12796/// about to send (`Scheduler::reserve_sigkill_group`). Every guest process has
12797/// the same credentials, so the members share the caller's real uid.
12798pub async fn reserve_sigkill_group<G, T>(
12799    guest: &mut G,
12800    pgid: DetPid,
12801) -> Option<crate::scheduler::sigkill_retirement::SigkillReservation>
12802where
12803    G: Guest<Detcore<T>>,
12804    T: RecordOrReplay,
12805{
12806    // Backends that cannot retire at the kill turn send no extra RPC.
12807    if !crate::scheduler::sigkill_retirement::retires_sigkill_at_the_kill_turn(
12808        &guest.config().backend,
12809    ) {
12810        return None;
12811    }
12812    let sender = guest.thread_state().dettid;
12813    let uid = crate::syscalls::real_uid(guest.tid().as_raw())
12814        .unwrap_or_else(|| unsafe { libc::getuid() });
12815    let response =
12816        send_and_update_time(guest, GlobalRequest::ReserveSigkillGroup(sender, pgid, uid)).await;
12817    match response.1 {
12818        GlobalResponse::ReserveSigkill(token) => token,
12819        _ => unreachable!(),
12820    }
12821}
12822
12823/// The threads the send of SIGKILL reservation `token` kills, with their
12824/// address spaces (`Scheduler::sigkill_dying_threads`).
12825pub(crate) async fn sigkill_dying_threads<G, T>(guest: &mut G, token: u64) -> Vec<(DetTid, MmId)>
12826where
12827    G: Guest<Detcore<T>>,
12828    T: RecordOrReplay,
12829{
12830    let response = send_and_update_time(guest, GlobalRequest::SigkillDyingThreads(token)).await;
12831    match response.1 {
12832        GlobalResponse::SigkillDyingThreads(dying) => dying,
12833        _ => unreachable!(),
12834    }
12835}
12836
12837/// Hand the scheduler the robust lists recorded before the send of SIGKILL
12838/// reservation `token` (`Scheduler::record_sigkill_robust_lists`).
12839pub(crate) async fn record_sigkill_robust_lists<G, T>(
12840    guest: &mut G,
12841    token: u64,
12842    lists: Vec<(
12843        DetTid,
12844        MmId,
12845        crate::syscalls::robust_list::RobustListSnapshot,
12846    )>,
12847) where
12848    G: Guest<Detcore<T>>,
12849    T: RecordOrReplay,
12850{
12851    let response =
12852        send_and_update_time(guest, GlobalRequest::RecordSigkillRobustLists(token, lists)).await;
12853    match response.1 {
12854        GlobalResponse::RecordSigkillRobustLists(()) => {}
12855        _ => unreachable!(),
12856    }
12857}
12858
12859/// From the exit callback of a thread a SIGKILL ended: take the robust list
12860/// the SIGKILL's sender recorded for it (`Scheduler::take_sigkill_robust_list`).
12861pub(crate) async fn take_sigkill_robust_list<R>(
12862    threads_time: DetTime,
12863    reverie: &R,
12864    mm: MmId,
12865) -> Option<crate::syscalls::robust_list::RobustListSnapshot>
12866where
12867    R: GlobalRPC<GlobalState>,
12868{
12869    let response = reverie
12870        .send_rpc((threads_time, mm, GlobalRequest::TakeSigkillRobustList))
12871        .await;
12872    match response.1 {
12873        GlobalResponse::TakeSigkillRobustList(list) => list,
12874        _ => unreachable!(),
12875    }
12876}
12877
12878/// Commit (`sent`) or unwind a SIGKILL reservation at the sending boundary.
12879pub async fn commit_sigkill<G, T>(guest: &mut G, token: u64, sent: bool)
12880where
12881    G: Guest<Detcore<T>>,
12882    T: RecordOrReplay,
12883{
12884    let response = send_and_update_time(guest, GlobalRequest::CommitSigkill(token, sent)).await;
12885    match response.1 {
12886        GlobalResponse::CommitSigkill(()) => {}
12887        _ => unreachable!(),
12888    }
12889}
12890
12891pub async fn create_session<G, T>(guest: &mut G, process: DetPid) -> bool
12892where
12893    G: Guest<Detcore<T>>,
12894    T: RecordOrReplay,
12895{
12896    let response = send_and_update_time(guest, GlobalRequest::CreateSession(process)).await;
12897    match response.1 {
12898        GlobalResponse::CreateSession(updated) => updated,
12899        _ => unreachable!(),
12900    }
12901}
12902
12903pub async fn consume_child_wait<G, T>(guest: &mut G, child: DetPid) -> bool
12904where
12905    G: Guest<Detcore<T>>,
12906    T: RecordOrReplay,
12907{
12908    let parent = guest.thread_state().detpid.expect("detpid unset");
12909    let response =
12910        send_and_update_time(guest, GlobalRequest::ConsumeChildWait(parent, child)).await;
12911    match response.1 {
12912        GlobalResponse::ConsumeChildWait(consumed) => consumed,
12913        _ => unreachable!(),
12914    }
12915}
12916
12917/// The live threads of `detpid`, and its exit state
12918/// (`Scheduler::exited_process_is_zombie`).
12919pub async fn resolve_kill_targets_and_exit<G, T>(
12920    guest: &mut G,
12921    detpid: DetPid,
12922) -> (Vec<DetTid>, Option<bool>)
12923where
12924    G: Guest<Detcore<T>>,
12925    T: RecordOrReplay,
12926{
12927    let response = send_and_update_time(guest, GlobalRequest::ResolveKillTargets(detpid)).await;
12928    match response.1 {
12929        GlobalResponse::ResolveKillTargets(targets, zombie) => (targets, zombie),
12930        _ => unreachable!(),
12931    }
12932}
12933
12934/// Whether `tid`, a pid or tid the calling guest task named (in thread group
12935/// `tgid`, for the calls that name one), is one of Hermit's own tasks; see
12936/// [`hermit_task_named`].
12937pub async fn names_hermit_task<G, T>(guest: &mut G, tgid: Option<i32>, tid: i32) -> bool
12938where
12939    G: Guest<Detcore<T>>,
12940    T: RecordOrReplay,
12941{
12942    let caller = guest.thread_state().dettid;
12943    let response =
12944        send_and_update_time(guest, GlobalRequest::NamesHermitTask(caller, tgid, tid)).await;
12945    match response.1 {
12946        GlobalResponse::NamesHermitTask(named) => named,
12947        _ => unreachable!(),
12948    }
12949}
12950
12951/// Whether guest task `caller`, naming `tid` (and thread group `tgid`, for the
12952/// calls that name one), names one of Hermit's own tasks: a thread of the
12953/// process Detcore's global state runs in (<https://github.com/rrnewton/hermit/issues/4066>).
12954/// Under ptrace that is Hermit's, the container init, PID 1 of the guest's PID
12955/// namespace. Under DBT the global state runs in the root guest process, whose
12956/// guest ids are virtual and are not host tasks descended from it, so the
12957/// answer there is always no, as for KVM below.
12958///
12959/// Hermit's tasks are identified directly, as its own thread group, not as
12960/// "not a registered guest task": a guest task Detcore has not registered yet
12961/// (a newborn, before its parent's `clone` returns) is not Hermit's. The
12962/// thread-group test is `tgkill(hermit, tid, 0)`, which sends nothing.
12963///
12964/// Only a caller that is a host process descended from Hermit, in Hermit's
12965/// own PID namespace, can name Hermit's tasks:
12966/// - a caller in a nested PID namespace names tasks by that namespace's ids,
12967///   where Hermit's tasks are not visible;
12968/// - a backend whose guest tasks are not host tasks (KVM, whose guest ids may
12969///   equal the ids of Hermit's own threads) never reaches Hermit by a pid;
12970/// - a caller that is one of Hermit's threads, or whose `/proc` entry cannot
12971///   be read, is not such a host task: a guest task in a system call is alive.
12972pub fn hermit_task_named(caller: DetTid, tgid: Option<i32>, tid: i32) -> bool {
12973    use std::os::unix::fs::MetadataExt;
12974    let hermit = std::process::id() as i32;
12975    if tid <= 0 || tgid.is_some_and(|tgid| tgid != hermit) {
12976        return false;
12977    }
12978    // SAFETY: signal 0 to a thread of this process only checks it exists.
12979    let in_hermits_group =
12980        tid == hermit || unsafe { libc::syscall(libc::SYS_tgkill, hermit, tid, 0) } == 0;
12981    if !in_hermits_group || !descends_from(caller.as_raw(), hermit) {
12982        return false;
12983    }
12984    let namespace = |path: String| fs::metadata(path).map(|meta| (meta.dev(), meta.ino()));
12985    match (
12986        namespace(format!("/proc/{}/ns/pid", caller.as_raw())),
12987        namespace("/proc/self/ns/pid".to_owned()),
12988    ) {
12989        (Ok(callers), Ok(hermits)) => callers == hermits,
12990        _ => false,
12991    }
12992}
12993
12994/// Whether host task `task`'s process is a descendant of process `ancestor`,
12995/// following parent pids in `/proc` (field 4 of `stat`, the process's parent
12996/// for any of its threads). A task that cannot be read is not.
12997fn descends_from(task: i32, ancestor: i32) -> bool {
12998    let parent = |pid: i32| -> Option<i32> {
12999        let stat = fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
13000        // The command name may hold spaces and parentheses: fields resume
13001        // after the last ')', as state, ppid.
13002        stat.rsplit_once(')')?
13003            .1
13004            .split_whitespace()
13005            .nth(1)?
13006            .parse()
13007            .ok()
13008    };
13009    let mut pid = task;
13010    // Bounded by the depth of any real process tree; a reparenting race
13011    // cannot make it loop.
13012    for _ in 0..4096 {
13013        match parent(pid) {
13014            Some(next) if next == ancestor => return true,
13015            Some(next) if next > 0 => pid = next,
13016            _ => return false,
13017        }
13018    }
13019    false
13020}
13021
13022/// Tell the scheduler that a SIGKILL it is not told about one victim at a time
13023/// succeeded: a process-group kill (`Some(group)`), or a pidfd with no recorded
13024/// process (`None`). See `Scheduler::record_unresolved_sigkill`.
13025pub async fn notify_unresolved_sigkill<G, T>(guest: &mut G, group: Option<DetPid>)
13026where
13027    G: Guest<Detcore<T>>,
13028    T: RecordOrReplay,
13029{
13030    let response = send_and_update_time(guest, GlobalRequest::NotifyUnresolvedSigkill(group)).await;
13031    match response.1 {
13032        GlobalResponse::NotifyUnresolvedSigkill(()) => {}
13033        _ => unreachable!(),
13034    }
13035}
13036
13037/// Tell the scheduler that a successful kill(2) left a physical
13038/// signal pending for a sole, known target.
13039/// The `nix` signal a timer will raise.
13040///
13041/// Timer registration still models a named signal: `setitimer`/`timer_create`
13042/// reach here with one, and the timer wheel stores it. Widening that is a
13043/// separate axis from the notification defect this change fixes, so the
13044/// conversion is asserted here rather than silently widened — a realtime timer
13045/// signal would be a NEW capability, not a regression of an existing one.
13046fn alarm_signal(sig: SigWrapper) -> Signal {
13047    sig.signal().unwrap_or_else(|| {
13048        panic!(
13049            "timer registration received unnameable signal {}",
13050            sig.raw()
13051        )
13052    })
13053}
13054
13055/// Ask that this thread's next request, its group `Exit`, be granted before
13056/// any other thread runs (`GlobalRequest::HoldTurnForGroupExit`). Returns
13057/// whether the scheduler agreed, which it does only for the thread holding the
13058/// serial grant.
13059pub async fn hold_turn_for_group_exit<G, T>(guest: &mut G, dettid: DetTid) -> bool
13060where
13061    G: Guest<Detcore<T>>,
13062    T: RecordOrReplay,
13063{
13064    let response = send_and_update_time(guest, GlobalRequest::HoldTurnForGroupExit(dettid)).await;
13065    match response.1 {
13066        GlobalResponse::HoldTurnForGroupExit(held) => held,
13067        _ => unreachable!(),
13068    }
13069}
13070
13071pub async fn notify_signal_pending<G, T>(
13072    guest: &mut G,
13073    dettid: DetTid,
13074    signal: SigWrapper,
13075    target_process: Option<DetPid>,
13076    scope: SignalScope,
13077) where
13078    G: Guest<Detcore<T>>,
13079    T: RecordOrReplay,
13080{
13081    let response = send_and_update_time(
13082        guest,
13083        GlobalRequest::NotifySignalPending(dettid, signal, target_process, scope),
13084    )
13085    .await;
13086    match response.1 {
13087        GlobalResponse::NotifySignalPending(()) => {}
13088        _ => unreachable!(),
13089    }
13090}
13091
13092/// Signal an unrecoverable error that exits the entire container.
13093/// Such exits are not determinizable (see "quasi-determinism").
13094///
13095/// ⚠️ `status` IS NOT A DEFAULTABLE PARAMETER, AND THAT IS THE POINT. This
13096/// function has four callers and they do not all mean the same thing: three are
13097/// fail-closed policy refusals (`HERMIT_POLICY_REFUSAL_EXIT`) and one is an
13098/// operator interrupt (`HERMIT_SIGINT_DEATH_EXIT`). While the status was baked in
13099/// here, every caller inherited whatever the last edit chose, so the SIGINT path
13100/// reported "hermit refused this run" for a run hermit did not refuse. Making it
13101/// an argument turns the meaning into a visible claim at each call site, which is
13102/// the same rule `scripts/check-exit-status-class.rs` enforces on the test side:
13103/// a status you can read is worth less than a status that says which channel
13104/// produced it.
13105///
13106/// Adding a caller means choosing; there is deliberately no default to fall into.
13107pub async fn unrecoverable_shutdown<G, T>(guest: &G, status: i32) -> !
13108where
13109    G: Guest<Detcore<T>>,
13110    T: RecordOrReplay,
13111{
13112    // The process ends here (`crate::guest_log`).
13113    crate::guest_log::flush_unsent();
13114    if cfg!(debug_assertions) {
13115        let mytime = guest.thread_state().thread_logical_time.clone();
13116        let mm = guest.thread_state().mm_id;
13117        // TODO: void_send_rpc
13118        let _ = guest
13119            .send_rpc((mytime, mm, GlobalRequest::UnrecoverableShutdown))
13120            .await;
13121    }
13122
13123    // In this scenario a backtrace doesn't really help us.
13124    //
13125    // ⚠️ THE STATUS IS THE ONLY THING THAT CROSSES THIS BOUNDARY, SO IT HAS TO
13126    // CARRY THE MEANING. This is a DELIBERATE, policy-driven shutdown: the run
13127    // hit an operation it cannot service under a fail-closed configuration and
13128    // hermit chose to stop. The parent sees only a container child that exited,
13129    // and its classifier treats an exit it cannot account for as unchosen — so
13130    // exiting 1 here made `classify_container_result` report
13131    // `class=container-child-exit` and 125, i.e. "hermit broke", for a shutdown
13132    // that worked exactly as designed. `HERMIT_POLICY_REFUSAL_EXIT` is the
13133    // agreed value for "hermit refused"; see its doc for why it is not 1.
13134    //
13135    // ⚠️ AND WHICH VALUE IS THE CALLER'S TO SAY, NOT THIS FUNCTION'S. Baking one
13136    // in here is what made an operator's Ctrl-C report as a policy refusal: the
13137    // condition differs per caller and only the caller knows it.
13138    //
13139    // The RPC above is `cfg!(debug_assertions)`-only, so it cannot be the
13140    // signal — in a release build the parent would learn nothing.
13141    std::process::exit(status);
13142}
13143
13144#[cfg(test)]
13145mod tests {
13146    #[tokio::test]
13147    async fn automatic_guest_ids_skip_registered_guest_choices() {
13148        let config = Config {
13149            guest_ids: detcore_model::pid::GuestIds::Chosen {
13150                base: 5000,
13151                refuse_host_ids: true,
13152            },
13153            ..Config::default()
13154        };
13155        let state = GlobalState::initialize(&config, false);
13156        let root = DetTid::from_raw(5000);
13157        let explicit = DetTid::from_raw(5001);
13158        {
13159            let mut sched = state.sched.lock().unwrap();
13160            sched.thread_tree.add_child(root, root, true);
13161            sched.thread_tree.add_child(root, explicit, true);
13162            assert!(sched.thread_was_registered(explicit));
13163        }
13164        assert_eq!(state.take_guest_id().await, Some(5002));
13165        assert_eq!(state.take_guest_id().await, Some(5003));
13166    }
13167
13168    /// The last positive candidate may be taken only once. Exhaustion stays
13169    /// exhausted, including when initialization cannot represent base + 1.
13170    #[tokio::test]
13171    async fn automatic_guest_ids_stop_instead_of_wrapping() {
13172        use std::sync::atomic::Ordering::SeqCst;
13173
13174        let config = |base| Config {
13175            guest_ids: detcore_model::pid::GuestIds::Chosen {
13176                base,
13177                refuse_host_ids: true,
13178            },
13179            ..Config::default()
13180        };
13181        let state = GlobalState::initialize(&config(i32::MAX - 1), false);
13182        assert_eq!(state.take_guest_id().await, Some(i32::MAX));
13183        assert_eq!(state.next_guest_id.load(SeqCst), 0);
13184        for _ in 0..3 {
13185            assert_eq!(state.take_guest_id().await, None);
13186            assert_eq!(state.next_guest_id.load(SeqCst), 0);
13187        }
13188        for base in [i32::MAX, 0, -1, i32::MIN] {
13189            let state = GlobalState::initialize(&config(base), false);
13190            assert_eq!(state.take_guest_id().await, None, "base {base}");
13191            assert_eq!(state.take_guest_id().await, None, "base {base}");
13192            assert_eq!(state.next_guest_id.load(SeqCst), 0);
13193        }
13194
13195        // Both kinds of already-used ID remain excluded at exhaustion.
13196        let state = GlobalState::initialize(&config(i32::MAX - 2), false);
13197        let root = DetTid::from_raw(i32::MAX - 2);
13198        let registered = DetTid::from_raw(i32::MAX - 1);
13199        let clocked = DetTid::from_raw(i32::MAX);
13200        {
13201            let mut scheduler = state.sched.lock().unwrap();
13202            scheduler.thread_tree.add_child(root, root, true);
13203            scheduler.thread_tree.add_child(root, registered, true);
13204            assert!(!scheduler.thread_was_registered(clocked));
13205        }
13206        {
13207            let mut clock = state.global_time.lock().unwrap();
13208            let now = clock.as_nanos();
13209            clock.update_global_time(clocked, now, Default::default());
13210            assert!(clock.contains_thread(clocked));
13211        }
13212        assert_eq!(state.take_guest_id().await, None);
13213        assert_eq!(state.next_guest_id.load(SeqCst), 0);
13214        assert_eq!(state.take_guest_id().await, None);
13215    }
13216
13217    /// The terminal receipt of a death by a fatal signal lets go of the
13218    /// tables the dying threads named (`GlobalState::release_tables_at_death`):
13219    /// a receiving socket whose last descriptor the dead child held takes
13220    /// the copy in flight its queue held, so the lock ledger stops counting
13221    /// it, and the dead thread's table is forgotten. A dying thread that
13222    /// named no table, beside one that named the process's table, decides
13223    /// nothing more where the process has no thread with a table of its own.
13224    /// Where it may hold a table no thread named (the process has such a
13225    /// thread, or no thread named any), and the ledger counts a copy in
13226    /// flight at the death, every later `/proc/locks` snapshot is refused by
13227    /// name, for the rest of the run (Codex's narrow check of K8-h reb8). A
13228    /// table that reached the global state through a transport that encodes
13229    /// requests arrives as no table, and is refused by name (`HeldTable`).
13230    #[test]
13231    fn a_death_by_a_fatal_signal_lets_go_of_the_tables_its_threads_named() {
13232        use nix::fcntl::OFlag;
13233
13234        use crate::fd::FdType;
13235        use crate::fd::SentDescriptor;
13236        use crate::procfs::locks::PlaceholderCopies;
13237        use crate::scheduler::signal_control::SignalDeath;
13238        use crate::types::DetPid;
13239        use crate::types::DetTid;
13240
13241        let config = Config {
13242            virtualize_time: true,
13243            sequentialize_threads: true,
13244            recordreplay_modes: false,
13245            ..Config::default()
13246        };
13247        let state = GlobalState::initialize(&config, false);
13248        let hold = |tid: DetTid, thread: &crate::ThreadState<()>| {
13249            assert_eq!(
13250                state.hold_table(
13251                    Tid::from_raw(tid.as_raw()),
13252                    &super::HeldTable::new(std::sync::Arc::clone(&thread.file_metadata)),
13253                ),
13254                (None, GlobalResponse::HoldTable(Ok(())))
13255            );
13256        };
13257
13258        // The parent sends its locked description to the socket the child
13259        // holds, and closes its own descriptor of it.
13260        let parent_id = DetTid::from_raw(9060);
13261        let child_id = DetTid::from_raw(9061);
13262        let parent = crate::ThreadState::new(parent_id, &config, ());
13263        parent
13264            .add_fd(3, OFlag::empty(), FdType::Socket, None)
13265            .unwrap();
13266        parent
13267            .add_fd(4, OFlag::empty(), FdType::Socket, None)
13268            .unwrap();
13269        let left = parent.with_detfd(3, |detfd| detfd.clone()).unwrap();
13270        let right = parent.with_detfd(4, |detfd| detfd.clone()).unwrap();
13271        left.pair_sockets(&right, false);
13272        drop(right);
13273        let child = crate::ThreadState::new(child_id, &config, ());
13274        let mut copy = parent.file_metadata.lock().unwrap().fork_for(child_id);
13275        copy.hold_alone(child_id);
13276        *child.file_metadata.lock().unwrap() = copy;
13277        assert_eq!(parent.remove_fd(4), None, "the child holds the receiver");
13278        assert_eq!(child.remove_fd(3), None, "the parent holds the sender");
13279        parent
13280            .add_fd(5, OFlag::empty(), FdType::Regular, None)
13281            .unwrap();
13282        let carried = parent
13283            .with_detfd(5, |detfd| detfd.in_flight_open_file(true))
13284            .unwrap();
13285        let sent_id = parent.with_detfd(5, |detfd| detfd.open_file_id()).unwrap();
13286        assert!(
13287            left.queue_rights_to_peer(
13288                vec![SentDescriptor {
13289                    file: (1, 5),
13290                    open_file: Some(carried),
13291                    lock_copy: Some(sent_id),
13292                }],
13293                1,
13294            )
13295            .is_ok()
13296        );
13297        assert_eq!(parent.remove_fd(5), None, "a copy is in flight");
13298        let ledger = || state.lock_ledger.lock().unwrap();
13299        assert_eq!(
13300            ledger().placeholder_copies(PlaceholderCopies::Sending(vec![sent_id])),
13301            Ok(())
13302        );
13303        hold(child_id, &child);
13304        state.release_tables_at_death(SignalDeath {
13305            process: DetPid::from_raw(child_id.as_raw()),
13306            threads: vec![child_id],
13307        });
13308        assert!(
13309            !ledger().counts_copies_in_flight(),
13310            "the copy in flight went with the receiving socket"
13311        );
13312        assert_eq!(ledger().unknown(), None);
13313        assert!(state.held_tables.lock().unwrap().is_empty());
13314        assert!(
13315            matches!(
13316                child
13317                    .file_metadata
13318                    .lock()
13319                    .unwrap()
13320                    .release_at_death(child_id),
13321                crate::tool_local::TableAtDeath::Undecided
13322            ),
13323            "the table went, with its holders"
13324        );
13325
13326        // A thread that named no table beside one that named the process's.
13327        let shared_id = DetTid::from_raw(9062);
13328        let unnamed_id = DetTid::from_raw(9063);
13329        let shared = crate::ThreadState::new(shared_id, &config, ());
13330        shared.file_metadata.lock().unwrap().join_holder(shared_id);
13331        hold(shared_id, &shared);
13332        assert_eq!(
13333            ledger().placeholder_copies(PlaceholderCopies::Sending(vec![sent_id])),
13334            Ok(())
13335        );
13336        state.release_tables_at_death(SignalDeath {
13337            process: DetPid::from_raw(shared_id.as_raw()),
13338            threads: vec![shared_id, unnamed_id],
13339        });
13340        assert_eq!(ledger().unknown(), None, "the unnamed thread shares it");
13341        assert!(ledger().counts_copies_in_flight(), "its table held no copy");
13342
13343        // In a process one of whose threads has a table of its own, a thread
13344        // that named none may hold a table no thread named: refused.
13345        let private_id = DetTid::from_raw(9064);
13346        let private = DetPid::from_raw(private_id.as_raw());
13347        ledger().mark_private_tables(private);
13348        state.release_tables_at_death(SignalDeath {
13349            process: private,
13350            threads: vec![private_id],
13351        });
13352        let refusal = ledger().unknown().map(str::to_owned);
13353        assert!(
13354            refusal
13355                .as_deref()
13356                .is_some_and(|reason| reason.contains("died by a fatal signal")),
13357            "{refusal:?}"
13358        );
13359        drop(left);
13360
13361        // Through a transport that encodes its requests, the table is encoded
13362        // as nothing and decoded as none, and refused by name.
13363        let request = GlobalRequest::HoldTable(super::HeldTable::new(std::sync::Arc::clone(
13364            &child.file_metadata,
13365        )));
13366        let encoded = bincode::serde::encode_to_vec(&request, bincode::config::legacy()).unwrap();
13367        let (decoded, _): (GlobalRequest, usize) =
13368            bincode::serde::decode_from_slice(&encoded, bincode::config::legacy()).unwrap();
13369        assert_ne!(decoded, request, "no table crosses the encoding");
13370        let GlobalRequest::HoldTable(decoded) = decoded else {
13371            panic!("a held table decodes as a held table: {decoded:?}");
13372        };
13373        let refused = state.hold_table(Tid::from_raw(child_id.as_raw()), &decoded);
13374        assert!(
13375            matches!(
13376                &refused,
13377                (None, GlobalResponse::HoldTable(Err(reason)))
13378                    if reason.contains("a transport that encodes its requests")
13379            ),
13380            "{refused:?}"
13381        );
13382        assert!(state.held_tables.lock().unwrap().is_empty());
13383    }
13384
13385    /// The host's `/proc/diskstats` is read once per run, in the
13386    /// coordinator's own process, and every request of the run, whatever
13387    /// process sends it, is answered the same bytes, with no time
13388    /// (`GlobalRequest::HostDiskstats`): a rendered `/proc/diskstats` rebuilt
13389    /// in another process needs no descriptor of its own to start from the
13390    /// table its open took.
13391    #[tokio::test]
13392    async fn the_host_diskstats_are_read_once_and_answered_alike() {
13393        use reverie::Tid;
13394        let config = Config::default();
13395        let state = GlobalState::initialize(&config, false);
13396        let ask = |from: i32| {
13397            state.receive_rpc(
13398                Tid::from_raw(from),
13399                (
13400                    DetTime::default(),
13401                    MmId::initial(DetTid::from_raw(0)),
13402                    super::GlobalRequest::HostDiskstats,
13403                ),
13404            )
13405        };
13406        let (time, first) = ask(3).await;
13407        assert_eq!(time, None, "answered with no time");
13408        let host = std::fs::read("/proc/diskstats").map_err(|error| error.raw_os_error());
13409        match (&first, &host) {
13410            (super::GlobalResponse::HostDiskstats(Ok(bytes)), Ok(_)) => {
13411                assert!(!bytes.is_empty() || host.as_ref().is_ok_and(Vec::is_empty));
13412            }
13413            (super::GlobalResponse::HostDiskstats(Err(errno)), Err(Some(expected))) => {
13414                assert_eq!(errno, expected);
13415            }
13416            other => panic!("the coordinator's read disagrees with this process's: {other:?}"),
13417        }
13418        for from in [4, 3, 9] {
13419            assert_eq!(ask(from).await.1, first, "the same table for every request");
13420        }
13421    }
13422
13423    /// The record sequence of a per-process file's open file description a
13424    /// fork copied where the tool runs in each guest process is kept here
13425    /// between operations, whichever process sends them, and answered with no
13426    /// time (`GlobalRequest::ProcfsSequence`,
13427    /// `GlobalRequest::StoreProcfsSequence`): none before the first store,
13428    /// then the last one stored, for that description alone.
13429    #[tokio::test]
13430    async fn a_shared_record_sequence_is_the_last_one_stored() {
13431        use reverie::Tid;
13432        let config = Config::default();
13433        let state = GlobalState::initialize(&config, false);
13434        let send = |from: i32, request: super::GlobalRequest| {
13435            state.receive_rpc(
13436                Tid::from_raw(from),
13437                (
13438                    DetTime::default(),
13439                    MmId::initial(DetTid::from_raw(0)),
13440                    request,
13441                ),
13442            )
13443        };
13444        let id = crate::OpenFileId::new(DetTid::from_raw(3), 7);
13445        let other = crate::OpenFileId::new(DetTid::from_raw(4), 7);
13446        let (time, none) = send(3, super::GlobalRequest::ProcfsSequence(id)).await;
13447        assert_eq!(time, None, "answered with no time");
13448        assert_eq!(none, super::GlobalResponse::ProcfsSequence(None));
13449        let first = crate::procfs::ProcfsSequence::for_test(4, Some(4), b"rchar: 0\n");
13450        let second = crate::procfs::ProcfsSequence::for_test(6, Some(6), b"rchar: 0\n");
13451        for (from, sequence) in [(3, &first), (5, &second)] {
13452            let (time, stored) = send(
13453                from,
13454                super::GlobalRequest::StoreProcfsSequence(id, sequence.clone()),
13455            )
13456            .await;
13457            assert_eq!(time, None, "answered with no time");
13458            assert_eq!(stored, super::GlobalResponse::StoreProcfsSequence(()));
13459        }
13460        for from in [3, 5, 9] {
13461            assert_eq!(
13462                send(from, super::GlobalRequest::ProcfsSequence(id)).await.1,
13463                super::GlobalResponse::ProcfsSequence(Some(second.clone())),
13464                "the last one stored, for every process"
13465            );
13466        }
13467        assert_eq!(
13468            send(3, super::GlobalRequest::ProcfsSequence(other)).await.1,
13469            super::GlobalResponse::ProcfsSequence(None),
13470            "another description has its own"
13471        );
13472    }
13473
13474    /// `response` as `receive_rpc` returns it after a turn was granted
13475    /// (`GlobalResponse::Granted`).
13476    fn granted(response: super::GlobalResponse) -> super::GlobalResponse {
13477        super::GlobalResponse::Granted(Box::new(response))
13478    }
13479
13480    /// A process reads its current image's vector; a forked child its
13481    /// creator's until it execs; an exec replaces it; a forgotten process
13482    /// (gone, or an exec with no recorded vector) has none.
13483    #[test]
13484    fn a_changed_root_follows_fork_and_goes_at_exit() {
13485        use super::ProcessAuxvTable;
13486        use crate::types::DetPid;
13487        let (parent, child, other) = (
13488            DetPid::from_raw(3),
13489            DetPid::from_raw(4),
13490            DetPid::from_raw(5),
13491        );
13492        let mut table = ProcessAuxvTable::default();
13493        table.clone_root(DetPid::from_raw(1), parent, false);
13494        assert!(!table.root_changed(parent));
13495        table.mark_changed_root(parent);
13496        assert!(table.root_changed(parent));
13497        // A new task without CLONE_FS starts with a copy of its creator's
13498        // root.
13499        table.clone_root(parent, child, false);
13500        assert!(table.root_changed(child));
13501        table.clone_root(other, child, false);
13502        assert!(!table.root_changed(child), "a reused number starts afresh");
13503        // An exec keeps the root, so only an exit forgets it.
13504        table.forget(parent);
13505        assert!(table.root_changed(parent));
13506        table.forget_process(parent);
13507        assert!(!table.root_changed(parent));
13508    }
13509
13510    /// Tasks created with CLONE_FS share one root, as Linux's `fs_struct`:
13511    /// a chroot by the child changes its creator's root too, a task created
13512    /// without CLONE_FS keeps its own, and a non-leader thread's exec takes
13513    /// its group to the process's number.
13514    #[test]
13515    fn a_root_is_shared_by_a_filesystem_group() {
13516        use super::ProcessAuxvTable;
13517        use crate::types::DetPid;
13518        let pid = DetPid::from_raw;
13519        let mut table = ProcessAuxvTable::default();
13520        table.clone_root(pid(1), pid(3), false);
13521        // A CLONE_FS|SIGCHLD child, and an ordinary fork as an observer.
13522        table.clone_root(pid(3), pid(4), true);
13523        table.clone_root(pid(3), pid(5), false);
13524        table.mark_changed_root(pid(4));
13525        assert!(
13526            table.root_changed(pid(3)),
13527            "the child's chroot is its creator's"
13528        );
13529        assert!(
13530            !table.root_changed(pid(5)),
13531            "a copied root stays where it was"
13532        );
13533        // A thread created without CLONE_FS has a root of its own.
13534        table.clone_root(pid(5), pid(6), false);
13535        table.mark_changed_root(pid(6));
13536        assert!(!table.root_changed(pid(5)));
13537        // Thread 6's exec gives it process 5's number, and its root.
13538        table.transfer_root(pid(6), pid(5));
13539        assert!(table.root_changed(pid(5)));
13540        // The group outlives a member that exits.
13541        table.forget_process(pid(4));
13542        assert!(table.root_changed(pid(3)));
13543    }
13544
13545    #[test]
13546    fn process_auxv_follows_exec_and_fork() {
13547        use super::ProcessAuxv;
13548        use super::ProcessAuxvTable;
13549        use crate::types::DetPid;
13550        let (parent, child) = (DetPid::from_raw(3), DetPid::from_raw(4));
13551        let mut table = ProcessAuxvTable::default();
13552        assert_eq!(table.current(parent), None);
13553        table.record(parent, vec![1; 16]);
13554        table.inherit(parent, child);
13555        assert_eq!(
13556            table.current(child),
13557            Some(ProcessAuxv {
13558                process: child,
13559                bytes: vec![1; 16]
13560            })
13561        );
13562        table.record(child, vec![2; 32]);
13563        assert_eq!(
13564            table.current(child).map(|entry| entry.bytes),
13565            Some(vec![2; 32])
13566        );
13567        assert_eq!(
13568            table.current(parent).map(|entry| entry.bytes),
13569            Some(vec![1; 16])
13570        );
13571        table.forget(parent);
13572        assert_eq!(table.current(parent), None);
13573        // A creator with no vector gives its child none.
13574        table.inherit(parent, child);
13575        assert_eq!(table.current(child), None);
13576        assert_eq!(table.len(), 0);
13577    }
13578
13579    /// A process's argument and environment areas follow its images as its
13580    /// vector does: recorded at exec, copied to a fork child (which runs its
13581    /// creator's image at the same addresses), replaced by the child's own
13582    /// exec, dropped by an exec Detcore read no areas for, and forgotten at
13583    /// exit.
13584    #[test]
13585    fn process_cmdline_areas_follow_exec_and_fork() {
13586        use super::ProcessAuxvTable;
13587        use crate::procfs::CmdlineArea;
13588        use crate::types::DetPid;
13589        let (parent, child) = (DetPid::from_raw(3), DetPid::from_raw(4));
13590        let area = |start| CmdlineArea {
13591            arg_start: start,
13592            arg_end: start + 8,
13593            env_start: start + 8,
13594            env_end: start + 16,
13595        };
13596        let mut table = ProcessAuxvTable::default();
13597        assert_eq!(table.current_cmdline(parent), None);
13598        table.record_cmdline(parent, Some(area(0x1000)));
13599        table.inherit(parent, child);
13600        assert_eq!(table.current_cmdline(child), Some(area(0x1000)));
13601        table.record_cmdline(child, Some(area(0x2000)));
13602        assert_eq!(table.current_cmdline(child), Some(area(0x2000)));
13603        assert_eq!(table.current_cmdline(parent), Some(area(0x1000)));
13604        // An exec Detcore read no areas for leaves the kernel's file.
13605        table.record_cmdline(child, None);
13606        assert_eq!(table.current_cmdline(child), None);
13607        // A vector forgotten keeps the areas; the process's exit does not.
13608        table.record_cmdline(child, Some(area(0x3000)));
13609        table.forget(child);
13610        assert_eq!(table.current_cmdline(child), Some(area(0x3000)));
13611        table.forget_process(child);
13612        assert_eq!(table.current_cmdline(child), None);
13613        table.forget_process(parent);
13614        // A creator with no areas gives its child none.
13615        table.record_cmdline(child, Some(area(0x4000)));
13616        table.inherit(parent, child);
13617        assert_eq!(table.current_cmdline(child), None);
13618    }
13619
13620    /// Hermit's tasks are exactly its own thread group, named from its own PID
13621    /// namespace (<https://github.com/rrnewton/hermit/issues/4066>). The test
13622    /// process stands in for Hermit.
13623    #[test]
13624    fn only_hermits_own_thread_group_is_hermits() {
13625        use super::hermit_task_named;
13626        use crate::types::DetTid;
13627        let me = std::process::id() as i32;
13628        // A child process stands in for a guest task.
13629        let mut guest = std::process::Command::new("sleep")
13630            .arg("30")
13631            .spawn()
13632            .unwrap();
13633        let caller = DetTid::from_raw(guest.id() as i32);
13634        assert!(hermit_task_named(caller, None, me));
13635        assert!(hermit_task_named(caller, Some(me), me));
13636        // One of Hermit's own threads is not a guest task, and names nothing
13637        // of Hermit's (KVM guest ids can equal them).
13638        // SAFETY: gettid has no preconditions.
13639        let own_thread = DetTid::from_raw(unsafe { libc::gettid() });
13640        assert!(!hermit_task_named(own_thread, None, me));
13641        let (send, receive) = std::sync::mpsc::channel();
13642        let (stop, stopped) = std::sync::mpsc::channel::<()>();
13643        let thread = std::thread::spawn(move || {
13644            // SAFETY: as above.
13645            send.send(unsafe { libc::gettid() }).unwrap();
13646            let _ = stopped.recv();
13647        });
13648        let other_thread = receive.recv().unwrap();
13649        assert!(hermit_task_named(caller, None, other_thread));
13650        assert!(hermit_task_named(caller, Some(me), other_thread));
13651        let guest_pid = guest.id() as i32;
13652        assert!(!hermit_task_named(caller, None, guest_pid));
13653        assert!(!hermit_task_named(caller, Some(guest_pid), guest_pid));
13654        assert!(!hermit_task_named(caller, Some(guest_pid), other_thread));
13655        assert!(!hermit_task_named(caller, None, 0));
13656        assert!(!hermit_task_named(caller, None, -1));
13657        // A caller that is not a host task descended from Hermit names nothing
13658        // of Hermit's: one that cannot be read, or Hermit's own parent.
13659        assert!(!hermit_task_named(DetTid::from_raw(i32::MAX), None, me));
13660        // SAFETY: getppid has no preconditions.
13661        let parent = DetTid::from_raw(unsafe { libc::getppid() });
13662        assert!(!hermit_task_named(parent, None, me));
13663        guest.kill().unwrap();
13664        guest.wait().unwrap();
13665        stop.send(()).unwrap();
13666        thread.join().unwrap();
13667    }
13668
13669    /// The DBT SIGKILL fence waits on a process pidfd: a live victim keeps it
13670    /// waiting to the valve, and a dead one, even unreaped, releases it
13671    /// (https://github.com/rrnewton/hermit/issues/4362).
13672    #[test]
13673    fn the_dbt_sigkill_fence_waits_until_the_victim_process_is_gone() {
13674        use super::open_process_pidfd;
13675        use super::wait_for_process_exit;
13676        let runtime = tokio::runtime::Builder::new_current_thread()
13677            .enable_all()
13678            .build()
13679            .unwrap();
13680        let mut victim = std::process::Command::new("sleep")
13681            .arg("30")
13682            .spawn()
13683            .unwrap();
13684        let pid = victim.id() as i32;
13685        let live = open_process_pidfd(pid).expect("a live child has a pidfd");
13686        let waited = runtime.block_on(wait_for_process_exit(
13687            live,
13688            std::time::Duration::from_millis(50),
13689        ));
13690        assert!(waited.unwrap_err().contains("not gone within"));
13691        let dying = open_process_pidfd(pid).expect("a live child has a pidfd");
13692        victim.kill().unwrap();
13693        assert_eq!(
13694            runtime.block_on(wait_for_process_exit(
13695                dying,
13696                std::time::Duration::from_secs(30)
13697            )),
13698            Ok(())
13699        );
13700        victim.wait().unwrap();
13701        assert!(open_process_pidfd(pid).is_err());
13702    }
13703
13704    /// The DBT SIGKILL fence never leaves the sender unfenced while the victim
13705    /// may still be running: an open failure refuses, and an ESRCH counts as
13706    /// the victim gone only once the thread the kill named has exited, as its
13707    /// registration pidfd shows (Codex design review of
13708    /// https://github.com/rrnewton/hermit/issues/4362 part 2, P2-3).
13709    #[test]
13710    fn the_dbt_sigkill_fence_refuses_rather_than_leave_the_sender_unfenced() {
13711        use super::sigkill_fence_for;
13712        use crate::scheduler::Scheduler;
13713        use crate::types::DetPid;
13714        use crate::types::DetTid;
13715        use crate::types::MmId;
13716        let config = crate::config::Config::default()
13717            .with_backend(|backend| *backend = reverie::BackendCapabilities::DBT);
13718        let mut scheduler = Scheduler::new(&config);
13719        let me = std::process::id() as i32;
13720        let errno = |code| move |_: i32| Err(std::io::Error::from_raw_os_error(code));
13721        // A live registered thread: EMFILE and an unconfirmed ESRCH refuse.
13722        let live = DetTid::from_raw(41);
13723        let live_process = DetPid::from_raw(41);
13724        // SAFETY: gettid has no preconditions.
13725        let my_tid = unsafe { libc::gettid() };
13726        scheduler
13727            .register_physical_thread(live, MmId::initial(live_process), me, my_tid)
13728            .unwrap();
13729        let refusal = sigkill_fence_for(&scheduler, live, live_process, true, errno(libc::EMFILE))
13730            .unwrap_err();
13731        assert_eq!(refusal.process, live_process);
13732        assert!(refusal.reason.contains("pidfd_open"), "{}", refusal.reason);
13733        assert!(
13734            sigkill_fence_for(&scheduler, live, live_process, true, errno(libc::ESRCH)).is_err()
13735        );
13736        assert!(
13737            sigkill_fence_for(&scheduler, live, live_process, true, errno(libc::EINVAL)).is_err()
13738        );
13739        assert!(
13740            sigkill_fence_for(
13741                &scheduler,
13742                live,
13743                live_process,
13744                true,
13745                super::open_process_pidfd
13746            )
13747            .unwrap()
13748            .is_some()
13749        );
13750        // A registered thread that has exited: an ESRCH means the victim is gone.
13751        let gone = DetTid::from_raw(43);
13752        let gone_process = DetPid::from_raw(43);
13753        let (send, receive) = std::sync::mpsc::channel();
13754        let (stop, stopped) = std::sync::mpsc::channel::<()>();
13755        let thread = std::thread::spawn(move || {
13756            // SAFETY: as above.
13757            send.send(unsafe { libc::gettid() }).unwrap();
13758            let _ = stopped.recv();
13759        });
13760        let gone_tid = receive.recv().unwrap();
13761        scheduler
13762            .register_physical_thread(gone, MmId::initial(gone_process), me, gone_tid)
13763            .unwrap();
13764        stop.send(()).unwrap();
13765        thread.join().unwrap();
13766        // pthread_join returns at the CLONE_CHILD_CLEARTID wake, before the
13767        // kernel marks the thread exited, so its pidfd can turn readable a
13768        // little later (Claude review of
13769        // https://github.com/rrnewton/hermit/pull/4423, F1).
13770        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
13771        while scheduler.registered_thread_has_exited(gone) != Some(true) {
13772            assert!(
13773                std::time::Instant::now() < deadline,
13774                "the joined thread's pidfd never reported its exit"
13775            );
13776            std::thread::sleep(std::time::Duration::from_millis(1));
13777        }
13778        assert!(
13779            sigkill_fence_for(&scheduler, gone, gone_process, true, errno(libc::EINVAL))
13780                .unwrap()
13781                .is_none()
13782        );
13783        assert!(
13784            sigkill_fence_for(&scheduler, gone, gone_process, true, errno(libc::ESRCH))
13785                .unwrap()
13786                .is_none()
13787        );
13788        assert!(
13789            sigkill_fence_for(&scheduler, gone, gone_process, true, errno(libc::EMFILE)).is_err()
13790        );
13791        // A named thread with no registration refuses, even when the victim
13792        // seems to have no live thread left: that can be its threads' exits
13793        // racing this notice after the kill reached the kernel (Claude
13794        // re-check of https://github.com/rrnewton/hermit/pull/4423, N1).
13795        let unregistered = DetTid::from_raw(47);
13796        let empty = DetPid::from_raw(47);
13797        let refusal = sigkill_fence_for(&scheduler, unregistered, empty, true, errno(libc::EMFILE))
13798            .unwrap_err();
13799        assert!(
13800            refusal.reason.contains("has a registered host process"),
13801            "{}",
13802            refusal.reason
13803        );
13804        // A zombie whose exit the schedule granted needs no fence
13805        // (https://github.com/rrnewton/hermit/issues/4411).
13806        scheduler.grant_exit_for_test(empty);
13807        assert!(
13808            sigkill_fence_for(&scheduler, unregistered, empty, true, errno(libc::EMFILE))
13809                .unwrap()
13810                .is_none()
13811        );
13812        // A named leader that has exited while a sibling runs on: the fence
13813        // goes through the sibling's registration, and an ESRCH counts as gone
13814        // only once that sibling has exited
13815        // (https://github.com/rrnewton/hermit/issues/4411).
13816        let leader = DetTid::from_raw(51);
13817        let process = DetPid::from_raw(51);
13818        let sibling = DetTid::from_raw(52);
13819        scheduler.thread_tree.add_child(leader, leader, true);
13820        scheduler.thread_tree.add_child(leader, sibling, false);
13821        // SAFETY: as above.
13822        let my_tid = unsafe { libc::gettid() };
13823        scheduler
13824            .register_physical_thread(sibling, MmId::initial(process), me, my_tid)
13825            .unwrap();
13826        assert!(
13827            sigkill_fence_for(&scheduler, leader, process, true, super::open_process_pidfd)
13828                .unwrap()
13829                .is_some()
13830        );
13831        assert!(sigkill_fence_for(&scheduler, leader, process, true, errno(libc::ESRCH)).is_err());
13832        // A thread-directed SIGKILL (tgkill, tkill, rt_tgsigqueueinfo) to the
13833        // exited leader gets no fallback and refuses; Linux does not end the
13834        // group for it (Claude review of
13835        // https://github.com/rrnewton/hermit/pull/4439, R1). Nor does a
13836        // thread-directed kill to a zombie skip the fence.
13837        assert!(
13838            sigkill_fence_for(
13839                &scheduler,
13840                leader,
13841                process,
13842                false,
13843                super::open_process_pidfd
13844            )
13845            .is_err()
13846        );
13847        assert!(
13848            sigkill_fence_for(&scheduler, unregistered, empty, false, errno(libc::EMFILE)).is_err()
13849        );
13850        // An exited leader whose surviving sibling has since exited too: an
13851        // ESRCH counts as gone, confirmed through the sibling's own
13852        // registration pidfd, not the leader's (Claude review of 4439, R2).
13853        let late_leader = DetTid::from_raw(61);
13854        let late_process = DetPid::from_raw(61);
13855        let late_sibling = DetTid::from_raw(62);
13856        scheduler
13857            .thread_tree
13858            .add_child(late_leader, late_leader, true);
13859        scheduler
13860            .thread_tree
13861            .add_child(late_leader, late_sibling, false);
13862        let (send, receive) = std::sync::mpsc::channel();
13863        let (stop, stopped) = std::sync::mpsc::channel::<()>();
13864        let thread = std::thread::spawn(move || {
13865            // SAFETY: as above.
13866            send.send(unsafe { libc::gettid() }).unwrap();
13867            let _ = stopped.recv();
13868        });
13869        let sibling_tid = receive.recv().unwrap();
13870        scheduler
13871            .register_physical_thread(late_sibling, MmId::initial(late_process), me, sibling_tid)
13872            .unwrap();
13873        stop.send(()).unwrap();
13874        thread.join().unwrap();
13875        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
13876        while scheduler.registered_thread_has_exited(late_sibling) != Some(true) {
13877            assert!(
13878                std::time::Instant::now() < deadline,
13879                "the joined thread's pidfd never reported its exit"
13880            );
13881            std::thread::sleep(std::time::Duration::from_millis(1));
13882        }
13883        assert!(
13884            sigkill_fence_for(
13885                &scheduler,
13886                late_leader,
13887                late_process,
13888                true,
13889                errno(libc::ESRCH)
13890            )
13891            .unwrap()
13892            .is_none()
13893        );
13894    }
13895
13896    /// DBT publishes READY_IMAGE between creating the scheduler future and
13897    /// polling it, so the announcement must already be logged when
13898    /// `run_external_scheduler` returns:
13899    /// <https://github.com/rrnewton/hermit/issues/3463>.
13900    #[test]
13901    fn external_scheduler_announces_before_its_future_is_polled() {
13902        #[derive(Clone, Default)]
13903        struct Capture(std::sync::Arc<Mutex<Vec<String>>>);
13904        struct Visitor(String);
13905        impl tracing::field::Visit for Visitor {
13906            fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
13907                if field.name() == "message" {
13908                    use std::fmt::Write;
13909                    write!(self.0, "{value:?}").unwrap();
13910                }
13911            }
13912        }
13913        impl tracing::Subscriber for Capture {
13914            fn enabled(&self, metadata: &tracing::Metadata<'_>) -> bool {
13915                *metadata.level() == tracing::Level::INFO
13916            }
13917            fn new_span(&self, _: &tracing::span::Attributes<'_>) -> tracing::span::Id {
13918                tracing::span::Id::from_u64(1)
13919            }
13920            fn record(&self, _: &tracing::span::Id, _: &tracing::span::Record<'_>) {}
13921            fn record_follows_from(&self, _: &tracing::span::Id, _: &tracing::span::Id) {}
13922            fn event(&self, event: &tracing::Event<'_>) {
13923                let mut visitor = Visitor(String::new());
13924                event.record(&mut visitor);
13925                self.0.lock().unwrap().push(visitor.0);
13926            }
13927            fn enter(&self, _: &tracing::span::Id) {}
13928            fn exit(&self, _: &tracing::span::Id) {}
13929        }
13930        let config = Config {
13931            sequentialize_threads: true,
13932            ..Config::default()
13933        };
13934        let state = GlobalState::init_for_external_scheduler(&config);
13935        let captured = Capture::default();
13936        let scheduler = tracing::subscriber::with_default(captured.clone(), || {
13937            state.run_external_scheduler(std::sync::Arc::new(|_| {}))
13938        });
13939        let announcement = "[scheduler] daemon task starting up, waiting for guest thread start..";
13940        assert_eq!(*captured.0.lock().unwrap(), [announcement]);
13941        drop(scheduler);
13942    }
13943
13944    #[test]
13945    fn schedule_event_host_markers_require_command_bootstrap_provenance() {
13946        let event = SchedEvent::branches(DetTid::from_raw(3), 223)
13947            .with_end_rip(std::num::NonZeroUsize::new(0x1234).unwrap())
13948            .with_time(LogicalTime::from_nanos(2230));
13949        let original = serde_json::to_string(&event).unwrap();
13950        let plain = format!(
13951            "{:?}",
13952            super::SchedEventForLog {
13953                event: &event,
13954                command_bootstrap: false
13955            }
13956        );
13957        assert_eq!(plain, format!("{event:?}"));
13958        let marked = format!(
13959            "{:?}",
13960            super::SchedEventForLog {
13961                event: &event,
13962                command_bootstrap: true
13963            }
13964        );
13965        assert_eq!(
13966            marked,
13967            "SchedEvent { dettid: DetPid(3), op: Branch, count: 223, start_rip: None, end_rip: Some(<hostaddr 0x1234>), end_time: Some(LogicalTime(2230)) }"
13968        );
13969        assert_eq!(serde_json::to_string(&event).unwrap(), original);
13970    }
13971
13972    #[test]
13973    fn summary_preemption_views_keep_counts_full_report_and_single_flush() {
13974        let (_config, state, tid, _) = cancellation_test_state();
13975        let directory = tempfile::tempdir().unwrap();
13976        let path = directory.path().join("recording");
13977        let mut writer = crate::preemptions::PreemptionWriter::new(Some(path.clone()));
13978        writer.register_thread(tid, DEFAULT_PRIORITY);
13979        writer.insert_reprioritization(tid, LogicalTime::from_nanos(100), 10, DEFAULT_PRIORITY, 20);
13980        let mut scheduler = state.sched.lock().unwrap();
13981        scheduler.preemption_writer = Some(writer);
13982        let (summary, info_description) = scheduler
13983            .generate_partial_run_summary_for_log(Some(&path))
13984            .unwrap();
13985        assert!(scheduler.preemption_writer.is_none());
13986        let recorded = std::fs::read(&path).unwrap();
13987        let parsed: serde_json::Value = serde_json::from_slice(&recorded).unwrap();
13988        assert_eq!(
13989            parsed["per_thread"][tid.to_string()]["prio_changes"],
13990            serde_json::json!([[100, DEFAULT_PRIORITY]])
13991        );
13992        let count_line = "Record of 1 preemption and reprioritization events:\n";
13993        assert_eq!(info_description.as_deref(), Some(count_line));
13994        assert_eq!(
13995            summary.reprio_descrip.as_deref(),
13996            Some(format!("{count_line}  (Writing to file {path:?})\n").as_str())
13997        );
13998        let full = summary.to_string();
13999        let json = serde_json::to_vec(&summary).unwrap();
14000        let info = summary.info(info_description.as_deref()).to_string();
14001        assert!(info.contains(count_line));
14002        assert!(!info.contains("Writing to file"));
14003        assert!(full.contains(&format!("Writing to file {path:?}")));
14004        // Formatting and another empty summary do not flush the consumed writer again.
14005        let _ = scheduler.generate_partial_run_summary(None).unwrap();
14006        assert_eq!(std::fs::read(&path).unwrap(), recorded);
14007        assert_eq!(summary.to_string(), full);
14008        assert_eq!(serde_json::to_vec(&summary).unwrap(), json);
14009    }
14010
14011    #[test]
14012    fn run_summary_records_the_effective_shared_preemption_configuration() {
14013        // Config::default() is unprepared: launch installs the timer capability.
14014        // Specify that mechanism fact here independently of the maximum.
14015        for (config, expected_maximum, expected_timer) in [
14016            (
14017                Config::default().with_backend(|backend| {
14018                    backend.delivers_preemption_timer_events = true;
14019                }),
14020                Some(200_000_000),
14021                true,
14022            ),
14023            (
14024                Config {
14025                    max_timeslice: None,
14026                    ..Config::default()
14027                }
14028                .with_backend(|backend| {
14029                    backend.delivers_preemption_timer_events = true;
14030                }),
14031                None,
14032                false,
14033            ),
14034            (
14035                Config {
14036                    max_timeslice: std::num::NonZeroU64::new(12_340),
14037                    ..Config::default()
14038                }
14039                .with_backend(|backend| {
14040                    backend.delivers_preemption_timer_events = true;
14041                }),
14042                Some(12_340),
14043                true,
14044            ),
14045            (
14046                Config::default().with_backend(|backend| {
14047                    backend.delivers_preemption_timer_events = false;
14048                }),
14049                Some(200_000_000),
14050                false,
14051            ),
14052            (
14053                Config {
14054                    max_timeslice: None,
14055                    ..Config::default()
14056                }
14057                .with_backend(|backend| {
14058                    backend.delivers_preemption_timer_events = false;
14059                }),
14060                None,
14061                false,
14062            ),
14063        ] {
14064            let summary = GlobalState::for_tests(&config).into_run_summary().unwrap();
14065            assert_eq!(
14066                summary.preemption,
14067                Some(super::PreemptionSummary {
14068                    max_timeslice_ns: expected_maximum,
14069                    timer_enabled: Some(expected_timer),
14070                    stopless_slice_ends: None,
14071                })
14072            );
14073        }
14074    }
14075
14076    #[test]
14077    fn run_summary_info_keeps_semantics_and_debug_retains_bookkeeping() {
14078        #[derive(Clone)]
14079        struct Capture(std::sync::Arc<Mutex<Vec<(tracing::Level, String)>>>);
14080        struct Visitor(String);
14081        impl tracing::field::Visit for Visitor {
14082            fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
14083                use std::fmt::Write;
14084                write!(self.0, "{}={value:?};", field.name()).unwrap();
14085            }
14086        }
14087        impl tracing::Subscriber for Capture {
14088            fn enabled(&self, _: &tracing::Metadata<'_>) -> bool {
14089                true
14090            }
14091            fn new_span(&self, _: &tracing::span::Attributes<'_>) -> tracing::span::Id {
14092                tracing::span::Id::from_u64(1)
14093            }
14094            fn record(&self, _: &tracing::span::Id, _: &tracing::span::Record<'_>) {}
14095            fn record_follows_from(&self, _: &tracing::span::Id, _: &tracing::span::Id) {}
14096            fn event(&self, event: &tracing::Event<'_>) {
14097                let mut visitor = Visitor(String::new());
14098                event.record(&mut visitor);
14099                self.0
14100                    .lock()
14101                    .unwrap()
14102                    .push((*event.metadata().level(), visitor.0));
14103            }
14104            fn enter(&self, _: &tracing::span::Id) {}
14105            fn exit(&self, _: &tracing::span::Id) {}
14106        }
14107        let summary = super::RunSummary {
14108            sched_turns: 4,
14109            schedevent_recorded: 11,
14110            schedevent_replayed: 11,
14111            schedevent_desynced: 2,
14112            desync_descrip: Some("two real desyncs\n".into()),
14113            num_processes: 1,
14114            num_threads: 1,
14115            threads_descrip: "[3]".into(),
14116            syscalls: Some(3),
14117            virttime_elapsed: 2_512_380,
14118            virttime_final: 2_512_380,
14119            timeslice_stats: TimesliceStats {
14120                count: 1,
14121                sum_ns: 512_380,
14122                min_ns: 512_380,
14123                max_ns: 512_380,
14124            },
14125            ..Default::default()
14126        };
14127        let description = "Record of 7 preemption and reprioritization events:\n";
14128        let captured = Capture(Default::default());
14129        tracing::subscriber::with_default(captured.clone(), || {
14130            super::log_run_summary(
14131                "report",
14132                &summary,
14133                Some(description),
14134                Some(std::path::Path::new("/host/recording")),
14135            );
14136        });
14137        let events = captured.0.lock().unwrap();
14138        assert_eq!(events.len(), 2);
14139        assert_eq!(events[0].0, tracing::Level::INFO);
14140        assert_eq!(events[1].0, tracing::Level::DEBUG);
14141        let info = &events[0].1;
14142        for text in [
14143            "1 group leaders of 1 thread(s)",
14144            "3 syscalls",
14145            "4 turns, recorded 11 events (2 desynced)",
14146            "two real desyncs",
14147            "Record of 7 preemption",
14148            "2_512_380ns",
14149            "min=512380ns max=512380ns mean=512380ns count=1",
14150        ] {
14151            assert!(info.contains(text), "missing semantic field {text}: {info}");
14152        }
14153        assert!(!info.contains("/host/recording"));
14154        assert!(!info.contains("replayed"));
14155        assert!(events[1].1.contains("replayed_events=11"));
14156        assert!(events[1].1.contains("/host/recording"));
14157        let baseline = summary.info(Some(description)).to_string();
14158        let mutations: [fn(&mut super::RunSummary); 8] = [
14159            |s| s.sched_turns += 1,
14160            |s| s.schedevent_recorded += 1,
14161            |s| s.schedevent_desynced += 1,
14162            |s| s.num_threads += 1,
14163            |s| s.syscalls = Some(4),
14164            |s| s.virttime_elapsed += 1,
14165            |s| s.timeslice_stats.count += 1,
14166            |s| s.threads_descrip.push_str(",4"),
14167        ];
14168        for mutate in mutations {
14169            let mut changed = summary.clone();
14170            mutate(&mut changed);
14171            assert_ne!(changed.info(Some(description)).to_string(), baseline);
14172        }
14173        assert_ne!(
14174            summary
14175                .info(Some(
14176                    "Record of 8 preemption and reprioritization events:\n"
14177                ))
14178                .to_string(),
14179            baseline
14180        );
14181    }
14182
14183    mod backend_failure_tests;
14184    mod comm_alias_open_tests;
14185    mod exec_identity_tests;
14186    mod exec_teardown_tests;
14187    mod fatal_delivery_tests;
14188    mod gated_wait_turn_tests;
14189    mod guest_mask_publication_tests;
14190    mod other_process_procfs_tests;
14191    mod replay_departure_tests;
14192    mod replay_departure_transport;
14193    mod reserved_input_exec_tests;
14194    mod sigkill_deregistration_tests;
14195    mod stopping_refusal_tests;
14196    mod temporary_mask_publication_tests;
14197    mod timerless_clock_tests;
14198    use std::collections::BTreeSet;
14199    use std::os::fd::AsRawFd;
14200    use std::os::fd::FromRawFd;
14201    use std::os::fd::OwnedFd;
14202    use std::sync::Mutex;
14203    use std::time::Duration;
14204
14205    use nix::sys::signal::Signal;
14206    use reverie::GlobalRPC;
14207    use reverie::GlobalTool;
14208    use reverie::Guest;
14209    use reverie::Tid;
14210    use reverie::syscalls::CloneFlags;
14211    use reverie::syscalls::Sysno;
14212
14213    use super::FutexAction;
14214    use super::GlobalRequest;
14215    use super::GlobalResponse;
14216    use super::GlobalState;
14217    use super::MountIdPool;
14218    use super::PendingExecState;
14219    use super::ResumeStatus;
14220    use super::RpcIncarnation;
14221    use super::SchedulerRpcResult;
14222    use super::SigWrapper;
14223    use super::ThreadDeregistration;
14224    use super::TimesliceStats;
14225    use super::format_unsupported_syscall_warning;
14226    use crate::Detcore;
14227    use crate::config::Config;
14228    use crate::config::RunsPostFork;
14229    use crate::ivar::Ivar;
14230    use crate::preemptions::PreemptionRecord;
14231    use crate::resources::ExternalOpId;
14232    use crate::resources::Permission;
14233    use crate::resources::ResourceID;
14234    use crate::resources::Resources;
14235    use crate::scheduler::DEFAULT_PRIORITY;
14236    use crate::scheduler::SchedRequest;
14237    use crate::scheduler::SchedResponse;
14238    use crate::scheduler::SchedValue;
14239    use crate::scheduler::ThreadNextTurn;
14240    use crate::tool_local::ExecFdBlockingOverrides;
14241    use crate::tool_local::ExecFdState;
14242    use crate::types::DetPid;
14243    use crate::types::DetTid;
14244    use crate::types::DetTime;
14245    use crate::types::FutexID;
14246    use crate::types::LogicalTime;
14247    use crate::types::MmId;
14248    use crate::types::Op;
14249    use crate::types::SchedEvent;
14250
14251    #[test]
14252    fn fdinfo_mount_ids_preserve_raw_equivalence_and_distinctness() {
14253        let mut pool = MountIdPool::from_config(&[10, 20, 30], true, &[]);
14254        // Numbered in first-observation order, not by row position: 20 is
14255        // the second captured row but the first ID the guest observes.
14256        assert_eq!(pool.determinize(20, None), Some(1));
14257
14258        // Unlisted nsfs, anon_inodefs, and pidfs IDs are distinct even when
14259        // their descriptors have the same broad Detcore FdType.
14260        assert_eq!(pool.determinize(700, None), Some(2));
14261        assert_eq!(pool.determinize(701, None), Some(3));
14262        assert_eq!(pool.determinize(702, None), Some(4));
14263        assert_eq!(pool.determinize(701, None), Some(3));
14264        assert_eq!(pool.determinize(20, None), Some(1));
14265    }
14266
14267    #[test]
14268    fn fdinfo_mount_ids_seed_from_low_level_snapshot_and_refuse_drift() {
14269        let mut pool = MountIdPool::from_config(&[], false, &[]);
14270        assert_eq!(pool.determinize(20, Some(&[10, 20, 30])), Some(1));
14271        assert_eq!(pool.determinize(700, Some(&[10, 20, 30])), Some(2));
14272        assert_eq!(pool.determinize(20, Some(&[10, 20, 30])), Some(1));
14273        assert_eq!(pool.determinize(20, Some(&[10, 99, 30])), None);
14274        assert_eq!(pool.determinize(20, Some(&[10, 20])), None);
14275        assert_eq!(pool.determinize(20, Some(&[10, 20, 30, 40])), None);
14276    }
14277
14278    /// The compat/lsof divergence: a run's namespace starts as a copy of the
14279    /// host's, so unrelated host mounts (here 25, 26, 27, as one loop mount
14280    /// propagated to three peers) can change the row count between two runs
14281    /// of the same guest. The IDs the guest observes must not move.
14282    #[test]
14283    fn mount_ids_do_not_depend_on_rows_the_guest_never_observed() {
14284        let observe = |mut pool: MountIdPool, visible: &[u64]| {
14285            vec![
14286                pool.determinize(700, None),
14287                pool.determinize(701, None),
14288                pool.determinize(20, None),
14289            ]
14290            .into_iter()
14291            .chain(
14292                pool.resolve_mountinfo(visible, visible)
14293                    .unwrap()
14294                    .into_iter()
14295                    .map(Some),
14296            )
14297            .collect::<Vec<_>>()
14298        };
14299        let quiet = observe(
14300            MountIdPool::from_config(&[10, 20, 30], true, &[]),
14301            &[10, 30],
14302        );
14303        let churned = observe(
14304            MountIdPool::from_config(&[10, 20, 25, 26, 27, 30], true, &[]),
14305            &[10, 30],
14306        );
14307        assert_eq!(quiet, [Some(1), Some(2), Some(3), Some(4), Some(5)]);
14308        assert_eq!(quiet, churned);
14309
14310        // Uncaptured runs validate each fdinfo read against a tracer-side
14311        // snapshot of the same namespace; the snapshot's size is irrelevant.
14312        let mut quiet = MountIdPool::from_config(&[], false, &[]);
14313        let mut churned = MountIdPool::from_config(&[], false, &[]);
14314        assert_eq!(quiet.determinize(700, Some(&[10, 20, 30])), Some(1));
14315        assert_eq!(
14316            churned.determinize(700, Some(&[10, 20, 25, 26, 27, 30])),
14317            Some(1)
14318        );
14319        assert_eq!(quiet.determinize(701, Some(&[10, 20, 30])), Some(2));
14320        assert_eq!(
14321            churned.determinize(701, Some(&[10, 20, 25, 26, 27, 30])),
14322            Some(2)
14323        );
14324    }
14325
14326    #[test]
14327    fn mountinfo_views_keep_earlier_numbers_and_number_the_rest_in_row_order() {
14328        let mut pool = MountIdPool::from_config(&[10, 20, 30, 40], true, &[]);
14329        assert_eq!(pool.determinize(30, None), Some(1));
14330        assert_eq!(pool.determinize(700, None), Some(2));
14331        assert_eq!(
14332            pool.resolve_mountinfo(&[10, 20, 30], &[10, 20, 30]),
14333            Some(vec![3, 4, 1])
14334        );
14335        assert_eq!(
14336            pool.resolve_mountinfo(&[20, 40], &[20, 40]),
14337            Some(vec![4, 5])
14338        );
14339        // A refused view numbers nothing.
14340        assert_eq!(pool.resolve_mountinfo(&[40, 10], &[40, 10]), None);
14341        assert_eq!(pool.resolve_mountinfo(&[10, 99], &[10, 99]), None);
14342        assert_eq!(pool.determinize(701, None), Some(6));
14343        assert_eq!(pool.determinize(10, None), Some(3));
14344    }
14345
14346    /// One chroot view: row 20 whose parent 10 is outside the view. The
14347    /// captured namespace may list 10 before 20 or after it, which the guest
14348    /// cannot see. The view's canonical order follows the capture, but the
14349    /// numbers must follow the view: row 20 first, then parent 10.
14350    #[test]
14351    fn captured_namespace_order_never_decides_mountinfo_numbers() {
14352        let mut parent_first = MountIdPool::from_config(&[10, 20, 9], true, &[]);
14353        let mut parent_last = MountIdPool::from_config(&[20, 10], true, &[]);
14354        assert_eq!(
14355            parent_first.resolve_mountinfo(&[10, 20], &[20, 10]),
14356            Some(vec![1, 2])
14357        );
14358        assert_eq!(
14359            parent_last.resolve_mountinfo(&[20, 10], &[20, 10]),
14360            Some(vec![1, 2])
14361        );
14362        assert_eq!(parent_first.determinize(10, None), Some(2));
14363        assert_eq!(parent_last.determinize(10, None), Some(2));
14364    }
14365
14366    #[test]
14367    fn mountinfo_observation_order_must_name_exactly_the_canonical_ids() {
14368        for (canonical, observation) in [
14369            (&[10, 20][..], &[20][..]),
14370            (&[10, 20][..], &[20, 30][..]),
14371            (&[10, 20][..], &[20, 20][..]),
14372            (&[10, 20][..], &[10, 20, 30][..]),
14373        ] {
14374            let mut captured = MountIdPool::from_config(&[10, 20, 30], true, &[]);
14375            assert_eq!(captured.resolve_mountinfo(canonical, observation), None);
14376            assert_eq!(captured.determinize(30, None), Some(1));
14377
14378            // A refused uncaptured view must not seed the namespace order.
14379            let mut uncaptured = MountIdPool::from_config(&[], false, &[]);
14380            assert_eq!(uncaptured.resolve_mountinfo(canonical, observation), None);
14381            assert!(matches!(uncaptured, MountIdPool::Uninitialized));
14382        }
14383    }
14384
14385    #[test]
14386    fn mountinfo_reads_seed_once_and_refuse_later_table_changes() {
14387        let mut pool = MountIdPool::from_config(&[], false, &[]);
14388        assert!(pool.validate_mountinfo_order(&[10, 20, 30]));
14389        assert!(pool.validate_mountinfo_order(&[10, 20, 30]));
14390        assert!(!pool.validate_mountinfo_order(&[10, 99, 30]));
14391
14392        let mut empty = MountIdPool::from_config(&[], false, &[]);
14393        assert!(empty.validate_mountinfo_order(&[]));
14394        assert!(!empty.validate_mountinfo_order(&[10]));
14395    }
14396
14397    #[test]
14398    fn configured_mountinfo_order_accepts_only_ordered_known_subsets() {
14399        let mut pool = MountIdPool::from_config(&[10, 20, 30, 40], true, &[]);
14400        assert!(pool.validate_mountinfo_order(&[10, 30, 40]));
14401        assert_eq!(pool.determinize(30, None), Some(1));
14402        assert!(pool.validate_mountinfo_order(&[20, 40]));
14403        assert!(!pool.validate_mountinfo_order(&[30, 20]));
14404        assert!(!pool.validate_mountinfo_order(&[10, 10]));
14405    }
14406
14407    #[test]
14408    fn configured_mountinfo_order_refuses_new_namespace_ids() {
14409        let mut pool = MountIdPool::from_config(&[10, 20, 30, 40], true, &[]);
14410        assert!(pool.validate_mountinfo_order(&[10, 30, 40]));
14411        assert!(
14412            !pool.validate_mountinfo_order(&[10, 99, 40]),
14413            "a namespace-local mount ID absent from captured provenance must fail closed"
14414        );
14415    }
14416
14417    #[test]
14418    fn fdinfo_mount_ids_refuse_malformed_configured_provenance() {
14419        let mut pool = MountIdPool::from_config(&[10, 10], true, &[]);
14420        assert_eq!(pool.determinize(10, None), None);
14421        let mut pool = MountIdPool::from_config(&[10], true, &[700, 700]);
14422        assert_eq!(pool.determinize(10, None), None);
14423        // An assignment order without a captured namespace has nothing to
14424        // validate later views against.
14425        let mut pool = MountIdPool::from_config(&[], false, &[700]);
14426        assert_eq!(pool.determinize(700, Some(&[10])), None);
14427        assert!(pool.provenance().is_err());
14428        // No recording numbers raw 0 unless mountinfo named it: an fdinfo zero
14429        // is the anonymous class, and a mountinfo zero must pass validation.
14430        let mut pool = MountIdPool::from_config(&[10], true, &[0]);
14431        assert_eq!(pool.determinize(10, None), None);
14432        assert!(pool.provenance().is_err());
14433        let mut pool = MountIdPool::from_config(&[10, 0], true, &[10, 0]);
14434        assert_eq!(pool.resolve_mountinfo(&[10, 0], &[10, 0]), Some(vec![1, 2]));
14435        assert_eq!(pool.determinize(0, None), Some(0));
14436    }
14437
14438    #[test]
14439    fn raw_zero_is_one_mount_identity_without_descriptor_type_partitioning() {
14440        let mut pool = MountIdPool::from_config(&[10, 0], true, &[]);
14441        // The pool API intentionally accepts no descriptor type: memfd and
14442        // every other Linux descriptor reporting raw mnt_id 0 share this one
14443        // equivalence class, even when mountinfo uses zero as an outside parent
14444        // ID for its root row.
14445        assert_eq!(pool.determinize(0, None), Some(0));
14446        assert_eq!(pool.determinize(20, None), Some(1));
14447        assert_eq!(pool.determinize(0, None), Some(0));
14448    }
14449
14450    #[tokio::test]
14451    async fn completing_an_unreported_exit_retires_it_and_records_determinism_loss() {
14452        if !crate::scheduler::exec_teardown_tests::in_isolated_log_test(
14453            module_path!(),
14454            "completing_an_unreported_exit_retires_it_and_records_determinism_loss",
14455        ) {
14456            return;
14457        }
14458        use std::os::unix::process::ExitStatusExt;
14459
14460        use super::open_process_pidfd;
14461        use super::wait_for_process_exit;
14462
14463        struct Reap(std::process::Child);
14464        impl Drop for Reap {
14465            fn drop(&mut self) {
14466                let _ = self.0.kill();
14467                let _ = self.0.wait();
14468            }
14469        }
14470        let mut child = Reap(
14471            std::process::Command::new("sleep")
14472                .arg("30")
14473                .spawn()
14474                .unwrap(),
14475        );
14476        let physical_pid = child.0.id() as i32;
14477        let pidfd = open_process_pidfd(physical_pid).expect("a live child has a process pidfd");
14478        let config = Config {
14479            sequentialize_threads: true,
14480            ..Config::default()
14481        }
14482        .with_backend(|backend| {
14483            backend.process_exits_complete_asynchronously = true;
14484            backend.needs_killed_thread_rpc_cancellation = true;
14485        });
14486        let state = GlobalState::initialize(&config, false);
14487        let dettid = DetTid::from_raw(41);
14488        let mm = MmId::initial(dettid);
14489        assert_ne!(dettid.as_raw(), physical_pid);
14490        let pending_request = {
14491            let mut sched = state.sched.lock().unwrap();
14492            sched.thread_tree.add_child(dettid, dettid, true);
14493            sched.record_thread_mm(dettid, mm);
14494            sched
14495                .register_physical_thread(dettid, mm, physical_pid, physical_pid)
14496                .unwrap();
14497            sched.next_turns.insert(
14498                dettid,
14499                crate::scheduler::ThreadNextTurn {
14500                    dettid,
14501                    child_tid_addr: 0,
14502                    req: Ivar::new(),
14503                    resp: Ivar::new(),
14504                    protocol: Default::default(),
14505                },
14506            );
14507            sched.next_turns[&dettid].req.clone()
14508        };
14509        assert!(pending_request.is_empty());
14510        child.0.kill().unwrap();
14511        assert!(
14512            wait_for_process_exit(pidfd, Duration::from_secs(5))
14513                .await
14514                .is_ok()
14515        );
14516
14517        // Physical readiness authenticates death, but cleanup takes the
14518        // registered logical process ID and must not reap the parent's child.
14519        state.complete_physical_process_exit(dettid.as_raw());
14520        assert!(pending_request.get().await.is_err());
14521        let sched = state.sched.lock().unwrap();
14522        assert!(!sched.next_turns.contains_key(&dettid));
14523        assert!(sched.process_physically_gone(DetPid::from_raw(41)));
14524        assert!(!sched.process_physically_gone(DetPid::from_raw(physical_pid)));
14525        drop(sched);
14526        assert_eq!(
14527            crate::detlog::determinism_loss().as_deref(),
14528            Some("process 41 exited without deregistering; its exit was not scheduled")
14529        );
14530        let status = child
14531            .0
14532            .wait()
14533            .expect("cleanup must leave the kernel status for the parent");
14534        assert_eq!(status.signal(), Some(libc::SIGKILL));
14535        assert_eq!(status.code(), None);
14536    }
14537
14538    #[tokio::test]
14539    async fn stale_process_exit_cannot_clear_replacement_and_normal_exit_adds_no_loss() {
14540        if !crate::scheduler::exec_teardown_tests::in_isolated_log_test(
14541            module_path!(),
14542            "stale_process_exit_cannot_clear_replacement_and_normal_exit_adds_no_loss",
14543        ) {
14544            return;
14545        }
14546        use nix::fcntl::OFlag;
14547
14548        use super::open_process_pidfd;
14549        use super::wait_for_process_exit;
14550        use crate::fd::DetFd;
14551        use crate::fd::FdType;
14552        use crate::shared_open_files::OpenFileControl;
14553        use crate::types::OpenFileId;
14554
14555        struct Reap(std::process::Child);
14556        impl Drop for Reap {
14557            fn drop(&mut self) {
14558                let _ = self.0.kill();
14559                let _ = self.0.wait();
14560            }
14561        }
14562        let mut child = Reap(
14563            std::process::Command::new("sleep")
14564                .arg("30")
14565                .spawn()
14566                .unwrap(),
14567        );
14568        let physical_pid = child.0.id() as i32;
14569        let pidfd = open_process_pidfd(physical_pid).unwrap();
14570        let config = Config {
14571            sequentialize_threads: true,
14572            backend: reverie::BackendCapabilities::DBT,
14573            ..Config::default()
14574        };
14575        let state = GlobalState::initialize(&config, false);
14576        let process = DetPid::from_raw(41);
14577        let old_mm = MmId::initial(process);
14578        let mm = old_mm.for_exec(process);
14579        let (old, current) = {
14580            let mut sched = state.sched.lock().unwrap();
14581            sched.thread_tree.add_child(process, process, true);
14582            sched.record_thread_mm(process, old_mm);
14583            sched.next_turns.insert(
14584                process,
14585                ThreadNextTurn {
14586                    dettid: process,
14587                    child_tid_addr: 0,
14588                    req: Ivar::new(),
14589                    resp: Ivar::new(),
14590                    protocol: Default::default(),
14591                },
14592            );
14593            sched
14594                .register_physical_thread(process, old_mm, physical_pid, physical_pid)
14595                .unwrap();
14596            let old = sched.arm_process_exit_watch(
14597                process,
14598                old_mm,
14599                physical_pid,
14600                open_process_pidfd(physical_pid).unwrap(),
14601            );
14602            sched.record_thread_mm(process, mm);
14603            sched
14604                .register_physical_thread(process, mm, physical_pid, physical_pid)
14605                .unwrap();
14606            let current = sched.arm_process_exit_watch(
14607                process,
14608                mm,
14609                physical_pid,
14610                open_process_pidfd(physical_pid).unwrap(),
14611            );
14612            (old, current)
14613        };
14614        let pending = PendingExecState {
14615            caller: process,
14616            process,
14617            mm,
14618            fd_blocking: Default::default(),
14619        };
14620        state
14621            .pending_exec_states
14622            .lock()
14623            .unwrap()
14624            .insert(process, pending.clone());
14625        state
14626            .post_exec_fd_blocking
14627            .lock()
14628            .unwrap()
14629            .insert(process, ExecFdState::default());
14630        let id = OpenFileId::new(process, 0);
14631        {
14632            let mut files = state.shared_open_files.lock().unwrap();
14633            files
14634                .insert(DetFd::new(3, OFlag::O_RDWR, FdType::Regular, id).model_for_test())
14635                .unwrap();
14636            files
14637                .handle(process, process, OpenFileControl::Take { id })
14638                .unwrap();
14639        }
14640        child.0.kill().unwrap();
14641        assert!(
14642            wait_for_process_exit(pidfd, Duration::from_secs(5))
14643                .await
14644                .is_ok()
14645        );
14646        let cleanup = state.process_exit_cleanup();
14647        {
14648            let mut sched = state.sched.lock().unwrap();
14649            assert_eq!(cleanup.complete_observed_locked(&mut sched, &old), None);
14650            assert!(sched.next_turns.contains_key(&process));
14651            assert_eq!(
14652                state.pending_exec_states.lock().unwrap().get(&process),
14653                Some(&pending)
14654            );
14655            assert!(
14656                state
14657                    .post_exec_fd_blocking
14658                    .lock()
14659                    .unwrap()
14660                    .contains_key(&process)
14661            );
14662            assert!(state.shared_open_files.lock().unwrap().is_leased(id));
14663            assert_eq!(crate::detlog::determinism_loss(), None);
14664
14665            // Model the normal hook winning the same mutex before the
14666            // readiness callback. Its later physical completion is no loss.
14667            state.account_deregistered_thread(
14668                &mut sched,
14669                ThreadDeregistration {
14670                    dettid: process,
14671                    detpid: process,
14672                    mm,
14673                    thread_start_entered: true,
14674                    vfork_origin: None,
14675                    timeslice_stats: TimesliceStats::default(),
14676                    syscall_count: 0,
14677                    syscall_histogram: Default::default(),
14678                    chaos_epochs: Vec::new(),
14679                    killed_by_group_signal: false,
14680                    passed_descriptors_out: false,
14681                },
14682            );
14683            assert_eq!(
14684                cleanup.complete_observed_locked(&mut sched, &current),
14685                Some((false, None))
14686            );
14687            assert_eq!(cleanup.complete_observed_locked(&mut sched, &current), None);
14688        }
14689        assert!(state.pending_exec_states.lock().unwrap().is_empty());
14690        assert!(state.post_exec_fd_blocking.lock().unwrap().is_empty());
14691        assert!(!state.shared_open_files.lock().unwrap().is_leased(id));
14692        assert_eq!(crate::detlog::determinism_loss(), None);
14693        assert!(child.0.wait().unwrap().code().is_none());
14694    }
14695
14696    /// An admitted process can die before any registration names it. The
14697    /// production completion callback must then record it physically gone,
14698    /// without panicking on the unknown thread group, so that its late
14699    /// registration is refused (Codex implementation review, HIGH 1).
14700    #[test]
14701    fn completing_a_process_that_never_registered_records_it_gone() {
14702        let config = Config {
14703            sequentialize_threads: true,
14704            ..Config::default()
14705        }
14706        .with_backend(|backend| {
14707            backend.process_exits_complete_asynchronously = true;
14708            backend.needs_killed_thread_rpc_cancellation = true;
14709        });
14710        let state = GlobalState::initialize(&config, false);
14711        state.complete_physical_process_exit(57);
14712        let sched = state.sched.lock().unwrap();
14713        assert!(sched.process_physically_gone(DetPid::from_raw(57)));
14714        assert!(!sched.thread_tree.contains(&DetTid::from_raw(57)));
14715        assert!(!sched.next_turns.contains_key(&DetTid::from_raw(57)));
14716    }
14717
14718    /// Detcore lets reverie-kvm skip empty signal boundaries. The answer is
14719    /// one constant for every configuration, as the Reverie contract requires
14720    /// that it never change during a run; only reverie-kvm reads it.
14721    #[test]
14722    fn detcore_lets_the_backend_skip_empty_signal_boundaries() {
14723        for (signal_control, sequentialize_threads) in
14724            [(true, true), (true, false), (false, true), (false, false)]
14725        {
14726            let config = Config {
14727                sequentialize_threads,
14728                ..Config::default()
14729            }
14730            .with_backend(|backend| {
14731                backend.provides_process_signal_control = signal_control;
14732            });
14733            let state = GlobalState::initialize(&config, false);
14734            assert!(state.skip_empty_signal_boundaries());
14735        }
14736    }
14737
14738    /// Two backends call `GlobalTool::on_backend_process_exited`, for two
14739    /// different events, and Detcore tells them apart only by capability. The
14740    /// ptrace tracer (PTRACE and E9PATCH declare
14741    /// `reports_child_exit_publication`) reports that the kernel published a
14742    /// leader's exit to its parent: that releases the scheduler's publication
14743    /// hold and must leave the physical-exit and exec state alone. In-guest
14744    /// LiteInst (`process_exits_complete_asynchronously`, no publication
14745    /// reports) reports the process's physical exit: that releases its
14746    /// physical-exit barrier and clears its exec state, and is not a
14747    /// publication. Each case starts from the state both paths could touch, so
14748    /// taking the other path fails it.
14749    #[test]
14750    fn backend_exit_reports_take_the_path_the_backend_capabilities_select() {
14751        use reverie::BackendCapabilities;
14752        for (name, backend) in [
14753            ("PTRACE", BackendCapabilities::PTRACE),
14754            ("E9PATCH", BackendCapabilities::E9PATCH),
14755            ("LITEINST_IN_GUEST", BackendCapabilities::LITEINST_IN_GUEST),
14756        ] {
14757            let publishes = backend.reports_child_exit_publication;
14758            assert_eq!(publishes, name != "LITEINST_IN_GUEST", "{name}");
14759            let config = Config {
14760                sequentialize_threads: true,
14761                backend,
14762                ..Config::default()
14763            };
14764            let state = GlobalState::initialize(&config, false);
14765            let detpid = DetPid::from_raw(43);
14766            let leader = DetTid::from_raw(43);
14767            state.pending_exec_states.lock().unwrap().insert(
14768                detpid,
14769                PendingExecState {
14770                    caller: leader,
14771                    process: detpid,
14772                    mm: MmId::initial(detpid),
14773                    fd_blocking: Default::default(),
14774                },
14775            );
14776            state.post_exec_fd_blocking.lock().unwrap().insert(
14777                leader,
14778                ExecFdState::from([42].into_iter().collect::<ExecFdBlockingOverrides>()),
14779            );
14780            {
14781                let mut sched = state.sched.lock().unwrap();
14782                sched.install_test_child_exit_publication_hold(detpid);
14783                // Only a backend whose exits complete asynchronously (or that
14784                // reports physical exits) installs a physical-exit barrier.
14785                assert_eq!(
14786                    sched.begin_physical_process_exit(detpid),
14787                    !publishes,
14788                    "{name}"
14789                );
14790            }
14791
14792            <GlobalState as GlobalTool>::on_backend_process_exited(&state, detpid.as_raw());
14793
14794            let sched = state.sched.lock().unwrap();
14795            let exec_state_kept = state
14796                .pending_exec_states
14797                .lock()
14798                .unwrap()
14799                .contains_key(&detpid);
14800            let fd_blocking_kept = state
14801                .post_exec_fd_blocking
14802                .lock()
14803                .unwrap()
14804                .contains_key(&leader);
14805            if publishes {
14806                // The hold is released, not recorded as an early report.
14807                assert_eq!(
14808                    sched.test_child_exit_publication_state(detpid),
14809                    (false, false),
14810                    "{name}"
14811                );
14812                assert!(!sched.process_physically_gone(detpid), "{name}");
14813                assert!(exec_state_kept, "{name}");
14814                assert!(fd_blocking_kept, "{name}");
14815            } else {
14816                assert!(sched.pending_async_process_exits().is_empty(), "{name}");
14817                assert!(sched.process_physically_gone(detpid), "{name}");
14818                assert!(!exec_state_kept, "{name}");
14819                assert!(!fd_blocking_kept, "{name}");
14820                // Not a publication: the hold is neither released nor recorded.
14821                assert_eq!(
14822                    sched.test_child_exit_publication_state(detpid),
14823                    (true, false),
14824                    "{name}"
14825                );
14826            }
14827        }
14828    }
14829
14830    #[test]
14831    fn recorded_assignment_order_rebuilds_the_same_mapping_in_any_replay_order() {
14832        let mut recording = MountIdPool::from_config(&[], false, &[]);
14833        assert_eq!(recording.determinize(700, Some(&[10, 20])), Some(1));
14834        assert_eq!(
14835            recording.resolve_mountinfo(&[10, 20], &[10, 20]),
14836            Some(vec![2, 3])
14837        );
14838        assert_eq!(recording.determinize(701, None), Some(4));
14839        let provenance = recording.provenance().unwrap().unwrap();
14840        assert_eq!(provenance.mountinfo_order, [10, 20]);
14841        assert_eq!(provenance.assignment_order, [700, 10, 20, 701]);
14842
14843        // Replay observes the same raw IDs in a different order and still
14844        // gets every recorded number.
14845        let mut replay = MountIdPool::from_config(
14846            &provenance.mountinfo_order,
14847            true,
14848            &provenance.assignment_order,
14849        );
14850        assert_eq!(replay.determinize(701, None), Some(4));
14851        assert_eq!(replay.resolve_mountinfo(&[20], &[20]), Some(vec![3]));
14852        assert_eq!(replay.determinize(10, None), Some(2));
14853        assert_eq!(replay.determinize(700, None), Some(1));
14854        assert_eq!(
14855            replay.provenance().unwrap().unwrap().assignment_order,
14856            provenance.assignment_order
14857        );
14858    }
14859
14860    #[tokio::test]
14861    async fn plain_replay_registration_records_actual_root_clone_and_vfork_policy() {
14862        use crate::preemptions::PreemptionRecord;
14863        use crate::preemptions::boundary_recording::Recording;
14864        use crate::scheduler::runqueue::REPLAY_DEFERRED_PRIORITY;
14865        use crate::scheduler::runqueue::REPLAY_FOREGROUND_PRIORITY;
14866        use crate::types::SyscallPhase;
14867        let root = DetTid::from_raw(3);
14868        let clone = DetTid::from_raw(5);
14869        let vfork = DetTid::from_raw(7);
14870        let directory = tempfile::tempdir().unwrap();
14871        let path = directory.path().join("schedule.json");
14872        Recording::Legacy(PreemptionRecord::from_sched_events(vec![
14873            SchedEvent::syscall(root, Sysno::getpid, SyscallPhase::Prehook),
14874        ]))
14875        .write(&path)
14876        .unwrap();
14877        let config = Config {
14878            sequentialize_threads: false,
14879            replay_schedule_from: Some(path),
14880            record_preemptions: true,
14881            ..Config::default()
14882        };
14883        let state = GlobalState::initialize(&config, false);
14884        let mm = MmId::initial(root);
14885        let before = {
14886            let sched = state.sched.lock().unwrap();
14887            (sched.turn, sched.committed_time)
14888        };
14889        for (child, flags, is_vfork, sender, priority) in [
14890            (root, None, false, root, REPLAY_FOREGROUND_PRIORITY),
14891            (
14892                clone,
14893                Some(CloneFlags::CLONE_THREAD | CloneFlags::CLONE_VM | CloneFlags::CLONE_SIGHAND),
14894                false,
14895                root,
14896                REPLAY_DEFERRED_PRIORITY,
14897            ),
14898            (
14899                vfork,
14900                Some(CloneFlags::CLONE_VFORK | CloneFlags::CLONE_VM),
14901                true,
14902                vfork,
14903                REPLAY_DEFERRED_PRIORITY,
14904            ),
14905        ] {
14906            assert_eq!(
14907                state
14908                    .recv_create_child_thread(
14909                        Tid::from_raw(sender.as_raw()),
14910                        mm,
14911                        super::ChildRegistration {
14912                            parent_dettid: root,
14913                            parent_detpid: root,
14914                            child_dettid: child,
14915                            child_tid_addr: 0,
14916                            flags,
14917                            exit_signal: libc::SIGCHLD,
14918                            physical_ids: None,
14919                            maybe_priority: Some(priority),
14920                            parent_is_kernel_blocked: is_vfork,
14921                        }
14922                    )
14923                    .await,
14924                SchedulerRpcResult::Continue(())
14925            );
14926            let sched = state.sched.lock().unwrap();
14927            assert_eq!(sched.priorities[&child], priority);
14928            assert_eq!(
14929                sched
14930                    .preemption_writer
14931                    .as_ref()
14932                    .unwrap()
14933                    .current_priority(child),
14934                if is_vfork {
14935                    DEFAULT_PRIORITY - 1
14936                } else {
14937                    DEFAULT_PRIORITY
14938                }
14939            );
14940            assert_eq!((sched.turn, sched.committed_time), before);
14941        }
14942    }
14943
14944    #[tokio::test]
14945    async fn boundary_owner_is_allocated_by_actual_root_registration() {
14946        for current_image in [false, true] {
14947            let config = Config {
14948                sequentialize_threads: false,
14949                ..Config::default()
14950            };
14951            let state = GlobalState::initialize(&config, false);
14952            state.sched.lock().unwrap().boundary_owners =
14953                Some(crate::slice_boundary::lifecycle::Registry::default());
14954            let root = DetTid::from_raw(3);
14955            let mm = if current_image {
14956                MmId::initial(root).for_exec(root)
14957            } else {
14958                MmId::initial(root)
14959            };
14960            let before = {
14961                let scheduler = state.sched.lock().unwrap();
14962                (scheduler.turn, scheduler.committed_time)
14963            };
14964            assert_eq!(
14965                state
14966                    .recv_create_child_thread(
14967                        Tid::from_raw(root.as_raw()),
14968                        mm,
14969                        super::ChildRegistration {
14970                            parent_dettid: root,
14971                            parent_detpid: root,
14972                            child_dettid: root,
14973                            child_tid_addr: 0,
14974                            flags: None,
14975                            exit_signal: libc::SIGCHLD,
14976                            physical_ids: None,
14977                            maybe_priority: Some(DEFAULT_PRIORITY),
14978                            parent_is_kernel_blocked: false,
14979                        }
14980                    )
14981                    .await,
14982                SchedulerRpcResult::Continue(())
14983            );
14984            let scheduler = state.sched.lock().unwrap();
14985            let owner = scheduler
14986                .boundary_owners
14987                .as_ref()
14988                .unwrap()
14989                .owner(root, mm)
14990                .unwrap();
14991            assert_eq!(owner.tid, root);
14992            assert_eq!(owner.birth_tid, root);
14993            assert_eq!(owner.birth_generation, 0);
14994            assert_eq!(owner.thread_generation, 0);
14995            assert_eq!(owner.address_space_creator, mm.creator());
14996            assert_eq!(owner.address_space_generation, mm.generation());
14997            assert_eq!((scheduler.turn, scheduler.committed_time), before);
14998        }
14999    }
15000
15001    #[tokio::test]
15002    async fn initial_current_root_registration_is_consistent_and_single_use() {
15003        let config = Config {
15004            sequentialize_threads: false,
15005            ..Config::default()
15006        }
15007        .with_backend(|backend| backend.tool_shares_guest_descriptor_table = true);
15008        let mut state = GlobalState::initialize(&config, false);
15009        let pipe = reverie::InheritedInput::pipe(0).unwrap();
15010        state.install_inherited_input(pipe.lease().unwrap());
15011        let root = DetTid::from_raw(3);
15012        let initial = MmId::initial(root);
15013        let current = initial.for_exec(root);
15014        let registration = || super::ChildRegistration {
15015            parent_dettid: root,
15016            parent_detpid: root,
15017            child_dettid: root,
15018            child_tid_addr: 0,
15019            flags: None,
15020            exit_signal: libc::SIGCHLD,
15021            physical_ids: None,
15022            maybe_priority: Some(DEFAULT_PRIORITY),
15023            parent_is_kernel_blocked: false,
15024        };
15025        assert!(matches!(
15026            state
15027                .recv_create_child_thread(Tid::from_raw(root.as_raw()), current, registration())
15028                .await,
15029            SchedulerRpcResult::Continue(())
15030        ));
15031        assert_eq!(state.root_dettid.get(), Some(&root));
15032        assert_eq!(
15033            state.inherited_input.lock().unwrap().as_ref().unwrap().root,
15034            Some((root, current))
15035        );
15036        // Completion consumes the launch input at its birth identity; there is
15037        // no intermediate old-MM registration and no changed Mark response.
15038        let response = state
15039            .receive_rpc(
15040                Tid::from_raw(root.as_raw()),
15041                (
15042                    DetTime::new(&config),
15043                    initial,
15044                    GlobalRequest::MarkPastFirstExecve(root, None, None, None),
15045                ),
15046            )
15047            .await
15048            .1;
15049        assert_eq!(response, GlobalResponse::ThreadExited);
15050        assert!(state.inherited_input.lock().unwrap().is_some());
15051        let response = state
15052            .receive_rpc(
15053                Tid::from_raw(root.as_raw()),
15054                (
15055                    DetTime::new(&config),
15056                    current,
15057                    GlobalRequest::MarkPastFirstExecve(root, None, None, None),
15058                ),
15059            )
15060            .await
15061            .1;
15062        let GlobalResponse::MarkPastFirstExecve(completion) = response else {
15063            panic!("{response:?}")
15064        };
15065        assert_eq!(completion.initial_input.as_ref(), Some(pipe.identity()));
15066        assert!(state.inherited_input.lock().unwrap().is_none());
15067        {
15068            let sched = state.sched.lock().unwrap();
15069            assert_eq!(sched.thread_mm(root), Some(current));
15070            assert!(sched.rpc_incarnation_matches(root, current));
15071            assert!(!sched.rpc_incarnation_matches(root, initial));
15072            assert!(sched.thread_was_registered(root));
15073        }
15074        assert!(matches!(
15075            state
15076                .recv_create_child_thread(Tid::from_raw(root.as_raw()), current, registration())
15077                .await,
15078            SchedulerRpcResult::ThreadExited
15079        ));
15080        assert_eq!(state.sched.lock().unwrap().thread_mm(root), Some(current));
15081    }
15082
15083    #[tokio::test]
15084    async fn boundary_same_leader_exec_completion_returns_actual_new_owner_without_a_turn() {
15085        let (config, state, tid, process) = cancellation_test_state();
15086        install_test_registration(&state, tid, Ivar::new());
15087        let old_mm = MmId::initial(tid);
15088        let new_mm = old_mm.for_exec(tid);
15089        let old_owner = {
15090            let mut scheduler = state.sched.lock().unwrap();
15091            scheduler.install_test_exec_incarnation(tid, old_mm);
15092            let mut owners = crate::slice_boundary::lifecycle::Registry::default();
15093            let owner = owners.fresh(tid, old_mm).unwrap();
15094            scheduler.boundary_owners = Some(owners);
15095            owner
15096        };
15097        state.pending_exec_states.lock().unwrap().insert(
15098            process,
15099            PendingExecState {
15100                caller: tid,
15101                process,
15102                mm: old_mm,
15103                fd_blocking: Default::default(),
15104            },
15105        );
15106        let before = {
15107            let scheduler = state.sched.lock().unwrap();
15108            (scheduler.turn, scheduler.committed_time)
15109        };
15110        let response = state
15111            .receive_rpc(
15112                Tid::from_raw(tid.as_raw()),
15113                (
15114                    DetTime::new(&config),
15115                    new_mm,
15116                    GlobalRequest::MarkPastFirstExecve(process, None, None, None),
15117                ),
15118            )
15119            .await;
15120        let GlobalResponse::WithSliceBoundaryOwner(owner, response) = response.1 else {
15121            panic!("lost actual exec owner")
15122        };
15123        assert!(matches!(*response, GlobalResponse::MarkPastFirstExecve(_)));
15124        assert_eq!(
15125            (owner.birth_tid, owner.birth_generation),
15126            (old_owner.birth_tid, old_owner.birth_generation)
15127        );
15128        assert_eq!(owner.thread_generation, old_owner.thread_generation);
15129        assert_eq!(owner.address_space_generation, new_mm.generation());
15130        let scheduler = state.sched.lock().unwrap();
15131        assert_eq!(
15132            scheduler
15133                .boundary_owners
15134                .as_ref()
15135                .unwrap()
15136                .owner(tid, new_mm)
15137                .unwrap(),
15138            owner
15139        );
15140        assert!(
15141            scheduler
15142                .boundary_owners
15143                .as_ref()
15144                .unwrap()
15145                .owner(tid, old_mm)
15146                .is_err()
15147        );
15148        assert_eq!((scheduler.turn, scheduler.committed_time), before);
15149    }
15150
15151    #[tokio::test]
15152    async fn initial_current_root_rejects_stale_nonroot_and_wrong_incarnations() {
15153        let root = DetTid::from_raw(3);
15154        let other = DetTid::from_raw(4);
15155        let current = MmId::initial(root).for_exec(root);
15156        for case in 0..8 {
15157            let config = Config {
15158                sequentialize_threads: false,
15159                ..Config::default()
15160            };
15161            let state = GlobalState::initialize(&config, false);
15162            let mut registration = super::ChildRegistration {
15163                parent_dettid: root,
15164                parent_detpid: root,
15165                child_dettid: root,
15166                child_tid_addr: 0,
15167                flags: None,
15168                exit_signal: libc::SIGCHLD,
15169                physical_ids: None,
15170                maybe_priority: Some(DEFAULT_PRIORITY),
15171                parent_is_kernel_blocked: false,
15172            };
15173            let mut sender = root;
15174            let mut mm = current;
15175            match case {
15176                0 => sender = other,
15177                1 => registration.parent_dettid = other,
15178                2 => registration.parent_detpid = other,
15179                3 => registration.child_tid_addr = 8,
15180                4 => registration.parent_is_kernel_blocked = true,
15181                5 => mm = current.for_exec(root),
15182                6 => mm = MmId::initial(other).for_exec(other),
15183                7 => {
15184                    // A registered but unrelated sender cannot make a
15185                    // root-shaped request ordinary parent creation.
15186                    sender = other;
15187                    state
15188                        .sched
15189                        .lock()
15190                        .unwrap()
15191                        .thread_tree
15192                        .add_child(other, other, true);
15193                }
15194                _ => unreachable!(),
15195            }
15196            assert!(matches!(
15197                state
15198                    .recv_create_child_thread(Tid::from_raw(sender.as_raw()), mm, registration)
15199                    .await,
15200                SchedulerRpcResult::ThreadExited
15201            ));
15202            assert!(state.root_dettid.get().is_none());
15203            assert_eq!(state.sched.lock().unwrap().thread_mm(root), None);
15204        }
15205    }
15206
15207    #[tokio::test]
15208    async fn registered_parent_creation_preserves_the_child_mm_after_initial_image_birth() {
15209        let parent = DetTid::from_raw(3);
15210        let child = DetTid::from_raw(4);
15211        for parent_mm in [
15212            MmId::initial(parent),
15213            MmId::initial(parent).for_exec(parent),
15214        ] {
15215            for flags in [None, Some(CloneFlags::empty())] {
15216                let config = Config {
15217                    sequentialize_threads: false,
15218                    ..Config::default()
15219                };
15220                let state = GlobalState::initialize(&config, false);
15221                let registration = |child_dettid, flags| super::ChildRegistration {
15222                    parent_dettid: parent,
15223                    parent_detpid: parent,
15224                    child_dettid,
15225                    child_tid_addr: 0,
15226                    flags,
15227                    exit_signal: libc::SIGCHLD,
15228                    physical_ids: None,
15229                    maybe_priority: Some(DEFAULT_PRIORITY),
15230                    parent_is_kernel_blocked: false,
15231                };
15232                assert!(matches!(
15233                    state
15234                        .recv_create_child_thread(
15235                            Tid::from_raw(parent.as_raw()),
15236                            parent_mm,
15237                            registration(parent, None),
15238                        )
15239                        .await,
15240                    SchedulerRpcResult::Continue(())
15241                ));
15242                assert!(matches!(
15243                    state
15244                        .recv_create_child_thread(
15245                            Tid::from_raw(parent.as_raw()),
15246                            parent_mm,
15247                            registration(child, flags),
15248                        )
15249                        .await,
15250                    SchedulerRpcResult::Continue(())
15251                ));
15252                let sched = state.sched.lock().unwrap();
15253                assert_eq!(sched.thread_mm(parent), Some(parent_mm));
15254                assert_eq!(sched.thread_mm(child), Some(MmId::initial(child)));
15255                assert!(sched.thread_was_registered(parent));
15256                assert!(sched.thread_was_registered(child));
15257                assert!(sched.rpc_incarnation_matches(parent, parent_mm));
15258                assert_eq!(state.root_dettid.get(), Some(&parent));
15259            }
15260        }
15261    }
15262
15263    /// The global tool's `/proc/<pid>/auxv` rows, driven through its RPC
15264    /// handlers and the backend's exit reports as a ptrace run drives them:
15265    /// a forked child (and a CLONE_VM child, sharing the creator's memory
15266    /// map) inherit the creator's vector; an exec whose vector could not be
15267    /// recorded drops the child's stale one (`AuxvFromKernel`), and the run
15268    /// warns only of the first; the exit reports a ptrace backend sends
15269    /// (child-exit publication, which bypasses the physical-exit cleanup)
15270    /// leave no row behind, the last process of the shared map included.
15271    #[tokio::test]
15272    async fn auxv_rows_follow_fork_failed_exec_and_exit_through_the_handlers() {
15273        use super::AuxvFromKernel;
15274        use super::ChildRegistration;
15275
15276        let config = Config {
15277            sequentialize_threads: false,
15278            ..Config::default()
15279        }
15280        .with_backend(|backend| backend.reports_child_exit_publication = true);
15281        let state = GlobalState::initialize(&config, false);
15282        let root = DetTid::from_raw(3);
15283        let (forked, shared) = (DetTid::from_raw(4), DetTid::from_raw(5));
15284        let root_mm = MmId::initial(root);
15285        assert!(matches!(
15286            state
15287                .recv_create_child_thread(
15288                    Tid::from_raw(root.as_raw()),
15289                    root_mm,
15290                    ChildRegistration {
15291                        parent_dettid: root,
15292                        parent_detpid: root,
15293                        child_dettid: root,
15294                        child_tid_addr: 0,
15295                        flags: None,
15296                        exit_signal: libc::SIGCHLD,
15297                        physical_ids: None,
15298                        maybe_priority: Some(DEFAULT_PRIORITY),
15299                        parent_is_kernel_blocked: false,
15300                    },
15301                )
15302                .await,
15303            SchedulerRpcResult::Continue(())
15304        ));
15305        let rpc = |task: DetTid, mm: MmId, request: GlobalRequest| {
15306            state.receive_rpc(
15307                Tid::from_raw(task.as_raw()),
15308                (DetTime::new(&config), mm, request),
15309            )
15310        };
15311        let response = rpc(
15312            root,
15313            root_mm,
15314            GlobalRequest::RecordProcessAuxv(vec![7; 32], None),
15315        )
15316        .await
15317        .1;
15318        assert_eq!(response, GlobalResponse::RecordProcessAuxv(()));
15319        for (task, flags) in [
15320            (forked, CloneFlags::empty()),
15321            (shared, CloneFlags::CLONE_VM),
15322        ] {
15323            assert!(matches!(
15324                state
15325                    .recv_create_child_thread(
15326                        Tid::from_raw(root.as_raw()),
15327                        root_mm,
15328                        ChildRegistration {
15329                            parent_dettid: root,
15330                            parent_detpid: root,
15331                            child_dettid: task,
15332                            child_tid_addr: 0,
15333                            flags: Some(flags),
15334                            exit_signal: libc::SIGCHLD,
15335                            physical_ids: None,
15336                            maybe_priority: Some(DEFAULT_PRIORITY),
15337                            parent_is_kernel_blocked: false,
15338                        },
15339                    )
15340                    .await,
15341                SchedulerRpcResult::Continue(())
15342            ));
15343        }
15344        let current = |task: DetTid| {
15345            rpc(
15346                root,
15347                root_mm,
15348                GlobalRequest::ProcessAuxv(DetPid::from_raw(task.as_raw())),
15349            )
15350        };
15351        for task in [root, forked, shared] {
15352            let GlobalResponse::ProcessAuxv(Some(entry)) = current(task).await.1 else {
15353                panic!("{task} has no vector");
15354            };
15355            assert_eq!(entry.bytes, vec![7; 32], "{task}");
15356        }
15357        assert_eq!(state.process_auxv.lock().unwrap().len(), 3);
15358        // The forked child's exec finds no plausible vector: its creator's is
15359        // stale and goes; only the first such exec of the run warns.
15360        let forked_mm = MmId::initial(forked);
15361        for first in [true, false] {
15362            let response = rpc(
15363                forked,
15364                forked_mm,
15365                GlobalRequest::AuxvFromKernel(AuxvFromKernel::Implausible, None),
15366            )
15367            .await
15368            .1;
15369            assert_eq!(response, GlobalResponse::AuxvFromKernel(first));
15370        }
15371        assert_eq!(current(forked).await.1, GlobalResponse::ProcessAuxv(None));
15372        assert_eq!(state.process_auxv.lock().unwrap().len(), 2);
15373        // The exits as a ptrace backend reports them.
15374        for task in [forked, shared, root] {
15375            state.on_backend_process_exited(task.as_raw());
15376        }
15377        assert_eq!(state.process_auxv.lock().unwrap().len(), 0);
15378    }
15379
15380    #[tokio::test]
15381    async fn inherited_input_belongs_only_to_the_registered_root_initial_image() {
15382        check_inherited_input_root_ownership(super::ROOT_DETPID, DetTid::from_raw(4)).await;
15383    }
15384
15385    #[tokio::test]
15386    async fn inherited_input_uses_registered_root_identity_instead_of_pid_three() {
15387        // A descendant with the old numeric root ID must not acquire the lease.
15388        check_inherited_input_root_ownership(DetTid::from_raw(37), DetTid::from_raw(3)).await;
15389    }
15390
15391    async fn check_inherited_input_root_ownership(root: DetTid, child: DetTid) {
15392        use super::ChildRegistration;
15393
15394        let config = Config {
15395            sequentialize_threads: false,
15396            ..Config::default()
15397        }
15398        .with_backend(|backend| backend.tool_shares_guest_descriptor_table = true);
15399        let mut state = GlobalState::initialize(&config, false);
15400        let pipe = reverie::InheritedInput::pipe(0).unwrap();
15401        state.install_inherited_input(pipe.lease().unwrap());
15402        let worker = DetTid::from_raw(root.as_raw() + 2);
15403        let root_mm = MmId::initial(root);
15404        for (parent, task, flags) in [
15405            (root, root, None),
15406            (root, child, Some(CloneFlags::empty())),
15407            (
15408                root,
15409                worker,
15410                Some(CloneFlags::CLONE_VM | CloneFlags::CLONE_SIGHAND | CloneFlags::CLONE_THREAD),
15411            ),
15412        ] {
15413            assert!(matches!(
15414                state
15415                    .recv_create_child_thread(
15416                        Tid::from_raw(parent.as_raw()),
15417                        root_mm,
15418                        ChildRegistration {
15419                            parent_dettid: parent,
15420                            parent_detpid: root,
15421                            child_dettid: task,
15422                            child_tid_addr: 0,
15423                            flags,
15424                            exit_signal: libc::SIGCHLD,
15425                            physical_ids: None,
15426                            maybe_priority: Some(DEFAULT_PRIORITY),
15427                            parent_is_kernel_blocked: false,
15428                        },
15429                    )
15430                    .await,
15431                SchedulerRpcResult::Continue(())
15432            ));
15433        }
15434        assert_eq!(
15435            state.inherited_input.lock().unwrap().as_ref().unwrap().root,
15436            Some((root, root_mm))
15437        );
15438
15439        // Neither a descendant nor a sibling sharing the root's exact MmId
15440        // may consume the seed before the root's own completion.
15441        for (task, detpid, mm) in [
15442            (child, child, MmId::initial(child)),
15443            (worker, root, root_mm),
15444        ] {
15445            let response = state
15446                .receive_rpc(
15447                    Tid::from_raw(task.as_raw()),
15448                    (
15449                        DetTime::new(&config),
15450                        mm,
15451                        GlobalRequest::MarkPastFirstExecve(detpid, None, None, None),
15452                    ),
15453                )
15454                .await
15455                .1;
15456            let GlobalResponse::MarkPastFirstExecve(non_root_state) = response else {
15457                panic!("{response:?}")
15458            };
15459            assert!(non_root_state.initial_input.is_none());
15460            assert!(state.inherited_input.lock().unwrap().is_some());
15461        }
15462
15463        // Nor can a later root image, even with the correct descriptor slot.
15464        let response = state
15465            .receive_rpc(
15466                Tid::from_raw(root.as_raw()),
15467                (
15468                    DetTime::new(&config),
15469                    root_mm.for_exec(root),
15470                    GlobalRequest::MarkPastFirstExecve(root, None, None, None),
15471                ),
15472            )
15473            .await
15474            .1;
15475        // An initial registration has no retired-exec incarnation entry, so
15476        // the existing RPC layer returns an empty completion here. The launch
15477        // lease's stricter root/image binding must still refuse the seed.
15478        assert_eq!(
15479            response,
15480            GlobalResponse::MarkPastFirstExecve(Default::default())
15481        );
15482        assert!(state.inherited_input.lock().unwrap().is_some());
15483
15484        let response = state
15485            .receive_rpc(
15486                Tid::from_raw(root.as_raw()),
15487                (
15488                    DetTime::new(&config),
15489                    root_mm,
15490                    GlobalRequest::MarkPastFirstExecve(root, None, None, None),
15491                ),
15492            )
15493            .await
15494            .1;
15495        let GlobalResponse::MarkPastFirstExecve(root_state) = response else {
15496            panic!("{response:?}")
15497        };
15498        assert_eq!(root_state.initial_input.as_ref(), Some(pipe.identity()));
15499        assert!(state.inherited_input.lock().unwrap().is_none());
15500        let response = state
15501            .receive_rpc(
15502                Tid::from_raw(root.as_raw()),
15503                (
15504                    DetTime::new(&config),
15505                    root_mm,
15506                    GlobalRequest::MarkPastFirstExecve(root, None, None, None),
15507                ),
15508            )
15509            .await
15510            .1;
15511        let GlobalResponse::MarkPastFirstExecve(repeated) = response else {
15512            panic!("{response:?}")
15513        };
15514        assert!(repeated.initial_input.is_none());
15515
15516        // An exec preparation is a return of existing OFD state, not an
15517        // installation channel for copied launch identities.
15518        let response = state
15519            .receive_rpc(
15520                Tid::from_raw(root.as_raw()),
15521                (
15522                    DetTime::new(&config),
15523                    root_mm,
15524                    GlobalRequest::PrepareExec(root, root_mm, root_state),
15525                ),
15526            )
15527            .await
15528            .1;
15529        assert_eq!(response, GlobalResponse::ThreadExited);
15530    }
15531
15532    #[tokio::test]
15533    async fn failed_root_registration_does_not_bind_inherited_input() {
15534        for raw_root in [3, 37] {
15535            let config = Config {
15536                sequentialize_threads: false,
15537                ..Config::default()
15538            }
15539            .with_backend(|backend| backend.tool_shares_guest_descriptor_table = true);
15540            let mut state = GlobalState::initialize(&config, false);
15541            let pipe = reverie::InheritedInput::pipe(0).unwrap();
15542            state.install_inherited_input(pipe.lease().unwrap());
15543            let root = DetTid::from_raw(raw_root);
15544            let root_mm = MmId::initial(root);
15545            let response = state
15546                .recv_create_child_thread(
15547                    Tid::from_raw(raw_root),
15548                    root_mm,
15549                    super::ChildRegistration {
15550                        parent_dettid: root,
15551                        parent_detpid: root,
15552                        child_dettid: root,
15553                        child_tid_addr: 0,
15554                        flags: None,
15555                        exit_signal: libc::SIGCHLD,
15556                        // The real physical-registration boundary rejects
15557                        // nonpositive IDs before opening any kernel pidfd.
15558                        physical_ids: Some((-1, -1)),
15559                        maybe_priority: Some(DEFAULT_PRIORITY),
15560                        parent_is_kernel_blocked: false,
15561                    },
15562                )
15563                .await;
15564            assert!(matches!(response, SchedulerRpcResult::ThreadExited));
15565            let input = state.inherited_input.lock().unwrap();
15566            let input = input
15567                .as_ref()
15568                .expect("failed registration consumed the lease");
15569            assert!(input.root.is_none(), "failed registration bound the root");
15570            assert_eq!(input.lease.identity(), pipe.identity());
15571        }
15572    }
15573
15574    fn cancellation_test_state() -> (Config, GlobalState, DetTid, DetPid) {
15575        let config = Config {
15576            sequentialize_threads: true,
15577            ..Config::default()
15578        }
15579        .with_backend(|backend| {
15580            backend.needs_killed_thread_rpc_cancellation = true;
15581        });
15582        cancellation_test_state_with_config(config)
15583    }
15584
15585    fn cancellation_test_state_with_config(
15586        config: Config,
15587    ) -> (Config, GlobalState, DetTid, DetPid) {
15588        let state = GlobalState::initialize(&config, false);
15589        let dettid = DetTid::from_raw(17);
15590        let detpid = DetPid::from_raw(17);
15591        state
15592            .sched
15593            .lock()
15594            .unwrap()
15595            .thread_tree
15596            .add_child(dettid, dettid, true);
15597        // The root inherits its process group from outside the namespace; this
15598        // test's root leads its own, as after setpgid(0, 0).
15599        state
15600            .sched
15601            .lock()
15602            .unwrap()
15603            .thread_tree
15604            .set_process_group(detpid, detpid);
15605        (config, state, dettid, detpid)
15606    }
15607
15608    fn install_test_registration(state: &GlobalState, dettid: DetTid, request: Ivar<SchedRequest>) {
15609        let mut scheduler = state.sched.lock().unwrap();
15610        assert!(!scheduler.thread_is_logically_killed(dettid));
15611        scheduler.next_turns.insert(
15612            dettid,
15613            ThreadNextTurn {
15614                dettid,
15615                child_tid_addr: 0,
15616                req: request,
15617                resp: Ivar::new(),
15618                protocol: Default::default(),
15619            },
15620        );
15621        scheduler.priorities.insert(dettid, DEFAULT_PRIORITY);
15622        scheduler.runqueue_push_back(dettid);
15623    }
15624
15625    // These tests stage a validated expectation, but use the real RPC and
15626    // real committed turn to deliver it. They do not enable replay lookahead
15627    // or claim callback/exec-lifecycle qualification.
15628    async fn boundary_producer_roundtrip(
15629        enabled: bool,
15630        input: Option<&std::path::Path>,
15631        output: Option<&std::path::Path>,
15632    ) -> (Vec<Resources>, LogicalTime, LogicalTime, u64, u64) {
15633        use detcore_model::schedule::boundary::Entry;
15634        use detcore_model::schedule::boundary::Phase;
15635        use detcore_model::schedule::boundary::Record;
15636        use reverie::Tool;
15637
15638        use crate::preemptions::boundary_recording::Recording;
15639        let (config, state, tid, process) = if input.is_some() || output.is_some() {
15640            cancellation_test_state_with_config(
15641                Config {
15642                    sequentialize_threads: true,
15643                    replay_schedule_from: input.map(std::path::Path::to_path_buf),
15644                    strict_schedule_boundaries: true,
15645                    record_preemptions: true,
15646                    record_preemptions_to: output.map(std::path::Path::to_path_buf),
15647                    ..Config::default()
15648                }
15649                .with_backend(|backend| backend.needs_killed_thread_rpc_cancellation = true),
15650            )
15651        } else {
15652            cancellation_test_state()
15653        };
15654        install_test_registration(&state, tid, Ivar::new());
15655        let mut thread = crate::ThreadState::new(tid, &config, ());
15656        thread.detpid = Some(process);
15657        thread.next_timeslice(&config);
15658        if enabled {
15659            thread.slice_boundary = Some(crate::slice_boundary::Observations::new(&thread, 0));
15660            let mut scheduler = state.sched.lock().unwrap();
15661            let mut owners = crate::slice_boundary::lifecycle::Registry::default();
15662            owners.fresh(tid, thread.mm_id).unwrap();
15663            scheduler.boundary_owners = Some(owners);
15664            scheduler.record_thread_mm(tid, thread.mm_id);
15665            if input.is_some() {
15666                let budget = scheduler
15667                    .boundary_budget_for_live_owner(tid)
15668                    .unwrap()
15669                    .unwrap();
15670                crate::slice_boundary::grant::install(&mut thread, budget).unwrap();
15671            }
15672            let mut writer =
15673                crate::preemptions::PreemptionWriter::new(output.map(std::path::Path::to_path_buf))
15674                    .with_slice_boundaries()
15675                    .unwrap();
15676            writer.register_thread(tid, DEFAULT_PRIORITY);
15677            scheduler.preemption_writer = Some(writer);
15678        }
15679        let mut guest = ExternalRegistrationGuest {
15680            global: &state,
15681            config: &config,
15682            thread,
15683            requests: Mutex::new(Vec::new()),
15684            post_exec: false,
15685            ppid: None,
15686            private_memory: None,
15687        };
15688        let tool: Detcore = Detcore::new(Tid::from_raw(tid.as_raw()), &config);
15689        let mut turns = Vec::new();
15690        for _ in 0..2 {
15691            if let Some(mut observed) = guest.thread.slice_boundary.take() {
15692                observed
15693                    .callback(&guest.thread, Entry::Cpuid, Phase::Prehook)
15694                    .unwrap();
15695                guest.thread.slice_boundary = Some(observed);
15696            }
15697            let mut request = Box::pin(tool.end_timeslice(&mut guest));
15698            for _ in 0..8 {
15699                assert!(futures::poll!(&mut request).is_pending());
15700                if state.sched.lock().unwrap().next_turns[&tid]
15701                    .req
15702                    .try_read()
15703                    .is_some()
15704                {
15705                    break;
15706                }
15707            }
15708            let posted = state.sched.lock().unwrap().next_turns[&tid]
15709                .req
15710                .try_read()
15711                .unwrap()
15712                .unwrap();
15713            assert!(
15714                posted.slice_boundary.is_none(),
15715                "stored/reposted resource must not retain an annotation"
15716            );
15717            assert!(posted.upgraded_to_runnable().slice_boundary.is_none());
15718            let turn = crate::scheduler::do_a_turn_blocking(
15719                state.sched.clone(),
15720                state.global_time.clone(),
15721                &Err(crate::scheduler::SkipTurn),
15722            )
15723            .await
15724            .expect("the original request must commit exactly one turn");
15725            assert!(turn.slice_boundary.is_none());
15726            assert!(
15727                turn.slice_expired,
15728                "a slice that ends because its time ran out marks its request"
15729            );
15730            turns.push(turn);
15731            request.await;
15732        }
15733        let mut scheduler = state.sched.lock().unwrap();
15734        if enabled {
15735            let writer = scheduler.preemption_writer.take().unwrap();
15736            let encoded = if let Some(path) = output {
15737                writer.flush().unwrap();
15738                std::fs::read_to_string(path).unwrap()
15739            } else {
15740                writer.into_string()
15741            };
15742            let Recording::Boundaries(recording) = Recording::decode(encoded.as_bytes()).unwrap()
15743            else {
15744                panic!("lost typed recording")
15745            };
15746            let records = recording.schedule().records();
15747            assert_eq!(records.len(), 2);
15748            for (index, record) in records.iter().enumerate() {
15749                let Record::TimesliceEnd(boundary) = record else {
15750                    panic!("introduced an ordinary event")
15751                };
15752                assert_eq!(boundary.identity.slice, index as u64 + 1);
15753                assert_eq!(boundary.identity.checkin, index as u64 + 1);
15754                assert_eq!(boundary.identity.committed_rcbs, 0);
15755                assert_eq!(boundary.identity.entry, Entry::Cpuid);
15756            }
15757            assert_eq!(scheduler.recorded_event_count, 2);
15758            if input.is_some() {
15759                assert_eq!(
15760                    scheduler
15761                        .generate_partial_run_summary(None)
15762                        .unwrap()
15763                        .schedevent_replayed,
15764                    2
15765                );
15766            }
15767        }
15768        (
15769            turns,
15770            scheduler.committed_time,
15771            guest.thread.thread_logical_time.as_nanos(),
15772            scheduler.turn,
15773            guest.thread.stats.timeslice_count,
15774        )
15775    }
15776
15777    #[tokio::test]
15778    async fn boundary_actual_reset_and_resource_transport_preserve_original_turns_and_clocks() {
15779        assert_eq!(
15780            boundary_producer_roundtrip(true, None, None).await,
15781            boundary_producer_roundtrip(false, None, None).await
15782        );
15783    }
15784
15785    #[tokio::test]
15786    async fn boundary_recorded_bytes_replay_two_original_yields_with_exact_clock_and_turn_parity() {
15787        let dir = tempfile::tempdir().unwrap();
15788        let first = dir.path().join("first.json");
15789        let replay = dir.path().join("replay.json");
15790        let recorded = boundary_producer_roundtrip(true, None, Some(&first)).await;
15791        let replayed = boundary_producer_roundtrip(true, Some(&first), Some(&replay)).await;
15792        assert_eq!(recorded, replayed);
15793        assert_eq!(
15794            std::fs::read(first).unwrap(),
15795            std::fs::read(replay).unwrap()
15796        );
15797    }
15798
15799    // Exercise the syscall handler and the original resource receiver/turn,
15800    // rather than calling close_slice or end_timeslice directly. No guest or
15801    // backend is executed by this fixture. Both timer modes remain supported.
15802    async fn boundary_sched_yield_handler(
15803        input: Option<&std::path::Path>,
15804        typed: bool,
15805        chaos: bool,
15806        timers: bool,
15807        stale_owner: bool,
15808    ) -> (
15809        crate::preemptions::boundary_recording::Recording,
15810        LogicalTime,
15811        LogicalTime,
15812        u64,
15813        u64,
15814    ) {
15815        use detcore_model::schedule::boundary::Cause;
15816        use detcore_model::schedule::boundary::Entry;
15817        use detcore_model::schedule::boundary::Phase;
15818        use detcore_model::schedule::boundary::Record;
15819        use reverie::Tool;
15820
15821        use crate::preemptions::boundary_recording::Recording;
15822
15823        let (config, state, tid, process) = cancellation_test_state_with_config(
15824            Config {
15825                sequentialize_threads: true,
15826                replay_schedule_from: input.map(std::path::Path::to_path_buf),
15827                strict_schedule_boundaries: typed && !chaos && input.is_some(),
15828                chaos,
15829                max_timeslice: if timers {
15830                    Config::default().max_timeslice
15831                } else {
15832                    None
15833                },
15834                target_timeslice: None,
15835                ..Config::default()
15836            }
15837            .with_backend(|backend| backend.needs_killed_thread_rpc_cancellation = true),
15838        );
15839        install_test_registration(&state, tid, Ivar::new());
15840        let mut thread = crate::ThreadState::new(tid, &config, ());
15841        thread.detpid = Some(process);
15842        thread.next_timeslice(&config);
15843        let initial_slices = thread.stats.timeslice_count;
15844        {
15845            let mut scheduler = state.sched.lock().unwrap();
15846            let mut writer = crate::preemptions::PreemptionWriter::new(None);
15847            if typed {
15848                thread.slice_boundary = Some(crate::slice_boundary::Observations::new(&thread, 0));
15849                let mut owners = crate::slice_boundary::lifecycle::Registry::default();
15850                owners.fresh(tid, thread.mm_id).unwrap();
15851                scheduler.boundary_owners = Some(owners);
15852                scheduler.record_thread_mm(tid, thread.mm_id);
15853                if let Some(budget) = scheduler.boundary_budget_for_live_owner(tid).unwrap() {
15854                    crate::slice_boundary::grant::install(&mut thread, budget).unwrap();
15855                }
15856                writer = writer.with_slice_boundaries().unwrap();
15857            }
15858            writer.register_thread(tid, DEFAULT_PRIORITY);
15859            scheduler.preemption_writer = Some(writer);
15860        }
15861        let mut guest = ExternalRegistrationGuest {
15862            global: &state,
15863            config: &config,
15864            thread,
15865            requests: Mutex::new(Vec::new()),
15866            post_exec: false,
15867            ppid: None,
15868            private_memory: None,
15869        };
15870        let tool: Detcore = Detcore::new(Tid::from_raw(tid.as_raw()), &config);
15871        for occurrence in 1..=2 {
15872            if let Some(mut observed) = guest.thread.slice_boundary.take() {
15873                observed
15874                    .syscall(&guest.thread, Sysno::sched_yield, Phase::Prehook)
15875                    .unwrap();
15876                guest.thread.slice_boundary = Some(observed);
15877            }
15878            if stale_owner {
15879                guest.thread.mm_id = guest.thread.mm_id.for_exec(tid);
15880            }
15881            let before_turn = state.sched.lock().unwrap().turn;
15882            let before_requests = guest.requests.lock().unwrap().len();
15883            let mut request = Box::pin(
15884                tool.handle_sched_yield(&mut guest, reverie::syscalls::SchedYield::default()),
15885            );
15886            for _ in 0..8 {
15887                assert!(futures::poll!(&mut request).is_pending());
15888                if state.sched.lock().unwrap().next_turns[&tid]
15889                    .req
15890                    .try_read()
15891                    .is_some()
15892                {
15893                    break;
15894                }
15895            }
15896            let posted = state.sched.lock().unwrap().next_turns[&tid]
15897                .req
15898                .try_read()
15899                .unwrap()
15900                .unwrap();
15901            assert!(posted.slice_boundary.is_none());
15902            assert!(posted.upgraded_to_runnable().slice_boundary.is_none());
15903            // Record's original strong yield and replay's original weak yield
15904            // remain distinct. Typed boundary consumption must not replace the
15905            // replay request with an independent one-turn exclusion.
15906            let expected = if input.is_none() && !chaos {
15907                ResourceID::SchedYield
15908            } else {
15909                ResourceID::SleepUntil(LogicalTime::from_nanos(0))
15910            };
15911            assert_eq!(posted.resources.len(), 1);
15912            assert_eq!(posted.resources.get(&expected), Some(&Permission::W));
15913            let turn = crate::scheduler::do_a_turn_blocking(
15914                state.sched.clone(),
15915                state.global_time.clone(),
15916                &Err(crate::scheduler::SkipTurn),
15917            )
15918            .await
15919            .expect("one sched_yield must commit its original turn");
15920            assert!(turn.slice_boundary.is_none());
15921            assert!(
15922                !turn.slice_expired,
15923                "an explicit sched_yield is a syscall turn, not an expired slice"
15924            );
15925            assert_eq!(request.await.unwrap(), 0);
15926            assert_eq!(state.sched.lock().unwrap().turn, before_turn + 1);
15927            assert_eq!(guest.requests.lock().unwrap().len(), before_requests + 1);
15928            if let Some(mut observed) = guest.thread.slice_boundary.take() {
15929                observed
15930                    .syscall(&guest.thread, Sysno::sched_yield, Phase::Posthook)
15931                    .unwrap();
15932                guest.thread.slice_boundary = Some(observed);
15933            }
15934            let closes = typed && !chaos;
15935            assert_eq!(
15936                guest.thread.stats.timeslice_count,
15937                initial_slices + if closes && timers { occurrence } else { 0 }
15938            );
15939        }
15940        let mut scheduler = state.sched.lock().unwrap();
15941        let recording = Recording::decode(
15942            scheduler
15943                .preemption_writer
15944                .take()
15945                .unwrap()
15946                .into_string()
15947                .as_bytes(),
15948        )
15949        .unwrap();
15950        if let Recording::Boundaries(recording) = &recording {
15951            let records = recording.schedule().records();
15952            assert_eq!(records.len(), if chaos { 0 } else { 2 });
15953            for (index, record) in records.iter().enumerate() {
15954                let Record::TimesliceEnd(boundary) = record else {
15955                    panic!("not a boundary")
15956                };
15957                assert_eq!(boundary.identity.cause, Cause::ExplicitSchedYield);
15958                assert_eq!(boundary.identity.phase, Phase::Prehook);
15959                assert_eq!(
15960                    boundary.identity.entry,
15961                    Entry::Syscall {
15962                        syscall: Sysno::sched_yield,
15963                        occurrence: index as u64 + 1
15964                    }
15965                );
15966                assert_eq!(boundary.identity.slice, index as u64 + 1);
15967                assert_eq!(boundary.identity.checkin, index as u64 * 2 + 1);
15968                assert_eq!(boundary.identity.committed_rcbs, 0);
15969                assert_eq!(boundary.count, 1);
15970            }
15971            assert_eq!(
15972                scheduler.recorded_event_count,
15973                u64::try_from(records.len()).unwrap()
15974            );
15975            if input.is_some() && !chaos {
15976                assert_eq!(
15977                    scheduler
15978                        .generate_partial_run_summary(None)
15979                        .unwrap()
15980                        .schedevent_replayed,
15981                    2
15982                );
15983            }
15984        } else {
15985            assert_eq!(scheduler.recorded_event_count, 0);
15986        }
15987        (
15988            recording,
15989            scheduler.committed_time,
15990            guest.thread.thread_logical_time.as_nanos(),
15991            scheduler.turn,
15992            guest.thread.stats.timeslice_count,
15993        )
15994    }
15995
15996    #[tokio::test]
15997    async fn boundary_actual_sched_yield_handler_replays_with_and_without_timers() {
15998        let directory = tempfile::tempdir().unwrap();
15999        for timers in [false, true] {
16000            let recorded = boundary_sched_yield_handler(None, true, false, timers, false).await;
16001            let input = directory.path().join(format!("{timers}.json"));
16002            recorded.0.write(&input).unwrap();
16003            let replayed =
16004                boundary_sched_yield_handler(Some(&input), true, false, timers, false).await;
16005            // Complete metadata, identities, clocks, turns and reset counts.
16006            assert_eq!(recorded, replayed);
16007        }
16008    }
16009
16010    #[tokio::test]
16011    async fn boundary_actual_sched_yield_handler_preserves_legacy_and_chaos_weak_yield() {
16012        use crate::preemptions::boundary_recording::Recording;
16013        let directory = tempfile::tempdir().unwrap();
16014        let input = directory.path().join("legacy.json");
16015        Recording::Legacy(PreemptionRecord::from_sched_events(vec![]))
16016            .write(&input)
16017            .unwrap();
16018        for timers in [false, true] {
16019            boundary_sched_yield_handler(Some(&input), false, false, timers, false).await;
16020        }
16021        // Even a typed observation state must not intercept the chaos route.
16022        // A configured maximum makes this the existing WeakYield chaos arm.
16023        boundary_sched_yield_handler(Some(&input), false, true, true, false).await;
16024        boundary_sched_yield_handler(Some(&input), true, true, true, false).await;
16025    }
16026
16027    async fn boundary_bad_yield_replay(kind: &str) {
16028        use detcore_model::schedule::boundary::Cause;
16029        use detcore_model::schedule::boundary::Entry;
16030        use detcore_model::schedule::boundary::Record;
16031        use detcore_model::schedule::boundary::Schedule;
16032
16033        use crate::preemptions::boundary_recording::BoundaryRecording;
16034        use crate::preemptions::boundary_recording::Recording;
16035
16036        let recorded = boundary_sched_yield_handler(None, true, false, false, false)
16037            .await
16038            .0;
16039        let Recording::Boundaries(recorded) = recorded else {
16040            panic!("not typed")
16041        };
16042        let mut records = recorded.schedule().records().to_vec();
16043        records.truncate(1);
16044        if kind == "missing" {
16045            records.clear();
16046        } else if kind != "owner" {
16047            let Record::TimesliceEnd(boundary) = &mut records[0] else {
16048                unreachable!()
16049            };
16050            match kind {
16051                "cause" => boundary.identity.cause = Cause::SoftwareCheckin,
16052                "occurrence" => {
16053                    boundary.identity.entry = Entry::Syscall {
16054                        syscall: Sysno::sched_yield,
16055                        occurrence: 2,
16056                    }
16057                }
16058                _ => panic!("unknown negative fixture"),
16059            }
16060        }
16061        let input = tempfile::tempdir().unwrap();
16062        let path = input.path().join("bad.json");
16063        Recording::Boundaries(
16064            BoundaryRecording::new(
16065                PreemptionRecord::from_sched_events(vec![]),
16066                Schedule::new(records).unwrap(),
16067            )
16068            .unwrap(),
16069        )
16070        .write(&path)
16071        .unwrap();
16072        boundary_sched_yield_handler(Some(&path), true, false, false, kind == "owner").await;
16073        panic!("actual sched_yield handler accepted {kind}");
16074    }
16075
16076    #[tokio::test]
16077    async fn boundary_actual_sched_yield_handler_refuses_missing_marker() {
16078        if in_child_expecting_refusal(
16079            "boundary_actual_sched_yield_handler_refuses_missing_marker",
16080            "Exhausted",
16081        ) {
16082            boundary_bad_yield_replay("missing").await;
16083        }
16084    }
16085
16086    #[tokio::test]
16087    async fn boundary_actual_sched_yield_handler_refuses_wrong_cause() {
16088        if in_child_expecting_refusal(
16089            "boundary_actual_sched_yield_handler_refuses_wrong_cause",
16090            "expected Identity",
16091        ) {
16092            boundary_bad_yield_replay("cause").await;
16093        }
16094    }
16095
16096    #[tokio::test]
16097    async fn boundary_actual_sched_yield_handler_refuses_wrong_occurrence() {
16098        if in_child_expecting_refusal(
16099            "boundary_actual_sched_yield_handler_refuses_wrong_occurrence",
16100            "expected Identity",
16101        ) {
16102            boundary_bad_yield_replay("occurrence").await;
16103        }
16104    }
16105
16106    #[tokio::test]
16107    async fn boundary_actual_sched_yield_handler_refuses_stale_owner() {
16108        if in_child_expecting_refusal(
16109            "boundary_actual_sched_yield_handler_refuses_stale_owner",
16110            "stale observation owner",
16111        ) {
16112            boundary_bad_yield_replay("owner").await;
16113        }
16114    }
16115
16116    #[test]
16117    fn boundary_resource_union_refuses_either_annotated_operand_before_mutation() {
16118        let tid = DetTid::from_raw(17);
16119        let config = Config::default();
16120        let mut thread = crate::ThreadState::new(tid, &config, ());
16121        let mut observed = crate::slice_boundary::Observations::new(&thread, 0);
16122        observed
16123            .callback(
16124                &thread,
16125                detcore_model::schedule::boundary::Entry::Cpuid,
16126                detcore_model::schedule::boundary::Phase::Prehook,
16127            )
16128            .unwrap();
16129        let boundary = observed
16130            .close_slice(&mut thread, &config, false, None)
16131            .unwrap()
16132            .boundary;
16133        for side in 0..2 {
16134            let mut left = Resources::new(tid);
16135            let mut right = Resources::new(tid);
16136            right.insert(ResourceID::MemAddrSpace(tid), Permission::RW);
16137            if side == 0 {
16138                left.slice_boundary = Some(Box::new(boundary.clone()));
16139            } else {
16140                right.slice_boundary = Some(Box::new(boundary.clone()));
16141            }
16142            let annotated = if side == 0 { &left } else { &right };
16143            assert!(
16144                std::panic::catch_unwind(std::panic::AssertUnwindSafe(
16145                    || annotated.upgraded_to_runnable()
16146                ))
16147                .is_err()
16148            );
16149            let before = left.clone();
16150            assert!(
16151                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| left.union(&right)))
16152                    .is_err()
16153            );
16154            assert_eq!(left, before);
16155        }
16156    }
16157
16158    async fn boundary_grant_roundtrip(
16159        route: u8,
16160        with_budget: bool,
16161        automatic: bool,
16162    ) -> (
16163        Resources,
16164        LogicalTime,
16165        LogicalTime,
16166        u64,
16167        (Option<LogicalTime>, Option<LogicalTime>, u64),
16168    ) {
16169        let input = tempfile::tempdir().unwrap();
16170        let (config, state, tid, process) = if automatic {
16171            use detcore_model::schedule::boundary::Boundary;
16172            use detcore_model::schedule::boundary::Record;
16173            use detcore_model::schedule::boundary::Schedule;
16174
16175            use crate::preemptions::boundary_recording::BoundaryRecording;
16176            use crate::preemptions::boundary_recording::Recording;
16177            let tid = DetTid::from_raw(17);
16178            let budget = crate::slice_boundary::grant::tests::budget(tid, MmId::initial(tid), 91);
16179            let recording = Recording::Boundaries(
16180                BoundaryRecording::new(
16181                    PreemptionRecord::from_sched_events(vec![]),
16182                    Schedule::new(vec![Record::TimesliceEnd(Boundary {
16183                        identity: budget.expected(),
16184                        count: 1,
16185                        start_rip: None,
16186                        end_rip: None,
16187                        end_time: None,
16188                    })])
16189                    .unwrap(),
16190                )
16191                .unwrap(),
16192            );
16193            let path = input.path().join("recording.json");
16194            recording.write(&path).unwrap();
16195            cancellation_test_state_with_config(
16196                Config {
16197                    sequentialize_threads: true,
16198                    replay_schedule_from: Some(path),
16199                    strict_schedule_boundaries: true,
16200                    ..Config::default()
16201                }
16202                .with_backend(|backend| backend.needs_killed_thread_rpc_cancellation = true),
16203            )
16204        } else {
16205            cancellation_test_state()
16206        };
16207        install_test_registration(&state, tid, Ivar::new());
16208        let mut thread = crate::ThreadState::new(tid, &config, ());
16209        thread.detpid = Some(process);
16210        if route != 1 {
16211            thread.next_timeslice(&config);
16212        }
16213        thread.slice_boundary = Some(crate::slice_boundary::Observations::new(&thread, 0));
16214        let budget = crate::slice_boundary::grant::tests::budget(tid, thread.mm_id, 91);
16215        if with_budget {
16216            let mut scheduler = state.sched.lock().unwrap();
16217            let mut owners = crate::slice_boundary::lifecycle::Registry::default();
16218            owners.fresh(tid, thread.mm_id).unwrap();
16219            scheduler.boundary_owners = Some(owners);
16220            if automatic {
16221                scheduler.record_thread_mm(tid, thread.mm_id);
16222                assert_eq!(
16223                    scheduler.boundary_budget_for_live_owner(tid).unwrap(),
16224                    Some(budget)
16225                );
16226            } else {
16227                scheduler
16228                    .boundary_budgets
16229                    .queue(thread.mm_id, budget)
16230                    .unwrap();
16231            }
16232            if route == 1 {
16233                // The real startup reply must initialize from lifecycle
16234                // authority before it stages the separate expected budget.
16235                thread.slice_boundary = None;
16236            }
16237        }
16238        let original_slices = thread.stats.timeslice_count;
16239        let original_maximum = thread.max_timeslice_end;
16240        let mut guest = ExternalRegistrationGuest {
16241            global: &state,
16242            config: &config,
16243            thread,
16244            requests: Mutex::new(Vec::new()),
16245            post_exec: false,
16246            ppid: None,
16247            private_memory: None,
16248        };
16249        let mut request = Box::pin(async {
16250            match route {
16251                0 => {
16252                    let mut resources = Resources::new(tid);
16253                    resources.insert(ResourceID::MemAddrSpace(process), Permission::RW);
16254                    assert_eq!(
16255                        super::resource_request(&mut guest, resources).await,
16256                        ResumeStatus::Normal
16257                    );
16258                }
16259                1 => assert_eq!(
16260                    super::thread_start_request(&config, &mut guest, process).await,
16261                    None
16262                ),
16263                2 => {
16264                    let response =
16265                        super::send_and_update_time(&mut guest, GlobalRequest::ResumeExec(process))
16266                            .await;
16267                    assert_eq!(
16268                        response.1,
16269                        GlobalResponse::ResumeExec(ResumeStatus::Normal, None)
16270                    );
16271                }
16272                _ => unreachable!(),
16273            }
16274        });
16275        for _ in 0..8 {
16276            assert!(futures::poll!(&mut request).is_pending());
16277            if state.sched.lock().unwrap().next_turns[&tid]
16278                .req
16279                .try_read()
16280                .is_some()
16281            {
16282                break;
16283            }
16284        }
16285        assert!(
16286            state.sched.lock().unwrap().next_turns[&tid]
16287                .req
16288                .try_read()
16289                .is_some()
16290        );
16291        let turn = crate::scheduler::do_a_turn_blocking(
16292            state.sched.clone(),
16293            state.global_time.clone(),
16294            &Err(crate::scheduler::SkipTurn),
16295        )
16296        .await
16297        .expect("the original resource turn must commit");
16298        request.await;
16299        if route == 1 {
16300            // The actual startup continuation runs the reset AFTER the RPC,
16301            // with its original history/cap order. Its budget must still be
16302            // pending here, not armed and then erased by next_timeslice.
16303            assert_eq!(guest.thread.replay_rcb_end, None);
16304            assert_eq!(guest.thread.stats.timeslice_count, original_slices);
16305            super::finish_thread_start(&config, &mut guest, None);
16306        }
16307        assert_eq!(guest.thread.replay_rcb_end, with_budget.then_some(91));
16308        assert_eq!(
16309            guest.thread.stats.timeslice_count,
16310            original_slices + u64::from(route == 1)
16311        );
16312        if route != 1 {
16313            assert_eq!(guest.thread.max_timeslice_end, original_maximum);
16314        }
16315        let scheduler = state.sched.lock().unwrap();
16316        (
16317            turn,
16318            scheduler.committed_time,
16319            guest.thread.thread_logical_time.as_nanos(),
16320            scheduler.turn,
16321            (
16322                guest.thread.max_timeslice_end,
16323                guest.thread.end_of_timeslice,
16324                guest.thread.stats.timeslice_count,
16325            ),
16326        )
16327    }
16328
16329    #[tokio::test]
16330    async fn boundary_budget_survives_real_resource_start_and_exec_resume_rpc_without_an_extra_turn()
16331     {
16332        for route in 0..3 {
16333            assert_eq!(
16334                boundary_grant_roundtrip(route, true, false).await,
16335                boundary_grant_roundtrip(route, false, false).await
16336            );
16337        }
16338    }
16339
16340    #[tokio::test]
16341    async fn boundary_loaded_cursor_delivers_only_through_the_existing_grant_and_start_reset() {
16342        for route in 0..3 {
16343            assert_eq!(
16344                boundary_grant_roundtrip(route, true, true).await,
16345                boundary_grant_roundtrip(route, false, false).await,
16346            );
16347        }
16348    }
16349
16350    // Exercise external registration and post-exec through the real global RPC
16351    // without a backend or guest process. Unexpected guest operations fail.
16352    struct ExternalRegistrationGuest<'a> {
16353        global: &'a GlobalState,
16354        config: &'a Config,
16355        thread: crate::ThreadState<()>,
16356        requests: Mutex<Vec<GlobalRequest>>,
16357        post_exec: bool,
16358        ppid: Option<reverie::Pid>,
16359        private_memory: Option<reverie::RuntimePrivateMemory>,
16360    }
16361
16362    struct ExternalRegistrationStack;
16363    struct ExternalRegistrationStackGuard;
16364
16365    impl Drop for ExternalRegistrationStackGuard {
16366        fn drop(&mut self) {}
16367    }
16368
16369    impl reverie::Stack for ExternalRegistrationStack {
16370        type StackGuard = ExternalRegistrationStackGuard;
16371
16372        fn size(&self) -> usize {
16373            panic!("external registration must not use a guest stack")
16374        }
16375
16376        fn capacity(&self) -> usize {
16377            panic!("external registration must not use a guest stack")
16378        }
16379
16380        fn push<'stack, T>(&mut self, _value: T) -> reverie::syscalls::Addr<'stack, T> {
16381            panic!("external registration must not use a guest stack")
16382        }
16383
16384        fn reserve<'stack, T>(&mut self) -> reverie::syscalls::AddrMut<'stack, T> {
16385            panic!("external registration must not use a guest stack")
16386        }
16387
16388        fn commit(self) -> Result<Self::StackGuard, reverie::syscalls::Errno> {
16389            panic!("external registration must not use a guest stack")
16390        }
16391    }
16392
16393    #[reverie::tool]
16394    impl GlobalRPC<GlobalState> for ExternalRegistrationGuest<'_> {
16395        async fn send_rpc(
16396            &self,
16397            message: <GlobalState as GlobalTool>::Request,
16398        ) -> <GlobalState as GlobalTool>::Response {
16399            self.requests.lock().unwrap().push(message.2.clone());
16400            self.global
16401                .receive_rpc(Tid::from_raw(self.thread.dettid.as_raw()), message)
16402                .await
16403        }
16404
16405        fn config(&self) -> &Config {
16406            self.config
16407        }
16408    }
16409
16410    #[reverie::tool]
16411    impl Guest<Detcore> for ExternalRegistrationGuest<'_> {
16412        type Memory = reverie::syscalls::LocalMemory;
16413        type Stack = ExternalRegistrationStack;
16414
16415        fn tid(&self) -> reverie::Pid {
16416            reverie::Pid::from_raw(self.thread.dettid.as_raw())
16417        }
16418
16419        fn pid(&self) -> reverie::Pid {
16420            reverie::Pid::from_raw(self.thread.detpid.unwrap().as_raw())
16421        }
16422
16423        fn ppid(&self) -> Option<reverie::Pid> {
16424            self.ppid
16425        }
16426
16427        fn runtime_private_memory_regions(&self) -> Option<reverie::RuntimePrivateMemory> {
16428            self.private_memory.clone()
16429        }
16430
16431        fn auxv(&self) -> reverie::Auxv {
16432            assert!(self.post_exec, "external registration must not read auxv");
16433            reverie::Auxv::from_entries([])
16434        }
16435
16436        fn memory(&self) -> Self::Memory {
16437            panic!("external registration must not access guest memory")
16438        }
16439
16440        fn thread_state_mut(&mut self) -> &mut crate::ThreadState<()> {
16441            &mut self.thread
16442        }
16443
16444        fn thread_state(&self) -> &crate::ThreadState<()> {
16445            &self.thread
16446        }
16447
16448        async fn regs(&mut self) -> libc::user_regs_struct {
16449            assert!(
16450                self.post_exec,
16451                "external registration must not read guest registers"
16452            );
16453            // SAFETY: every field is an integer register word. Post-exec hooks
16454            // may log these synthetic registers but never execute them.
16455            unsafe { std::mem::zeroed() }
16456        }
16457
16458        async fn stack(&mut self) -> Self::Stack {
16459            panic!("external registration must not use a guest stack")
16460        }
16461
16462        async fn daemonize(&mut self) {
16463            panic!("external registration must not daemonize")
16464        }
16465
16466        async fn inject<S: reverie::syscalls::SyscallInfo>(
16467            &mut self,
16468            _syscall: S,
16469        ) -> Result<i64, reverie::syscalls::Errno> {
16470            panic!("external registration must not inject a syscall")
16471        }
16472
16473        async fn tail_inject<S: reverie::syscalls::SyscallInfo>(
16474            &mut self,
16475            _syscall: S,
16476        ) -> reverie::Never {
16477            panic!("external registration unexpectedly retired its live parent")
16478        }
16479
16480        fn set_timer(&mut self, _schedule: reverie::TimerSchedule) -> Result<(), reverie::Error> {
16481            panic!("external registration must not set a timer")
16482        }
16483
16484        fn set_timer_precise(
16485            &mut self,
16486            _schedule: reverie::TimerSchedule,
16487        ) -> Result<(), reverie::Error> {
16488            panic!("external registration must not set a timer")
16489        }
16490
16491        fn read_clock(&mut self) -> Result<u64, reverie::Error> {
16492            panic!("external registration must not read a host clock")
16493        }
16494    }
16495
16496    struct RetirementGuest<'a> {
16497        global: &'a GlobalState,
16498        config: &'a Config,
16499        thread: crate::ThreadState<()>,
16500        requests: Mutex<Vec<GlobalRequest>>,
16501        retired: std::sync::atomic::AtomicBool,
16502        hold_private_memory_publication: bool,
16503    }
16504
16505    #[reverie::tool]
16506    impl GlobalRPC<GlobalState> for RetirementGuest<'_> {
16507        async fn send_rpc(
16508            &self,
16509            message: <GlobalState as GlobalTool>::Request,
16510        ) -> <GlobalState as GlobalTool>::Response {
16511            self.requests.lock().unwrap().push(message.2.clone());
16512            if self.hold_private_memory_publication
16513                && matches!(message.2, GlobalRequest::PublishRuntimePrivateMemory(_))
16514            {
16515                // DbtGuest::send_rpc waits for a stopped caller before
16516                // forwarding to its endpoint (reverie-dbt/src/lib.rs:
16517                // wait_if_caller_stopped). Model that supported suspension
16518                // at the pre-request publication, not ptrace's inline RPC.
16519                futures::future::pending::<()>().await;
16520            }
16521            self.global
16522                .receive_rpc(Tid::from_raw(self.thread.dettid.as_raw()), message)
16523                .await
16524        }
16525
16526        fn config(&self) -> &Config {
16527            self.config
16528        }
16529    }
16530
16531    #[reverie::tool]
16532    impl Guest<Detcore> for RetirementGuest<'_> {
16533        type Memory = reverie::syscalls::LocalMemory;
16534        type Stack = ExternalRegistrationStack;
16535
16536        fn tid(&self) -> reverie::Pid {
16537            reverie::Pid::from_raw(self.thread.dettid.as_raw())
16538        }
16539
16540        fn pid(&self) -> reverie::Pid {
16541            reverie::Pid::from_raw(self.thread.detpid.unwrap().as_raw())
16542        }
16543
16544        fn ppid(&self) -> Option<reverie::Pid> {
16545            None
16546        }
16547
16548        fn memory(&self) -> Self::Memory {
16549            panic!("external registration must not access guest memory")
16550        }
16551
16552        fn thread_state_mut(&mut self) -> &mut crate::ThreadState<()> {
16553            &mut self.thread
16554        }
16555
16556        fn thread_state(&self) -> &crate::ThreadState<()> {
16557            &self.thread
16558        }
16559
16560        fn runtime_private_memory_regions(&self) -> Option<reverie::RuntimePrivateMemory> {
16561            self.hold_private_memory_publication
16562                .then_some(reverie::RuntimePrivateMemory::Unavailable)
16563        }
16564
16565        async fn regs(&mut self) -> libc::user_regs_struct {
16566            panic!("external registration must not read guest registers")
16567        }
16568
16569        async fn stack(&mut self) -> Self::Stack {
16570            panic!("external registration must not use a guest stack")
16571        }
16572
16573        async fn daemonize(&mut self) {
16574            panic!("external registration must not daemonize")
16575        }
16576
16577        async fn inject<S: reverie::syscalls::SyscallInfo>(
16578            &mut self,
16579            _syscall: S,
16580        ) -> Result<i64, reverie::syscalls::Errno> {
16581            panic!("external registration must not inject a syscall")
16582        }
16583
16584        async fn retire_current_thread(&mut self) -> reverie::Never {
16585            self.retired
16586                .store(true, std::sync::atomic::Ordering::SeqCst);
16587            futures::future::pending().await
16588        }
16589
16590        async fn cancel_current_thread(&mut self) -> reverie::Never {
16591            panic!("scheduler retirement must not request group cancellation")
16592        }
16593
16594        async fn tail_inject<S: reverie::syscalls::SyscallInfo>(
16595            &mut self,
16596            _syscall: S,
16597        ) -> reverie::Never {
16598            panic!("external registration unexpectedly retired its live parent")
16599        }
16600
16601        fn set_timer(&mut self, _schedule: reverie::TimerSchedule) -> Result<(), reverie::Error> {
16602            panic!("external registration must not set a timer")
16603        }
16604
16605        fn set_timer_precise(
16606            &mut self,
16607            _schedule: reverie::TimerSchedule,
16608        ) -> Result<(), reverie::Error> {
16609            panic!("external registration must not set a timer")
16610        }
16611
16612        fn read_clock(&mut self) -> Result<u64, reverie::Error> {
16613            panic!("external registration must not read a host clock")
16614        }
16615    }
16616
16617    /// A supported asynchronous transport can suspend while publishing
16618    /// runtime memory before it forwards RequestResources. Cancellation must
16619    /// leave the descriptor export available to the real consuming exit hook.
16620    #[tokio::test]
16621    async fn a_cancelled_request_preserves_descriptor_exports_for_its_exit() {
16622        use reverie::Tool;
16623
16624        let config = Config {
16625            sequentialize_threads: true,
16626            ..Config::default()
16627        };
16628        let state = GlobalState::initialize(&config, false);
16629        let reader = DetTid::from_raw(3);
16630        let sender = DetTid::from_raw(5);
16631        let tool: Detcore = Detcore::new(Tid::from_raw(sender.as_raw()), &config);
16632        let mut thread = crate::ThreadState::new(sender, &config, ());
16633        thread.detpid = Some(sender);
16634        thread.passed_descriptors_out_unreported = true;
16635        {
16636            let mut sched = state.sched.lock().unwrap();
16637            sched.thread_tree.add_child(reader, reader, true);
16638            sched.thread_tree.add_child(reader, sender, true);
16639            for tid in [reader, sender] {
16640                sched.register_thread_for_test(tid);
16641                sched.install_test_exec_incarnation(tid, MmId::initial(tid));
16642            }
16643        }
16644        let mut guest = RetirementGuest {
16645            global: &state,
16646            config: &config,
16647            thread,
16648            requests: Mutex::new(Vec::new()),
16649            retired: std::sync::atomic::AtomicBool::new(false),
16650            hold_private_memory_publication: true,
16651        };
16652        {
16653            let request = super::resource_request(&mut guest, Resources::new(sender));
16654            let mut request = std::pin::pin!(request);
16655            assert!(futures::poll!(request.as_mut()).is_pending());
16656            // Dropping the actual request future models backend cancellation
16657            // while the pre-request RPC has not reached the global tool.
16658        }
16659        assert!(matches!(
16660            guest.requests.lock().unwrap().as_slice(),
16661            [GlobalRequest::PublishRuntimePrivateMemory(
16662                reverie::RuntimePrivateMemory::Unavailable
16663            )]
16664        ));
16665        assert!(
16666            state.sched.lock().unwrap().next_turns[&sender]
16667                .req
16668                .try_read()
16669                .is_none(),
16670            "no resource request reached the scheduler"
16671        );
16672        assert!(
16673            guest.thread.passed_descriptors_out_unreported,
16674            "cancellation before publication must retain the export for exit"
16675        );
16676        let thread = guest.thread.clone();
16677        tool.on_exit_thread(
16678            Tid::from_raw(sender.as_raw()),
16679            &guest,
16680            thread,
16681            reverie::ExitStatus::Signaled(reverie::Signal::SIGKILL, false),
16682        )
16683        .await
16684        .unwrap();
16685        {
16686            let sched = state.sched.lock().unwrap();
16687            assert!(!sched.next_turns.contains_key(&sender));
16688            assert_eq!(sched.turn, 0, "exit publication must not take a turn");
16689        }
16690        super::robust_exit_clock_tests::assert_reader_after_sender_exit(&state, reader, true).await;
16691    }
16692
16693    /// FUTEX_WAIT files its own RPC rather than a Resources request. Its
16694    /// admission must publish an earlier descriptor export before parking
16695    /// the sender and leaving a separate process as the only runnable reader.
16696    #[tokio::test]
16697    async fn a_direct_futex_wait_publishes_descriptor_exports_before_parking() {
16698        for exported in [false, true] {
16699            let config = Config {
16700                sequentialize_threads: true,
16701                ..Config::default()
16702            };
16703            let state = GlobalState::initialize(&config, false);
16704            let reader = DetTid::from_raw(3);
16705            let sender = DetTid::from_raw(5);
16706            let mut thread = crate::ThreadState::new(sender, &config, ());
16707            thread.detpid = Some(sender);
16708            thread.passed_descriptors_out_unreported = exported;
16709            {
16710                let mut sched = state.sched.lock().unwrap();
16711                sched.thread_tree.add_child(reader, reader, true);
16712                sched.thread_tree.add_child(reader, sender, true);
16713                for tid in [reader, sender] {
16714                    sched.register_thread_for_test(tid);
16715                    sched.install_test_exec_incarnation(tid, MmId::initial(tid));
16716                    sched.default_signal_state_for_test(tid);
16717                }
16718                sched.runqueue_push_back(sender);
16719            }
16720            let mut guest = RetirementGuest {
16721                global: &state,
16722                config: &config,
16723                thread,
16724                requests: Mutex::new(Vec::new()),
16725                retired: std::sync::atomic::AtomicBool::new(false),
16726                hold_private_memory_publication: false,
16727            };
16728            let futex = FutexID::private(MmId::initial(sender), 0x1000);
16729            {
16730                let request = super::futex_action(
16731                    &mut guest,
16732                    FutexAction::WaitRequest(
16733                        None,
16734                        Some(super::FutexSignalWatch {
16735                            unblocked: u64::MAX,
16736                            defers_default_stops: false,
16737                            pid: sender.as_raw(),
16738                            tid: sender.as_raw(),
16739                        }),
16740                    ),
16741                    &futex,
16742                    0,
16743                    u32::MAX,
16744                );
16745                let mut request = std::pin::pin!(request);
16746                assert!(futures::poll!(request.as_mut()).is_pending());
16747                {
16748                    let sched = state.sched.lock().unwrap();
16749                    assert_eq!(sched.blocked.futex_waiters[&futex].len(), 1);
16750                    assert_eq!(sched.futex_waiters_for_test(futex), [sender]);
16751                    assert!(!sched.run_queue.contains_tid(sender));
16752                    assert_eq!(sched.turn, 0, "export publication must not take a turn");
16753                }
16754                super::robust_exit_clock_tests::assert_reader_after_sender_exit(
16755                    &state, reader, exported,
16756                )
16757                .await;
16758            }
16759            assert!(matches!(
16760                guest.requests.lock().unwrap().as_slice(),
16761                [GlobalRequest::FutexAction(..)]
16762            ));
16763        }
16764    }
16765
16766    /// While the caller's own process handles SIGALRM, every SIGALRM send and
16767    /// producer arming the handlers check is refused with EPERM before the
16768    /// kernel: the guest injects nothing (`RetirementGuest` panics on an
16769    /// injection) and sends no RPC but the ledger's control message.
16770    #[tokio::test]
16771    async fn sigalrm_sends_and_producer_armings_are_refused_before_the_kernel() {
16772        use reverie::Tool;
16773        use reverie::syscalls;
16774
16775        let (config, state, tid, pid) = cancellation_test_state();
16776        install_test_registration(&state, tid, Ivar::new());
16777        {
16778            let mut sched = state.sched.lock().unwrap();
16779            sched.set_sigalrm_handled(pid, true);
16780            sched.set_running_for_test(tid);
16781        }
16782        let tool: Detcore = Detcore::new(Tid::from_raw(tid.as_raw()), &config);
16783        let mut thread = tool.init_thread_state(Tid::from_raw(tid.as_raw()), None);
16784        thread.detpid = Some(pid);
16785        let mut guest = RetirementGuest {
16786            global: &state,
16787            config: &config,
16788            thread,
16789            requests: Mutex::new(Vec::new()),
16790            retired: std::sync::atomic::AtomicBool::new(false),
16791            hold_private_memory_publication: false,
16792        };
16793        let (raw_pid, raw_tid) = (pid.as_raw(), tid.as_raw());
16794        let eperm = |result: Result<i64, reverie::Error>, what: &str| {
16795            assert!(
16796                matches!(result, Err(reverie::Error::Errno(errno)) if errno == reverie::Errno::EPERM),
16797                "{what}: {result:?}"
16798            );
16799        };
16800
16801        let sigalrm = libc::SIGALRM;
16802        let tgkill = syscalls::Tgkill::new()
16803            .with_tgid(raw_pid)
16804            .with_tid(raw_tid)
16805            .with_sig(sigalrm);
16806        eperm(tool.handle_tgkill(&mut guest, tgkill).await, "tgkill");
16807        let tkill = syscalls::Tkill::new().with_tid(raw_tid).with_sig(sigalrm);
16808        eperm(tool.handle_tkill(&mut guest, tkill).await, "tkill");
16809        let kill = syscalls::Kill::new().with_pid(raw_pid).with_sig(sigalrm);
16810        eperm(tool.handle_kill(&mut guest, kill).await, "kill");
16811        let queue = syscalls::RtSigqueueinfo::new()
16812            .with_tgid(raw_pid)
16813            .with_sig(sigalrm);
16814        eperm(
16815            tool.handle_rt_sigqueueinfo(&mut guest, queue).await,
16816            "rt_sigqueueinfo",
16817        );
16818        let tgqueue = syscalls::RtTgsigqueueinfo::new()
16819            .with_tgid(raw_pid)
16820            .with_tid(raw_tid)
16821            .with_sig(sigalrm);
16822        eperm(
16823            tool.handle_rt_tgsigqueueinfo(&mut guest, tgqueue).await,
16824            "rt_tgsigqueueinfo",
16825        );
16826        let pdeathsig = syscalls::Prctl::new()
16827            .with_option(libc::PR_SET_PDEATHSIG)
16828            .with_arg2(sigalrm as libc::c_ulong);
16829        eperm(
16830            tool.handle_prctl(&mut guest, pdeathsig).await,
16831            "PR_SET_PDEATHSIG",
16832        );
16833        // A NULL sigevent names SIGALRM; the id pointer is never written.
16834        let timer = syscalls::TimerCreate::new()
16835            .with_clockid(syscalls::ClockId::CLOCK_MONOTONIC)
16836            .with_timerid(reverie::syscalls::AddrMut::from_raw(0x1000));
16837        eperm(
16838            tool.handle_timer_create(&mut guest, timer).await,
16839            "timer_create",
16840        );
16841
16842        let requests = guest.requests.lock().unwrap();
16843        assert_eq!(requests.len(), 7);
16844        assert!(
16845            requests
16846                .iter()
16847                .all(|request| matches!(request, GlobalRequest::Sigalrm(_))),
16848            "{requests:?}"
16849        );
16850    }
16851
16852    /// In a process that handles SIGALRM, signal phase 1's gate refuses what
16853    /// its table does not place, with EOPNOTSUPP, and allows what it places,
16854    /// with no RPC either way.
16855    #[tokio::test]
16856    async fn sigalrm_phase1_gate_refuses_unplaced_calls() {
16857        use reverie::Tool;
16858        use reverie::syscalls::Syscall;
16859        use reverie::syscalls::SyscallArgs;
16860        use reverie::syscalls::Sysno;
16861
16862        let (config, state, tid, pid) = cancellation_test_state();
16863        install_test_registration(&state, tid, Ivar::new());
16864        let tool: Detcore = Detcore::new(Tid::from_raw(tid.as_raw()), &config);
16865        let mut thread = tool.init_thread_state(Tid::from_raw(tid.as_raw()), None);
16866        thread.detpid = Some(pid);
16867        thread.sigalrm_handled = true;
16868        let guest = RetirementGuest {
16869            global: &state,
16870            config: &config,
16871            thread,
16872            requests: Mutex::new(Vec::new()),
16873            retired: std::sync::atomic::AtomicBool::new(false),
16874            hold_private_memory_publication: false,
16875        };
16876        let raw = |sysno, a0, a1| Syscall::from_raw(sysno, SyscallArgs::new(a0, a1, 0, 0, 0, 0));
16877
16878        for (sysno, a0, a1) in [
16879            (Sysno::rt_sigsuspend, 0x1000, 8),
16880            (Sysno::signalfd4, usize::MAX, 0x1000),
16881            (Sysno::epoll_pwait2, 3, 0),
16882            (Sysno::nanosleep, 0x1000, 0),
16883            (Sysno::fcntl, 3, libc::F_SETLKW as usize),
16884            (Sysno::execve, 0x1000, 0),
16885            // stdin, and a descriptor Detcore does not model.
16886            (Sysno::read, 0, 0x1000),
16887            (Sysno::read, 77, 0x1000),
16888            // stderr, which may be a socket Detcore labels Regular.
16889            (Sysno::close, 2, 0),
16890            // No ioctl is placed.
16891            (Sysno::ioctl, 1, libc::TCGETS as usize),
16892            // Buffered credentials and native process relations remain outside
16893            // the four scalar identity queries.
16894            (Sysno::getresuid, 0x1000, 0x2000),
16895            (Sysno::getresgid, 0x1000, 0x2000),
16896            (Sysno::getgroups, 1, 0x1000),
16897            (Sysno::getppid, 0, 0),
16898            (Sysno::getpgid, 0, 0),
16899            (Sysno::getsid, 0, 0),
16900        ] {
16901            assert_eq!(
16902                tool.sigalrm_phase1_gate(&guest, raw(sysno, a0, a1)),
16903                Some(nix::errno::Errno::EOPNOTSUPP),
16904                "{sysno}"
16905            );
16906        }
16907        for (sysno, a0, a1) in [
16908            (Sysno::getpid, 0, 0),
16909            (Sysno::write, 1, 0x1000),
16910            (Sysno::getuid, 0, 0),
16911            (Sysno::geteuid, 0, 0),
16912            (Sysno::getgid, 0, 0),
16913            (Sysno::getegid, 0, 0),
16914        ] {
16915            assert_eq!(
16916                tool.sigalrm_phase1_gate(&guest, raw(sysno, a0, a1)),
16917                None,
16918                "{sysno}"
16919            );
16920        }
16921        assert!(guest.requests.lock().unwrap().is_empty(), "no RPC");
16922    }
16923
16924    /// While any process handles SIGALRM, a file lease is refused before the
16925    /// kernel: its break would make another process's open wait with no
16926    /// scheduler admission. Releasing a lease is not refused.
16927    #[tokio::test]
16928    async fn a_file_lease_is_refused_while_a_guest_handles_sigalrm() {
16929        use reverie::Tool;
16930        use reverie::syscalls;
16931        use reverie::syscalls::FcntlCmd;
16932
16933        let (config, state, tid, pid) = cancellation_test_state();
16934        install_test_registration(&state, tid, Ivar::new());
16935        {
16936            let mut sched = state.sched.lock().unwrap();
16937            sched.set_sigalrm_handled(DetPid::from_raw(99), true);
16938            sched.set_running_for_test(tid);
16939        }
16940        let tool: Detcore = Detcore::new(Tid::from_raw(tid.as_raw()), &config);
16941        let mut thread = tool.init_thread_state(Tid::from_raw(tid.as_raw()), None);
16942        thread.detpid = Some(pid);
16943        let mut guest = RetirementGuest {
16944            global: &state,
16945            config: &config,
16946            thread,
16947            requests: Mutex::new(Vec::new()),
16948            retired: std::sync::atomic::AtomicBool::new(false),
16949            hold_private_memory_publication: false,
16950        };
16951        for lease in [libc::F_RDLCK, libc::F_WRLCK] {
16952            let fcntl = syscalls::Fcntl::new()
16953                .with_fd(3)
16954                .with_cmd(FcntlCmd::F_SETLEASE(lease));
16955            let result = tool.handle_fcntl(&mut guest, fcntl).await;
16956            assert!(
16957                matches!(result, Err(reverie::Error::Errno(errno)) if errno == reverie::Errno::EPERM),
16958                "{lease}: {result:?}"
16959            );
16960        }
16961        let requests = guest.requests.lock().unwrap();
16962        assert_eq!(requests.len(), 2);
16963        assert!(requests.iter().all(|request| matches!(
16964            request,
16965            GlobalRequest::Sigalrm(super::SigalrmControl::ArmProducer)
16966        )));
16967    }
16968
16969    /// A committed SIGKILL victim's late ordinary request is never answered
16970    /// (https://github.com/rrnewton/hermit/issues/3994, design revision 4,
16971    /// R1). The two late states are a background operation's rejoin posted
16972    /// after the commit (`BlockedExternalContinue`, through
16973    /// `resource_request`, as `helpers.rs` posts it once the operation's
16974    /// outcome is in) and a plain request. In both, the callback stays
16975    /// pending: no response, no injection (`RetirementGuest` panics on one),
16976    /// and no `retire_current_thread`, so the backend's terminal owner
16977    /// consumes the real death. Its consuming cleanup is still answered.
16978    #[tokio::test]
16979    async fn a_sigkilled_threads_late_ordinary_request_stays_unanswered() {
16980        use std::sync::atomic::Ordering;
16981
16982        use reverie::Tool;
16983
16984        let config = Config {
16985            sequentialize_threads: true,
16986            ..Config::default()
16987        };
16988        let [root, sender, victim] = [1, 2, 3].map(DetPid::from_raw);
16989        let commit = |state: &GlobalState| {
16990            let mut sched = state.sched.lock().unwrap();
16991            let token = sched.reserve_sigkill(sender, victim, 0).unwrap().token;
16992            assert_eq!(sched.commit_sigkill(token, true).len(), 1);
16993            assert!(sched.is_unaccounted_sigkill_victim(victim));
16994        };
16995        // "filed": the rejoin is admitted and waiting for its turn when the
16996        // commit happens. "rejoin" and "plain" arrive after the commit.
16997        for late in ["filed", "rejoin", "plain"] {
16998            let state = GlobalState::initialize(&config, false);
16999            {
17000                let mut sched = state.sched.lock().unwrap();
17001                sched.thread_tree.add_child(root, root, true);
17002                sched.thread_tree.add_child(root, sender, true);
17003                sched.thread_tree.add_child(root, victim, true);
17004            }
17005            for tid in [root, sender, victim] {
17006                install_test_registration(&state, tid, Ivar::new());
17007                state
17008                    .sched
17009                    .lock()
17010                    .unwrap()
17011                    .record_thread_mm(tid, MmId::initial(tid));
17012            }
17013            if late != "filed" {
17014                commit(&state);
17015            }
17016            let tool: Detcore = Detcore::new(Tid::from_raw(victim.as_raw()), &config);
17017            let mut thread = tool.init_thread_state(Tid::from_raw(victim.as_raw()), None);
17018            thread.detpid = Some(victim);
17019            let mut guest = RetirementGuest {
17020                global: &state,
17021                config: &config,
17022                thread,
17023                requests: Mutex::new(Vec::new()),
17024                retired: std::sync::atomic::AtomicBool::new(false),
17025                hold_private_memory_publication: false,
17026            };
17027            {
17028                let mut rejoin = Resources::new(victim);
17029                rejoin.insert(
17030                    ResourceID::BlockedExternalContinue(ExternalOpId::new(victim, 7)),
17031                    Permission::RW,
17032                );
17033                let pending = if late == "plain" {
17034                    let mut call = std::pin::pin!(super::send_and_update_time(
17035                        &mut guest,
17036                        GlobalRequest::GlobalTimeLowerBound
17037                    ));
17038                    futures::poll!(call.as_mut()).is_pending()
17039                } else {
17040                    let mut call = std::pin::pin!(super::resource_request(&mut guest, rejoin));
17041                    let mut pending = futures::poll!(call.as_mut()).is_pending();
17042                    if late == "filed" {
17043                        commit(&state);
17044                        for _ in 0..8 {
17045                            tokio::task::yield_now().await;
17046                            pending &= futures::poll!(call.as_mut()).is_pending();
17047                        }
17048                    }
17049                    pending
17050                };
17051                assert!(pending, "{late}: the callback resumed");
17052            }
17053            assert!(
17054                !guest.retired.load(Ordering::SeqCst),
17055                "{late}: retire_current_thread tail-injects exit"
17056            );
17057            assert_eq!(guest.requests.lock().unwrap().len(), 1, "{late}");
17058            // The consuming cleanup of the same victim is admitted and answered.
17059            let (_, ack) = state
17060                .receive_rpc(
17061                    Tid::from_raw(victim.as_raw()),
17062                    (
17063                        guest.thread.thread_logical_time.clone(),
17064                        guest.thread.mm_id,
17065                        GlobalRequest::RobustListWakes(Vec::new()),
17066                    ),
17067                )
17068                .await;
17069            assert_eq!(ack, GlobalResponse::RobustListWakes(Vec::new()), "{late}");
17070            // Requests answered before ordinary admission get no answer either:
17071            // a SIGALRM ledger change is refused, and a loss notice is
17072            // recorded but not answered.
17073            for request in [
17074                GlobalRequest::Sigalrm(super::SigalrmControl::ArmProducer),
17075                GlobalRequest::RecordDeterminismLoss("victim notice".to_string()),
17076            ] {
17077                let (_, response) = state
17078                    .receive_rpc(
17079                        Tid::from_raw(victim.as_raw()),
17080                        (
17081                            guest.thread.thread_logical_time.clone(),
17082                            guest.thread.mm_id,
17083                            request,
17084                        ),
17085                    )
17086                    .await;
17087                assert_eq!(response, GlobalResponse::Retired, "{late}");
17088            }
17089        }
17090    }
17091
17092    /// A victim's request admitted before the commit whose response is
17093    /// granted, and observed by the handler, only after the commit reaches the
17094    /// handler's single response exit, which must not answer it
17095    /// (https://github.com/rrnewton/hermit/issues/3994, R1). Without that exit
17096    /// guard the granted response would resume the callback.
17097    #[tokio::test]
17098    async fn a_victims_request_granted_across_the_commit_stays_unanswered() {
17099        use reverie::Tool;
17100
17101        let config = Config {
17102            sequentialize_threads: true,
17103            ..Config::default()
17104        };
17105        let [root, sender, victim] = [1, 2, 3].map(DetPid::from_raw);
17106        let state = GlobalState::initialize(&config, false);
17107        {
17108            let mut sched = state.sched.lock().unwrap();
17109            sched.thread_tree.add_child(root, root, true);
17110            sched.thread_tree.add_child(root, sender, true);
17111            sched.thread_tree.add_child(root, victim, true);
17112        }
17113        for tid in [root, sender, victim] {
17114            install_test_registration(&state, tid, Ivar::new());
17115            state
17116                .sched
17117                .lock()
17118                .unwrap()
17119                .record_thread_mm(tid, MmId::initial(tid));
17120        }
17121        let tool: Detcore = Detcore::new(Tid::from_raw(victim.as_raw()), &config);
17122        let mut thread = tool.init_thread_state(Tid::from_raw(victim.as_raw()), None);
17123        thread.detpid = Some(victim);
17124        let mut guest = RetirementGuest {
17125            global: &state,
17126            config: &config,
17127            thread,
17128            requests: Mutex::new(Vec::new()),
17129            retired: std::sync::atomic::AtomicBool::new(false),
17130            hold_private_memory_publication: false,
17131        };
17132        let mut rejoin = Resources::new(victim);
17133        rejoin.insert(
17134            ResourceID::BlockedExternalContinue(ExternalOpId::new(victim, 7)),
17135            Permission::RW,
17136        );
17137        let response = state.sched.lock().unwrap().next_turns[&victim].resp.clone();
17138        {
17139            let mut call = std::pin::pin!(super::resource_request(&mut guest, rejoin));
17140            // Admitted and filed: the handler waits for the grant.
17141            assert!(futures::poll!(call.as_mut()).is_pending());
17142            // The scheduler grants it, and only then does the commit retire the
17143            // victim, before the handler has looked at the grant.
17144            response.put(SchedResponse::Go(None));
17145            {
17146                let mut sched = state.sched.lock().unwrap();
17147                let token = sched.reserve_sigkill(sender, victim, 0).unwrap().token;
17148                assert_eq!(sched.commit_sigkill(token, true).len(), 1);
17149            }
17150            for _ in 0..8 {
17151                tokio::task::yield_now().await;
17152                assert!(
17153                    futures::poll!(call.as_mut()).is_pending(),
17154                    "the granted response resumed a SIGKILLed thread's callback"
17155                );
17156            }
17157        }
17158        assert!(!guest.retired.load(std::sync::atomic::Ordering::SeqCst));
17159    }
17160
17161    /// Runs the calling test's body in a child copy of this test binary, and
17162    /// requires the child to exit with the policy-refusal status and print
17163    /// `expected`. Returns whether this is the child, which runs the body.
17164    /// Used where the body ends the process by refusing.
17165    fn in_child_expecting_refusal(name: &str, expected: &str) -> bool {
17166        const CHILD: &str = "HERMIT_TEST_REFUSAL_CHILD";
17167        let test = format!("{}::{name}", module_path!().split_once("::").unwrap().1);
17168        if std::env::var(CHILD).as_deref() == Ok(test.as_str()) {
17169            return true;
17170        }
17171        let output = std::process::Command::new(std::env::current_exe().unwrap())
17172            .args(["--exact", &test, "--nocapture"])
17173            .env(CHILD, &test)
17174            .output()
17175            .unwrap();
17176        let stderr = String::from_utf8_lossy(&output.stderr);
17177        assert_eq!(
17178            output.status.code(),
17179            Some(detcore_model::HERMIT_POLICY_REFUSAL_EXIT),
17180            "{test}: the child was not refused\n{stderr}"
17181        );
17182        assert!(
17183            stderr.contains(expected),
17184            "{test}: missing {expected:?}\n{stderr}"
17185        );
17186        false
17187    }
17188
17189    /// Sends the DBT SIGKILL notice for process 3, from process 1, with a
17190    /// 20 ms fence valve. Process 3's thread is registered on the host as
17191    /// `victim`'s, when given, and otherwise only modelled.
17192    async fn send_dbt_sigkill_notice(victim: Option<i32>) {
17193        let config = Config {
17194            sequentialize_threads: true,
17195            ..Config::default()
17196        }
17197        .with_backend(|backend| *backend = reverie::BackendCapabilities::DBT);
17198        let state = GlobalState::initialize(&config, false);
17199        let [root, target] = [1, 3].map(DetPid::from_raw);
17200        {
17201            let mut sched = state.sched.lock().unwrap();
17202            sched.thread_tree.add_child(root, root, true);
17203            sched.thread_tree.add_child(root, target, true);
17204        }
17205        for tid in [root, target] {
17206            install_test_registration(&state, tid, Ivar::new());
17207            state
17208                .sched
17209                .lock()
17210                .unwrap()
17211                .record_thread_mm(tid, MmId::initial(tid));
17212        }
17213        {
17214            let mut sched = state.sched.lock().unwrap();
17215            sched.sigkill_fence_valve = std::time::Duration::from_millis(20);
17216            if let Some(pid) = victim {
17217                sched
17218                    .register_physical_thread(target, MmId::initial(target), pid, pid)
17219                    .unwrap();
17220            }
17221        }
17222        let rpc = state.receive_rpc(
17223            Tid::from_raw(root.as_raw()),
17224            (
17225                crate::ThreadState::<()>::new(root, &config, ())
17226                    .thread_logical_time
17227                    .clone(),
17228                MmId::initial(root),
17229                GlobalRequest::NotifySignalPending(
17230                    target,
17231                    SigWrapper(libc::SIGKILL),
17232                    Some(target),
17233                    super::SignalScope::Process,
17234                ),
17235            ),
17236        );
17237        let _ = tokio::time::timeout(std::time::Duration::from_secs(10), rpc).await;
17238    }
17239
17240    /// On DBT, a SIGKILL notice whose victim is not gone within the fence
17241    /// valve ends the run with the named refusal before the sender is
17242    /// answered (shortened valve; Claude re-check of
17243    /// https://github.com/rrnewton/hermit/pull/4423, N3).
17244    #[tokio::test]
17245    async fn a_dbt_sigkill_notice_past_its_valve_refuses_before_answering() {
17246        if !in_child_expecting_refusal(
17247            "a_dbt_sigkill_notice_past_its_valve_refuses_before_answering",
17248            "cannot be held until it is gone: it was not gone within",
17249        ) {
17250            return;
17251        }
17252        // The victim outlives the valve by far, and dies with this child
17253        // (PR_SET_PDEATHSIG) rather than outliving it (Claude re-check of
17254        // 4423 at 858d81ec, N5). Its stdio is not this child's, which the
17255        // parent reads to the end.
17256        let mut victim = std::process::Command::new("sleep");
17257        victim
17258            .arg("60")
17259            .stdin(std::process::Stdio::null())
17260            .stdout(std::process::Stdio::null())
17261            .stderr(std::process::Stdio::null());
17262        // SAFETY: prctl is async-signal-safe and touches no shared state.
17263        unsafe {
17264            use std::os::unix::process::CommandExt;
17265            victim.pre_exec(|| {
17266                if libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL) == 0 {
17267                    Ok(())
17268                } else {
17269                    Err(std::io::Error::last_os_error())
17270                }
17271            });
17272        }
17273        let mut victim = victim.spawn().unwrap();
17274        send_dbt_sigkill_notice(Some(victim.id() as i32)).await;
17275        let _ = victim.kill();
17276        let _ = victim.wait();
17277        panic!("the SIGKILL notice answered its sender although the victim was not gone");
17278    }
17279
17280    /// On DBT, a SIGKILL notice for a victim with a live thread but no thread
17281    /// registered on the host ends the run with the named refusal before the
17282    /// sender is answered, never left unfenced (Claude review of
17283    /// https://github.com/rrnewton/hermit/pull/4423, F2: the end-to-end form
17284    /// of this case became a passing run with
17285    /// https://github.com/rrnewton/hermit/issues/4411's fix).
17286    #[tokio::test]
17287    async fn a_dbt_sigkill_notice_with_no_host_registration_refuses_before_answering() {
17288        if !in_child_expecting_refusal(
17289            "a_dbt_sigkill_notice_with_no_host_registration_refuses_before_answering",
17290            "nor any other thread of it has a registered host process",
17291        ) {
17292            return;
17293        }
17294        send_dbt_sigkill_notice(None).await;
17295        panic!("the SIGKILL notice answered its sender without a fence");
17296    }
17297
17298    /// A returning sender's wait for its victims is bounded by the valve: a
17299    /// commit whose victim never dies returns once the valve expires and
17300    /// refuses the run by name, instead of hanging the sender (shortened
17301    /// valve; the 30 s production bound is the same field).
17302    #[tokio::test]
17303    async fn a_sigkill_commit_wait_past_its_valve_refuses_the_run() {
17304        let config = Config {
17305            sequentialize_threads: true,
17306            ..Config::default()
17307        };
17308        let state = GlobalState::initialize(&config, false);
17309        let [root, sender, target] = [1, 2, 3].map(DetPid::from_raw);
17310        {
17311            let mut sched = state.sched.lock().unwrap();
17312            sched.thread_tree.add_child(root, root, true);
17313            sched.thread_tree.add_child(root, sender, true);
17314            sched.thread_tree.add_child(root, target, true);
17315        }
17316        for tid in [root, sender, target] {
17317            install_test_registration(&state, tid, Ivar::new());
17318            state
17319                .sched
17320                .lock()
17321                .unwrap()
17322                .record_thread_mm(tid, MmId::initial(tid));
17323        }
17324        let token = {
17325            let mut sched = state.sched.lock().unwrap();
17326            sched.sigkill_fence_valve = std::time::Duration::from_millis(20);
17327            sched.reserve_sigkill(sender, target, 0).unwrap().token
17328        };
17329        let rpc = state.receive_rpc(
17330            Tid::from_raw(sender.as_raw()),
17331            (
17332                crate::ThreadState::<()>::new(sender, &config, ())
17333                    .thread_logical_time
17334                    .clone(),
17335                MmId::initial(sender),
17336                GlobalRequest::CommitSigkill(token, true),
17337            ),
17338        );
17339        let (_, response) = tokio::time::timeout(std::time::Duration::from_secs(10), rpc)
17340            .await
17341            .expect("the commit's wait outlived its valve");
17342        assert_eq!(response, GlobalResponse::CommitSigkill(()));
17343        let sched = state.sched.lock().unwrap();
17344        let refusal = sched
17345            .sigkill_fence_refusal_for_test()
17346            .expect("no refusal recorded");
17347        assert!(refusal.waited >= std::time::Duration::from_millis(20));
17348        assert_eq!(refusal.unaccounted, [target]);
17349    }
17350
17351    /// Two victims of one SIGKILL send their owner-death batches through this
17352    /// real RPC handler, as authenticated registered victims, in either order,
17353    /// with a step2 between the receipts (3994 design revision 4, R2). Each
17354    /// batch is acknowledged with no counts and runs only when the cohort is
17355    /// complete, so the waiters woken, the run-queue order and the fuzz arm's
17356    /// next draw do not depend on the order, under FIFO and under the fuzz
17357    /// arm. This covers the retention protocol, not a SIGKILL producer: a
17358    /// SIGKILL victim stages no batch today
17359    /// (<https://github.com/rrnewton/hermit/issues/2082>).
17360    #[tokio::test]
17361    async fn victims_owner_death_wakes_through_the_handler_do_not_depend_on_receipt_order() {
17362        async fn run(fuzz: bool, reverse: bool) -> (Vec<Vec<DetTid>>, Vec<DetTid>, u64) {
17363            let config = Config {
17364                sequentialize_threads: true,
17365                fuzz_futexes: fuzz,
17366                ..Config::default()
17367            };
17368            let state = GlobalState::initialize(&config, false);
17369            let [root, sender, target, worker] = [1, 2, 3, 4].map(DetPid::from_raw);
17370            let mm = MmId::initial(target);
17371            let futexes = [0x404100, 0x404200].map(|addr| FutexID::private(mm, addr));
17372            {
17373                let mut sched = state.sched.lock().unwrap();
17374                sched.thread_tree.add_child(root, root, true);
17375                sched.thread_tree.add_child(root, sender, true);
17376                sched.thread_tree.add_child(root, target, true);
17377                sched.thread_tree.add_child(target, worker, false);
17378            }
17379            for tid in [root, sender, target, worker] {
17380                install_test_registration(&state, tid, Ivar::new());
17381                state
17382                    .sched
17383                    .lock()
17384                    .unwrap()
17385                    .record_thread_mm(tid, MmId::initial(tid));
17386            }
17387            for (futex, waiters) in futexes.iter().zip([[11, 12, 13], [14, 15, 16]]) {
17388                for raw in waiters {
17389                    let tid = DetTid::from_raw(raw);
17390                    let mut sched = state.sched.lock().unwrap();
17391                    sched.thread_tree.add_child(root, tid, true);
17392                    sched.next_turns.insert(
17393                        tid,
17394                        ThreadNextTurn {
17395                            dettid: tid,
17396                            child_tid_addr: 0,
17397                            req: Ivar::new(),
17398                            resp: Ivar::new(),
17399                            protocol: Default::default(),
17400                        },
17401                    );
17402                    sched.priorities.insert(tid, DEFAULT_PRIORITY);
17403                    sched.sleep_futex_waiter(&tid, *futex, None, u32::MAX, None);
17404                }
17405            }
17406            {
17407                let mut sched = state.sched.lock().unwrap();
17408                let token = sched.reserve_sigkill(sender, target, 0).unwrap().token;
17409                assert_eq!(sched.commit_sigkill(token, true).len(), 2);
17410            }
17411            let mut receipts = [(target, futexes[0]), (worker, futexes[1])];
17412            if reverse {
17413                receipts.reverse();
17414            }
17415            for (victim, futex) in receipts {
17416                let (_, ack) = state
17417                    .receive_rpc(
17418                        Tid::from_raw(victim.as_raw()),
17419                        (
17420                            crate::ThreadState::<()>::new(victim, &config, ())
17421                                .thread_logical_time
17422                                .clone(),
17423                            MmId::initial(victim),
17424                            GlobalRequest::RobustListWakes(vec![(victim, futex)]),
17425                        ),
17426                    )
17427                    .await;
17428                assert_eq!(
17429                    ack,
17430                    GlobalResponse::RobustListWakes(Vec::new()),
17431                    "a victim's batch is retained, not run at receipt"
17432                );
17433                state.sched.lock().unwrap().step2_drain_prefix_for_test();
17434                // The victim's real consuming deregistration, and a duplicate:
17435                // the first is accounted once, the second changes nothing.
17436                let deregistration = ThreadDeregistration {
17437                    dettid: victim,
17438                    detpid: target,
17439                    mm: MmId::initial(victim),
17440                    thread_start_entered: true,
17441                    vfork_origin: None,
17442                    timeslice_stats: TimesliceStats {
17443                        count: 1,
17444                        sum_ns: 10,
17445                        min_ns: 10,
17446                        max_ns: 10,
17447                    },
17448                    syscall_count: 3,
17449                    syscall_histogram: Default::default(),
17450                    chaos_epochs: Vec::new(),
17451                    killed_by_group_signal: false,
17452                    passed_descriptors_out: false,
17453                };
17454                for _ in 0..2 {
17455                    let (_, done) = state
17456                        .receive_rpc(
17457                            Tid::from_raw(victim.as_raw()),
17458                            (
17459                                crate::ThreadState::<()>::new(victim, &config, ())
17460                                    .thread_logical_time
17461                                    .clone(),
17462                                MmId::initial(victim),
17463                                GlobalRequest::DeregisterThread(deregistration.clone()),
17464                            ),
17465                        )
17466                        .await;
17467                    assert_eq!(done, GlobalResponse::DeregisterThread(()));
17468                }
17469                state.sched.lock().unwrap().step2_drain_prefix_for_test();
17470            }
17471            assert!(
17472                state.sched.lock().unwrap().sigkill_fence_holds(),
17473                "the victim's exit is not yet published to its parent"
17474            );
17475            <GlobalState as GlobalTool>::on_backend_process_exited(&state, target.as_raw());
17476            state.sched.lock().unwrap().step2_drain_prefix_for_test();
17477            {
17478                let sched = state.sched.lock().unwrap();
17479                assert!(
17480                    !sched.sigkill_fence_holds(),
17481                    "every victim is accounted and published"
17482                );
17483                for victim in [target, worker] {
17484                    assert_eq!(
17485                        sched
17486                            .timeslice_stats_for_test(victim)
17487                            .map(|stats| stats.count),
17488                        Some(1),
17489                        "victim {victim} accounted exactly once"
17490                    );
17491                }
17492            }
17493            let mut sched = state.sched.lock().unwrap();
17494            let still_waiting = futexes
17495                .iter()
17496                .map(|futex| sched.futex_waiters_for_test(*futex))
17497                .collect();
17498            let queue = sched.run_queue_for_test();
17499            (still_waiting, queue, sched.next_fuzz_draw_for_test())
17500        }
17501        for fuzz in [false, true] {
17502            let forward = run(fuzz, false).await;
17503            assert_eq!(forward.0.iter().map(Vec::len).collect::<Vec<_>>(), [2, 2]);
17504            assert_eq!(forward, run(fuzz, true).await, "fuzz={fuzz}");
17505        }
17506    }
17507
17508    /// A victim with a filed request, whose real consuming deregistration
17509    /// reaches the handler before the sender's commit (an early receipt, held)
17510    /// or after it. Both readiness orders give the same retirement: the
17511    /// callback stays pending with nothing injected (`RetirementGuest` panics
17512    /// on an injection) and is not retired, the cleanup is accounted once, the
17513    /// fence is released, and the victim's process is waitable
17514    /// (https://github.com/rrnewton/hermit/issues/3994, R1).
17515    #[tokio::test]
17516    async fn a_victims_receipt_before_or_after_the_commit_retires_it_the_same_way() {
17517        use reverie::Tool;
17518
17519        let config = Config {
17520            sequentialize_threads: true,
17521            ..Config::default()
17522        };
17523        let [root, sender, victim] = [1, 2, 3].map(DetPid::from_raw);
17524        for early in [true, false] {
17525            let state = GlobalState::initialize(&config, false);
17526            {
17527                let mut sched = state.sched.lock().unwrap();
17528                sched.thread_tree.add_child(root, root, true);
17529                sched.thread_tree.add_child(root, sender, true);
17530                sched.thread_tree.add_child(root, victim, true);
17531            }
17532            for tid in [root, sender, victim] {
17533                install_test_registration(&state, tid, Ivar::new());
17534                state
17535                    .sched
17536                    .lock()
17537                    .unwrap()
17538                    .record_thread_mm(tid, MmId::initial(tid));
17539            }
17540            let tool: Detcore = Detcore::new(Tid::from_raw(victim.as_raw()), &config);
17541            let mut thread = tool.init_thread_state(Tid::from_raw(victim.as_raw()), None);
17542            thread.detpid = Some(victim);
17543            let clock = thread.thread_logical_time.clone();
17544            let mut guest = RetirementGuest {
17545                global: &state,
17546                config: &config,
17547                thread,
17548                requests: Mutex::new(Vec::new()),
17549                retired: std::sync::atomic::AtomicBool::new(false),
17550                hold_private_memory_publication: false,
17551            };
17552            let token = state
17553                .sched
17554                .lock()
17555                .unwrap()
17556                .reserve_sigkill(sender, victim, 0)
17557                .unwrap()
17558                .token;
17559            let deregister = || {
17560                state.receive_rpc(
17561                    Tid::from_raw(victim.as_raw()),
17562                    (
17563                        clock.clone(),
17564                        MmId::initial(victim),
17565                        GlobalRequest::DeregisterThread(ThreadDeregistration {
17566                            dettid: victim,
17567                            detpid: victim,
17568                            mm: MmId::initial(victim),
17569                            thread_start_entered: true,
17570                            vfork_origin: None,
17571                            timeslice_stats: TimesliceStats {
17572                                count: 1,
17573                                sum_ns: 10,
17574                                min_ns: 10,
17575                                max_ns: 10,
17576                            },
17577                            syscall_count: 3,
17578                            syscall_histogram: Default::default(),
17579                            chaos_epochs: Vec::new(),
17580                            killed_by_group_signal: false,
17581                            passed_descriptors_out: false,
17582                        }),
17583                    ),
17584                )
17585            };
17586            {
17587                let mut rejoin = Resources::new(victim);
17588                rejoin.insert(
17589                    ResourceID::BlockedExternalContinue(ExternalOpId::new(victim, 7)),
17590                    Permission::RW,
17591                );
17592                let mut call = std::pin::pin!(super::resource_request(&mut guest, rejoin));
17593                assert!(futures::poll!(call.as_mut()).is_pending());
17594                if early {
17595                    assert_eq!(deregister().await.1, GlobalResponse::DeregisterThread(()));
17596                }
17597                state.sched.lock().unwrap().commit_sigkill(token, true);
17598                if !early {
17599                    assert_eq!(deregister().await.1, GlobalResponse::DeregisterThread(()));
17600                }
17601                for _ in 0..4 {
17602                    tokio::task::yield_now().await;
17603                    assert!(futures::poll!(call.as_mut()).is_pending(), "early={early}");
17604                }
17605            }
17606            assert!(!guest.retired.load(std::sync::atomic::Ordering::SeqCst));
17607            assert!(
17608                state.sched.lock().unwrap().sigkill_fence_holds(),
17609                "early={early}: the exit is not yet published"
17610            );
17611            <GlobalState as GlobalTool>::on_backend_process_exited(&state, victim.as_raw());
17612            let sched = state.sched.lock().unwrap();
17613            assert!(!sched.sigkill_fence_holds(), "early={early}");
17614            assert!(sched.process_is_unreaped_zombie(victim), "early={early}");
17615            assert_eq!(
17616                sched
17617                    .timeslice_stats_for_test(victim)
17618                    .map(|stats| stats.count),
17619                Some(1),
17620                "early={early}"
17621            );
17622        }
17623    }
17624
17625    #[tokio::test]
17626    async fn thread_exited_uses_natural_retirement_for_killed_stale_and_failed_rpcs() {
17627        use std::sync::atomic::Ordering;
17628
17629        use reverie::Tool;
17630
17631        for cause in ["killed", "stale image", "backend failure", "live"] {
17632            let (config, state, tid, pid) = cancellation_test_state();
17633            install_test_registration(&state, tid, Ivar::new());
17634            let tool: Detcore = Detcore::new(Tid::from_raw(tid.as_raw()), &config);
17635            let mut thread = tool.init_thread_state(Tid::from_raw(tid.as_raw()), None);
17636            thread.detpid = Some(pid);
17637            thread.thread_logical_time.add_syscall_with_cost(37);
17638            match cause {
17639                "killed" => {
17640                    state
17641                        .sched
17642                        .lock()
17643                        .unwrap()
17644                        .logically_kill_thread(&tid, &pid, thread.mm_id)
17645                }
17646                "stale image" => {
17647                    state
17648                        .sched
17649                        .lock()
17650                        .unwrap()
17651                        .install_test_exec_incarnation(tid, thread.mm_id);
17652                    thread.mm_id = thread.mm_id.for_exec(pid);
17653                }
17654                "backend failure" => state.report_backend_failure(reverie::BackendFailure {
17655                    pid: Tid::from_raw(pid.as_raw()),
17656                    tid: Tid::from_raw(tid.as_raw()),
17657                    phase: "natural retirement control",
17658                }),
17659                "live" => {}
17660                _ => unreachable!(),
17661            }
17662            let mut guest = RetirementGuest {
17663                global: &state,
17664                config: &config,
17665                thread,
17666                requests: Mutex::new(Vec::new()),
17667                retired: std::sync::atomic::AtomicBool::new(false),
17668                hold_private_memory_publication: false,
17669            };
17670            let before = guest.thread.thread_logical_time.as_nanos();
17671            {
17672                let mut call = std::pin::pin!(super::send_and_update_time(
17673                    &mut guest,
17674                    GlobalRequest::GlobalTimeLowerBound
17675                ));
17676                if cause == "live" {
17677                    assert!(matches!(
17678                        futures::poll!(call.as_mut()),
17679                        std::task::Poll::Ready((_, GlobalResponse::GlobalTimeLowerBound(_)))
17680                    ));
17681                } else {
17682                    assert!(futures::poll!(call.as_mut()).is_pending(), "{cause}");
17683                }
17684            }
17685            // A terminal backend failure closes ordinary RPC admission before
17686            // any ThreadExited response. The backend failure subscription owns
17687            // cancellation of that pending callback; it must not retire as success.
17688            assert_eq!(
17689                guest.retired.load(Ordering::SeqCst),
17690                matches!(cause, "killed" | "stale image"),
17691                "{cause}"
17692            );
17693            assert_eq!(guest.requests.lock().unwrap().len(), 1);
17694            assert_eq!(guest.thread.thread_logical_time.as_nanos(), before);
17695            if cause == "backend failure" {
17696                assert!(state.sched.lock().unwrap().backend_failed());
17697                assert!(
17698                    futures::poll!(std::pin::pin!(state.wait_for_backend_failure())).is_ready()
17699                );
17700            }
17701        }
17702    }
17703
17704    /// A5 step 2b: an ordinary request first publishes the caller's process's
17705    /// copied dispositions, through the real global RPC, when they differ
17706    /// from what the scheduler holds, and not otherwise; the publication
17707    /// carries the next generation, and a copy whose actions became unknown
17708    /// withdraws them. A discard reads the result.
17709    #[tokio::test]
17710    async fn an_ordinary_request_first_publishes_changed_dispositions() {
17711        use reverie::Tool;
17712
17713        use crate::syscalls::signal_shadow::CopiedDispositions;
17714        use crate::syscalls::signal_shadow::PublishedDispositions;
17715
17716        let config = Config {
17717            sequentialize_threads: true,
17718            ..Config::default()
17719        }
17720        .with_backend(|backend| detcore_model::config::set_kernel_signal_facts(backend, true));
17721        let state = GlobalState::initialize(&config, false);
17722        let leader = DetTid::from_raw(17);
17723        let process = DetPid::from_raw(17);
17724        state
17725            .sched
17726            .lock()
17727            .unwrap()
17728            .thread_tree
17729            .add_child(leader, leader, true);
17730        install_test_registration(&state, leader, Ivar::new());
17731        let tool = <Detcore as Tool>::new(reverie::Pid::from_raw(leader.as_raw()), &config);
17732        let mut thread = tool.init_thread_state(Tid::from_raw(leader.as_raw()), None);
17733        thread.detpid = Some(process);
17734        let usr1 = 1 << (libc::SIGUSR1 - 1);
17735        thread.signal_shadow = Some(crate::signal_state::SignalShadow::seeded(usr1, 0));
17736        let mut guest = ExternalRegistrationGuest {
17737            global: &state,
17738            config: &config,
17739            thread,
17740            requests: Mutex::new(Vec::new()),
17741            post_exec: false,
17742            ppid: None,
17743            private_memory: None,
17744        };
17745        let mutation = |n: u32| GlobalRequest::RecordHostMutation {
17746            path: format!("/a5-step-2b/{n}"),
17747        };
17748        let ignoring = CopiedDispositions {
17749            ignored: usr1,
17750            caught: 0,
17751        };
17752        let held = |state: &GlobalState| {
17753            state
17754                .sched
17755                .lock()
17756                .unwrap()
17757                .published_dispositions
17758                .get(&process)
17759                .copied()
17760        };
17761
17762        super::send_and_update_time(&mut guest, mutation(1)).await;
17763        super::send_and_update_time(&mut guest, mutation(2)).await;
17764        assert_eq!(
17765            *guest.requests.lock().unwrap(),
17766            vec![
17767                GlobalRequest::PublishSignalDispositions(1, Some(ignoring)),
17768                GlobalRequest::PublishSignalMask(Some(0)),
17769                mutation(1),
17770                mutation(2),
17771            ],
17772            "published once, before the first request"
17773        );
17774        // The scheduler's entry, and the process's record of it.
17775        let entry = |generation, dispositions| PublishedDispositions {
17776            generation,
17777            dispositions,
17778            ..Default::default()
17779        };
17780        let record = |generation, accepted, dispositions| PublishedDispositions {
17781            generation,
17782            accepted,
17783            dispositions,
17784        };
17785        assert_eq!(held(&state), Some(entry(1, Some(ignoring))));
17786        assert_eq!(
17787            *guest.thread.published_dispositions.lock().unwrap(),
17788            record(1, 1, Some(ignoring))
17789        );
17790        assert!(
17791            state
17792                .sched
17793                .lock()
17794                .unwrap()
17795                .published_dispositions_for(leader, libc::SIGUSR1)
17796                .is_some()
17797        );
17798
17799        // A clone that shares the actions with a new process withdraws them
17800        // before it runs (`before_clone`).
17801        guest.requests.lock().unwrap().clear();
17802        crate::syscalls::signal_shadow::before_clone(
17803            &mut guest,
17804            (libc::CLONE_VM | libc::CLONE_SIGHAND) as u64,
17805        )
17806        .await;
17807        assert_eq!(
17808            *guest.requests.lock().unwrap(),
17809            vec![GlobalRequest::PublishSignalDispositions(2, None)]
17810        );
17811        assert_eq!(held(&state), Some(entry(2, None)));
17812        assert_eq!(
17813            *guest.thread.published_dispositions.lock().unwrap(),
17814            record(2, 2, None)
17815        );
17816        super::send_and_update_time(&mut guest, mutation(3)).await;
17817        assert_eq!(
17818            guest.requests.lock().unwrap().len(),
17819            2,
17820            "nothing more to withdraw"
17821        );
17822
17823        // A thread the scheduler does not know is refused: the record keeps
17824        // its dispositions, and once known, the next request publishes again.
17825        guest.thread.signal_shadow = Some(crate::signal_state::SignalShadow::seeded(usr1, 0));
17826        let mut turn = state.sched.lock().unwrap().next_turns.remove(&leader);
17827        guest.requests.lock().unwrap().clear();
17828        super::send_and_update_time(&mut guest, mutation(4)).await;
17829        assert_eq!(held(&state), Some(entry(2, None)), "refused");
17830        assert_eq!(
17831            *guest.thread.published_dispositions.lock().unwrap(),
17832            record(3, 2, None)
17833        );
17834        state
17835            .sched
17836            .lock()
17837            .unwrap()
17838            .next_turns
17839            .insert(leader, turn.take().unwrap());
17840        super::send_and_update_time(&mut guest, mutation(5)).await;
17841        assert_eq!(held(&state), Some(entry(4, Some(ignoring))));
17842        assert_eq!(
17843            *guest.thread.published_dispositions.lock().unwrap(),
17844            record(4, 4, Some(ignoring))
17845        );
17846
17847        // The scheduler holds a generation this process never used (a stale
17848        // entry): the publication is not taken, the record carries on after
17849        // that generation, and the thread publishes again at once, before its
17850        // request goes out, and is taken.
17851        let stale = entry(
17852            50,
17853            Some(CopiedDispositions {
17854                ignored: 0,
17855                caught: usr1,
17856            }),
17857        );
17858        state
17859            .sched
17860            .lock()
17861            .unwrap()
17862            .published_dispositions
17863            .insert(process, stale);
17864        *guest.thread.published_dispositions.lock().unwrap() = record(4, 4, None);
17865        super::send_and_update_time(&mut guest, mutation(6)).await;
17866        assert_eq!(held(&state), Some(entry(51, Some(ignoring))));
17867        assert_eq!(
17868            *guest.thread.published_dispositions.lock().unwrap(),
17869            record(51, 51, Some(ignoring))
17870        );
17871        // The same with a stale entry of the very generation the process uses
17872        // next, the likeliest shape: not taken, so not mistaken for it.
17873        state
17874            .sched
17875            .lock()
17876            .unwrap()
17877            .published_dispositions
17878            .insert(process, entry(52, stale.dispositions));
17879        *guest.thread.published_dispositions.lock().unwrap() = record(51, 51, None);
17880        super::send_and_update_time(&mut guest, mutation(7)).await;
17881        assert_eq!(held(&state), Some(entry(53, Some(ignoring))));
17882        assert_eq!(
17883            *guest.thread.published_dispositions.lock().unwrap(),
17884            record(53, 53, Some(ignoring))
17885        );
17886        assert_eq!(
17887            *guest.requests.lock().unwrap(),
17888            vec![
17889                GlobalRequest::PublishSignalDispositions(3, Some(ignoring)),
17890                mutation(4),
17891                GlobalRequest::PublishSignalDispositions(4, Some(ignoring)),
17892                mutation(5),
17893                GlobalRequest::PublishSignalDispositions(5, Some(ignoring)),
17894                GlobalRequest::PublishSignalDispositions(51, Some(ignoring)),
17895                mutation(6),
17896                GlobalRequest::PublishSignalDispositions(52, Some(ignoring)),
17897                GlobalRequest::PublishSignalDispositions(53, Some(ignoring)),
17898                mutation(7),
17899            ]
17900        );
17901    }
17902
17903    /// A5 step 2b: an ordinary request first publishes the caller's mask
17904    /// from Detcore's copy, through the real global RPC, when it differs
17905    /// from what the scheduler holds, withdraws it inside a temporary-mask
17906    /// wait, and is refused for a thread the scheduler does not know, which
17907    /// publishes again once known. The scheduler's reader answers from it.
17908    #[tokio::test]
17909    async fn an_ordinary_request_first_publishes_a_changed_mask() {
17910        use reverie::Tool;
17911
17912        let config = Config {
17913            sequentialize_threads: true,
17914            ..Config::default()
17915        }
17916        .with_backend(|backend| detcore_model::config::set_kernel_signal_facts(backend, true));
17917        let state = GlobalState::initialize(&config, false);
17918        let leader = DetTid::from_raw(17);
17919        let process = DetPid::from_raw(17);
17920        state
17921            .sched
17922            .lock()
17923            .unwrap()
17924            .thread_tree
17925            .add_child(leader, leader, true);
17926        install_test_registration(&state, leader, Ivar::new());
17927        let tool = <Detcore as Tool>::new(reverie::Pid::from_raw(leader.as_raw()), &config);
17928        let mut thread = tool.init_thread_state(Tid::from_raw(leader.as_raw()), None);
17929        thread.detpid = Some(process);
17930        let usr1 = 1 << (libc::SIGUSR1 - 1);
17931        thread.signal_shadow = Some(crate::signal_state::SignalShadow::seeded(0, usr1));
17932        let mut guest = ExternalRegistrationGuest {
17933            global: &state,
17934            config: &config,
17935            thread,
17936            requests: Mutex::new(Vec::new()),
17937            post_exec: false,
17938            ppid: None,
17939            private_memory: None,
17940        };
17941        let mutation = |n: u32| GlobalRequest::RecordHostMutation {
17942            path: format!("/a5-step-2b-mask/{n}"),
17943        };
17944        let masks = |guest: &ExternalRegistrationGuest<'_>| -> Vec<GlobalRequest> {
17945            guest
17946                .requests
17947                .lock()
17948                .unwrap()
17949                .iter()
17950                .filter(|request| !matches!(request, GlobalRequest::PublishSignalDispositions(..)))
17951                .cloned()
17952                .collect()
17953        };
17954        let held = |state: &GlobalState| {
17955            state
17956                .sched
17957                .lock()
17958                .unwrap()
17959                .published_masks
17960                .get(&leader)
17961                .copied()
17962        };
17963
17964        super::send_and_update_time(&mut guest, mutation(1)).await;
17965        super::send_and_update_time(&mut guest, mutation(2)).await;
17966        // The whole stream: the process's dispositions, then the mask, then
17967        // the request.
17968        assert_eq!(
17969            *guest.requests.lock().unwrap(),
17970            vec![
17971                GlobalRequest::PublishSignalDispositions(
17972                    1,
17973                    Some(crate::syscalls::signal_shadow::CopiedDispositions {
17974                        ignored: 0,
17975                        caught: 0,
17976                    })
17977                ),
17978                GlobalRequest::PublishSignalMask(Some(usr1)),
17979                mutation(1),
17980                mutation(2),
17981            ],
17982            "published once, before the first request"
17983        );
17984        assert_eq!(held(&state), Some(usr1));
17985        assert_eq!(guest.thread.published_mask, Some(Some(usr1)));
17986        assert_eq!(
17987            state
17988                .sched
17989                .lock()
17990                .unwrap()
17991                .published_mask_for(leader, libc::SIGUSR1),
17992            Some(usr1)
17993        );
17994
17995        // Inside a temporary-mask wait the kernel may hold the wait's mask:
17996        // withdrawn.
17997        guest
17998            .thread
17999            .signal_shadow
18000            .as_mut()
18001            .unwrap()
18002            .thread
18003            .wait_mask = Some(0);
18004        guest.requests.lock().unwrap().clear();
18005        super::send_and_update_time(&mut guest, mutation(3)).await;
18006        assert_eq!(
18007            masks(&guest),
18008            vec![GlobalRequest::PublishSignalMask(None), mutation(3)]
18009        );
18010        assert_eq!(held(&state), None);
18011
18012        // Refused while the scheduler does not know the thread; published
18013        // again once it does.
18014        guest
18015            .thread
18016            .signal_shadow
18017            .as_mut()
18018            .unwrap()
18019            .thread
18020            .wait_mask = None;
18021        let mut turn = state.sched.lock().unwrap().next_turns.remove(&leader);
18022        guest.requests.lock().unwrap().clear();
18023        super::send_and_update_time(&mut guest, mutation(4)).await;
18024        assert_eq!(held(&state), None, "refused");
18025        assert_eq!(guest.thread.published_mask, Some(None));
18026        state
18027            .sched
18028            .lock()
18029            .unwrap()
18030            .next_turns
18031            .insert(leader, turn.take().unwrap());
18032        super::send_and_update_time(&mut guest, mutation(5)).await;
18033        assert_eq!(
18034            masks(&guest),
18035            vec![
18036                GlobalRequest::PublishSignalMask(Some(usr1)),
18037                mutation(4),
18038                GlobalRequest::PublishSignalMask(Some(usr1)),
18039                mutation(5),
18040            ]
18041        );
18042        assert_eq!(held(&state), Some(usr1));
18043
18044        // An exec that loses the copy: what the scheduler holds is unknown
18045        // after an exec, so the next request withdraws the mask.
18046        guest
18047            .thread
18048            .signal_shadow
18049            .as_ref()
18050            .unwrap()
18051            .forget_dispositions();
18052        crate::syscalls::signal_shadow::at_exec(&mut guest);
18053        assert!(guest.thread.signal_shadow.is_none(), "the copy was lost");
18054        guest.requests.lock().unwrap().clear();
18055        super::send_and_update_time(&mut guest, mutation(6)).await;
18056        assert_eq!(
18057            masks(&guest),
18058            vec![GlobalRequest::PublishSignalMask(None), mutation(6)]
18059        );
18060        assert_eq!(held(&state), None);
18061    }
18062
18063    async fn check_external_child_tid_registration(
18064        flags: CloneFlags,
18065        supplied_address: usize,
18066        expected_address: usize,
18067    ) {
18068        use reverie::Tool;
18069
18070        let config = Config {
18071            sequentialize_threads: true,
18072            // This control observes registration before the child starts; use
18073            // the supported parent-first order for the one turn it drives.
18074            runs_post_fork: crate::RunsPostFork::Parent,
18075            ..Config::default()
18076        }
18077        .with_backend(|backend| {
18078            backend.needs_killed_thread_rpc_cancellation = true;
18079        });
18080        let state = GlobalState::initialize(&config, false);
18081        let parent = DetTid::from_raw(17);
18082        let parent_pid = DetPid::from_raw(17);
18083        state
18084            .sched
18085            .lock()
18086            .unwrap()
18087            .thread_tree
18088            .add_child(parent, parent, true);
18089        install_test_registration(&state, parent, Ivar::new());
18090        let tool = Detcore::new(reverie::Pid::from_raw(parent.as_raw()), &config);
18091        let mut thread = tool.init_thread_state(Tid::from_raw(parent.as_raw()), None);
18092        thread.detpid = Some(parent_pid);
18093        let mut guest = ExternalRegistrationGuest {
18094            global: &state,
18095            config: &config,
18096            thread,
18097            requests: Mutex::new(Vec::new()),
18098            post_exec: false,
18099            ppid: None,
18100            private_memory: None,
18101        };
18102        let child = DetTid::from_raw(18);
18103        let exit_signal = if flags.contains(CloneFlags::CLONE_THREAD) {
18104            0
18105        } else {
18106            libc::SIGCHLD
18107        };
18108        tokio::time::timeout(Duration::from_secs(2), async {
18109            let mut registration = std::pin::pin!(tool.register_external_child(
18110                &mut guest,
18111                Tid::from_raw(child.as_raw()),
18112                supplied_address,
18113                flags,
18114                exit_signal,
18115                None,
18116            ));
18117            assert!(futures::poll!(registration.as_mut()).is_pending());
18118            // The production RPC parks the parent on ParentContinue. Drive
18119            // that actual scheduler turn instead of pre-filling a response or
18120            // disabling sequentialization to make registration return.
18121            let committed = crate::scheduler::do_a_turn_blocking(
18122                state.sched.clone(),
18123                state.global_time.clone(),
18124                &Err(crate::scheduler::SkipTurn),
18125            )
18126            .await
18127            .expect("the parent continuation must commit");
18128            assert_eq!(committed.tid, parent);
18129            assert_eq!(
18130                committed.resources,
18131                std::collections::HashMap::from([(
18132                    crate::resources::ResourceID::ParentContinue { parent, child },
18133                    crate::resources::Permission::W,
18134                )]),
18135            );
18136            registration.await;
18137        })
18138        .await
18139        .expect("external child registration must return without running a guest");
18140
18141        assert_eq!(guest.thread.clone_flags, None);
18142        assert_eq!(guest.thread.dettid, parent);
18143        let mut scheduler = state.sched.lock().unwrap();
18144        assert_eq!(scheduler.next_turns.len(), 2);
18145        assert!(scheduler.next_turns.contains_key(&parent));
18146        assert_eq!(
18147            scheduler.next_turns[&child].child_tid_addr,
18148            expected_address
18149        );
18150        assert_eq!(
18151            *guest.requests.lock().unwrap(),
18152            vec![GlobalRequest::CreateChildThread(
18153                child,
18154                parent_pid,
18155                expected_address,
18156                Some(flags),
18157                exit_signal,
18158                None,
18159                Some(DEFAULT_PRIORITY),
18160            )],
18161            "external registration must send one correctly gated real RPC"
18162        );
18163        let child_pid = if flags.contains(CloneFlags::CLONE_THREAD) {
18164            parent_pid
18165        } else {
18166            child
18167        };
18168        let child_mm = MmId::for_clone(
18169            MmId::initial(parent_pid),
18170            child,
18171            flags.contains(CloneFlags::CLONE_VM),
18172        );
18173        scheduler.logically_kill_thread(&child, &child_pid, child_mm);
18174        assert!(!scheduler.next_turns.contains_key(&child));
18175        assert!(scheduler.next_turns.contains_key(&parent));
18176        assert_eq!(
18177            scheduler.child_tid_was_cleared(
18178                FutexID::private(child_mm, supplied_address),
18179                child.as_raw(),
18180            ),
18181            expected_address != 0,
18182            "exit must use only the registered child-TID address"
18183        );
18184        assert!(!scheduler.child_tid_was_cleared(FutexID::private(child_mm, 0), child.as_raw(),));
18185        assert!(!scheduler.child_tid_was_cleared(
18186            FutexID::private(child_mm, supplied_address),
18187            parent.as_raw(),
18188        ));
18189    }
18190
18191    #[tokio::test]
18192    async fn external_registration_without_child_cleartid_disables_exit_wake() {
18193        for kind in [
18194            CloneFlags::empty(),
18195            CloneFlags::CLONE_THREAD | CloneFlags::CLONE_VM | CloneFlags::CLONE_SIGHAND,
18196        ] {
18197            for registration in [CloneFlags::empty(), CloneFlags::CLONE_CHILD_SETTID] {
18198                check_external_child_tid_registration(kind | registration, 0x1234, 0).await;
18199            }
18200        }
18201    }
18202
18203    #[tokio::test]
18204    async fn external_registration_with_child_cleartid_preserves_exact_exit_wake() {
18205        for kind in [
18206            CloneFlags::empty(),
18207            CloneFlags::CLONE_THREAD | CloneFlags::CLONE_VM | CloneFlags::CLONE_SIGHAND,
18208        ] {
18209            for registration in [
18210                CloneFlags::CLONE_CHILD_CLEARTID,
18211                CloneFlags::CLONE_CHILD_CLEARTID | CloneFlags::CLONE_CHILD_SETTID,
18212            ] {
18213                check_external_child_tid_registration(kind | registration, 0x1234, 0x1234).await;
18214                check_external_child_tid_registration(kind | registration, 0, 0).await;
18215            }
18216        }
18217    }
18218
18219    #[tokio::test]
18220    async fn set_child_tid_address_rpc_updates_and_resets_the_registration() {
18221        let (config, state, dettid, detpid) = cancellation_test_state();
18222        install_test_registration(&state, dettid, Ivar::new());
18223        state
18224            .sched
18225            .lock()
18226            .unwrap()
18227            .next_turns
18228            .get_mut(&dettid)
18229            .unwrap()
18230            .child_tid_addr = 0x1234;
18231
18232        let response = state
18233            .receive_rpc(
18234                reverie::Tid::from_raw(dettid.as_raw()),
18235                (
18236                    DetTime::new(&config),
18237                    MmId::initial(detpid),
18238                    GlobalRequest::SetChildTidAddress(0),
18239                ),
18240            )
18241            .await;
18242
18243        assert_eq!(response.1, GlobalResponse::SetChildTidAddress(()));
18244        assert_eq!(
18245            state
18246                .sched
18247                .lock()
18248                .unwrap()
18249                .next_turns
18250                .get(&dettid)
18251                .unwrap()
18252                .child_tid_addr,
18253            0
18254        );
18255    }
18256
18257    /// A successful SIGKILL reaches the scheduler's record through the RPCs a
18258    /// sender makes in its turn: a process named by `kill`, and a SIGKILL with
18259    /// no named victim; another signal records nothing
18260    /// (https://github.com/rrnewton/hermit/issues/3904).
18261    #[tokio::test]
18262    async fn sigkill_notifications_record_the_doomed_process() {
18263        let (config, state, dettid, detpid) = cancellation_test_state();
18264        let notify = |request| {
18265            state.receive_rpc(
18266                reverie::Tid::from_raw(dettid.as_raw()),
18267                (DetTime::new(&config), MmId::initial(detpid), request),
18268            )
18269        };
18270        let victim = DetPid::from_raw(41);
18271        assert_eq!(
18272            notify(GlobalRequest::NotifySignalPending(
18273                DetTid::from_raw(41),
18274                SigWrapper(libc::SIGKILL),
18275                Some(victim),
18276                super::SignalScope::Process,
18277            ))
18278            .await
18279            .1,
18280            GlobalResponse::NotifySignalPending(())
18281        );
18282        assert_eq!(
18283            state.sched.lock().unwrap().sigkill_recorded(victim),
18284            (true, false)
18285        );
18286        let other = DetPid::from_raw(43);
18287        notify(GlobalRequest::NotifySignalPending(
18288            DetTid::from_raw(43),
18289            SigWrapper(libc::SIGTERM),
18290            Some(other),
18291            super::SignalScope::Process,
18292        ))
18293        .await;
18294        assert_eq!(
18295            state.sched.lock().unwrap().sigkill_recorded(other),
18296            (false, false)
18297        );
18298        // A process-group kill records every process in the group.
18299        assert_eq!(
18300            notify(GlobalRequest::NotifyUnresolvedSigkill(Some(detpid)))
18301                .await
18302                .1,
18303            GlobalResponse::NotifyUnresolvedSigkill(())
18304        );
18305        assert_eq!(
18306            state.sched.lock().unwrap().sigkill_recorded(detpid),
18307            (true, false)
18308        );
18309        assert_eq!(
18310            notify(GlobalRequest::NotifyUnresolvedSigkill(None)).await.1,
18311            GlobalResponse::NotifyUnresolvedSigkill(())
18312        );
18313        assert_eq!(
18314            state.sched.lock().unwrap().sigkill_recorded(other),
18315            (false, true)
18316        );
18317    }
18318
18319    async fn child_start_clock_trajectory(
18320        start_before_selection: bool,
18321        child_first: bool,
18322    ) -> (Vec<LogicalTime>, Vec<DetTid>, bool) {
18323        let config = Config {
18324            sequentialize_threads: true,
18325            runs_post_fork: if child_first {
18326                RunsPostFork::Child
18327            } else {
18328                RunsPostFork::Parent
18329            },
18330            ..Config::default()
18331        };
18332        let state = GlobalState::initialize(&config, false);
18333        let parent = DetTid::from_raw(17);
18334        let parent_pid = parent;
18335        state
18336            .sched
18337            .lock()
18338            .unwrap()
18339            .thread_tree
18340            .add_child(parent, parent, true);
18341        let child = DetTid::from_raw(parent.as_raw() + 1);
18342        let parent_mm = MmId::initial(parent_pid);
18343        let child_mm = MmId::for_clone(parent_mm, child, false);
18344        install_test_registration(&state, parent, Ivar::new());
18345        let epoch = DetTime::new(&config).as_nanos();
18346        let mut parent_clock = DetTime::new(&config);
18347        parent_clock.advance_to(epoch + LogicalTime::from_nanos(1_001));
18348        let child_clock = parent_clock.clone_for_child();
18349
18350        // Polling the real parent RPC publishes both admission and
18351        // ParentContinue before it waits, just as the clone handler does.
18352        let mut registration = Box::pin(state.receive_rpc(
18353            Tid::from_raw(parent.as_raw()),
18354            (
18355                parent_clock.clone(),
18356                parent_mm,
18357                GlobalRequest::CreateChildThread(
18358                    child,
18359                    parent_pid,
18360                    0,
18361                    Some(CloneFlags::empty()),
18362                    libc::SIGCHLD,
18363                    None,
18364                    Some(DEFAULT_PRIORITY),
18365                ),
18366            ),
18367        ));
18368        assert!(futures::poll!(&mut registration).is_pending());
18369        let child_request = state.sched.lock().unwrap().next_turns[&child].req.clone();
18370        let mut startup = Box::pin(state.receive_rpc(
18371            Tid::from_raw(child.as_raw()),
18372            (
18373                child_clock.clone(),
18374                child_mm,
18375                GlobalRequest::StartNewThread(child, child, None, None),
18376            ),
18377        ));
18378        if start_before_selection {
18379            // recv_start_new_thread yields once before filling its request.
18380            assert!(futures::poll!(&mut startup).is_pending());
18381            assert!(futures::poll!(&mut startup).is_pending());
18382            assert!(child_request.try_read().is_some());
18383        }
18384
18385        let skipped = Err(crate::scheduler::SkipTurn);
18386        let mut turn = Box::pin(crate::scheduler::do_a_turn_blocking(
18387            state.sched.clone(),
18388            state.global_time.clone(),
18389            &skipped,
18390        ));
18391        if !start_before_selection && child_first {
18392            assert!(futures::poll!(&mut turn).is_pending());
18393            assert_eq!(child_request.to_string(), "<ivar HasWaiter>");
18394            assert!(futures::poll!(&mut startup).is_pending());
18395            assert!(futures::poll!(&mut startup).is_pending());
18396            assert!(child_request.try_read().is_some());
18397        }
18398        let first = turn.await.expect("first post-fork turn must commit");
18399        if !start_before_selection && !child_first {
18400            assert!(child_request.try_read().is_none());
18401            assert!(futures::poll!(&mut startup).is_pending());
18402            assert!(futures::poll!(&mut startup).is_pending());
18403            assert!(child_request.try_read().is_some());
18404        }
18405        let child_resource = ResourceID::MemAddrSpace(child);
18406        let parent_resource = ResourceID::ParentContinue { parent, child };
18407        let (first_tid, first_resource, first_permission, second_resource, second_permission) =
18408            if child_first {
18409                (
18410                    child,
18411                    &child_resource,
18412                    Permission::RW,
18413                    &parent_resource,
18414                    Permission::W,
18415                )
18416            } else {
18417                (
18418                    parent,
18419                    &parent_resource,
18420                    Permission::W,
18421                    &child_resource,
18422                    Permission::RW,
18423                )
18424            };
18425        assert_eq!(first.tid, first_tid);
18426        assert_eq!(first.resources.len(), 1);
18427        assert_eq!(first.resources.get(first_resource), Some(&first_permission));
18428        if child_first {
18429            assert_eq!(
18430                startup.as_mut().await,
18431                (None, granted(GlobalResponse::StartNewThread(None)))
18432            );
18433        } else {
18434            assert_eq!(
18435                registration.as_mut().await,
18436                (None, granted(GlobalResponse::CreateChildThread(None)))
18437            );
18438        }
18439        let first_time = state.sched.lock().unwrap().committed_time;
18440        assert_eq!(first_time, epoch + LogicalTime::from_nanos(1_001));
18441        assert_eq!(
18442            state.global_time.lock().unwrap().threads_time(child),
18443            child_clock.as_nanos()
18444        );
18445
18446        // The selected thread's first new nanosecond and the ordinary scheduler
18447        // increment must both remain observable on the other thread's turn.
18448        let (mut first_clock, first_mm) = if child_first {
18449            (child_clock, child_mm)
18450        } else {
18451            (parent_clock, parent_mm)
18452        };
18453        first_clock.advance_to(first_clock.as_nanos() + LogicalTime::from_nanos(1));
18454        let mut resources = Resources::new(first_tid);
18455        resources.insert(ResourceID::MemAddrSpace(first_tid), Permission::RW);
18456        let mut work = Box::pin(state.receive_rpc(
18457            Tid::from_raw(first_tid.as_raw()),
18458            (
18459                first_clock,
18460                first_mm,
18461                GlobalRequest::RequestResources(resources, first_tid),
18462            ),
18463        ));
18464        assert!(futures::poll!(&mut work).is_pending());
18465        let second = crate::scheduler::do_a_turn_blocking(
18466            state.sched.clone(),
18467            state.global_time.clone(),
18468            &Ok(first),
18469        )
18470        .await
18471        .expect("other thread's continuation must commit");
18472        assert_eq!(second.resources.len(), 1);
18473        assert_eq!(
18474            second.resources.get(second_resource),
18475            Some(&second_permission)
18476        );
18477        if child_first {
18478            assert_eq!(
18479                registration.await,
18480                (None, granted(GlobalResponse::CreateChildThread(None)))
18481            );
18482        } else {
18483            assert_eq!(
18484                startup.await,
18485                (None, granted(GlobalResponse::StartNewThread(None)))
18486            );
18487        }
18488        let mut scheduler = state.sched.lock().unwrap();
18489        let next_time = scheduler.committed_time;
18490        assert_eq!(next_time, epoch + LogicalTime::from_nanos(501_002));
18491        let queue = scheduler.run_queue.tids().copied().collect();
18492        let next_random = scheduler.child_runs_first_post_fork(RunsPostFork::Random);
18493        (vec![first_time, next_time], queue, next_random)
18494    }
18495
18496    /// A5 step 3c.1d (review round 4, lane ruling 1): a clock observation
18497    /// (`GlobalTimeLowerBound`, what `thread_observe_time` sends) grants no
18498    /// turn and moves no time. With threads sequentialized, the thread holding
18499    /// the turn observes the clock twice while a sibling's request is filed and
18500    /// waiting: both answers are the same time, neither is a hand-off
18501    /// (`GlobalResponse::Granted`), the scheduler commits no turn, and the
18502    /// sibling's request stays pending. So a temporary-mask wait that takes its
18503    /// deadline after its first probe and check reads the time it would have
18504    /// read at entry; only a sibling running concurrently (a background call)
18505    /// can move global time meanwhile, as for the base's own timed waits, which
18506    /// observe through the same RPC.
18507    #[tokio::test]
18508    async fn a_clock_observation_grants_no_turn_and_moves_no_time() {
18509        let (config, state, holder, detpid) = cancellation_test_state();
18510        let sibling = DetTid::from_raw(holder.as_raw() + 1);
18511        let mm = MmId::initial(detpid);
18512        state
18513            .sched
18514            .lock()
18515            .unwrap()
18516            .thread_tree
18517            .add_child(holder, sibling, false);
18518        install_test_registration(&state, holder, Ivar::new());
18519        install_test_registration(&state, sibling, Ivar::new());
18520        let mut clock = DetTime::new(&config);
18521        let epoch = clock.as_nanos();
18522        clock.advance_to(epoch + LogicalTime::from_nanos(1_000));
18523        let sibling_clock = clock.clone_for_child();
18524        let request = |tid: DetTid| {
18525            let mut resources = Resources::new(tid);
18526            resources.insert(ResourceID::MemAddrSpace(tid), Permission::RW);
18527            GlobalRequest::RequestResources(resources, detpid)
18528        };
18529        let mut holder_turn = Box::pin(state.receive_rpc(
18530            Tid::from_raw(holder.as_raw()),
18531            (clock.clone(), mm, request(holder)),
18532        ));
18533        assert!(futures::poll!(&mut holder_turn).is_pending());
18534        let mut sibling_turn = Box::pin(state.receive_rpc(
18535            Tid::from_raw(sibling.as_raw()),
18536            (sibling_clock, mm, request(sibling)),
18537        ));
18538        assert!(futures::poll!(&mut sibling_turn).is_pending());
18539        let committed = crate::scheduler::do_a_turn_blocking(
18540            state.sched.clone(),
18541            state.global_time.clone(),
18542            &Err(crate::scheduler::SkipTurn),
18543        )
18544        .await
18545        .expect("one of the two requests is granted");
18546        // The granted thread holds the turn; the other one's request waits.
18547        let (granted, granted_turn, mut waiting, granted_clock) = if committed.tid == holder {
18548            (holder, holder_turn, sibling_turn, clock.clone())
18549        } else {
18550            (sibling, sibling_turn, holder_turn, clock.clone_for_child())
18551        };
18552        assert!(matches!(granted_turn.await.1, GlobalResponse::Granted(_)));
18553        let turn = state.sched.lock().unwrap().turn;
18554        let before = state.global_time.lock().unwrap().as_nanos();
18555        let mut answers = Vec::new();
18556        for _ in 0..2 {
18557            let (_, answer) = state
18558                .receive_rpc(
18559                    Tid::from_raw(granted.as_raw()),
18560                    (
18561                        granted_clock.clone(),
18562                        mm,
18563                        GlobalRequest::GlobalTimeLowerBound,
18564                    ),
18565                )
18566                .await;
18567            answers.push(answer);
18568            for _ in 0..8 {
18569                tokio::task::yield_now().await;
18570            }
18571            assert!(
18572                futures::poll!(&mut waiting).is_pending(),
18573                "the waiting thread gets no turn"
18574            );
18575        }
18576        let after = state.global_time.lock().unwrap().as_nanos();
18577        assert_eq!(answers[0], answers[1], "the same time twice");
18578        assert!(
18579            matches!(answers[0], GlobalResponse::GlobalTimeLowerBound(_)),
18580            "a plain answer, not a hand-off: {:?}",
18581            answers[0]
18582        );
18583        assert_eq!(after, before, "no time moved");
18584        assert_eq!(state.sched.lock().unwrap().turn, turn, "no turn committed");
18585    }
18586
18587    #[tokio::test]
18588    async fn child_start_clock_is_independent_of_first_rpc_arrival() {
18589        for child_first in [true, false] {
18590            assert_eq!(
18591                child_start_clock_trajectory(true, child_first).await,
18592                child_start_clock_trajectory(false, child_first).await,
18593            );
18594        }
18595    }
18596
18597    #[tokio::test]
18598    async fn vfork_registration_does_not_charge_inherited_work_before_startup() {
18599        let (config, state, parent, parent_pid) = cancellation_test_state();
18600        let child = DetTid::from_raw(parent.as_raw() + 1);
18601        let mm = MmId::initial(parent_pid);
18602        install_test_registration(&state, parent, Ivar::new());
18603        let mut parent_clock = DetTime::new(&config);
18604        let epoch = parent_clock.as_nanos();
18605        parent_clock.advance_to(epoch + LogicalTime::from_nanos(1_001));
18606        let child_clock = parent_clock.clone_for_child();
18607        let mut resources = Resources::new(parent);
18608        resources.insert(
18609            ResourceID::BlockingVfork(ExternalOpId::new(parent, 1)),
18610            Permission::RW,
18611        );
18612        let mut blocking = Box::pin(state.receive_rpc(
18613            Tid::from_raw(parent.as_raw()),
18614            (
18615                parent_clock,
18616                mm,
18617                GlobalRequest::RequestResources(resources, parent_pid),
18618            ),
18619        ));
18620        assert!(futures::poll!(&mut blocking).is_pending());
18621        let background = crate::scheduler::do_a_turn_blocking(
18622            state.sched.clone(),
18623            state.global_time.clone(),
18624            &Err(crate::scheduler::SkipTurn),
18625        )
18626        .await;
18627        assert!(background.is_err());
18628        assert_eq!(
18629            blocking.await,
18630            (
18631                None,
18632                granted(GlobalResponse::RequestResources(ResumeStatus::Normal))
18633            )
18634        );
18635        let before_child = state.global_time.lock().unwrap().as_nanos();
18636        assert_eq!(before_child, epoch + LogicalTime::from_nanos(1_001));
18637
18638        // Unlike ordinary clone, this first RPC is sent by the child itself.
18639        let created = state
18640            .receive_rpc(
18641                Tid::from_raw(child.as_raw()),
18642                (
18643                    child_clock.clone(),
18644                    mm,
18645                    GlobalRequest::CreateVforkChildThread(
18646                        parent,
18647                        parent_pid,
18648                        child,
18649                        0,
18650                        CloneFlags::CLONE_VFORK | CloneFlags::CLONE_VM,
18651                        libc::SIGCHLD,
18652                        Some(DEFAULT_PRIORITY - 1),
18653                    ),
18654                ),
18655            )
18656            .await;
18657        assert_eq!(created, (None, GlobalResponse::CreateChildThread(None)));
18658        assert_eq!(state.global_time.lock().unwrap().as_nanos(), before_child);
18659        assert_eq!(
18660            state.global_time.lock().unwrap().threads_time(child),
18661            child_clock.as_nanos()
18662        );
18663
18664        let mut startup = Box::pin(state.receive_rpc(
18665            Tid::from_raw(child.as_raw()),
18666            (
18667                child_clock,
18668                mm,
18669                GlobalRequest::StartNewThread(child, child, None, None),
18670            ),
18671        ));
18672        assert!(futures::poll!(&mut startup).is_pending());
18673        assert!(futures::poll!(&mut startup).is_pending());
18674        let first = crate::scheduler::do_a_turn_blocking(
18675            state.sched.clone(),
18676            state.global_time.clone(),
18677            &background,
18678        )
18679        .await
18680        .expect("vfork child must receive its first turn");
18681        assert_eq!(first.tid, child);
18682        assert_eq!(
18683            startup.await,
18684            (None, granted(GlobalResponse::StartNewThread(None)))
18685        );
18686        assert_eq!(state.global_time.lock().unwrap().as_nanos(), before_child);
18687    }
18688
18689    /// The state an earlier holder of `id` leaves behind under the ptrace
18690    /// backend once it is gone: its scheduler registration (never removed)
18691    /// and no turn state (`logically_kill_thread` removed it). A holder that
18692    /// ran also leaves its clock component, which nothing retires at exit; one
18693    /// killed before its first message never published a clock.
18694    fn leave_earlier_thread_behind(
18695        state: &GlobalState,
18696        parent: DetTid,
18697        id: DetTid,
18698        clock: Option<&DetTime>,
18699    ) {
18700        state
18701            .sched
18702            .lock()
18703            .unwrap()
18704            .thread_tree
18705            .add_child(parent, id, true);
18706        if let Some(clock) = clock {
18707            state.global_time.lock().unwrap().update_global_time(
18708                id,
18709                clock.as_nanos(),
18710                clock.inherited_nanos(),
18711            );
18712        }
18713    }
18714
18715    /// A parent at virtual time 1_001 ns whose earlier child with ID
18716    /// `parent + 1` is gone, after running 500 ns when `earlier_ran`; the
18717    /// parent then ran on to 600_000 ns. Returns the parent's clock, ready to
18718    /// create the next child.
18719    fn reused_id_state(earlier_ran: bool) -> (GlobalState, DetTid, DetPid, DetTid, DetTime) {
18720        let config = Config {
18721            sequentialize_threads: true,
18722            runs_post_fork: RunsPostFork::Child,
18723            ..Config::default()
18724        };
18725        let (config, state, parent, parent_pid) = cancellation_test_state_with_config(config);
18726        let reused = DetTid::from_raw(parent.as_raw() + 1);
18727        install_test_registration(&state, parent, Ivar::new());
18728        let mut parent_clock = DetTime::new(&config);
18729        let epoch = parent_clock.as_nanos();
18730        parent_clock.advance_to(epoch + LogicalTime::from_nanos(1_001));
18731        let mut first_holder = parent_clock.clone_for_child();
18732        first_holder.advance_to(first_holder.as_nanos() + LogicalTime::from_nanos(500));
18733        leave_earlier_thread_behind(&state, parent, reused, earlier_ran.then_some(&first_holder));
18734        parent_clock.advance_to(epoch + LogicalTime::from_nanos(600_000));
18735        (state, parent, parent_pid, reused, parent_clock)
18736    }
18737
18738    fn fork_registration(child: DetTid, parent_pid: DetPid) -> GlobalRequest {
18739        GlobalRequest::CreateChildThread(
18740            child,
18741            parent_pid,
18742            0,
18743            Some(CloneFlags::empty()),
18744            libc::SIGCHLD,
18745            None,
18746            Some(DEFAULT_PRIORITY),
18747        )
18748    }
18749
18750    /// Linux gave a new child the ID of a child that already exited, as it
18751    /// does once IDs wrap at `pid_max` without a PID namespace. The new
18752    /// child's startup is a new thread, not a reconnect of the old one: it
18753    /// publishes its own clock, inherited from its parent, and none of that
18754    /// inherited history is charged to global time.
18755    #[tokio::test]
18756    async fn a_child_on_a_reused_id_starts_its_own_clock() {
18757        let (state, parent, parent_pid, child, parent_clock) = reused_id_state(true);
18758        let parent_mm = MmId::initial(parent_pid);
18759        let child_mm = MmId::for_clone(parent_mm, child, false);
18760        let child_clock = parent_clock.clone_for_child();
18761        let mut registration = Box::pin(state.receive_rpc(
18762            Tid::from_raw(parent.as_raw()),
18763            (
18764                parent_clock,
18765                parent_mm,
18766                fork_registration(child, parent_pid),
18767            ),
18768        ));
18769        assert!(futures::poll!(&mut registration).is_pending());
18770        let before_child = state.global_time.lock().unwrap().as_nanos();
18771
18772        let mut startup = Box::pin(state.receive_rpc(
18773            Tid::from_raw(child.as_raw()),
18774            (
18775                child_clock.clone(),
18776                child_mm,
18777                GlobalRequest::StartNewThread(child, child, None, None),
18778            ),
18779        ));
18780        assert!(futures::poll!(&mut startup).is_pending());
18781        assert!(futures::poll!(&mut startup).is_pending());
18782        assert_eq!(
18783            state.global_time.lock().unwrap().threads_time(child),
18784            child_clock.as_nanos()
18785        );
18786        assert_eq!(state.global_time.lock().unwrap().as_nanos(), before_child);
18787
18788        let first = crate::scheduler::do_a_turn_blocking(
18789            state.sched.clone(),
18790            state.global_time.clone(),
18791            &Err(crate::scheduler::SkipTurn),
18792        )
18793        .await
18794        .expect("the new child must receive its first turn");
18795        assert_eq!(first.tid, child);
18796        assert_eq!(
18797            startup.await,
18798            (None, granted(GlobalResponse::StartNewThread(None)))
18799        );
18800    }
18801
18802    /// Ptrace can deliver the new child's startup before its parent's clone
18803    /// callback registers it. On a reused ID, the clock there is still the
18804    /// earlier child's, so the startup publishes nothing until the
18805    /// registration has retired it.
18806    #[tokio::test]
18807    async fn an_early_start_on_a_reused_id_waits_for_its_registration() {
18808        let (state, parent, parent_pid, child, parent_clock) = reused_id_state(true);
18809        let parent_mm = MmId::initial(parent_pid);
18810        let child_mm = MmId::for_clone(parent_mm, child, false);
18811        let child_clock = parent_clock.clone_for_child();
18812        let earlier = state.global_time.lock().unwrap().threads_time(child);
18813
18814        let mut startup = Box::pin(state.receive_rpc(
18815            Tid::from_raw(child.as_raw()),
18816            (
18817                child_clock.clone(),
18818                child_mm,
18819                GlobalRequest::StartNewThread(child, child, None, None),
18820            ),
18821        ));
18822        assert!(futures::poll!(&mut startup).is_pending());
18823        assert!(futures::poll!(&mut startup).is_pending());
18824        assert_eq!(
18825            state.global_time.lock().unwrap().threads_time(child),
18826            earlier,
18827            "no clock is published before the registration"
18828        );
18829
18830        let mut registration = Box::pin(state.receive_rpc(
18831            Tid::from_raw(parent.as_raw()),
18832            (
18833                parent_clock,
18834                parent_mm,
18835                fork_registration(child, parent_pid),
18836            ),
18837        ));
18838        assert!(futures::poll!(&mut registration).is_pending());
18839        assert!(!state.global_time.lock().unwrap().contains_thread(child));
18840        let before_child = state.global_time.lock().unwrap().as_nanos();
18841        assert!(futures::poll!(&mut startup).is_pending());
18842        assert!(futures::poll!(&mut startup).is_pending());
18843        assert_eq!(
18844            state.global_time.lock().unwrap().threads_time(child),
18845            child_clock.as_nanos()
18846        );
18847        assert_eq!(state.global_time.lock().unwrap().as_nanos(), before_child);
18848
18849        let first = crate::scheduler::do_a_turn_blocking(
18850            state.sched.clone(),
18851            state.global_time.clone(),
18852            &Err(crate::scheduler::SkipTurn),
18853        )
18854        .await
18855        .expect("the new child must receive its first turn");
18856        assert_eq!(first.tid, child);
18857        assert_eq!(
18858            startup.await,
18859            (None, granted(GlobalResponse::StartNewThread(None)))
18860        );
18861    }
18862
18863    /// An earlier holder killed before its first message left no clock. A
18864    /// new child's early start on its ID still waits for the registration:
18865    /// a clock it published first would be taken for the earlier holder's
18866    /// and retired by that registration.
18867    #[tokio::test]
18868    async fn an_early_start_on_an_id_whose_holder_never_ran_keeps_its_clock() {
18869        let (state, parent, parent_pid, child, parent_clock) = reused_id_state(false);
18870        let parent_mm = MmId::initial(parent_pid);
18871        let child_mm = MmId::for_clone(parent_mm, child, false);
18872        let child_clock = parent_clock.clone_for_child();
18873        let mut startup = Box::pin(state.receive_rpc(
18874            Tid::from_raw(child.as_raw()),
18875            (
18876                child_clock.clone(),
18877                child_mm,
18878                GlobalRequest::StartNewThread(child, child, None, None),
18879            ),
18880        ));
18881        assert!(futures::poll!(&mut startup).is_pending());
18882        assert!(futures::poll!(&mut startup).is_pending());
18883        assert!(!state.global_time.lock().unwrap().contains_thread(child));
18884
18885        let mut registration = Box::pin(state.receive_rpc(
18886            Tid::from_raw(parent.as_raw()),
18887            (
18888                parent_clock,
18889                parent_mm,
18890                fork_registration(child, parent_pid),
18891            ),
18892        ));
18893        assert!(futures::poll!(&mut registration).is_pending());
18894        assert!(futures::poll!(&mut startup).is_pending());
18895        assert!(futures::poll!(&mut startup).is_pending());
18896        assert_eq!(
18897            state.global_time.lock().unwrap().threads_time(child),
18898            child_clock.as_nanos()
18899        );
18900        let first = crate::scheduler::do_a_turn_blocking(
18901            state.sched.clone(),
18902            state.global_time.clone(),
18903            &Err(crate::scheduler::SkipTurn),
18904        )
18905        .await
18906        .expect("the new child must receive its first turn");
18907        assert_eq!(first.tid, child);
18908        assert_eq!(
18909            startup.await,
18910            (None, granted(GlobalResponse::StartNewThread(None)))
18911        );
18912    }
18913
18914    /// The whole sequence through real messages: a child registers, starts,
18915    /// runs and deregisters, and the parent's next child gets the same ID.
18916    #[tokio::test]
18917    async fn a_child_on_the_id_of_a_child_that_ran_and_exited_starts_its_own_clock() {
18918        let config = Config {
18919            sequentialize_threads: true,
18920            runs_post_fork: RunsPostFork::Child,
18921            ..Config::default()
18922        };
18923        let (config, state, parent, parent_pid) = cancellation_test_state_with_config(config);
18924        let child = DetTid::from_raw(parent.as_raw() + 1);
18925        let parent_mm = MmId::initial(parent_pid);
18926        let child_mm = MmId::for_clone(parent_mm, child, false);
18927        install_test_registration(&state, parent, Ivar::new());
18928        let mut parent_clock = DetTime::new(&config);
18929        let epoch = parent_clock.as_nanos();
18930        parent_clock.advance_to(epoch + LogicalTime::from_nanos(1_001));
18931
18932        let mut first_registration = Box::pin(state.receive_rpc(
18933            Tid::from_raw(parent.as_raw()),
18934            (
18935                parent_clock.clone(),
18936                parent_mm,
18937                fork_registration(child, parent_pid),
18938            ),
18939        ));
18940        assert!(futures::poll!(&mut first_registration).is_pending());
18941        let mut first_holder = parent_clock.clone_for_child();
18942        let mut first_start = Box::pin(state.receive_rpc(
18943            Tid::from_raw(child.as_raw()),
18944            (
18945                first_holder.clone(),
18946                child_mm,
18947                GlobalRequest::StartNewThread(child, child, None, None),
18948            ),
18949        ));
18950        assert!(futures::poll!(&mut first_start).is_pending());
18951        assert!(futures::poll!(&mut first_start).is_pending());
18952        let first_turn = crate::scheduler::do_a_turn_blocking(
18953            state.sched.clone(),
18954            state.global_time.clone(),
18955            &Err(crate::scheduler::SkipTurn),
18956        )
18957        .await
18958        .expect("the first child must receive its first turn");
18959        assert_eq!(first_turn.tid, child);
18960        assert_eq!(
18961            first_start.await,
18962            (None, granted(GlobalResponse::StartNewThread(None)))
18963        );
18964        first_holder.advance_to(first_holder.as_nanos() + LogicalTime::from_nanos(500));
18965        let deregistered = state
18966            .receive_rpc(
18967                Tid::from_raw(child.as_raw()),
18968                (
18969                    first_holder,
18970                    child_mm,
18971                    GlobalRequest::DeregisterThread(ThreadDeregistration {
18972                        dettid: child,
18973                        detpid: child,
18974                        mm: child_mm,
18975                        thread_start_entered: true,
18976                        vfork_origin: None,
18977                        timeslice_stats: TimesliceStats::default(),
18978                        syscall_count: 0,
18979                        syscall_histogram: Default::default(),
18980                        chaos_epochs: Vec::new(),
18981                        passed_descriptors_out: false,
18982                        killed_by_group_signal: false,
18983                    }),
18984                ),
18985            )
18986            .await;
18987        assert_eq!(deregistered, (None, GlobalResponse::DeregisterThread(())));
18988        let parent_turn = crate::scheduler::do_a_turn_blocking(
18989            state.sched.clone(),
18990            state.global_time.clone(),
18991            &Ok(first_turn),
18992        )
18993        .await
18994        .expect("the parent must continue after the clone");
18995        assert_eq!(parent_turn.tid, parent);
18996        assert_eq!(
18997            first_registration.await,
18998            (None, granted(GlobalResponse::CreateChildThread(None)))
18999        );
19000
19001        // Linux gives the parent's next child the same ID.
19002        parent_clock.advance_to(epoch + LogicalTime::from_nanos(600_000));
19003        let second_holder = parent_clock.clone_for_child();
19004        let mut registration = Box::pin(state.receive_rpc(
19005            Tid::from_raw(parent.as_raw()),
19006            (
19007                parent_clock,
19008                parent_mm,
19009                fork_registration(child, parent_pid),
19010            ),
19011        ));
19012        assert!(futures::poll!(&mut registration).is_pending());
19013        let before_child = state.global_time.lock().unwrap().as_nanos();
19014        let mut startup = Box::pin(state.receive_rpc(
19015            Tid::from_raw(child.as_raw()),
19016            (
19017                second_holder.clone(),
19018                child_mm,
19019                GlobalRequest::StartNewThread(child, child, None, None),
19020            ),
19021        ));
19022        assert!(futures::poll!(&mut startup).is_pending());
19023        assert!(futures::poll!(&mut startup).is_pending());
19024        assert_eq!(
19025            state.global_time.lock().unwrap().threads_time(child),
19026            second_holder.as_nanos()
19027        );
19028        assert_eq!(state.global_time.lock().unwrap().as_nanos(), before_child);
19029        let second_turn = crate::scheduler::do_a_turn_blocking(
19030            state.sched.clone(),
19031            state.global_time.clone(),
19032            &Ok(parent_turn),
19033        )
19034        .await
19035        .expect("the new child must receive its first turn");
19036        assert_eq!(second_turn.tid, child);
19037        assert_eq!(
19038            startup.await,
19039            (None, granted(GlobalResponse::StartNewThread(None)))
19040        );
19041    }
19042
19043    /// A vfork child registers itself. On a reused ID its registration
19044    /// starts its own clock rather than advancing the earlier child's, which
19045    /// would charge the vfork child's whole inherited history.
19046    #[tokio::test]
19047    async fn a_vfork_child_on_a_reused_id_starts_its_own_clock() {
19048        let (state, parent, parent_pid, child, parent_clock) = reused_id_state(true);
19049        let mm = MmId::initial(parent_pid);
19050        let child_clock = parent_clock.clone_for_child();
19051        // The parent's vfork, which the kernel blocks until the child execs
19052        // or exits.
19053        let mut resources = Resources::new(parent);
19054        resources.insert(
19055            ResourceID::BlockingVfork(ExternalOpId::new(parent, 1)),
19056            Permission::RW,
19057        );
19058        let mut blocking = Box::pin(state.receive_rpc(
19059            Tid::from_raw(parent.as_raw()),
19060            (
19061                parent_clock,
19062                mm,
19063                GlobalRequest::RequestResources(resources, parent_pid),
19064            ),
19065        ));
19066        assert!(futures::poll!(&mut blocking).is_pending());
19067        let background = crate::scheduler::do_a_turn_blocking(
19068            state.sched.clone(),
19069            state.global_time.clone(),
19070            &Err(crate::scheduler::SkipTurn),
19071        )
19072        .await;
19073        assert!(background.is_err());
19074        assert_eq!(
19075            blocking.await,
19076            (
19077                None,
19078                granted(GlobalResponse::RequestResources(ResumeStatus::Normal))
19079            )
19080        );
19081        let before_child = state.global_time.lock().unwrap().as_nanos();
19082
19083        let created = state
19084            .receive_rpc(
19085                Tid::from_raw(child.as_raw()),
19086                (
19087                    child_clock.clone(),
19088                    mm,
19089                    GlobalRequest::CreateVforkChildThread(
19090                        parent,
19091                        parent_pid,
19092                        child,
19093                        0,
19094                        CloneFlags::CLONE_VFORK | CloneFlags::CLONE_VM,
19095                        libc::SIGCHLD,
19096                        Some(DEFAULT_PRIORITY - 1),
19097                    ),
19098                ),
19099            )
19100            .await;
19101        assert_eq!(created, (None, GlobalResponse::CreateChildThread(None)));
19102        assert_eq!(
19103            state.global_time.lock().unwrap().threads_time(child),
19104            child_clock.as_nanos()
19105        );
19106        assert_eq!(state.global_time.lock().unwrap().as_nanos(), before_child);
19107    }
19108
19109    #[tokio::test]
19110    async fn first_nonstartup_rpc_counts_only_new_child_work() {
19111        let config = Config {
19112            sequentialize_threads: false,
19113            ..Config::default()
19114        };
19115        let state = GlobalState::initialize(&config, false);
19116        let parent = DetTid::from_raw(17);
19117        let child = DetTid::from_raw(18);
19118        let mut parent_clock = DetTime::new(&config);
19119        let epoch = parent_clock.as_nanos();
19120        parent_clock.advance_to(epoch + LogicalTime::from_nanos(101));
19121        let mut child_clock = parent_clock.clone_for_child();
19122        let _ = state
19123            .receive_rpc(
19124                Tid::from_raw(parent.as_raw()),
19125                (
19126                    parent_clock,
19127                    MmId::initial(parent),
19128                    GlobalRequest::GlobalTimeLowerBound,
19129                ),
19130            )
19131            .await;
19132        child_clock.advance_to(child_clock.as_nanos() + LogicalTime::from_nanos(1));
19133        let observed = state
19134            .receive_rpc(
19135                Tid::from_raw(child.as_raw()),
19136                (
19137                    child_clock,
19138                    MmId::initial(child),
19139                    GlobalRequest::GlobalTimeLowerBound,
19140                ),
19141            )
19142            .await;
19143        assert_eq!(
19144            observed,
19145            (
19146                None,
19147                GlobalResponse::GlobalTimeLowerBound(epoch + LogicalTime::from_nanos(102))
19148            )
19149        );
19150    }
19151
19152    #[tokio::test]
19153    async fn exec_reconnect_retains_inherited_work_accounting_across_local_reload() {
19154        let (config, state, leader, detpid) = cancellation_test_state();
19155        let ancestor = DetTid::from_raw(leader.as_raw() - 1);
19156        let worker = DetTid::from_raw(leader.as_raw() + 1);
19157        let old_mm = MmId::initial(detpid);
19158        install_test_registration(&state, leader, Ivar::new());
19159        state
19160            .sched
19161            .lock()
19162            .unwrap()
19163            .thread_tree
19164            .add_child(leader, worker, false);
19165        install_test_registration(&state, worker, Ivar::new());
19166
19167        let mut ancestor_clock = DetTime::new(&config);
19168        let epoch = ancestor_clock.as_nanos();
19169        ancestor_clock.advance_to(epoch + LogicalTime::from_nanos(1_000));
19170        let mut leader_clock = ancestor_clock.clone_for_child();
19171        leader_clock.advance_to(epoch + LogicalTime::from_nanos(1_100));
19172        let mut worker_clock = leader_clock.clone_for_child();
19173        worker_clock.advance_to(epoch + LogicalTime::from_nanos(1_350));
19174        for (tid, clock) in [
19175            (ancestor, ancestor_clock),
19176            (leader, leader_clock),
19177            (worker, worker_clock.clone()),
19178        ] {
19179            let _ = state
19180                .receive_rpc(
19181                    Tid::from_raw(tid.as_raw()),
19182                    (clock, old_mm, GlobalRequest::GlobalTimeLowerBound),
19183                )
19184                .await;
19185        }
19186        let total = state.global_time.lock().unwrap().as_nanos();
19187        assert_eq!(total, epoch + LogicalTime::from_nanos(1_350));
19188        // A failed exec cancels its pending transfer without changing either
19189        // inherited component. The next successful attempt must use the same
19190        // clocks, not charge either component's inherited work again.
19191        let _ = state
19192            .receive_rpc(
19193                Tid::from_raw(worker.as_raw()),
19194                (
19195                    worker_clock.clone(),
19196                    old_mm,
19197                    GlobalRequest::PrepareExec(detpid, old_mm, Default::default()),
19198                ),
19199            )
19200            .await;
19201        let cancelled = state
19202            .receive_rpc(
19203                Tid::from_raw(worker.as_raw()),
19204                (
19205                    worker_clock.clone(),
19206                    old_mm,
19207                    GlobalRequest::CancelExec(detpid),
19208                ),
19209            )
19210            .await;
19211        assert_eq!(cancelled, (None, GlobalResponse::CancelExec(())));
19212        assert!(state.pending_exec_states.lock().unwrap().is_empty());
19213        assert_eq!(state.global_time.lock().unwrap().as_nanos(), total);
19214        assert_eq!(
19215            state.global_time.lock().unwrap().threads_time(worker),
19216            worker_clock.as_nanos()
19217        );
19218
19219        let prepared = state
19220            .receive_rpc(
19221                Tid::from_raw(worker.as_raw()),
19222                (
19223                    worker_clock.clone(),
19224                    old_mm,
19225                    GlobalRequest::PrepareExec(detpid, old_mm, Default::default()),
19226                ),
19227            )
19228            .await;
19229        assert_eq!(prepared, (None, GlobalResponse::PrepareExec(())));
19230
19231        let mut fresh = DetTime::new(&config);
19232        let recreated = state
19233            .receive_rpc(
19234                Tid::from_raw(leader.as_raw()),
19235                (
19236                    fresh.clone(),
19237                    MmId::initial(leader),
19238                    GlobalRequest::CreateChildThread(
19239                        leader,
19240                        detpid,
19241                        0,
19242                        None,
19243                        libc::SIGCHLD,
19244                        None,
19245                        Some(DEFAULT_PRIORITY),
19246                    ),
19247                ),
19248            )
19249            .await;
19250        assert_eq!(
19251            recreated,
19252            (
19253                Some(worker_clock.as_nanos()),
19254                GlobalResponse::CreateChildThread(Some(old_mm.for_exec(detpid)))
19255            )
19256        );
19257        // A delayed request from the destroyed image must be rejected before
19258        // its absolute clock or its inherited metadata reaches accounting.
19259        let stale = state
19260            .receive_rpc(
19261                Tid::from_raw(leader.as_raw()),
19262                (
19263                    worker_clock.clone(),
19264                    old_mm,
19265                    GlobalRequest::RequestResources(Resources::new(leader), detpid),
19266                ),
19267            )
19268            .await;
19269        assert_eq!(stale, (None, GlobalResponse::ThreadExited));
19270        assert_eq!(state.global_time.lock().unwrap().as_nanos(), total);
19271
19272        fresh.advance_to(recreated.0.unwrap());
19273        assert_eq!(fresh.inherited_nanos(), LogicalTime::ZERO);
19274        let mut startup = Box::pin(state.receive_rpc(
19275            Tid::from_raw(leader.as_raw()),
19276            (
19277                fresh.clone(),
19278                old_mm.for_exec(detpid),
19279                GlobalRequest::StartNewThread(leader, detpid, None, None),
19280            ),
19281        ));
19282        assert!(futures::poll!(&mut startup).is_pending());
19283        assert!(futures::poll!(&mut startup).is_pending());
19284        let first = crate::scheduler::do_a_turn_blocking(
19285            state.sched.clone(),
19286            state.global_time.clone(),
19287            &Err(crate::scheduler::SkipTurn),
19288        )
19289        .await
19290        .expect("replacement leader must run");
19291        assert_eq!(first.tid, leader);
19292        assert_eq!(
19293            startup.await,
19294            (None, granted(GlobalResponse::StartNewThread(None)))
19295        );
19296        assert_eq!(state.global_time.lock().unwrap().as_nanos(), total);
19297
19298        fresh.advance_to(fresh.as_nanos() + LogicalTime::from_nanos(1));
19299        let observed = state
19300            .receive_rpc(
19301                Tid::from_raw(leader.as_raw()),
19302                (
19303                    fresh.clone(),
19304                    old_mm.for_exec(detpid),
19305                    GlobalRequest::GlobalTimeLowerBound,
19306                ),
19307            )
19308            .await;
19309        assert_eq!(
19310            observed,
19311            (
19312                None,
19313                GlobalResponse::GlobalTimeLowerBound(total + LogicalTime::from_nanos(1))
19314            )
19315        );
19316        let global = state.global_time.lock().unwrap();
19317        assert_eq!(global.threads_time(leader), fresh.as_nanos());
19318        assert!(!global.contains_thread(worker));
19319    }
19320
19321    #[test]
19322    fn live_registration_without_next_turn_is_not_terminal() {
19323        let (_, state, dettid, detpid) = cancellation_test_state();
19324        install_test_registration(&state, dettid, Ivar::new());
19325        state.sched.lock().unwrap().next_turns.remove(&dettid);
19326
19327        assert!(
19328            !state
19329                .sched
19330                .lock()
19331                .unwrap()
19332                .thread_is_logically_killed(dettid),
19333            "transient next-turn absence must not imply logical death"
19334        );
19335
19336        state
19337            .sched
19338            .lock()
19339            .unwrap()
19340            .logically_kill_thread(&dettid, &detpid, MmId::initial(detpid));
19341        assert!(
19342            state
19343                .sched
19344                .lock()
19345                .unwrap()
19346                .thread_is_logically_killed(dettid),
19347            "explicit logical death must install a permanent TID tombstone"
19348        );
19349    }
19350
19351    #[tokio::test]
19352    async fn exec_reconnect_retires_siblings_and_reuses_live_scheduler_and_clock_state() {
19353        let (config, state, dettid, detpid) = cancellation_test_state();
19354        let old_mm = MmId::initial(detpid).for_exec(detpid);
19355        install_test_registration(&state, dettid, Ivar::new());
19356        let sibling = DetTid::from_raw(dettid.as_raw() + 1);
19357        let sibling_request = Ivar::new();
19358        state
19359            .sched
19360            .lock()
19361            .unwrap()
19362            .thread_tree
19363            .add_child(dettid, sibling, false);
19364        install_test_registration(&state, sibling, sibling_request.clone());
19365        {
19366            let mut scheduler = state.sched.lock().unwrap();
19367            scheduler.next_turns.get_mut(&dettid).unwrap().resp =
19368                Ivar::full(SchedResponse::Go(None));
19369            scheduler
19370                .next_turns
19371                .get_mut(&sibling)
19372                .unwrap()
19373                .child_tid_addr = 0x1234;
19374        }
19375        let mut existing_time = DetTime::new(&config);
19376        existing_time.add_syscall();
19377        existing_time.add_syscall();
19378        state.global_time.lock().unwrap().update_global_time(
19379            dettid,
19380            existing_time.as_nanos(),
19381            LogicalTime::ZERO,
19382        );
19383        let (global_before, thread_before) = {
19384            let global_time = state.global_time.lock().unwrap();
19385            (global_time.as_nanos(), global_time.threads_time(dettid))
19386        };
19387        let fresh_local_time = DetTime::new(&config);
19388        let physical_pid = std::process::id() as i32;
19389        let physical_tid = unsafe { libc::syscall(libc::SYS_gettid) as i32 };
19390        let physical_ids = Some((physical_pid, physical_tid));
19391        state.pending_exec_states.lock().unwrap().insert(
19392            detpid,
19393            PendingExecState {
19394                caller: dettid,
19395                process: detpid,
19396                mm: old_mm,
19397                fd_blocking: Default::default(),
19398            },
19399        );
19400        state
19401            .sched
19402            .lock()
19403            .unwrap()
19404            .install_test_exec_incarnation(dettid, old_mm);
19405        let in_flight_exec_response = state
19406            .receive_rpc(
19407                reverie::Tid::from_raw(dettid.as_raw()),
19408                (
19409                    existing_time.clone(),
19410                    old_mm.for_exec(detpid),
19411                    GlobalRequest::ReportUnsupportedSyscall("exec-in-flight".to_owned()),
19412                ),
19413            )
19414            .await;
19415        assert_eq!(
19416            in_flight_exec_response.1,
19417            GlobalResponse::ReportUnsupportedSyscall(())
19418        );
19419
19420        let create_response = state
19421            .receive_rpc(
19422                reverie::Tid::from_raw(dettid.as_raw()),
19423                (
19424                    fresh_local_time.clone(),
19425                    MmId::initial(dettid),
19426                    GlobalRequest::CreateChildThread(
19427                        dettid,
19428                        detpid,
19429                        0,
19430                        None,
19431                        libc::SIGCHLD,
19432                        physical_ids,
19433                        Some(DEFAULT_PRIORITY),
19434                    ),
19435                ),
19436            )
19437            .await;
19438        assert_eq!(
19439            create_response,
19440            (
19441                Some(thread_before),
19442                GlobalResponse::CreateChildThread(Some(old_mm.for_exec(detpid)))
19443            )
19444        );
19445        assert_eq!(
19446            state.sched.lock().unwrap().physical_thread_identity(dettid),
19447            Some((old_mm.for_exec(detpid), physical_pid, physical_tid)),
19448            "post-exec host identity must be installed by CreateChildThread before admission"
19449        );
19450
19451        let start_response = state
19452            .receive_rpc(
19453                reverie::Tid::from_raw(dettid.as_raw()),
19454                (
19455                    fresh_local_time,
19456                    old_mm.for_exec(detpid),
19457                    GlobalRequest::StartNewThread(dettid, detpid, physical_ids, None),
19458                ),
19459            )
19460            .await;
19461        assert_eq!(
19462            start_response,
19463            (Some(thread_before), GlobalResponse::StartNewThread(None))
19464        );
19465
19466        let scheduler = state.sched.lock().unwrap();
19467        assert!(!scheduler.thread_is_logically_killed(dettid));
19468        assert!(scheduler.thread_is_logically_killed(sibling));
19469        assert_eq!(scheduler.next_turns.len(), 1);
19470        assert!(matches!(sibling_request.try_read(), Some(Err(_))));
19471        assert!(
19472            scheduler.child_tid_was_cleared(FutexID::private(old_mm, 0x1234), sibling.as_raw())
19473        );
19474        drop(scheduler);
19475        let global_time = state.global_time.lock().unwrap();
19476        assert_eq!(global_time.as_nanos(), global_before);
19477        assert_eq!(global_time.threads_time(dettid), thread_before);
19478    }
19479
19480    /// A waiter in another process, parked on a robust word the exec's caller
19481    /// owned, for the exec owner-death tests below.
19482    fn park_exec_robust_waiter(state: &GlobalState, futex: FutexID) -> DetTid {
19483        park_exec_robust_waiter_as(state, futex, 40)
19484    }
19485
19486    /// [`park_exec_robust_waiter`] for the thread `raw`.
19487    fn park_exec_robust_waiter_as(state: &GlobalState, futex: FutexID, raw: i32) -> DetTid {
19488        let waiter = DetTid::from_raw(raw);
19489        state
19490            .sched
19491            .lock()
19492            .unwrap()
19493            .thread_tree
19494            .add_child(waiter, waiter, true);
19495        install_test_registration(state, waiter, Ivar::new());
19496        let mut scheduler = state.sched.lock().unwrap();
19497        scheduler.sleep_futex_waiter(&waiter, futex, None, u32::MAX, None);
19498        assert!(scheduler.run_queue.remove_tid(waiter));
19499        waiter
19500    }
19501
19502    /// On a backend whose new image reconnects with a fresh runtime (DBT),
19503    /// the owner-death wakes the exec's caller read before the native exec
19504    /// travel with the prepared exec, and the reconnect admits their waiters
19505    /// at the next deterministic drain
19506    /// (https://github.com/rrnewton/hermit/issues/4268).
19507    #[tokio::test]
19508    async fn exec_reconnect_wakes_the_robust_waiters_its_exec_marked() {
19509        let (config, state, dettid, detpid) = cancellation_test_state();
19510        let old_mm = MmId::initial(detpid).for_exec(detpid);
19511        install_test_registration(&state, dettid, Ivar::new());
19512        let mut existing_time = DetTime::new(&config);
19513        existing_time.add_syscall();
19514        state.global_time.lock().unwrap().update_global_time(
19515            dettid,
19516            existing_time.as_nanos(),
19517            LogicalTime::ZERO,
19518        );
19519        let futex = FutexID::private(old_mm, 0x4000);
19520        let waiter = park_exec_robust_waiter(&state, futex);
19521        state.pending_exec_states.lock().unwrap().insert(
19522            detpid,
19523            PendingExecState {
19524                caller: dettid,
19525                process: detpid,
19526                mm: old_mm,
19527                fd_blocking: ExecFdState {
19528                    robust_wakes: vec![(dettid, futex)],
19529                    ..Default::default()
19530                },
19531            },
19532        );
19533        state
19534            .sched
19535            .lock()
19536            .unwrap()
19537            .install_test_exec_incarnation(dettid, old_mm);
19538        assert!(!state.sched.lock().unwrap().admission_pending(waiter));
19539
19540        let create_response = state
19541            .receive_rpc(
19542                reverie::Tid::from_raw(dettid.as_raw()),
19543                (
19544                    DetTime::new(&config),
19545                    MmId::initial(dettid),
19546                    GlobalRequest::CreateChildThread(
19547                        dettid,
19548                        detpid,
19549                        0,
19550                        None,
19551                        libc::SIGCHLD,
19552                        None,
19553                        Some(DEFAULT_PRIORITY),
19554                    ),
19555                ),
19556            )
19557            .await;
19558        assert_eq!(
19559            create_response.1,
19560            GlobalResponse::CreateChildThread(Some(old_mm.for_exec(detpid)))
19561        );
19562        assert!(state.sched.lock().unwrap().admission_pending(waiter));
19563        assert!(
19564            state
19565                .post_exec_fd_blocking
19566                .lock()
19567                .unwrap()
19568                .get(&dettid)
19569                .is_none_or(|carried| carried.robust_wakes.is_empty()),
19570            "the wakes are delivered once, at the reconnect"
19571        );
19572    }
19573
19574    /// An exec that fails before its point of no return (here ENOENT, which
19575    /// takes CancelExec) marks no robust word, so its prepared wakes are
19576    /// dropped with the prepared exec and the waiter stays parked. An exec
19577    /// that ends its group without a reconnect delivers them instead
19578    /// (`a_terminal_exec_wakes_its_robust_waiters`).
19579    #[tokio::test]
19580    async fn a_failed_exec_drops_its_robust_wakes() {
19581        let (config, state, leader, detpid) = cancellation_test_state();
19582        install_test_registration(&state, leader, Ivar::new());
19583        let futex = FutexID::private(MmId::initial(detpid), 0x4000);
19584        let waiter = park_exec_robust_waiter(&state, futex);
19585        let clock = DetTime::new(&config);
19586        let prepared = state
19587            .receive_rpc(
19588                reverie::Tid::from_raw(leader.as_raw()),
19589                (
19590                    clock.clone(),
19591                    MmId::initial(leader),
19592                    GlobalRequest::PrepareExec(
19593                        detpid,
19594                        MmId::initial(detpid),
19595                        Box::new(ExecFdState {
19596                            robust_wakes: vec![(leader, futex)],
19597                            ..Default::default()
19598                        }),
19599                    ),
19600                ),
19601            )
19602            .await;
19603        assert_eq!(prepared.1, GlobalResponse::PrepareExec(()));
19604        let cancelled = state
19605            .receive_rpc(
19606                reverie::Tid::from_raw(leader.as_raw()),
19607                (
19608                    clock,
19609                    MmId::initial(leader),
19610                    GlobalRequest::CancelExec(detpid),
19611                ),
19612            )
19613            .await;
19614        assert_eq!(cancelled.1, GlobalResponse::CancelExec(()));
19615        assert!(state.pending_exec_states.lock().unwrap().is_empty());
19616        assert!(!state.sched.lock().unwrap().admission_pending(waiter));
19617    }
19618
19619    /// An exec caller whose old image deregisters before any reconnect (its
19620    /// group killed during the exec) still had Linux walk the group's robust
19621    /// lists. The deregistration delivers the prepared wakes and holds turns
19622    /// until the kernel has ended the group, instead of dropping them with
19623    /// the preparation (Codex review of
19624    /// https://github.com/rrnewton/hermit/pull/4352, P2). This builds the
19625    /// deregistration directly: no guest action is known to reach it on DBT
19626    /// (an exec whose target cannot be loaded reconnects first, then hangs
19627    /// the run: <https://github.com/rrnewton/hermit/issues/4362>).
19628    #[tokio::test]
19629    async fn a_terminal_exec_wakes_its_robust_waiters() {
19630        let (config, state, leader, detpid) = cancellation_test_state();
19631        install_test_registration(&state, leader, Ivar::new());
19632        let (tid_tx, tid_rx) = std::sync::mpsc::channel();
19633        let (exit_tx, exit_rx) = std::sync::mpsc::channel::<()>();
19634        let host_thread = std::thread::spawn(move || {
19635            tid_tx
19636                .send(unsafe { libc::syscall(libc::SYS_gettid) as i32 })
19637                .unwrap();
19638            let _ = exit_rx.recv();
19639        });
19640        state
19641            .sched
19642            .lock()
19643            .unwrap()
19644            .register_physical_thread(
19645                leader,
19646                MmId::initial(detpid),
19647                std::process::id() as i32,
19648                tid_rx.recv().unwrap(),
19649            )
19650            .unwrap();
19651        let futex = FutexID::private(MmId::initial(detpid), 0x4000);
19652        let waiter = park_exec_robust_waiter(&state, futex);
19653        // A second waiter on the same word stays parked: one wake per marked
19654        // word (Claude re-check of
19655        // https://github.com/rrnewton/hermit/pull/4352, N3).
19656        let second = park_exec_robust_waiter_as(&state, futex, 41);
19657        let prepared = state
19658            .receive_rpc(
19659                reverie::Tid::from_raw(leader.as_raw()),
19660                (
19661                    DetTime::new(&config),
19662                    MmId::initial(leader),
19663                    GlobalRequest::PrepareExec(
19664                        detpid,
19665                        MmId::initial(detpid),
19666                        Box::new(ExecFdState {
19667                            robust_wakes: vec![(leader, futex)],
19668                            ..Default::default()
19669                        }),
19670                    ),
19671                ),
19672            )
19673            .await;
19674        assert_eq!(prepared.1, GlobalResponse::PrepareExec(()));
19675        assert!(!state.sched.lock().unwrap().admission_pending(waiter));
19676
19677        state
19678            .recv_deregister_thread(
19679                reverie::Tid::from_raw(leader.as_raw()),
19680                ThreadDeregistration {
19681                    thread_start_entered: true,
19682                    vfork_origin: None,
19683                    dettid: leader,
19684                    detpid,
19685                    mm: MmId::initial(detpid),
19686                    timeslice_stats: TimesliceStats::default(),
19687                    syscall_count: 0,
19688                    syscall_histogram: Default::default(),
19689                    chaos_epochs: Vec::new(),
19690                    killed_by_group_signal: false,
19691                    passed_descriptors_out: false,
19692                },
19693            )
19694            .await;
19695        assert!(state.pending_exec_states.lock().unwrap().is_empty());
19696        {
19697            let scheduler = state.sched.lock().unwrap();
19698            assert!(scheduler.admission_pending(waiter));
19699            assert!(!scheduler.admission_pending(second));
19700            assert!(scheduler.holds_turns_until_gone(leader));
19701        }
19702        exit_tx.send(()).unwrap();
19703        host_thread.join().unwrap();
19704    }
19705
19706    #[tokio::test]
19707    async fn nonleader_exec_rebinds_caller_to_leader_and_preserves_its_clock() {
19708        let (config, state, leader, detpid) = cancellation_test_state();
19709        let worker = DetTid::from_raw(leader.as_raw() + 1);
19710        let sibling = DetTid::from_raw(leader.as_raw() + 2);
19711        let old_mm = MmId::initial(detpid).for_exec(detpid);
19712        let leader_request = Ivar::new();
19713        let worker_request = Ivar::new();
19714        let sibling_request = Ivar::new();
19715        install_test_registration(&state, leader, leader_request.clone());
19716        {
19717            let mut scheduler = state.sched.lock().unwrap();
19718            scheduler.thread_tree.add_child(leader, worker, false);
19719            scheduler.thread_tree.add_child(leader, sibling, false);
19720        }
19721        install_test_registration(&state, worker, worker_request.clone());
19722        install_test_registration(&state, sibling, sibling_request.clone());
19723        {
19724            let mut scheduler = state.sched.lock().unwrap();
19725            scheduler
19726                .next_turns
19727                .get_mut(&sibling)
19728                .unwrap()
19729                .child_tid_addr = 0x5678;
19730            scheduler
19731                .timeslices
19732                .insert(leader, Some(LogicalTime::from_nanos(99)));
19733            scheduler.install_test_vfork_barrier(leader, sibling);
19734        }
19735
19736        let mut leader_clock = DetTime::new(&config);
19737        leader_clock.add_syscall();
19738        let mut worker_clock = DetTime::new(&config);
19739        worker_clock.add_syscall();
19740        worker_clock.add_syscall();
19741        worker_clock.add_syscall();
19742        {
19743            let mut global_time = state.global_time.lock().unwrap();
19744            global_time.update_global_time(leader, leader_clock.as_nanos(), LogicalTime::ZERO);
19745            global_time.update_global_time(worker, worker_clock.as_nanos(), LogicalTime::ZERO);
19746        }
19747        let total_before = state.global_time.lock().unwrap().as_nanos();
19748        let fd_blocking = ExecFdState {
19749            blocking: [42].into_iter().collect(),
19750            inherited_reads: std::collections::BTreeMap::from([(
19751                43,
19752                crate::fd::InheritedExternalRead {
19753                    origin: crate::types::OpenFileId::new(leader, 0),
19754                    dev: 1,
19755                    inode: 2,
19756                    rdev: 3,
19757                    file_type: libc::S_IFCHR,
19758                    access_mode: libc::O_RDONLY,
19759                    resource: Some(ResourceID::Device(crate::resources::Device::ContainerStdin)),
19760                },
19761            )]),
19762            initialize_inherited_stdio: false,
19763            initial_input: None,
19764            robust_wakes: Vec::new(),
19765            open_files: None,
19766            maps: None,
19767            procfs_records: None,
19768            channels: Default::default(),
19769            open_file_namespace: None,
19770            random_device_sequence: 0,
19771            exe: None,
19772        };
19773        state.pending_exec_states.lock().unwrap().insert(
19774            detpid,
19775            PendingExecState {
19776                caller: worker,
19777                process: detpid,
19778                mm: old_mm,
19779                fd_blocking: fd_blocking.clone(),
19780            },
19781        );
19782        let fresh_local_time = DetTime::new(&config);
19783
19784        let create_response = state
19785            .receive_rpc(
19786                reverie::Tid::from_raw(leader.as_raw()),
19787                (
19788                    fresh_local_time.clone(),
19789                    MmId::initial(leader),
19790                    GlobalRequest::CreateChildThread(
19791                        leader,
19792                        detpid,
19793                        0,
19794                        None,
19795                        libc::SIGCHLD,
19796                        None,
19797                        Some(DEFAULT_PRIORITY),
19798                    ),
19799                ),
19800            )
19801            .await;
19802        assert_eq!(
19803            create_response,
19804            (
19805                Some(worker_clock.as_nanos()),
19806                GlobalResponse::CreateChildThread(Some(old_mm.for_exec(detpid)))
19807            )
19808        );
19809        assert!(state.pending_exec_states.lock().unwrap().is_empty());
19810        assert_eq!(
19811            state.post_exec_fd_blocking.lock().unwrap().get(&leader),
19812            Some(&fd_blocking)
19813        );
19814
19815        let late_old_request = state
19816            .receive_rpc(
19817                reverie::Tid::from_raw(leader.as_raw()),
19818                (
19819                    leader_clock.clone(),
19820                    old_mm,
19821                    GlobalRequest::RequestResources(Resources::new(leader), detpid),
19822                ),
19823            )
19824            .await;
19825        assert_eq!(late_old_request, (None, GlobalResponse::ThreadExited));
19826        let admitted_before_fence = state
19827            .recv_request_resources(
19828                reverie::Tid::from_raw(leader.as_raw()),
19829                detpid,
19830                Resources::new(leader),
19831                Some(old_mm),
19832            )
19833            .await;
19834        assert_eq!(
19835            admitted_before_fence,
19836            (SchedulerRpcResult::ThreadExited, None)
19837        );
19838        let late_old_deregister = state
19839            .receive_rpc(
19840                reverie::Tid::from_raw(leader.as_raw()),
19841                (
19842                    leader_clock.clone(),
19843                    old_mm,
19844                    GlobalRequest::DeregisterThread(ThreadDeregistration {
19845                        thread_start_entered: true,
19846                        vfork_origin: None,
19847                        dettid: leader,
19848                        detpid,
19849                        mm: old_mm,
19850                        timeslice_stats: TimesliceStats::default(),
19851                        syscall_count: 0,
19852                        syscall_histogram: Default::default(),
19853                        chaos_epochs: Vec::new(),
19854                        killed_by_group_signal: false,
19855                        passed_descriptors_out: false,
19856                    }),
19857                ),
19858            )
19859            .await;
19860        assert_eq!(
19861            late_old_deregister,
19862            (None, GlobalResponse::DeregisterThread(()))
19863        );
19864        let duplicate_create = state
19865            .receive_rpc(
19866                reverie::Tid::from_raw(leader.as_raw()),
19867                (
19868                    fresh_local_time.clone(),
19869                    MmId::initial(leader),
19870                    GlobalRequest::CreateChildThread(
19871                        leader,
19872                        detpid,
19873                        0,
19874                        None,
19875                        libc::SIGCHLD,
19876                        None,
19877                        Some(DEFAULT_PRIORITY),
19878                    ),
19879                ),
19880            )
19881            .await;
19882        assert_eq!(duplicate_create, (None, GlobalResponse::ThreadExited));
19883
19884        {
19885            let scheduler = state.sched.lock().unwrap();
19886            assert!(!scheduler.thread_is_logically_killed(leader));
19887            assert!(scheduler.thread_is_logically_killed(worker));
19888            assert!(scheduler.thread_is_logically_killed(sibling));
19889            assert_eq!(scheduler.next_turns.len(), 1);
19890            assert!(scheduler.next_turns.contains_key(&leader));
19891            assert!(!scheduler.timeslices.contains_key(&leader));
19892            assert!(!scheduler.vfork_barrier_mentions(leader));
19893            assert!(!scheduler.vfork_barrier_mentions(sibling));
19894            assert!(matches!(leader_request.try_read(), Some(Err(_))));
19895            assert!(matches!(worker_request.try_read(), Some(Err(_))));
19896            assert!(matches!(sibling_request.try_read(), Some(Err(_))));
19897            assert!(
19898                scheduler
19899                    .child_tid_was_cleared(FutexID::private(old_mm, 0x5678), sibling.as_raw(),)
19900            );
19901        }
19902
19903        // Drive the real daemon through step2 rather than manually pre-filling
19904        // the replacement's response. This drains the destroyed leader's
19905        // physical removal and the same-raw-TID replacement admission before
19906        // StartNewThread supplies the fresh image's first request.
19907        let turn_sched = state.sched.clone();
19908        let turn_time = state.global_time.clone();
19909        let turn = tokio::spawn(async move {
19910            let last: Result<Resources, crate::scheduler::SkipTurn> =
19911                Err(crate::scheduler::SkipTurn);
19912            crate::scheduler::do_a_turn_blocking(turn_sched, turn_time, &last).await
19913        });
19914        let start_response = state
19915            .receive_rpc(
19916                reverie::Tid::from_raw(leader.as_raw()),
19917                (
19918                    fresh_local_time,
19919                    old_mm.for_exec(detpid),
19920                    GlobalRequest::StartNewThread(leader, detpid, None, None),
19921                ),
19922            )
19923            .await;
19924        assert!(
19925            turn.await
19926                .expect("replacement scheduler turn panicked")
19927                .is_ok(),
19928            "replacement leader did not survive the first step2 drain"
19929        );
19930        assert_eq!(
19931            start_response,
19932            (
19933                Some(worker_clock.as_nanos()),
19934                granted(GlobalResponse::StartNewThread(None))
19935            )
19936        );
19937        {
19938            let scheduler = state.sched.lock().unwrap();
19939            assert_eq!(
19940                scheduler
19941                    .run_queue
19942                    .tids()
19943                    .filter(|dettid| **dettid == leader)
19944                    .count(),
19945                1
19946            );
19947            assert!(!scheduler.run_queue.contains_tid(worker));
19948            assert!(!scheduler.run_queue.contains_tid(sibling));
19949        }
19950        {
19951            let global_time = state.global_time.lock().unwrap();
19952            assert_eq!(global_time.as_nanos(), total_before);
19953            assert_eq!(global_time.threads_time(leader), worker_clock.as_nanos());
19954            assert!(!global_time.contains_thread(worker));
19955        }
19956
19957        let mark_response = state
19958            .receive_rpc(
19959                reverie::Tid::from_raw(leader.as_raw()),
19960                (
19961                    worker_clock,
19962                    old_mm.for_exec(detpid),
19963                    GlobalRequest::MarkPastFirstExecve(detpid, None, None, None),
19964                ),
19965            )
19966            .await;
19967        assert_eq!(
19968            mark_response.1,
19969            GlobalResponse::MarkPastFirstExecve(Box::new(fd_blocking))
19970        );
19971        assert!(state.post_exec_fd_blocking.lock().unwrap().is_empty());
19972    }
19973
19974    async fn start_external_image(
19975        tool: &Detcore,
19976        guest: &mut ExternalRegistrationGuest<'_>,
19977        expected_grants: &[ResourceID],
19978    ) {
19979        use reverie::Tool;
19980
19981        let state = guest.global;
19982        let tid = guest.thread.dettid;
19983        tokio::time::timeout(Duration::from_secs(2), async {
19984            let mut start = std::pin::pin!(tool.handle_thread_start(guest));
19985            assert!(futures::poll!(start.as_mut()).is_pending());
19986            // Initial self-registration requests ParentContinue before
19987            // StartNewThread. Reload registration has no parent turn.
19988            // Drive these actual grants, without fabricating responses.
19989            let grants = async {
19990                let mut last = Err(crate::scheduler::SkipTurn);
19991                for expected in expected_grants {
19992                    let granted = crate::scheduler::do_a_turn_blocking(
19993                        state.sched.clone(),
19994                        state.global_time.clone(),
19995                        &last,
19996                    )
19997                    .await
19998                    .expect("image admission must receive its scheduler grant");
19999                    assert_eq!(granted.tid, tid);
20000                    assert!(granted.resources.contains_key(expected));
20001                    last = Ok(granted);
20002                }
20003            };
20004            let (started, ()) = tokio::join!(start, grants);
20005            started.unwrap();
20006        })
20007        .await
20008        .expect("image admission must finish without guest instructions");
20009    }
20010
20011    // Startup's posthook reads the backend clock even before guest instructions.
20012    // This dedicated fixture reports exactly zero and counts those reads;
20013    // the older ExternalRegistrationGuest's host-clock refusal stays intact.
20014    struct BoundaryAdmissionGuest<'a> {
20015        inner: ExternalRegistrationGuest<'a>,
20016        clock_reads: usize,
20017    }
20018
20019    impl<'a> std::ops::Deref for BoundaryAdmissionGuest<'a> {
20020        type Target = ExternalRegistrationGuest<'a>;
20021        fn deref(&self) -> &Self::Target {
20022            &self.inner
20023        }
20024    }
20025    impl std::ops::DerefMut for BoundaryAdmissionGuest<'_> {
20026        fn deref_mut(&mut self) -> &mut Self::Target {
20027            &mut self.inner
20028        }
20029    }
20030
20031    #[reverie::tool]
20032    impl GlobalRPC<GlobalState> for BoundaryAdmissionGuest<'_> {
20033        async fn send_rpc(
20034            &self,
20035            message: <GlobalState as GlobalTool>::Request,
20036        ) -> <GlobalState as GlobalTool>::Response {
20037            self.inner.send_rpc(message).await
20038        }
20039        fn config(&self) -> &Config {
20040            self.inner.config
20041        }
20042    }
20043
20044    #[reverie::tool]
20045    impl Guest<Detcore> for BoundaryAdmissionGuest<'_> {
20046        type Memory = reverie::syscalls::LocalMemory;
20047        type Stack = ExternalRegistrationStack;
20048        fn tid(&self) -> reverie::Pid {
20049            self.inner.tid()
20050        }
20051        fn pid(&self) -> reverie::Pid {
20052            self.inner.pid()
20053        }
20054        fn ppid(&self) -> Option<reverie::Pid> {
20055            self.inner.ppid()
20056        }
20057        fn memory(&self) -> Self::Memory {
20058            self.inner.memory()
20059        }
20060        fn thread_state_mut(&mut self) -> &mut crate::ThreadState<()> {
20061            &mut self.inner.thread
20062        }
20063        fn thread_state(&self) -> &crate::ThreadState<()> {
20064            &self.inner.thread
20065        }
20066        async fn regs(&mut self) -> libc::user_regs_struct {
20067            self.inner.regs().await
20068        }
20069        async fn stack(&mut self) -> Self::Stack {
20070            self.inner.stack().await
20071        }
20072        async fn daemonize(&mut self) {
20073            self.inner.daemonize().await
20074        }
20075        async fn inject<S: reverie::syscalls::SyscallInfo>(
20076            &mut self,
20077            syscall: S,
20078        ) -> Result<i64, reverie::syscalls::Errno> {
20079            self.inner.inject(syscall).await
20080        }
20081        async fn tail_inject<S: reverie::syscalls::SyscallInfo>(
20082            &mut self,
20083            syscall: S,
20084        ) -> reverie::Never {
20085            self.inner.tail_inject(syscall).await
20086        }
20087        fn set_timer(&mut self, schedule: reverie::TimerSchedule) -> Result<(), reverie::Error> {
20088            self.inner.set_timer(schedule)
20089        }
20090        fn set_timer_precise(
20091            &mut self,
20092            schedule: reverie::TimerSchedule,
20093        ) -> Result<(), reverie::Error> {
20094            self.inner.set_timer_precise(schedule)
20095        }
20096        fn read_clock(&mut self) -> Result<u64, reverie::Error> {
20097            self.clock_reads += 1;
20098            Ok(0)
20099        }
20100    }
20101
20102    #[tokio::test]
20103    async fn boundary_actual_root_admission_keeps_registration_before_start_and_one_reset() {
20104        use detcore_model::schedule::boundary::Boundary;
20105        use detcore_model::schedule::boundary::Cause;
20106        use detcore_model::schedule::boundary::Entry;
20107        use detcore_model::schedule::boundary::Record;
20108        use detcore_model::schedule::boundary::Schedule;
20109        use reverie::Tool;
20110
20111        use crate::preemptions::boundary_recording::BoundaryRecording;
20112        use crate::preemptions::boundary_recording::Recording;
20113
20114        for timers in [true, false] {
20115            let mut trajectories = Vec::new();
20116            for mode in 0..3 {
20117                let temp = tempfile::tempdir().unwrap();
20118                let root = super::ROOT_DETPID;
20119                let mut expected =
20120                    crate::slice_boundary::grant::tests::budget(root, MmId::initial(root), 0)
20121                        .expected();
20122                expected.checkin = 3;
20123                expected.entry = Entry::Cpuid;
20124                expected.cause = Cause::SoftwareCheckin;
20125                if !timers {
20126                    expected.entry = Entry::Syscall {
20127                        syscall: reverie::syscalls::Sysno::sched_yield,
20128                        occurrence: 1,
20129                    };
20130                    expected.cause = Cause::ExplicitSchedYield;
20131                }
20132                let input = temp.path().join("input");
20133                Recording::Boundaries(
20134                    BoundaryRecording::new(
20135                        PreemptionRecord::from_sched_events(vec![]),
20136                        Schedule::new(vec![Record::TimesliceEnd(Boundary {
20137                            identity: expected,
20138                            count: 1,
20139                            start_rip: None,
20140                            end_rip: None,
20141                            end_time: None,
20142                        })])
20143                        .unwrap(),
20144                    )
20145                    .unwrap(),
20146                )
20147                .write(&input)
20148                .unwrap();
20149                let config = Config {
20150                    sequentialize_threads: true,
20151                    no_rcb_time: true,
20152                    max_timeslice: if timers {
20153                        Config::default().max_timeslice
20154                    } else {
20155                        None
20156                    },
20157                    record_preemptions: mode == 1,
20158                    strict_schedule_boundaries: mode != 0,
20159                    record_preemptions_to: (mode == 1).then(|| temp.path().join("output")),
20160                    replay_schedule_from: (mode == 2).then_some(input),
20161                    ..Config::default()
20162                }
20163                .with_backend(|backend| backend.delivers_preemption_timer_events = false);
20164                let state = GlobalState::initialize(&config, false);
20165                let tool = Detcore::new(Tid::from_raw(root.as_raw()), &config);
20166                let mut guest = BoundaryAdmissionGuest {
20167                    inner: ExternalRegistrationGuest {
20168                        global: &state,
20169                        config: &config,
20170                        thread: tool.init_thread_state(Tid::from_raw(root.as_raw()), None),
20171                        requests: Mutex::new(Vec::new()),
20172                        post_exec: false,
20173                        ppid: None,
20174                        private_memory: None,
20175                    },
20176                    clock_reads: 0,
20177                };
20178                guest.thread.detpid = Some(root);
20179                assert!(guest.thread.slice_boundary.is_none());
20180                tokio::time::timeout(Duration::from_secs(2), async {
20181                    let mut start = std::pin::pin!(tool.handle_thread_start(&mut guest));
20182                    assert!(futures::poll!(start.as_mut()).is_pending());
20183                    let grants = async {
20184                        let first = crate::scheduler::do_a_turn_blocking(
20185                            state.sched.clone(),
20186                            state.global_time.clone(),
20187                            &Err(crate::scheduler::SkipTurn),
20188                        )
20189                        .await
20190                        .unwrap();
20191                        assert_eq!(first.resources.len(), 1);
20192                        assert!(first.resources.contains_key(&ResourceID::ParentContinue {
20193                            parent: root,
20194                            child: root,
20195                        }));
20196                        // Poll the real startup continuation concurrently. Wait for
20197                        // its next actual request before inspecting the mailbox.
20198                        let next = state.sched.lock().unwrap().next_turns[&root].req.clone();
20199                        let pending = next.get().await.unwrap();
20200                        assert!(
20201                            pending
20202                                .resources
20203                                .contains_key(&ResourceID::MemAddrSpace(root))
20204                        );
20205                        if mode == 2 {
20206                            let mut different = expected;
20207                            different.committed_rcbs = 1;
20208                            let schedule = Schedule::new(vec![Record::TimesliceEnd(Boundary {
20209                                identity: different,
20210                                count: 1,
20211                                start_rip: None,
20212                                end_rip: None,
20213                                end_time: None,
20214                            })])
20215                            .unwrap();
20216                            let different =
20217                                crate::slice_boundary::grant::Budget::at(&schedule, 0).unwrap();
20218                            // A conflicting queue would succeed if ParentContinue had
20219                            // consumed the pending budget. It must still be present.
20220                            assert!(
20221                                state
20222                                    .sched
20223                                    .lock()
20224                                    .unwrap()
20225                                    .boundary_budgets
20226                                    .queue(MmId::initial(root), different)
20227                                    .is_err()
20228                            );
20229                        }
20230                        let second = crate::scheduler::do_a_turn_blocking(
20231                            state.sched.clone(),
20232                            state.global_time.clone(),
20233                            &Ok(first),
20234                        )
20235                        .await
20236                        .unwrap();
20237                        assert_eq!(second.resources.len(), 1);
20238                        assert!(
20239                            second
20240                                .resources
20241                                .contains_key(&ResourceID::MemAddrSpace(root))
20242                        );
20243                    };
20244                    let (started, ()) = tokio::join!(start, grants);
20245                    started.unwrap();
20246                })
20247                .await
20248                .expect("the two original admission grants must finish");
20249                assert_eq!(guest.thread.stats.timeslice_count, u64::from(timers));
20250                let requests = guest.requests.lock().unwrap();
20251                let registration = requests
20252                    .iter()
20253                    .position(|r| matches!(r, GlobalRequest::CreateChildThread(..)))
20254                    .unwrap();
20255                let start = requests
20256                    .iter()
20257                    .position(|r| matches!(r, GlobalRequest::StartNewThread(..)))
20258                    .unwrap();
20259                assert!(registration < start);
20260                if mode != 0 {
20261                    let observed =
20262                        serde_json::to_value(guest.thread.slice_boundary.as_ref().unwrap())
20263                            .unwrap();
20264                    assert_eq!(observed["start_reset_done"], true);
20265                    assert_eq!(observed["checkin"], 2);
20266                    assert_eq!(observed["completed_slices"], 0);
20267                    assert_eq!(observed["current"][0], "ThreadStart");
20268                    assert_eq!(observed["current"][1], "Posthook");
20269                    assert_eq!(observed["pending_start_budget"], serde_json::Value::Null);
20270                    assert_eq!(observed["expected_budget"].is_null(), mode != 2);
20271                }
20272                let sched = state.sched.lock().unwrap();
20273                trajectories.push((
20274                    sched.turn,
20275                    sched.committed_time,
20276                    guest.thread.thread_logical_time.as_nanos(),
20277                    guest.thread.stats.timeslice_count,
20278                    guest.thread.committed_clock_value,
20279                    guest.thread.scheduler_grants,
20280                    guest.clock_reads,
20281                ));
20282            }
20283            assert_eq!(trajectories[0], trajectories[1]);
20284            assert_eq!(trajectories[0], trajectories[2]);
20285        }
20286    }
20287
20288    #[test]
20289    fn boundary_self_registration_is_not_a_parent_child_or_ordinary_grant() {
20290        let root = super::ROOT_DETPID;
20291        let request =
20292            GlobalRequest::CreateChildThread(root, root, 0, None, libc::SIGCHLD, None, None);
20293        assert!(super::registers_own_image(root, &request));
20294        assert!(!super::registers_own_image(DetTid::from_raw(17), &request));
20295        assert!(super::registers_own_image(
20296            root,
20297            &GlobalRequest::WithForwardedCount(4, Box::new(request))
20298        ));
20299        assert!(!super::registers_own_image(
20300            root,
20301            &GlobalRequest::StartNewThread(root, root, None, None)
20302        ));
20303        assert!(!super::registers_own_image(
20304            root,
20305            &GlobalRequest::ResumeExec(root)
20306        ));
20307        assert!(!super::registers_own_image(
20308            root,
20309            &GlobalRequest::GlobalTimeLowerBound
20310        ));
20311        let vfork = GlobalRequest::CreateVforkChildThread(
20312            root,
20313            root,
20314            DetTid::from_raw(17),
20315            0,
20316            CloneFlags::CLONE_VFORK | CloneFlags::CLONE_VM,
20317            libc::SIGCHLD,
20318            None,
20319        );
20320        assert!(!super::registers_own_image(root, &vfork));
20321        assert!(super::registers_own_image(DetTid::from_raw(17), &vfork));
20322
20323        let mut thread = crate::ThreadState::new(root, &Config::default(), ());
20324        let owner = crate::slice_boundary::lifecycle::Registry::default()
20325            .fresh(root, thread.mm_id)
20326            .unwrap();
20327        assert!(crate::slice_boundary::adopt_owner(&mut thread, owner, false).is_err());
20328        assert!(thread.slice_boundary.is_none());
20329    }
20330
20331    /// A reloading backend supplies a real in-tree parent for a fresh image.
20332    /// That image must reconnect its prepared exec before ordinary requests can
20333    /// publish the replacement native memory owner.
20334    #[tokio::test]
20335    async fn a_reloaded_descendant_reconnects_before_publishing_its_new_memory_owner() {
20336        use reverie::Tool;
20337
20338        let parent = super::ROOT_DETPID;
20339        let child = DetTid::from_raw(17);
20340        for (case, old_mm) in [
20341            ("first exec", MmId::initial(child)),
20342            ("later exec", MmId::initial(child).for_exec(child)),
20343        ] {
20344            let config = Config {
20345                sequentialize_threads: true,
20346                max_timeslice: None,
20347                no_rcb_time: true,
20348                ..Config::default()
20349            }
20350            .with_backend(|backend| backend.needs_killed_thread_rpc_cancellation = true);
20351            let state = GlobalState::initialize(&config, false);
20352            let facts = |epoch| {
20353                reverie::RuntimePrivateMemory::Ready(reverie::RuntimePrivateMemorySnapshot {
20354                    owner: [1, 2, 3, epoch],
20355                    version: 1,
20356                    regions: vec![reverie::RuntimePrivateMemoryRegion {
20357                        start: 0x1000,
20358                        end: 0x2000,
20359                    }],
20360                })
20361            };
20362            // Seed the already registered descendant's old image. The exec and
20363            // replacement-image admission below use actual global RPCs and grants.
20364            {
20365                let mut sched = state.sched.lock().unwrap();
20366                sched.thread_tree.add_child(parent, parent, true);
20367                sched.thread_tree.add_child(parent, child, true);
20368                assert_eq!(sched.thread_tree.process_of(child), Some(child), "{case}");
20369                assert_eq!(
20370                    sched.thread_tree.parent_process(&child),
20371                    Some(parent),
20372                    "{case}"
20373                );
20374                sched.record_thread_mm(child, old_mm);
20375                if old_mm != MmId::initial(child) {
20376                    sched.install_test_exec_incarnation(child, old_mm);
20377                }
20378            }
20379            install_test_registration(&state, child, Ivar::new());
20380            let tool = Detcore::new(reverie::Pid::from_raw(child.as_raw()), &config);
20381            let mut old = ExternalRegistrationGuest {
20382                global: &state,
20383                config: &config,
20384                thread: tool.init_thread_state(Tid::from_raw(child.as_raw()), None),
20385                requests: Mutex::new(Vec::new()),
20386                post_exec: false,
20387                ppid: Some(reverie::Pid::from_raw(parent.as_raw())),
20388                private_memory: Some(facts(1)),
20389            };
20390            old.thread.detpid = Some(child);
20391            old.thread.mm_id = old_mm;
20392            old.thread.past_global_first_execve = true;
20393            start_external_image(&tool, &mut old, &[ResourceID::MemAddrSpace(child)]).await;
20394            super::send_and_update_time(&mut old, GlobalRequest::GlobalTimeLowerBound).await;
20395            assert_eq!(
20396                state.sched.lock().unwrap().runtime_private_memory(old_mm),
20397                Some(facts(1)),
20398                "{case}"
20399            );
20400            super::prepare_exec(&mut old, old_mm, Default::default(), Vec::new(), None).await;
20401            assert!(
20402                state
20403                    .pending_exec_states
20404                    .lock()
20405                    .unwrap()
20406                    .contains_key(&child),
20407                "{case}"
20408            );
20409            // Assert the old image's authority before the successor's first
20410            // request. The obsolete key need not remain Ready after exec.
20411            assert_eq!(
20412                state.sched.lock().unwrap().runtime_private_memory(old_mm),
20413                Some(facts(1)),
20414                "{case}: prepared old image"
20415            );
20416            let registered_children = state
20417                .sched
20418                .lock()
20419                .unwrap()
20420                .thread_tree
20421                .get_children(&parent)
20422                .clone();
20423            drop(old);
20424
20425            let mut successor = ExternalRegistrationGuest {
20426                global: &state,
20427                config: &config,
20428                thread: tool.init_thread_state(Tid::from_raw(child.as_raw()), None),
20429                requests: Mutex::new(Vec::new()),
20430                post_exec: false,
20431                ppid: Some(reverie::Pid::from_raw(parent.as_raw())),
20432                private_memory: Some(facts(2)),
20433            };
20434            successor.thread.detpid = Some(child);
20435            assert!(!successor.is_root_process(), "{case}");
20436            assert!(!successor.thread.past_global_first_execve, "{case}");
20437            start_external_image(&tool, &mut successor, &[ResourceID::MemAddrSpace(child)]).await;
20438            let new_mm = old_mm.for_exec(child);
20439            assert_eq!(successor.thread.mm_id, new_mm, "{case}");
20440            assert!(
20441                state.pending_exec_states.lock().unwrap().is_empty(),
20442                "{case}"
20443            );
20444            super::send_and_update_time(&mut successor, GlobalRequest::GlobalTimeLowerBound).await;
20445            let sched = state.sched.lock().unwrap();
20446            assert_eq!(sched.thread_mm(child), Some(new_mm), "{case}");
20447            assert_eq!(
20448                sched.thread_tree.get_children(&parent),
20449                &registered_children,
20450                "{case}: exec is not a new child birth"
20451            );
20452            assert_eq!(sched.thread_tree.process_of(child), Some(child), "{case}");
20453            assert_eq!(
20454                sched.thread_tree.parent_process(&child),
20455                Some(parent),
20456                "{case}"
20457            );
20458            assert_eq!(
20459                sched.runtime_private_memory(new_mm),
20460                Some(facts(2)),
20461                "{case}"
20462            );
20463            let requests = successor.requests.lock().unwrap();
20464            assert_eq!(
20465                requests
20466                    .iter()
20467                    .filter(|request| matches!(request, GlobalRequest::CreateChildThread(..)))
20468                    .count(),
20469                1,
20470                "{case}"
20471            );
20472        }
20473    }
20474
20475    #[tokio::test]
20476    async fn reloaded_post_exec_restores_prepared_input_aliases_once() {
20477        use nix::fcntl::OFlag;
20478        use reverie::Tool;
20479
20480        let config = Config {
20481            sequentialize_threads: true,
20482            max_timeslice: None,
20483            ..Config::default()
20484        }
20485        .with_backend(|backend| {
20486            backend.needs_killed_thread_rpc_cancellation = true;
20487            backend.tool_shares_guest_descriptor_table = true;
20488        });
20489        let mut state = GlobalState::initialize(&config, false);
20490        let leader = super::ROOT_DETPID;
20491        let tool = Detcore::new(reverie::Pid::from_raw(leader.as_raw()), &config);
20492
20493        // Use the real launch-input owner and its registered-root seed, in a
20494        // privately owned slot rather than the test harness's stdin.
20495        let reader: OwnedFd = std::fs::File::open("/dev/null").unwrap().into();
20496        let fd = reader.as_raw_fd();
20497        let input = reverie::InheritedInput::pipe(fd).unwrap();
20498        let identity = input.identity().clone();
20499        state.install_inherited_input(input.lease().unwrap());
20500        let (source, _writer) = input.into_reader_and_writer();
20501        // SAFETY: dup2 replaces only this test's owned destination.
20502        assert_eq!(unsafe { libc::dup2(source.as_raw_fd(), fd) }, fd);
20503        drop(source);
20504        let alias = reader.try_clone().unwrap();
20505        let cloexec = reader.try_clone().unwrap();
20506        let alias_fd = alias.as_raw_fd();
20507        let cloexec_fd = cloexec.as_raw_fd();
20508        // SAFETY: only the test-owned alias's descriptor flag is changed.
20509        assert_eq!(unsafe { libc::fcntl(alias_fd, libc::F_SETFD, 0) }, 0);
20510        let flags = |fd| {
20511            // SAFETY: both no-argument queries inspect owned descriptors.
20512            let result = unsafe {
20513                (
20514                    libc::fcntl(fd, libc::F_GETFL),
20515                    libc::fcntl(fd, libc::F_GETFD),
20516                )
20517            };
20518            assert!(result.0 >= 0 && result.1 >= 0);
20519            result
20520        };
20521        let kernel_before = [flags(fd), flags(alias_fd)];
20522        assert_eq!(
20523            kernel_before[0].0 & (libc::O_ACCMODE | libc::O_NONBLOCK),
20524            libc::O_RDONLY
20525        );
20526        assert_eq!(flags(cloexec_fd).1 & libc::FD_CLOEXEC, libc::FD_CLOEXEC);
20527
20528        let mut old = ExternalRegistrationGuest {
20529            global: &state,
20530            config: &config,
20531            thread: tool.init_thread_state(Tid::from_raw(leader.as_raw()), None),
20532            requests: Mutex::new(Vec::new()),
20533            post_exec: true,
20534            ppid: None,
20535            private_memory: None,
20536        };
20537        old.thread.detpid = Some(leader);
20538        start_external_image(
20539            &tool,
20540            &mut old,
20541            &[
20542                ResourceID::ParentContinue {
20543                    parent: leader,
20544                    child: leader,
20545                },
20546                ResourceID::MemAddrSpace(leader),
20547            ],
20548        )
20549        .await;
20550        tool.handle_post_exec(&mut old).await.unwrap();
20551        assert!(state.inherited_input.lock().unwrap().is_none());
20552        old.thread.dup_fd(fd, alias_fd, OFlag::empty()).unwrap();
20553        old.thread.dup_fd(fd, cloexec_fd, OFlag::O_CLOEXEC).unwrap();
20554        let origin = old.thread.file_metadata.lock().unwrap().file_handles[&fd]
20555            .inherited_external_read()
20556            .expect("the actual registered launch seed must establish read provenance");
20557        assert_eq!(
20558            (origin.dev, origin.inode, origin.rdev),
20559            (identity.dev(), identity.inode(), identity.rdev())
20560        );
20561        let old_mm = old.thread.mm_id;
20562        super::prepare_exec(&mut old, old_mm, Default::default(), Vec::new(), None).await;
20563        {
20564            let pending = state.pending_exec_states.lock().unwrap();
20565            let prepared = &pending[&leader].fd_blocking.inherited_reads;
20566            assert_eq!(
20567                prepared.keys().copied().collect::<BTreeSet<_>>(),
20568                BTreeSet::from([fd, alias_fd])
20569            );
20570            assert!(prepared.values().all(|entry| entry == &origin));
20571            assert!(!prepared.contains_key(&cloexec_fd));
20572        }
20573        // Model the physical successful-exec boundary: the old Tool state is
20574        // gone and Linux has closed the CLOEXEC alias. No native exec is claimed.
20575        drop(old);
20576        drop(cloexec);
20577        let mut successor = ExternalRegistrationGuest {
20578            global: &state,
20579            config: &config,
20580            thread: tool.init_thread_state(Tid::from_raw(leader.as_raw()), None),
20581            requests: Mutex::new(Vec::new()),
20582            post_exec: true,
20583            ppid: None,
20584            private_memory: None,
20585        };
20586        successor.thread.detpid = Some(leader);
20587        start_external_image(&tool, &mut successor, &[ResourceID::MemAddrSpace(leader)]).await;
20588        assert_eq!(successor.thread.mm_id, old_mm.for_exec(leader));
20589        assert!(state.pending_exec_states.lock().unwrap().is_empty());
20590        assert_eq!(
20591            state.post_exec_fd_blocking.lock().unwrap()[&leader]
20592                .inherited_reads
20593                .len(),
20594            2
20595        );
20596        assert!(
20597            !successor
20598                .thread
20599                .file_metadata
20600                .lock()
20601                .unwrap()
20602                .file_handles
20603                .contains_key(&fd)
20604        );
20605
20606        // This is the actual Tool callback, not a raw completion-RPC check or
20607        // a direct metadata restore. Empty auxv deliberately excludes AT_RANDOM.
20608        tool.handle_post_exec(&mut successor).await.unwrap();
20609        assert!(state.post_exec_fd_blocking.lock().unwrap().is_empty());
20610        {
20611            let metadata = successor.thread.file_metadata.lock().unwrap();
20612            for slot in [fd, alias_fd] {
20613                let entry = metadata
20614                    .file_handles
20615                    .get(&slot)
20616                    .expect("post-exec must restore every prepared surviving input alias");
20617                assert_eq!(
20618                    entry.inherited_external_read(),
20619                    Some(origin.clone()),
20620                    "post-exec must restore transferred origin, not plain pipe discovery"
20621                );
20622                assert_eq!(entry.ty(), crate::fd::FdType::Pipe);
20623                assert!(!entry.is_nonblocking());
20624                assert!(!entry.physically_nonblocking());
20625                assert!(!entry.is_cloexec());
20626                assert_eq!(entry.resource(), origin.resource);
20627            }
20628            assert_eq!(
20629                metadata.file_handles[&fd].open_file_id(),
20630                metadata.file_handles[&alias_fd].open_file_id()
20631            );
20632            assert_eq!(metadata.file_handles[&fd].open_file_alias_count(), 2);
20633            assert!(!metadata.file_handles.contains_key(&cloexec_fd));
20634        }
20635        assert_eq!([flags(fd), flags(alias_fd)], kernel_before);
20636        let snapshot =
20637            || serde_json::to_value(&*successor.thread.file_metadata.lock().unwrap()).unwrap();
20638        let before_rpc = snapshot();
20639        let response = successor
20640            .send_rpc((
20641                successor.thread.thread_logical_time.clone(),
20642                successor.thread.mm_id,
20643                GlobalRequest::GlobalTimeLowerBound,
20644            ))
20645            .await;
20646        assert!(matches!(
20647            response.1,
20648            GlobalResponse::GlobalTimeLowerBound(_)
20649        ));
20650        assert_eq!(
20651            snapshot(),
20652            before_rpc,
20653            "ordinary admitted RPC must leave restored metadata unchanged"
20654        );
20655        assert!(state.post_exec_fd_blocking.lock().unwrap().is_empty());
20656        assert!(state.pending_exec_states.lock().unwrap().is_empty());
20657        let requests = successor.requests.lock().unwrap();
20658        assert_eq!(
20659            requests
20660                .iter()
20661                .filter(|r| matches!(r, GlobalRequest::MarkPastFirstExecve(..)))
20662                .count(),
20663            1
20664        );
20665        assert!(matches!(
20666            requests.first(),
20667            Some(GlobalRequest::CreateChildThread(..))
20668        ));
20669        assert!(matches!(
20670            requests.get(1),
20671            Some(GlobalRequest::StartNewThread(..))
20672        ));
20673        assert!(matches!(
20674            requests.last(),
20675            Some(GlobalRequest::GlobalTimeLowerBound)
20676        ));
20677    }
20678
20679    #[tokio::test]
20680    async fn post_exec_deletes_armed_disarmed_and_non_notifying_timers() {
20681        use reverie::Tool;
20682
20683        let config = Config {
20684            sequentialize_threads: true,
20685            // This test enters one callback while the caller owns its turn;
20686            // no guest instructions or PMU timer are needed to drive it.
20687            max_timeslice: None,
20688            ..Config::default()
20689        }
20690        .with_backend(|backend| {
20691            backend.needs_killed_thread_rpc_cancellation = true;
20692        });
20693        let state = GlobalState::initialize(&config, false);
20694        let leader = DetTid::from_raw(17);
20695        let detpid = DetPid::from_raw(17);
20696        state
20697            .sched
20698            .lock()
20699            .unwrap()
20700            .thread_tree
20701            .add_child(leader, leader, true);
20702        let next_request = Ivar::new();
20703        install_test_registration(&state, leader, next_request.clone());
20704        let tool = Detcore::new(reverie::Pid::from_raw(leader.as_raw()), &config);
20705        let mut thread = tool.init_thread_state(Tid::from_raw(leader.as_raw()), None);
20706        thread.detpid = Some(detpid);
20707        // The exec's own syscall, intercepted and counted, as a backend that
20708        // traces the guest counts it before post-exec. (A backend that starts
20709        // the guest already executing reaches its first post-exec with nothing
20710        // counted, and post-exec charges the execve itself.)
20711        thread.stats.count_syscall(Sysno::execve);
20712        thread.thread_logical_time.add_syscall_with_cost(137);
20713        let mut guest = ExternalRegistrationGuest {
20714            global: &state,
20715            config: &config,
20716            thread,
20717            requests: Mutex::new(Vec::new()),
20718            post_exec: true,
20719            ppid: None,
20720            private_memory: None,
20721        };
20722        let t = LogicalTime::from_nanos;
20723        let [periodic, disarmed, silent] = {
20724            let mut timers = guest.thread.posix_timers.lock().unwrap();
20725            let periodic = timers.create(Some(libc::SIGUSR2), None, None, false);
20726            let disarmed = timers.create(Some(libc::SIGALRM), None, None, false);
20727            let silent = timers.create(None, None, None, false);
20728            timers.settime(periodic, 50, Some(t(100)), t(0));
20729            timers.settime(silent, 0, Some(t(200)), t(0));
20730            [periodic, disarmed, silent]
20731        };
20732
20733        // Drive the production callback, including its actual global RPCs.
20734        // Removing its clear_for_exec call must leave these IDs live and fail.
20735        // A second successful exec also exercises deletion of an empty table.
20736        for _ in 0..2 {
20737            let old_mm = guest.thread.mm_id;
20738            super::prepare_exec(&mut guest, old_mm, Default::default(), Vec::new(), None).await;
20739            guest.thread.mm_id = old_mm.for_exec(detpid);
20740            let before = guest.thread.thread_logical_time.as_nanos();
20741            tokio::time::timeout(Duration::from_secs(2), tool.handle_post_exec(&mut guest))
20742                .await
20743                .expect("post-exec must finish within the caller's scheduler turn")
20744                .unwrap();
20745            assert_eq!(guest.thread.thread_logical_time.as_nanos(), before);
20746            assert!(
20747                next_request.try_read().is_none(),
20748                "post-exec unexpectedly yielded"
20749            );
20750            let mut timers = guest.thread.posix_timers.lock().unwrap();
20751            for id in [periodic, disarmed, silent] {
20752                assert!(!timers.contains(id), "post-exec retained POSIX timer {id}");
20753                assert_eq!(timers.gettime(id, t(10)), None);
20754                assert_eq!(timers.settime(id, 0, Some(t(300)), t(10)), None);
20755                assert_eq!(timers.signal(id), None);
20756                assert!(!timers.remove(id));
20757            }
20758        }
20759        let mut timers = guest.thread.posix_timers.lock().unwrap();
20760        let new = timers.create(Some(libc::SIGUSR1), None, None, false);
20761        assert_eq!(new, 3);
20762        assert_eq!(timers.settime(new, 0, Some(t(300)), t(10)), Some((0, 0)));
20763        assert_eq!(timers.gettime(new, t(20)), Some((280, 0)));
20764    }
20765
20766    /// A state like [`cancellation_test_state`]'s for a run that declared
20767    /// `launcher` as the image the kernel execs in the guest's place.
20768    fn exec_record_test_state(
20769        sequentialize_threads: bool,
20770        launcher: &std::path::Path,
20771    ) -> (Config, GlobalState, DetTid, DetPid) {
20772        let config = Config {
20773            sequentialize_threads,
20774            launch_image: Some(crate::LaunchImage {
20775                exe: None,
20776                launcher: Some(launcher.to_path_buf()),
20777            }),
20778            ..Config::default()
20779        };
20780        let state = GlobalState::initialize(&config, false);
20781        let dettid = DetTid::from_raw(17);
20782        state
20783            .sched
20784            .lock()
20785            .unwrap()
20786            .thread_tree
20787            .add_child(dettid, dettid, true);
20788        (config, state, dettid, DetPid::from_raw(17))
20789    }
20790
20791    /// An exec's record stands only where the kernel's link, as the new
20792    /// image read it, names the recorded file or the launcher the run
20793    /// declared; it is what other processes then read for the process, and a
20794    /// dropped record leaves them nothing. Without sequentialized threads no
20795    /// record is kept.
20796    #[tokio::test]
20797    #[allow(clippy::disallowed_methods)] // std::fs::canonicalize is the independent oracle
20798    async fn an_exec_record_stands_only_where_the_kernel_names_it_or_the_launcher() {
20799        use std::os::unix::fs::MetadataExt;
20800        let directory = tempfile::tempdir().unwrap();
20801        let directory = std::fs::canonicalize(directory.path()).unwrap();
20802        let (image, launcher, other) = (
20803            directory.join("image"),
20804            directory.join("launcher"),
20805            directory.join("other"),
20806        );
20807        for file in [&image, &launcher, &other] {
20808            std::fs::write(file, file.as_os_str().as_encoded_bytes()).unwrap();
20809        }
20810        let identity = |path: &std::path::Path| {
20811            let meta = std::fs::metadata(path).unwrap();
20812            (meta.dev(), meta.ino())
20813        };
20814        let record = crate::exec_record::ExecRecord::at(std::path::Path::new("/"), &image).unwrap();
20815        for (sequentialize, kernel, kept) in [
20816            (true, identity(&image), true),
20817            (true, identity(&launcher), true),
20818            (true, identity(&other), false),
20819            (false, identity(&image), false),
20820        ] {
20821            let (config, state, leader, detpid) = exec_record_test_state(sequentialize, &launcher);
20822            install_test_registration(&state, leader, Ivar::new());
20823            let clock = DetTime::new(&config);
20824            let mm = MmId::initial(detpid);
20825            let tid = reverie::Tid::from_raw(leader.as_raw());
20826            let prepared = ExecFdState {
20827                exe: Some(record.clone()),
20828                ..Default::default()
20829            };
20830            let response = state
20831                .receive_rpc(
20832                    tid,
20833                    (
20834                        clock.clone(),
20835                        mm,
20836                        GlobalRequest::PrepareExec(detpid, mm, Box::new(prepared)),
20837                    ),
20838                )
20839                .await;
20840            assert_eq!(response.1, GlobalResponse::PrepareExec(()));
20841            let response = state
20842                .receive_rpc(
20843                    tid,
20844                    (
20845                        clock,
20846                        mm.for_exec(detpid),
20847                        GlobalRequest::MarkPastFirstExecve(detpid, None, None, Some(kernel)),
20848                    ),
20849                )
20850                .await;
20851            let GlobalResponse::MarkPastFirstExecve(overrides) = response.1 else {
20852                panic!("unexpected response {:?}", response.1);
20853            };
20854            let expected = kept.then(|| record.clone());
20855            assert_eq!(overrides.exe, expected, "{sequentialize} {kernel:?}");
20856            assert_eq!(
20857                state.exec_images.lock().unwrap().get(&detpid).cloned(),
20858                expected,
20859                "{sequentialize} {kernel:?}"
20860            );
20861        }
20862    }
20863
20864    /// The launch's first image goes to the registered root's first
20865    /// completed image only: another thread's completion, first or not,
20866    /// gets none, and the root's later images get none from the launch. The
20867    /// root's record stands without sequentialized threads too, as it comes
20868    /// from no preparation.
20869    #[tokio::test]
20870    #[allow(clippy::disallowed_methods)] // std::fs::canonicalize is the independent oracle
20871    async fn the_launch_image_goes_to_the_registered_roots_first_image_only() {
20872        let directory = tempfile::tempdir().unwrap();
20873        let directory = std::fs::canonicalize(directory.path()).unwrap();
20874        let (image, launcher) = (directory.join("image"), directory.join("launcher"));
20875        for file in [&image, &launcher] {
20876            std::fs::write(file, file.as_os_str().as_encoded_bytes()).unwrap();
20877        }
20878        let record = crate::exec_record::ExecRecord::at(std::path::Path::new("/"), &image).unwrap();
20879        for sequentialize in [true, false] {
20880            let config = Config {
20881                sequentialize_threads: sequentialize,
20882                launch_image: Some(crate::LaunchImage {
20883                    exe: Some(image.clone()),
20884                    launcher: Some(launcher.clone()),
20885                }),
20886                ..Config::default()
20887            };
20888            let state = GlobalState::initialize(&config, false);
20889            let (root, other) = (DetTid::from_raw(17), DetTid::from_raw(23));
20890            for tid in [root, other] {
20891                state
20892                    .sched
20893                    .lock()
20894                    .unwrap()
20895                    .thread_tree
20896                    .add_child(tid, tid, true);
20897                install_test_registration(&state, tid, Ivar::new());
20898            }
20899            state.root_dettid.set(root).unwrap();
20900            let clock = DetTime::new(&config);
20901            let mark = |tid: DetTid| {
20902                let process = DetPid::from_raw(tid.as_raw());
20903                (
20904                    reverie::Tid::from_raw(tid.as_raw()),
20905                    (
20906                        clock.clone(),
20907                        MmId::initial(process),
20908                        GlobalRequest::MarkPastFirstExecve(process, None, None, None),
20909                    ),
20910                )
20911            };
20912            for (tid, expected) in [(other, None), (root, Some(&record)), (root, None)] {
20913                let (from, request) = mark(tid);
20914                let response = state.receive_rpc(from, request).await;
20915                let GlobalResponse::MarkPastFirstExecve(overrides) = response.1 else {
20916                    panic!("unexpected response {:?}", response.1);
20917                };
20918                assert_eq!(overrides.exe.as_ref(), expected, "{sequentialize} {tid:?}");
20919            }
20920        }
20921    }
20922
20923    /// The record a new image resolved from its own `AT_EXECFN` (the
20924    /// unprepared one) fills in only where no exec was prepared, never
20925    /// outranks a prepared record, never stands for the launcher, and is
20926    /// dropped without sequentialized threads, as a prepared one is. For the
20927    /// registered root's first image it outranks the launch's own search,
20928    /// which stands where the image gave none or named the launcher; in a run
20929    /// that declares no launcher, the launch's record stands at the root
20930    /// whatever the image's exec prepared, as before.
20931    #[tokio::test]
20932    #[allow(clippy::disallowed_methods)] // std::fs::canonicalize is the independent oracle
20933    async fn an_images_own_record_fills_in_and_outranks_only_the_launchs_search() {
20934        let directory = tempfile::tempdir().unwrap();
20935        let directory = std::fs::canonicalize(directory.path()).unwrap();
20936        let (image, other, launcher) = (
20937            directory.join("image"),
20938            directory.join("other"),
20939            directory.join("launcher"),
20940        );
20941        for file in [&image, &other, &launcher] {
20942            std::fs::write(file, file.as_os_str().as_encoded_bytes()).unwrap();
20943        }
20944        let record_of = |path: &std::path::Path| {
20945            crate::exec_record::ExecRecord::at(std::path::Path::new("/"), path)
20946        };
20947        let (image_record, other_record, launcher_record) =
20948            (record_of(&image), record_of(&other), record_of(&launcher));
20949
20950        // A process other than the root.
20951        for (sequentialize, prepared, unprepared, expected) in [
20952            (true, None, other_record.clone(), other_record.clone()),
20953            (
20954                true,
20955                image_record.clone(),
20956                other_record.clone(),
20957                image_record.clone(),
20958            ),
20959            (true, None, launcher_record.clone(), None),
20960            (false, None, other_record.clone(), None),
20961        ] {
20962            let (config, state, leader, detpid) = exec_record_test_state(sequentialize, &launcher);
20963            install_test_registration(&state, leader, Ivar::new());
20964            let clock = DetTime::new(&config);
20965            let mm = MmId::initial(detpid);
20966            let tid = reverie::Tid::from_raw(leader.as_raw());
20967            let preparation = ExecFdState {
20968                exe: prepared.clone(),
20969                ..Default::default()
20970            };
20971            let response = state
20972                .receive_rpc(
20973                    tid,
20974                    (
20975                        clock.clone(),
20976                        mm,
20977                        GlobalRequest::PrepareExec(detpid, mm, Box::new(preparation)),
20978                    ),
20979                )
20980                .await;
20981            assert_eq!(response.1, GlobalResponse::PrepareExec(()));
20982            let response = state
20983                .receive_rpc(
20984                    tid,
20985                    (
20986                        clock,
20987                        mm.for_exec(detpid),
20988                        GlobalRequest::MarkPastFirstExecve(detpid, None, unprepared.clone(), None),
20989                    ),
20990                )
20991                .await;
20992            let GlobalResponse::MarkPastFirstExecve(overrides) = response.1 else {
20993                panic!("unexpected response {:?}", response.1);
20994            };
20995            assert_eq!(
20996                overrides.exe, expected,
20997                "{sequentialize} prepared {prepared:?} unprepared {unprepared:?}"
20998            );
20999        }
21000
21001        // The registered root's first image of a run that declares no
21002        // launcher (ptrace): the launch's record stands over the one its
21003        // bootstrap exec prepared.
21004        {
21005            let config = Config {
21006                sequentialize_threads: true,
21007                launch_image: Some(crate::LaunchImage {
21008                    exe: Some(image.clone()),
21009                    launcher: None,
21010                }),
21011                ..Config::default()
21012            };
21013            let state = GlobalState::initialize(&config, false);
21014            let root = DetTid::from_raw(17);
21015            state
21016                .sched
21017                .lock()
21018                .unwrap()
21019                .thread_tree
21020                .add_child(root, root, true);
21021            install_test_registration(&state, root, Ivar::new());
21022            state.root_dettid.set(root).unwrap();
21023            let process = DetPid::from_raw(root.as_raw());
21024            let tid = reverie::Tid::from_raw(root.as_raw());
21025            let clock = DetTime::new(&config);
21026            let mm = MmId::initial(process);
21027            let preparation = ExecFdState {
21028                exe: other_record.clone(),
21029                ..Default::default()
21030            };
21031            let response = state
21032                .receive_rpc(
21033                    tid,
21034                    (
21035                        clock.clone(),
21036                        mm,
21037                        GlobalRequest::PrepareExec(process, mm, Box::new(preparation)),
21038                    ),
21039                )
21040                .await;
21041            assert_eq!(response.1, GlobalResponse::PrepareExec(()));
21042            let response = state
21043                .receive_rpc(
21044                    tid,
21045                    (
21046                        clock,
21047                        mm.for_exec(process),
21048                        GlobalRequest::MarkPastFirstExecve(process, None, None, None),
21049                    ),
21050                )
21051                .await;
21052            let GlobalResponse::MarkPastFirstExecve(overrides) = response.1 else {
21053                panic!("unexpected response {:?}", response.1);
21054            };
21055            assert_eq!(
21056                overrides.exe, image_record,
21057                "no launcher: the launch's record"
21058            );
21059        }
21060
21061        // The registered root's first image of a run that declares a
21062        // launcher.
21063        for (launch_exe, unprepared, expected) in [
21064            (Some(&image), other_record.clone(), other_record.clone()),
21065            (Some(&image), None, image_record.clone()),
21066            (None, other_record.clone(), other_record.clone()),
21067            (Some(&image), launcher_record.clone(), image_record.clone()),
21068        ] {
21069            let config = Config {
21070                sequentialize_threads: true,
21071                launch_image: Some(crate::LaunchImage {
21072                    exe: launch_exe.cloned(),
21073                    launcher: Some(launcher.clone()),
21074                }),
21075                ..Config::default()
21076            };
21077            let state = GlobalState::initialize(&config, false);
21078            let root = DetTid::from_raw(17);
21079            state
21080                .sched
21081                .lock()
21082                .unwrap()
21083                .thread_tree
21084                .add_child(root, root, true);
21085            install_test_registration(&state, root, Ivar::new());
21086            state.root_dettid.set(root).unwrap();
21087            let process = DetPid::from_raw(root.as_raw());
21088            let response = state
21089                .receive_rpc(
21090                    reverie::Tid::from_raw(root.as_raw()),
21091                    (
21092                        DetTime::new(&config),
21093                        MmId::initial(process),
21094                        GlobalRequest::MarkPastFirstExecve(process, None, unprepared.clone(), None),
21095                    ),
21096                )
21097                .await;
21098            let GlobalResponse::MarkPastFirstExecve(overrides) = response.1 else {
21099                panic!("unexpected response {:?}", response.1);
21100            };
21101            assert_eq!(
21102                overrides.exe, expected,
21103                "launch {launch_exe:?} unprepared {unprepared:?}"
21104            );
21105        }
21106    }
21107
21108    #[tokio::test]
21109    async fn only_successful_exec_cancels_posix_deadlines() {
21110        let (config, state, leader, detpid) = cancellation_test_state();
21111        install_test_registration(&state, leader, Ivar::new());
21112        let clock = DetTime::new(&config);
21113        let mm = MmId::initial(detpid);
21114        let deadline = LogicalTime::from_nanos(1_000_000);
21115        {
21116            let mut sched = state.sched.lock().unwrap();
21117            sched.register_alarm(
21118                detpid,
21119                leader,
21120                LogicalTime::ZERO,
21121                deadline,
21122                LogicalTime::ZERO,
21123                Signal::SIGALRM,
21124            );
21125            sched.register_posix_timer(
21126                detpid,
21127                crate::scheduler::timed_waiters::SignalTarget::Process(leader),
21128                0,
21129                Some(deadline),
21130                deadline,
21131                Signal::SIGUSR2.into(),
21132            );
21133        }
21134        let before: Vec<_> = state
21135            .sched
21136            .lock()
21137            .unwrap()
21138            .blocked
21139            .timed_waiters
21140            .iter()
21141            .collect();
21142        for request in [
21143            GlobalRequest::PrepareExec(detpid, mm, Default::default()),
21144            GlobalRequest::CancelExec(detpid),
21145            GlobalRequest::PrepareExec(detpid, mm, Default::default()),
21146        ] {
21147            state
21148                .receive_rpc(
21149                    reverie::Tid::from_raw(leader.as_raw()),
21150                    (clock.clone(), mm, request),
21151                )
21152                .await;
21153            assert_eq!(
21154                state
21155                    .sched
21156                    .lock()
21157                    .unwrap()
21158                    .blocked
21159                    .timed_waiters
21160                    .iter()
21161                    .collect::<Vec<_>>(),
21162                before
21163            );
21164        }
21165
21166        let response = state
21167            .receive_rpc(
21168                reverie::Tid::from_raw(leader.as_raw()),
21169                (
21170                    clock,
21171                    mm.for_exec(detpid),
21172                    GlobalRequest::MarkPastFirstExecve(detpid, None, None, None),
21173                ),
21174            )
21175            .await;
21176        assert_eq!(
21177            response.1,
21178            GlobalResponse::MarkPastFirstExecve(Default::default())
21179        );
21180        let sched = state.sched.lock().unwrap();
21181        assert_eq!(
21182            sched.blocked.timed_waiters.alarm_state(detpid),
21183            Some((deadline, LogicalTime::ZERO))
21184        );
21185        assert_eq!(sched.blocked.timed_waiters.iter().count(), 1);
21186    }
21187
21188    #[tokio::test]
21189    async fn failed_exec_clears_prepared_state_without_retiring_siblings() {
21190        let (config, state, leader, detpid) = cancellation_test_state();
21191        let sibling = DetTid::from_raw(leader.as_raw() + 1);
21192        install_test_registration(&state, leader, Ivar::new());
21193        state
21194            .sched
21195            .lock()
21196            .unwrap()
21197            .thread_tree
21198            .add_child(leader, sibling, false);
21199        install_test_registration(&state, sibling, Ivar::new());
21200        let clock = DetTime::new(&config);
21201
21202        let prepared = state
21203            .receive_rpc(
21204                reverie::Tid::from_raw(leader.as_raw()),
21205                (
21206                    clock.clone(),
21207                    MmId::initial(leader),
21208                    GlobalRequest::PrepareExec(detpid, MmId::initial(detpid), Default::default()),
21209                ),
21210            )
21211            .await;
21212        assert_eq!(prepared.1, GlobalResponse::PrepareExec(()));
21213        assert!(
21214            state
21215                .pending_exec_states
21216                .lock()
21217                .unwrap()
21218                .contains_key(&detpid)
21219        );
21220
21221        let cancelled = state
21222            .receive_rpc(
21223                reverie::Tid::from_raw(leader.as_raw()),
21224                (
21225                    clock,
21226                    MmId::initial(leader),
21227                    GlobalRequest::CancelExec(detpid),
21228                ),
21229            )
21230            .await;
21231        assert_eq!(cancelled.1, GlobalResponse::CancelExec(()));
21232        assert!(state.pending_exec_states.lock().unwrap().is_empty());
21233        {
21234            let scheduler = state.sched.lock().unwrap();
21235            assert!(!scheduler.thread_is_logically_killed(leader));
21236            assert!(!scheduler.thread_is_logically_killed(sibling));
21237            assert_eq!(scheduler.next_turns.len(), 2);
21238        }
21239
21240        state.pending_exec_states.lock().unwrap().insert(
21241            detpid,
21242            PendingExecState {
21243                caller: leader,
21244                process: detpid,
21245                mm: MmId::initial(detpid),
21246                fd_blocking: Default::default(),
21247            },
21248        );
21249        state.post_exec_fd_blocking.lock().unwrap().insert(
21250            leader,
21251            ExecFdState::from([42].into_iter().collect::<ExecFdBlockingOverrides>()),
21252        );
21253        state
21254            .recv_deregister_thread(
21255                reverie::Tid::from_raw(leader.as_raw()),
21256                ThreadDeregistration {
21257                    thread_start_entered: true,
21258                    vfork_origin: None,
21259                    dettid: leader,
21260                    detpid,
21261                    mm: MmId::initial(detpid),
21262                    timeslice_stats: TimesliceStats::default(),
21263                    syscall_count: 0,
21264                    syscall_histogram: Default::default(),
21265                    chaos_epochs: Vec::new(),
21266                    killed_by_group_signal: false,
21267                    passed_descriptors_out: false,
21268                },
21269            )
21270            .await;
21271        assert!(state.pending_exec_states.lock().unwrap().is_empty());
21272        assert!(state.post_exec_fd_blocking.lock().unwrap().is_empty());
21273
21274        state.pending_exec_states.lock().unwrap().insert(
21275            detpid,
21276            PendingExecState {
21277                caller: leader,
21278                process: detpid,
21279                mm: MmId::initial(detpid),
21280                fd_blocking: Default::default(),
21281            },
21282        );
21283        state
21284            .recv_deregister_thread(
21285                reverie::Tid::from_raw(leader.as_raw()),
21286                ThreadDeregistration {
21287                    thread_start_entered: true,
21288                    vfork_origin: None,
21289                    dettid: leader,
21290                    detpid,
21291                    mm: MmId::initial(detpid).for_exec(detpid),
21292                    timeslice_stats: TimesliceStats::default(),
21293                    syscall_count: 0,
21294                    syscall_histogram: Default::default(),
21295                    chaos_epochs: Vec::new(),
21296                    killed_by_group_signal: false,
21297                    passed_descriptors_out: false,
21298                },
21299            )
21300            .await;
21301        assert!(state.pending_exec_states.lock().unwrap().is_empty());
21302
21303        state.pending_exec_states.lock().unwrap().insert(
21304            detpid,
21305            PendingExecState {
21306                caller: leader,
21307                process: detpid,
21308                mm: MmId::initial(detpid),
21309                fd_blocking: Default::default(),
21310            },
21311        );
21312        state.post_exec_fd_blocking.lock().unwrap().insert(
21313            leader,
21314            ExecFdState::from([42].into_iter().collect::<ExecFdBlockingOverrides>()),
21315        );
21316        state.complete_physical_process_exit(detpid.as_raw());
21317        assert!(state.pending_exec_states.lock().unwrap().is_empty());
21318        assert!(state.post_exec_fd_blocking.lock().unwrap().is_empty());
21319    }
21320
21321    #[test]
21322    fn completed_unsupported_syscalls_transfer_once_without_duplicates() {
21323        let mut state = GlobalState::initialize(&Config::default(), false);
21324        for name in ["restart_syscall", "io_setup", "restart_syscall"] {
21325            state
21326                .unsupported_syscalls
21327                .get_mut()
21328                .unwrap()
21329                .insert(name.to_owned());
21330        }
21331        assert_eq!(
21332            state.take_unsupported_syscalls(),
21333            ["io_setup".to_owned(), "restart_syscall".to_owned()]
21334                .into_iter()
21335                .collect()
21336        );
21337        assert!(state.take_unsupported_syscalls().is_empty());
21338    }
21339
21340    #[test]
21341    fn unsupported_syscall_report_duplicate_is_close_on_exec() {
21342        let mut descriptors = [-1; 2];
21343        assert_eq!(
21344            unsafe { libc::pipe2(descriptors.as_mut_ptr(), libc::O_CLOEXEC) },
21345            0
21346        );
21347        // SAFETY: pipe2 initialized both descriptors and transfers ownership.
21348        let _reader = unsafe { OwnedFd::from_raw_fd(descriptors[0]) };
21349        let writer = unsafe { OwnedFd::from_raw_fd(descriptors[1]) };
21350        let config = Config {
21351            unsupported_syscall_report_fd: Some(writer.as_raw_fd()),
21352            ..Config::default()
21353        };
21354
21355        let state = GlobalState::initialize(&config, false);
21356        let duplicate = state
21357            .unsupported_syscall_report_fd
21358            .as_ref()
21359            .expect("report writer should be duplicated")
21360            .lock()
21361            .unwrap();
21362        let flags = unsafe { libc::fcntl(duplicate.as_raw_fd(), libc::F_GETFD) };
21363        assert_ne!(flags, -1);
21364        assert_ne!(flags & libc::FD_CLOEXEC, 0);
21365    }
21366
21367    // AUTONOMOUS-BOT-IMPLEMENTED
21368    // TODO-HUMAN-REVIEW(PR-1056): Deterministic st_dev remapping test.
21369    #[test]
21370    fn device_pool_remaps_deterministically() {
21371        use super::DevicePool;
21372
21373        // Raw device numbers a guest might observe; the procfs/tmpfs ones
21374        // (large anon-bdev values) are exactly what drifts between runs.
21375        let raw_root = 0x20; // e.g. a real block device
21376        let raw_proc_run1 = 3_145_792; // anon bdev in run 1
21377        let raw_proc_run2 = 3_145_788; // same procfs, different number in run 2
21378
21379        // Run 1: observe root then proc.
21380        let mut pool1 = DevicePool::new();
21381        let root1 = pool1.determinize(raw_root);
21382        let proc1 = pool1.determinize(raw_proc_run1);
21383        // Run 2: same observation order, different raw proc number.
21384        let mut pool2 = DevicePool::new();
21385        let root2 = pool2.determinize(raw_root);
21386        let proc2 = pool2.determinize(raw_proc_run2);
21387
21388        // The synthetic ids depend only on first-observation order, so they are
21389        // identical across the two runs despite the raw proc number differing.
21390        assert_eq!(root1, root2);
21391        assert_eq!(proc1, proc2);
21392
21393        // Distinct raw devices get distinct ids; ids start at 1 (never 0).
21394        assert_ne!(root1, proc1);
21395        assert_eq!(root1, 1);
21396        assert_eq!(proc1, 2);
21397
21398        // Re-observing a raw device is stable within a run.
21399        assert_eq!(pool1.determinize(raw_root), root1);
21400        assert_eq!(pool1.determinize(raw_proc_run1), proc1);
21401    }
21402
21403    #[test]
21404    fn the_procfs_device_is_never_minted_for_another_device() {
21405        use super::DevicePool;
21406
21407        let mut pool = DevicePool::new();
21408        pool.next_device = crate::procfs_inode::VIRTUAL_PROC_DEVICE - 1;
21409        let before = pool.determinize(libc::makedev(8, 1));
21410        let after = pool.determinize(libc::makedev(8, 2));
21411        assert_eq!(before, crate::procfs_inode::VIRTUAL_PROC_DEVICE - 1);
21412        assert_eq!(after, crate::procfs_inode::VIRTUAL_PROC_DEVICE + 1);
21413    }
21414
21415    #[test]
21416    fn this_namespaces_procfs_device_is_detcores() {
21417        use std::os::unix::fs::MetadataExt;
21418
21419        use super::DevicePool;
21420
21421        let mut pool = DevicePool::for_this_mount_namespace();
21422        let proc = std::fs::metadata("/proc").unwrap().dev();
21423        assert_eq!(
21424            pool.determinize(proc),
21425            crate::procfs_inode::VIRTUAL_PROC_DEVICE
21426        );
21427        assert_eq!(pool.determinize(libc::makedev(8, 1)), 1);
21428    }
21429
21430    #[test]
21431    fn mountinfo_prepopulation_is_reused_by_later_stat_observations() {
21432        use super::DevicePool;
21433
21434        let mountinfo_devices = [libc::makedev(8, 1), libc::makedev(0, 44)];
21435        let mut pool = DevicePool::new();
21436        let rendered = mountinfo_devices
21437            .into_iter()
21438            .map(|raw| pool.determinize(raw))
21439            .collect::<Vec<_>>();
21440
21441        assert_eq!(rendered, [1, 2]);
21442        assert_eq!(pool.determinize(libc::makedev(0, 44)), rendered[1]);
21443        assert_eq!(pool.determinize(libc::makedev(8, 1)), rendered[0]);
21444        assert_eq!(pool.determinize(libc::makedev(259, 7)), 3);
21445    }
21446
21447    #[tokio::test]
21448    async fn late_futex_rpc_after_thread_removal_returns_eintr() {
21449        let config = Config {
21450            sequentialize_threads: true,
21451            ..Config::default()
21452        };
21453        let state = GlobalState::initialize(&config, false);
21454        let dettid = DetTid::from_raw(17);
21455        let detpid = DetPid::from_raw(17);
21456        let response = state
21457            .recv_futex_action(
21458                RpcIncarnation {
21459                    dettid,
21460                    mm: MmId::initial(detpid),
21461                },
21462                FutexAction::WaitRequest(None, None),
21463                FutexID::private(MmId::initial(detpid), 0x1000),
21464                0,
21465                u32::MAX,
21466                true,
21467            )
21468            .await;
21469
21470        assert!(matches!(
21471            response,
21472            Some(SchedValue::Value(value)) if value == nix::errno::Errno::EINTR as u64
21473        ));
21474        assert!(
21475            !state
21476                .sched
21477                .lock()
21478                .unwrap()
21479                .guest_held_marks_revoked_for_test()
21480        );
21481    }
21482
21483    #[tokio::test]
21484    async fn late_child_tid_wait_after_exit_returns_spurious_wake() {
21485        let config = Config {
21486            sequentialize_threads: true,
21487            ..Config::default()
21488        };
21489        let state = GlobalState::initialize(&config, false);
21490        let detpid = DetPid::from_raw(17);
21491        let child = DetTid::from_raw(18);
21492        let futex = FutexID::private(MmId::initial(detpid), 0x1000);
21493        state.sched.lock().unwrap().next_turns.insert(
21494            detpid,
21495            ThreadNextTurn {
21496                dettid: detpid,
21497                child_tid_addr: 0,
21498                req: Ivar::new(),
21499                resp: Ivar::new(),
21500                protocol: Default::default(),
21501            },
21502        );
21503        state
21504            .sched
21505            .lock()
21506            .unwrap()
21507            .wake_futex_child_cleartid(futex, child);
21508        assert!(
21509            state
21510                .sched
21511                .lock()
21512                .unwrap()
21513                .child_tid_was_cleared(futex, child.as_raw())
21514        );
21515        assert!(
21516            !state
21517                .sched
21518                .lock()
21519                .unwrap()
21520                .child_tid_was_cleared(futex, child.as_raw() + 1)
21521        );
21522
21523        let response = state
21524            .recv_futex_action(
21525                RpcIncarnation {
21526                    dettid: detpid,
21527                    mm: MmId::initial(detpid),
21528                },
21529                FutexAction::WaitRequest(None, None),
21530                futex,
21531                child.as_raw(),
21532                u32::MAX,
21533                false,
21534            )
21535            .await;
21536
21537        assert!(matches!(response, Some(SchedValue::Value(0))));
21538        assert!(state.sched.lock().unwrap().blocked.futex_waiters.is_empty());
21539    }
21540
21541    #[tokio::test]
21542    async fn late_resource_request_after_logical_kill_is_cancelled() {
21543        let (config, state, dettid, detpid) = cancellation_test_state();
21544        install_test_registration(&state, dettid, Ivar::new());
21545        let mut current_time = DetTime::new(&config);
21546        current_time.add_syscall();
21547        state.global_time.lock().unwrap().update_global_time(
21548            dettid,
21549            current_time.as_nanos(),
21550            LogicalTime::ZERO,
21551        );
21552        state
21553            .sched
21554            .lock()
21555            .unwrap()
21556            .logically_kill_thread(&dettid, &detpid, MmId::initial(detpid));
21557        let (global_before, thread_before) = {
21558            let global_time = state.global_time.lock().unwrap();
21559            (global_time.as_nanos(), global_time.threads_time(dettid))
21560        };
21561        let mut late_time = current_time;
21562        late_time.add_syscall();
21563
21564        let response = state
21565            .receive_rpc(
21566                reverie::Tid::from_raw(dettid.as_raw()),
21567                (
21568                    late_time,
21569                    MmId::initial(dettid),
21570                    GlobalRequest::RequestResources(Resources::new(dettid), detpid),
21571                ),
21572            )
21573            .await;
21574
21575        assert_eq!(response, (None, GlobalResponse::ThreadExited));
21576        assert!(!state.sched.lock().unwrap().next_turns.contains_key(&dettid));
21577        let global_time = state.global_time.lock().unwrap();
21578        assert_eq!(global_time.as_nanos(), global_before);
21579        assert_eq!(global_time.threads_time(dettid), thread_before);
21580    }
21581
21582    #[tokio::test]
21583    async fn duplicate_deregistration_is_acknowledged_without_clock_or_scheduler_mutation() {
21584        let (config, state, dettid, detpid) = cancellation_test_state();
21585        install_test_registration(&state, dettid, Ivar::new());
21586        let mut current_time = DetTime::new(&config);
21587        current_time.add_syscall();
21588        state.global_time.lock().unwrap().update_global_time(
21589            dettid,
21590            current_time.as_nanos(),
21591            LogicalTime::ZERO,
21592        );
21593        state
21594            .sched
21595            .lock()
21596            .unwrap()
21597            .logically_kill_thread(&dettid, &detpid, MmId::initial(detpid));
21598        let (global_before, thread_before) = {
21599            let global_time = state.global_time.lock().unwrap();
21600            (global_time.as_nanos(), global_time.threads_time(dettid))
21601        };
21602        let mut late_time = current_time;
21603        late_time.add_syscall();
21604        let mut final_stats = TimesliceStats::default();
21605        final_stats.record(7);
21606
21607        let first_response = state
21608            .receive_rpc(
21609                reverie::Tid::from_raw(dettid.as_raw()),
21610                (
21611                    late_time.clone(),
21612                    MmId::initial(dettid),
21613                    GlobalRequest::DeregisterThread(ThreadDeregistration {
21614                        thread_start_entered: true,
21615                        vfork_origin: None,
21616                        dettid,
21617                        detpid,
21618                        mm: MmId::initial(detpid),
21619                        timeslice_stats: final_stats,
21620                        syscall_count: 17,
21621                        syscall_histogram: Default::default(),
21622                        chaos_epochs: Vec::new(),
21623                        killed_by_group_signal: false,
21624                        passed_descriptors_out: false,
21625                    }),
21626                ),
21627            )
21628            .await;
21629        assert_eq!(first_response, (None, GlobalResponse::DeregisterThread(())));
21630        assert_eq!(
21631            state
21632                .sched
21633                .lock()
21634                .unwrap()
21635                .per_thread_timeslice
21636                .get(&dettid),
21637            Some(&final_stats)
21638        );
21639        assert_eq!(
21640            state.sched.lock().unwrap().per_thread_syscalls.get(&dettid),
21641            Some(&17)
21642        );
21643
21644        late_time.add_syscall();
21645        let duplicate_response = state
21646            .receive_rpc(
21647                reverie::Tid::from_raw(dettid.as_raw()),
21648                (
21649                    late_time,
21650                    MmId::initial(dettid),
21651                    GlobalRequest::DeregisterThread(ThreadDeregistration {
21652                        thread_start_entered: true,
21653                        vfork_origin: None,
21654                        dettid,
21655                        detpid,
21656                        mm: MmId::initial(detpid),
21657                        timeslice_stats: final_stats,
21658                        syscall_count: 99,
21659                        syscall_histogram: Default::default(),
21660                        chaos_epochs: Vec::new(),
21661                        killed_by_group_signal: false,
21662                        passed_descriptors_out: false,
21663                    }),
21664                ),
21665            )
21666            .await;
21667        assert_eq!(
21668            duplicate_response,
21669            (None, GlobalResponse::DeregisterThread(()))
21670        );
21671        assert_eq!(
21672            state
21673                .sched
21674                .lock()
21675                .unwrap()
21676                .per_thread_timeslice
21677                .get(&dettid),
21678            Some(&final_stats)
21679        );
21680        assert_eq!(
21681            state.sched.lock().unwrap().per_thread_syscalls.get(&dettid),
21682            Some(&17),
21683            "a duplicate deregistration must not double-count or replace final accounting"
21684        );
21685        let summary = state
21686            .sched
21687            .lock()
21688            .unwrap()
21689            .generate_partial_run_summary(None)
21690            .unwrap();
21691        assert_eq!(summary.syscalls, Some(17));
21692        assert!(!state.sched.lock().unwrap().next_turns.contains_key(&dettid));
21693        let global_time = state.global_time.lock().unwrap();
21694        assert_eq!(global_time.as_nanos(), global_before);
21695        assert_eq!(global_time.threads_time(dettid), thread_before);
21696    }
21697
21698    #[tokio::test]
21699    async fn child_registration_fails_closed_for_a_tombstoned_tid() {
21700        let (config, state, parent, detpid) = cancellation_test_state();
21701        install_test_registration(&state, parent, Ivar::new());
21702        let child = DetTid::from_raw(18);
21703        state
21704            .sched
21705            .lock()
21706            .unwrap()
21707            .thread_tree
21708            .add_child(parent, child, false);
21709        install_test_registration(&state, child, Ivar::new());
21710        state
21711            .sched
21712            .lock()
21713            .unwrap()
21714            .logically_kill_thread(&child, &detpid, MmId::initial(detpid));
21715
21716        let response = state
21717            .receive_rpc(
21718                reverie::Tid::from_raw(parent.as_raw()),
21719                (
21720                    DetTime::new(&config),
21721                    MmId::initial(parent),
21722                    GlobalRequest::CreateChildThread(
21723                        child,
21724                        detpid,
21725                        0,
21726                        None,
21727                        libc::SIGCHLD,
21728                        None,
21729                        Some(DEFAULT_PRIORITY),
21730                    ),
21731                ),
21732            )
21733            .await;
21734
21735        assert_eq!(response, (None, GlobalResponse::ThreadExited));
21736        let scheduler = state.sched.lock().unwrap();
21737        assert!(scheduler.thread_is_logically_killed(child));
21738        assert!(!scheduler.next_turns.contains_key(&child));
21739        assert!(!scheduler.priorities.contains_key(&child));
21740        assert!(!state.global_time.lock().unwrap().contains_thread(parent));
21741    }
21742
21743    #[tokio::test]
21744    async fn pending_resource_request_woken_by_logical_kill_is_terminal() {
21745        let (_, state, dettid, detpid) = cancellation_test_state();
21746        let request_seen = Ivar::new();
21747        install_test_registration(&state, dettid, request_seen.clone());
21748
21749        let request = state.recv_request_resources(
21750            reverie::Tid::from_raw(dettid.as_raw()),
21751            detpid,
21752            Resources::new(dettid),
21753            None,
21754        );
21755        let kill_after_request = async {
21756            while request_seen.try_read().is_none() {
21757                tokio::task::yield_now().await;
21758            }
21759            state.sched.lock().unwrap().logically_kill_thread(
21760                &dettid,
21761                &detpid,
21762                MmId::initial(detpid),
21763            );
21764        };
21765
21766        let (response, ()) = tokio::join!(request, kill_after_request);
21767        assert_eq!(response, (SchedulerRpcResult::ThreadExited, None));
21768    }
21769
21770    /// An `exit_group`'s granted response (`finish_resource_response`) takes
21771    /// the turns of the caller's own process's other threads, as Linux's
21772    /// `zap_other_threads` does: not those of another process's live thread
21773    /// whose ID the thread tree still lists under the caller's process, and,
21774    /// for a caller whose ID once led a reaped process, those of the process
21775    /// it is a thread of now.
21776    #[tokio::test]
21777    async fn an_exit_group_grant_response_takes_only_the_callers_own_threads() {
21778        let tid = DetTid::from_raw;
21779        let exit_group = |caller: DetTid, process: DetPid| {
21780            let mut rs = Resources::new(caller);
21781            rs.insert(
21782                ResourceID::Exit {
21783                    group: true,
21784                    process,
21785                    mm: MmId::initial(process),
21786                },
21787                Permission::RW,
21788            );
21789            rs
21790        };
21791        let granted = || SchedResponse::Go(Some(SchedValue::Value(1)));
21792
21793        // 38's thread 39 exited, and Linux gave 39 to a thread of process 50.
21794        let state = GlobalState::initialize(&Config::default(), false);
21795        let (parent, process, reused, other) = (tid(37), tid(38), tid(39), tid(50));
21796        {
21797            let mut sched = state.sched.lock().unwrap();
21798            sched.thread_tree.add_child(parent, parent, true);
21799            sched.thread_tree.add_child(parent, process, true);
21800            sched.thread_tree.add_child(process, reused, false);
21801            sched.thread_tree.add_child(parent, other, true);
21802            sched.thread_tree.add_child(other, reused, false);
21803        }
21804        for thread in [parent, process, reused, other] {
21805            install_test_registration(&state, thread, Ivar::new());
21806        }
21807        let (result, _) = state
21808            .finish_resource_response(
21809                reverie::Tid::from_raw(process.as_raw()),
21810                process,
21811                exit_group(process, process),
21812                None,
21813                granted(),
21814            )
21815            .await;
21816        assert!(matches!(result, SchedulerRpcResult::Continue(_)));
21817        assert!(state.sched.lock().unwrap().next_turns.contains_key(&reused));
21818
21819        // Process 38 was reaped and Linux gave 38 to a thread of process 50,
21820        // beside 50's thread 51.
21821        let state = GlobalState::initialize(&Config::default(), false);
21822        let (parent, caller, other, sibling) = (tid(37), tid(38), tid(50), tid(51));
21823        {
21824            let mut sched = state.sched.lock().unwrap();
21825            sched.thread_tree.add_child(parent, parent, true);
21826            sched.thread_tree.add_child(parent, caller, true);
21827            sched.thread_tree.add_child(parent, other, true);
21828            sched.thread_tree.add_child(other, caller, false);
21829            sched.thread_tree.add_child(other, sibling, false);
21830        }
21831        for thread in [parent, caller, other, sibling] {
21832            install_test_registration(&state, thread, Ivar::new());
21833        }
21834        let (result, _) = state
21835            .finish_resource_response(
21836                reverie::Tid::from_raw(caller.as_raw()),
21837                other,
21838                exit_group(caller, other),
21839                None,
21840                granted(),
21841            )
21842            .await;
21843        assert!(matches!(result, SchedulerRpcResult::Continue(_)));
21844        let sched = state.sched.lock().unwrap();
21845        assert!(!sched.next_turns.contains_key(&sibling));
21846        assert!(sched.next_turns.contains_key(&caller));
21847    }
21848
21849    #[tokio::test]
21850    async fn trace_replay_yield_propagates_terminal_scheduler_cancellation() {
21851        let dettid = DetTid::from_raw(17);
21852        let detpid = DetPid::from_raw(17);
21853        let next_tid = DetTid::from_raw(18);
21854        let event = SchedEvent {
21855            dettid,
21856            op: Op::OtherInstructions,
21857            count: 1,
21858            start_rip: None,
21859            end_rip: None,
21860            end_time: Some(LogicalTime::from_nanos(1)),
21861        };
21862        let next_event = SchedEvent {
21863            dettid: next_tid,
21864            end_time: Some(LogicalTime::from_nanos(2)),
21865            ..event.clone()
21866        };
21867        let trace_file = tempfile::NamedTempFile::new().unwrap();
21868        std::fs::write(
21869            trace_file.path(),
21870            PreemptionRecord::from_sched_events(vec![event.clone(), next_event]).to_string(),
21871        )
21872        .unwrap();
21873        let config = Config {
21874            sequentialize_threads: true,
21875            replay_schedule_from: Some(trace_file.path().to_path_buf()),
21876            ..Config::default()
21877        }
21878        .with_backend(|backend| {
21879            backend.needs_killed_thread_rpc_cancellation = true;
21880        });
21881        let state = GlobalState::initialize(&config, false);
21882        state
21883            .sched
21884            .lock()
21885            .unwrap()
21886            .thread_tree
21887            .add_child(dettid, dettid, true);
21888        state
21889            .sched
21890            .lock()
21891            .unwrap()
21892            .thread_tree
21893            .add_child(dettid, next_tid, false);
21894        let request_seen = Ivar::new();
21895        install_test_registration(&state, dettid, request_seen.clone());
21896        install_test_registration(&state, next_tid, Ivar::new());
21897
21898        let replay = state.recv_trace_schedevent(event, detpid, MmId::initial(detpid), false, None);
21899        let kill_after_replay_yield = async {
21900            while request_seen.try_read().is_none() {
21901                tokio::task::yield_now().await;
21902            }
21903            state.sched.lock().unwrap().logically_kill_thread(
21904                &dettid,
21905                &detpid,
21906                MmId::initial(detpid),
21907            );
21908        };
21909        let (response, ()) = tokio::time::timeout(Duration::from_secs(1), async {
21910            tokio::join!(replay, kill_after_replay_yield)
21911        })
21912        .await
21913        .expect("trace replay cancellation did not terminate the pending scheduler RPC");
21914
21915        assert_eq!(response, SchedulerRpcResult::ThreadExited);
21916    }
21917
21918    #[tokio::test]
21919    async fn pending_start_request_woken_by_logical_kill_is_terminal() {
21920        let (config, state, dettid, detpid) = cancellation_test_state();
21921        let request_seen = Ivar::new();
21922        install_test_registration(&state, dettid, request_seen.clone());
21923        let request = state.receive_rpc(
21924            reverie::Tid::from_raw(dettid.as_raw()),
21925            (
21926                DetTime::new(&config),
21927                MmId::initial(dettid),
21928                GlobalRequest::StartNewThread(dettid, detpid, None, None),
21929            ),
21930        );
21931        let kill_after_request = async {
21932            while request_seen.try_read().is_none() {
21933                tokio::task::yield_now().await;
21934            }
21935            state.sched.lock().unwrap().logically_kill_thread(
21936                &dettid,
21937                &detpid,
21938                MmId::initial(detpid),
21939            );
21940        };
21941
21942        let (response, ()) = tokio::join!(request, kill_after_request);
21943        assert_eq!(response, (None, GlobalResponse::ThreadExited));
21944        assert!(!state.sched.lock().unwrap().priorities.contains_key(&dettid));
21945    }
21946
21947    #[tokio::test]
21948    async fn required_physical_thread_id_missing_is_terminal() {
21949        let config = Config {
21950            sequentialize_threads: true,
21951            ..Config::default()
21952        }
21953        .with_backend(|backend| {
21954            backend.needs_killed_thread_rpc_cancellation = true;
21955            backend.requires_thread_directed_process_signals = true;
21956        });
21957        let state = GlobalState::initialize(&config, false);
21958        let dettid = DetTid::from_raw(17);
21959        let detpid = DetPid::from_raw(17);
21960        state
21961            .sched
21962            .lock()
21963            .unwrap()
21964            .thread_tree
21965            .add_child(dettid, dettid, true);
21966        install_test_registration(&state, dettid, Ivar::new());
21967
21968        let response = state
21969            .receive_rpc(
21970                reverie::Tid::from_raw(dettid.as_raw()),
21971                (
21972                    DetTime::new(&config),
21973                    MmId::initial(detpid),
21974                    GlobalRequest::StartNewThread(dettid, detpid, None, None),
21975                ),
21976            )
21977            .await;
21978
21979        assert_eq!(response, (None, GlobalResponse::ThreadExited));
21980        let scheduler = state.sched.lock().unwrap();
21981        assert!(!scheduler.next_turns.contains_key(&dettid));
21982    }
21983
21984    #[tokio::test]
21985    async fn tombstoned_timer_registration_cannot_mutate_scheduler_state() {
21986        let (_, state, dettid, detpid) = cancellation_test_state();
21987        install_test_registration(&state, dettid, Ivar::new());
21988        state
21989            .sched
21990            .lock()
21991            .unwrap()
21992            .logically_kill_thread(&dettid, &detpid, MmId::initial(detpid));
21993        let now = LogicalTime::from_nanos(100);
21994
21995        let alarm = state
21996            .recv_register_alarm(
21997                detpid,
21998                RpcIncarnation {
21999                    dettid,
22000                    mm: MmId::initial(detpid),
22001                },
22002                now,
22003                LogicalTime::from_nanos(10),
22004                LogicalTime::ZERO,
22005                SigWrapper::from(Signal::SIGALRM),
22006            )
22007            .await;
22008        assert_eq!(alarm, SchedulerRpcResult::ThreadExited);
22009
22010        let posix = state
22011            .recv_register_posix_timer(
22012                detpid,
22013                RpcIncarnation {
22014                    dettid,
22015                    mm: MmId::initial(detpid),
22016                },
22017                1,
22018                Some(now + LogicalTime::from_nanos(10)),
22019                LogicalTime::ZERO,
22020                super::PosixTimerNotification {
22021                    sig: SigWrapper::from(Signal::SIGALRM),
22022                    thread: None,
22023                    value: 1,
22024                    deleted: false,
22025                },
22026            )
22027            .await;
22028        assert_eq!(posix, SchedulerRpcResult::ThreadExited);
22029        assert!(state.sched.lock().unwrap().blocked.timed_waiters.is_empty());
22030    }
22031
22032    #[tokio::test]
22033    async fn pending_futex_request_woken_by_logical_kill_is_terminal() {
22034        let (config, state, dettid, detpid) = cancellation_test_state();
22035        install_test_registration(&state, dettid, Ivar::new());
22036        let request = state.receive_rpc(
22037            reverie::Tid::from_raw(dettid.as_raw()),
22038            (
22039                DetTime::new(&config),
22040                MmId::initial(dettid),
22041                GlobalRequest::FutexAction(
22042                    dettid,
22043                    FutexAction::WaitRequest(None, None),
22044                    FutexID::private(MmId::initial(detpid), 0x1000),
22045                    0,
22046                    u32::MAX,
22047                    false,
22048                ),
22049            ),
22050        );
22051        let kill_after_wait = async {
22052            while state.sched.lock().unwrap().blocked.futex_waiters.is_empty() {
22053                tokio::task::yield_now().await;
22054            }
22055            state.sched.lock().unwrap().logically_kill_thread(
22056                &dettid,
22057                &detpid,
22058                MmId::initial(detpid),
22059            );
22060        };
22061
22062        let (response, ()) = tokio::time::timeout(Duration::from_secs(1), async {
22063            tokio::join!(request, kill_after_wait)
22064        })
22065        .await
22066        .expect("futex teardown did not wake the blocked RPC");
22067        assert_eq!(response, (None, GlobalResponse::ThreadExited));
22068    }
22069
22070    #[tokio::test]
22071    async fn parent_continue_propagates_terminal_scheduler_cancellation() {
22072        let (config, state, parent, detpid) = cancellation_test_state();
22073        let parent_request = Ivar::new();
22074        install_test_registration(&state, parent, parent_request.clone());
22075        let child = DetTid::from_raw(18);
22076        let physical_pid = std::process::id() as i32;
22077        let physical_tid = unsafe { libc::syscall(libc::SYS_gettid) as i32 };
22078        let request = state.receive_rpc(
22079            reverie::Tid::from_raw(parent.as_raw()),
22080            (
22081                DetTime::new(&config),
22082                MmId::initial(parent),
22083                GlobalRequest::CreateChildThread(
22084                    child,
22085                    detpid,
22086                    0,
22087                    None,
22088                    libc::SIGCHLD,
22089                    Some((physical_pid, physical_tid)),
22090                    Some(DEFAULT_PRIORITY),
22091                ),
22092            ),
22093        );
22094        let kill_after_parent_parks = async {
22095            while parent_request.try_read().is_none() {
22096                tokio::task::yield_now().await;
22097            }
22098            let mut scheduler = state.sched.lock().unwrap();
22099            let (registered_mm, registered_pid, registered_tid) = scheduler
22100                .physical_thread_identity(child)
22101                .expect("parent registration must install the child pidfd before continuing");
22102            assert_eq!(
22103                registered_mm,
22104                MmId::for_clone(MmId::initial(parent), child, false)
22105            );
22106            assert_eq!(
22107                (registered_pid, registered_tid),
22108                (physical_pid, physical_tid)
22109            );
22110            scheduler.logically_kill_thread(&parent, &detpid, MmId::initial(detpid));
22111        };
22112
22113        let (response, ()) = tokio::join!(request, kill_after_parent_parks);
22114        assert_eq!(response, (None, GlobalResponse::ThreadExited));
22115    }
22116
22117    #[test]
22118    fn unsupported_syscall_warning_is_sorted_and_aggregated() {
22119        let syscalls = BTreeSet::from([
22120            "vmsplice".to_owned(),
22121            "getppid".to_owned(),
22122            "getppid".to_owned(),
22123        ]);
22124
22125        assert_eq!(
22126            format_unsupported_syscall_warning(&syscalls).as_deref(),
22127            Some("syscalls getppid,vmsplice used but not yet supported")
22128        );
22129        assert_eq!(format_unsupported_syscall_warning(&BTreeSet::new()), None);
22130    }
22131
22132    #[tokio::test]
22133    async fn abnormal_cleanup_cancels_an_unstarted_scheduler() {
22134        let config = Config {
22135            sequentialize_threads: true,
22136            ..Config::default()
22137        };
22138        let mut state = GlobalState::initialize(&config, true);
22139        state.cancel_internal_scheduler().await;
22140        let summary_path = None;
22141        let cleanup = state.clean_up(false, &summary_path);
22142
22143        assert!(
22144            matches!(
22145                tokio::time::timeout(Duration::from_millis(100), cleanup).await,
22146                Ok(Ok(()))
22147            ),
22148            "cleanup waited for a scheduler whose guest never registered"
22149        );
22150    }
22151
22152    #[tokio::test]
22153    async fn abnormal_cleanup_cancels_a_registered_scheduler() {
22154        let config = Config {
22155            sequentialize_threads: true,
22156            ..Config::default()
22157        };
22158        let mut state = GlobalState::initialize(&config, true);
22159        let dettid = DetTid::from_raw(1);
22160        {
22161            let mut scheduler = state.sched.lock().unwrap();
22162            scheduler.priorities.insert(dettid, DEFAULT_PRIORITY);
22163            scheduler.next_turns.insert(
22164                dettid,
22165                ThreadNextTurn {
22166                    dettid,
22167                    child_tid_addr: 0,
22168                    req: Ivar::new(),
22169                    resp: Ivar::new(),
22170                    protocol: Default::default(),
22171                },
22172            );
22173            scheduler.runqueue_push_back(dettid);
22174            scheduler.started_up.put(());
22175        }
22176        tokio::task::yield_now().await;
22177
22178        state.cancel_internal_scheduler().await;
22179        let summary_path = None;
22180        assert!(
22181            matches!(
22182                tokio::time::timeout(
22183                    Duration::from_millis(100),
22184                    state.clean_up(false, &summary_path),
22185                )
22186                .await,
22187                Ok(Ok(()))
22188            ),
22189            "cleanup waited after cancelling a registered scheduler"
22190        );
22191    }
22192
22193    /// A deterministic inode must be minted from the monotonic counter, never
22194    /// derived from the host inode's bits. This is the behavioural half of the
22195    /// guarantee whose static half is `DetInode` being a newtype: even for a
22196    /// large, realistic host inode the det value stays small, so a leaked host
22197    /// inode is distinguishable from a genuine det one.
22198    #[test]
22199    fn det_inodes_are_minted_not_passed_through() {
22200        use crate::types::DetInode;
22201
22202        let mut pool = super::InodePool::new();
22203        let t = LogicalTime::from_nanos(0);
22204        let seen = super::ObservedMtime::Unobserved;
22205
22206        // 221_742_951 is the value observed leaking into FileContents.
22207        let host_a = crate::types::RawInode::new(33, 221_742_951);
22208        let host_b = crate::types::RawInode::new(33, 998_877_665);
22209        let (a, _) = pool.add_inode(host_a, seen, t);
22210        let (b, _) = pool.add_inode(host_b, seen, t);
22211
22212        assert_ne!(
22213            a.as_raw(),
22214            host_a.ino,
22215            "det inode must not be the host inode"
22216        );
22217        assert_ne!(
22218            b.as_raw(),
22219            host_b.ino,
22220            "det inode must not be the host inode"
22221        );
22222        assert_eq!(
22223            a,
22224            crate::consts::DET_INODE_OFFSET,
22225            "minting starts above the fixed stdio inodes"
22226        );
22227        assert_eq!(b, DetInode::mint(9001), "minting is monotonic");
22228
22229        // Re-determinizing the same host inode is stable, not a fresh mint.
22230        let (a_again, _) = pool.add_inode(host_a, seen, t);
22231        assert_eq!(a, a_again, "mapping must be stable per host inode");
22232    }
22233
22234    /// <https://github.com/rrnewton/hermit/issues/3840>: a filesystem such as
22235    /// ext4 or XFS gives a freed inode to a new file, and whether it does
22236    /// differs between runs. The same guest steps (number a file, remove its
22237    /// last name, create another file, look it up) must number the new file,
22238    /// and every file after it, the same way whether or not the host reused
22239    /// the inode, however the new file is first reached.
22240    #[test]
22241    fn a_host_reusing_a_freed_inode_does_not_renumber_the_new_file() {
22242        use super::InodeSighting::Descriptor;
22243        use super::InodeSighting::Name;
22244        use crate::types::DetInode;
22245        use crate::types::RawInode;
22246
22247        let t = LogicalTime::from_nanos(0);
22248        let seen = super::ObservedMtime::Unobserved;
22249        const EXT4: u64 = 2049;
22250        let old = RawInode::new(EXT4, 12);
22251        let fresh = RawInode::new(EXT4, 13);
22252        // `opened` says whether the new file is created by an `open`, which
22253        // discards a retired mapping, before its first numbering request.
22254        let steps = |new: RawInode, opened: bool, first: super::InodeSighting| {
22255            let mut pool = super::InodePool::new();
22256            let old_number = pool.add_sighted_inode(old, seen, t, Name).0;
22257            pool.retire(old);
22258            if opened {
22259                pool.forget_retired(new);
22260            }
22261            let new_number = pool.add_sighted_inode(new, seen, t, first).0;
22262            let listed = pool.add_sighted_inode(new, seen, t, Name).0;
22263            let later: Vec<DetInode> = [RawInode::new(EXT4, 40), RawInode::new(EXT4, 41)]
22264                .into_iter()
22265                .map(|raw| pool.add_sighted_inode(raw, seen, t, Name).0)
22266                .collect();
22267            (old_number, new_number, listed, later)
22268        };
22269        for (opened, first) in [(true, Descriptor), (true, Name), (false, Name)] {
22270            let reused = steps(old, opened, first);
22271            let not_reused = steps(fresh, opened, first);
22272            assert_eq!(
22273                reused, not_reused,
22274                "opened={opened} first={first:?}: reuse changed a number"
22275            );
22276            let (old_number, new_number, listed, _) = reused;
22277            assert_ne!(old_number, new_number, "the new file took the old number");
22278            assert_eq!(new_number, listed, "the new file's number is stable");
22279        }
22280    }
22281
22282    /// An unlinked file reached through a descriptor (open, unlink, fstat)
22283    /// keeps its number and its mtime updates, as on Linux, until a name shows
22284    /// that the host gave its inode to another file. A path that finds no
22285    /// link (`/proc/self/fd/N`) is a `Descriptor` sighting.
22286    #[test]
22287    fn an_unlinked_file_keeps_its_number_through_its_descriptors() {
22288        use super::InodeSighting::Descriptor;
22289        use super::InodeSighting::Name;
22290        use crate::types::RawInode;
22291
22292        let t = LogicalTime::from_nanos(0);
22293        let seen = super::ObservedMtime::Unobserved;
22294        let raw = RawInode::new(2049, 12);
22295        let mut pool = super::InodePool::new();
22296        let (number, _) = pool.add_sighted_inode(raw, seen, t, Name);
22297        pool.retire(raw);
22298        assert_eq!(pool.add_sighted_inode(raw, seen, t, Descriptor).0, number);
22299        let written = LogicalTime::from_nanos(7_000);
22300        pool.set_mtime(raw, written);
22301        assert_eq!(
22302            pool.add_sighted_inode(raw, seen, t, Descriptor),
22303            (number, written),
22304            "a write through the descriptor reached the retired file"
22305        );
22306        let (reused, _) = pool.add_sighted_inode(raw, seen, t, Name);
22307        assert_ne!(reused, number, "a name of a retired inode is another file");
22308        assert_eq!(
22309            pool.add_sighted_inode(raw, seen, t, Descriptor).0,
22310            reused,
22311            "descriptors now reach the new file"
22312        );
22313        // Without a retire, a name finds the live mapping, as before.
22314        let other = RawInode::new(2049, 99);
22315        let (live, _) = pool.add_sighted_inode(other, seen, t, Descriptor);
22316        assert_eq!(pool.add_sighted_inode(other, seen, t, Name).0, live);
22317    }
22318
22319    /// A write's mtime update can reach a file before anything numbers it
22320    /// (`echo x > f`, then `rm f`). The pending mtime must stay with that file,
22321    /// for a descriptor that still reaches it, and must not reach the next
22322    /// file the host gives the inode (claude review of
22323    /// <https://github.com/rrnewton/hermit/pull/3849>, P2-2).
22324    #[test]
22325    fn a_pending_mtime_stays_with_its_file_after_the_last_name_goes() {
22326        use super::InodeSighting::Descriptor;
22327        use super::InodeSighting::Name;
22328        use crate::types::RawInode;
22329
22330        let t = LogicalTime::from_nanos(0);
22331        let seen = super::ObservedMtime::Unobserved;
22332        let old = RawInode::new(2049, 12);
22333        let fresh = RawInode::new(2049, 13);
22334        let written = LogicalTime::from_nanos(7_000);
22335        // The next file is created by an open, or first seen in a listing.
22336        let next_file = |new: RawInode, opened: bool| {
22337            let mut pool = super::InodePool::new();
22338            pool.set_mtime(old, written);
22339            pool.retire(old);
22340            if opened {
22341                pool.forget_retired(new);
22342            }
22343            pool.add_sighted_inode(new, seen, t, Name)
22344        };
22345        for opened in [true, false] {
22346            assert_eq!(
22347                next_file(old, opened),
22348                next_file(fresh, opened),
22349                "opened={opened}: the reused inode inherited the removed file's mtime"
22350            );
22351        }
22352
22353        // Through a remaining descriptor, the removed file keeps its mtime,
22354        // and later writes through it reach it.
22355        let mut pool = super::InodePool::new();
22356        pool.set_mtime(old, written);
22357        pool.retire(old);
22358        let (number, mtime) = pool.add_sighted_inode(old, seen, t, Descriptor);
22359        assert_eq!(mtime, written);
22360        let rewritten = LogicalTime::from_nanos(9_000);
22361        pool.set_mtime(old, rewritten);
22362        assert_eq!(
22363            pool.add_sighted_inode(old, seen, t, Descriptor),
22364            (number, rewritten)
22365        );
22366        // It was numbered as retired: a name is another file.
22367        let (other, _) = pool.add_sighted_inode(old, seen, t, Name);
22368        assert_ne!(other, number);
22369    }
22370
22371    /// A directory stream snapshots the whole directory at its first
22372    /// `getdents` and can return an entry after the entry's file lost its
22373    /// last name, while a descriptor still holds that file. Such an entry is
22374    /// not a new file, so it must not renumber the held one; an entry from a
22375    /// snapshot taken after the retirement is (codex review of
22376    /// <https://github.com/rrnewton/hermit/pull/3849>, P1).
22377    #[test]
22378    fn a_stale_directory_entry_does_not_renumber_a_held_file() {
22379        use super::InodeSighting::Descriptor;
22380        use super::InodeSighting::Listed;
22381        use super::InodeSighting::Name;
22382        use crate::types::RawInode;
22383
22384        let t = LogicalTime::from_nanos(0);
22385        let seen = super::ObservedMtime::Unobserved;
22386        let raw = RawInode::new(2049, 12);
22387        let mut pool = super::InodePool::new();
22388        let (number, _) = pool.add_sighted_inode(raw, seen, t, Name);
22389        let before = pool.retirements;
22390        pool.retire(raw);
22391        assert_eq!(
22392            pool.add_sighted_inode(raw, seen, t, Listed(before)).0,
22393            number
22394        );
22395        assert_eq!(pool.add_sighted_inode(raw, seen, t, Descriptor).0, number);
22396        let after = pool.retirements;
22397        let (reused, _) = pool.add_sighted_inode(raw, seen, t, Listed(after));
22398        assert_ne!(
22399            reused, number,
22400            "a snapshot taken after the retirement shows a new file"
22401        );
22402    }
22403
22404    /// A snapshot entry must name the file that held the inode when the
22405    /// snapshot was taken, even after a later file the host gave the freed
22406    /// inode discarded that file's retirement. Codex re-review of
22407    /// https://github.com/rrnewton/hermit/pull/3849, P1 (Claude re-review
22408    /// P2-A): number F, take a snapshot, unlink F, create and number G, then
22409    /// read F's cached entry; reuse and a fresh inode must agree on every
22410    /// number, whether or not F was numbered before the snapshot.
22411    #[test]
22412    fn a_stale_directory_entry_keeps_its_file_after_the_inode_is_reused() {
22413        use super::InodeSighting::Listed;
22414        use super::InodeSighting::Name;
22415        use crate::types::DetInode;
22416        use crate::types::RawInode;
22417
22418        let t = LogicalTime::from_nanos(0);
22419        let seen = super::ObservedMtime::Unobserved;
22420        let f = RawInode::new(2049, 12);
22421        let fresh = RawInode::new(2049, 13);
22422        let history = |g: RawInode, numbered_first: bool| -> Vec<DetInode> {
22423            let mut pool = super::InodePool::new();
22424            let mut numbers = Vec::new();
22425            if numbered_first {
22426                numbers.push(pool.add_sighted_inode(f, seen, t, Name).0);
22427            }
22428            let snapshot = pool.retirements;
22429            pool.retire(f);
22430            pool.forget_retired(g); // G's open.
22431            numbers.push(pool.add_sighted_inode(g, seen, t, Name).0);
22432            numbers.push(pool.add_sighted_inode(f, seen, t, Listed(snapshot)).0);
22433            numbers.push(pool.add_sighted_inode(f, seen, t, Listed(snapshot)).0);
22434            numbers.push(
22435                pool.add_sighted_inode(RawInode::new(2049, 40), seen, t, Name)
22436                    .0,
22437            );
22438            numbers
22439        };
22440        for numbered_first in [true, false] {
22441            let reused = history(f, numbered_first);
22442            assert_eq!(
22443                reused,
22444                history(fresh, numbered_first),
22445                "numbered_first={numbered_first}: host reuse changed a number"
22446            );
22447            let cached = reused[reused.len() - 3];
22448            let g = reused[reused.len() - 4];
22449            assert_ne!(cached, g, "F's cached entry took G's number");
22450            assert_eq!(
22451                cached,
22452                reused[reused.len() - 2],
22453                "the cached entry is stable"
22454            );
22455        }
22456    }
22457
22458    /// A write through a descriptor after the last name of a never-numbered
22459    /// file went away reaches that file's history record too, so a directory
22460    /// snapshot that names it after the host reused the inode sees the same
22461    /// mtime as one that names it without reuse (claude re-check of
22462    /// https://github.com/rrnewton/hermit/pull/3849).
22463    #[test]
22464    fn a_write_after_the_last_name_reaches_the_snapshot_record() {
22465        use super::InodeSighting::Listed;
22466        use super::InodeSighting::Name;
22467        use crate::types::RawInode;
22468
22469        let t = LogicalTime::from_nanos(0);
22470        let seen = super::ObservedMtime::Unobserved;
22471        let f = RawInode::new(2049, 12);
22472        let fresh = RawInode::new(2049, 13);
22473        let cached = |g: RawInode| {
22474            let mut pool = super::InodePool::new();
22475            pool.set_mtime(f, LogicalTime::from_nanos(1_000));
22476            let snapshot = pool.retirements;
22477            pool.retire(f);
22478            pool.set_mtime(f, LogicalTime::from_nanos(2_000));
22479            pool.forget_retired(g);
22480            pool.add_sighted_inode(g, seen, t, Name);
22481            pool.add_sighted_inode(f, seen, t, Listed(snapshot))
22482        };
22483        assert_eq!(cached(f), cached(fresh));
22484        assert_eq!(cached(f).1, LogicalTime::from_nanos(2_000));
22485    }
22486
22487    /// Ordinary churn reuses one host inode many times. A snapshot lookup
22488    /// must not scan the inode's whole history (codex re-check of
22489    /// https://github.com/rrnewton/hermit/pull/3849): a snapshot newer than
22490    /// the last retirement is answered without examining any record, and an
22491    /// older one examines about log2 of the history's length, while still
22492    /// naming the file that held the inode when it was taken.
22493    #[test]
22494    fn a_snapshot_lookup_does_not_scan_the_inodes_history() {
22495        use super::InodeSighting::Listed;
22496        use super::InodeSighting::Name;
22497        use crate::types::RawInode;
22498
22499        const GENERATIONS: usize = 4096;
22500        let t = LogicalTime::from_nanos(0);
22501        let seen = super::ObservedMtime::Unobserved;
22502        let raw = RawInode::new(2049, 12);
22503        let mut pool = super::InodePool::new();
22504        // Each holder is numbered, seen in a snapshot, and retired; the next
22505        // file the host gives the inode is opened by name.
22506        let mut held = Vec::with_capacity(GENERATIONS);
22507        for _ in 0..GENERATIONS {
22508            pool.forget_retired(raw);
22509            let number = pool.add_sighted_inode(raw, seen, t, Name).0;
22510            held.push((pool.retirements, number));
22511            pool.retire(raw);
22512        }
22513        // The current holder, numbered after the last retirement.
22514        pool.forget_retired(raw);
22515        let current = pool.add_sighted_inode(raw, seen, t, Name).0;
22516
22517        pool.listed_probes = 0;
22518        for _ in 0..1000 {
22519            let snapshot = pool.retirements;
22520            assert_eq!(
22521                pool.add_sighted_inode(raw, seen, t, Listed(snapshot)).0,
22522                current
22523            );
22524        }
22525        assert_eq!(
22526            pool.listed_probes, 0,
22527            "a current snapshot examined history records"
22528        );
22529
22530        let bound = (GENERATIONS.ilog2() + 2) as u64;
22531        for step in (0..GENERATIONS).step_by(37) {
22532            let (snapshot, number) = held[step];
22533            pool.listed_probes = 0;
22534            assert_eq!(
22535                pool.add_sighted_inode(raw, seen, t, Listed(snapshot)).0,
22536                number,
22537                "the snapshot taken while holder {step} held the inode"
22538            );
22539            assert!(
22540                pool.listed_probes <= bound,
22541                "holder {step}: {} probes for {GENERATIONS} generations",
22542                pool.listed_probes
22543            );
22544        }
22545    }
22546
22547    /// Retiring and discarding follow host state (a link count, whether the
22548    /// host reused an inode), so they must not consume counter values: files
22549    /// numbered after them get the same numbers either way.
22550    #[test]
22551    fn retiring_and_forgetting_consume_no_counter_values() {
22552        use crate::types::RawInode;
22553
22554        let t = LogicalTime::from_nanos(0);
22555        let seen = super::ObservedMtime::Unobserved;
22556        let raw = RawInode::new(2049, 12);
22557        let numbers = |retire: bool| {
22558            let mut pool = super::InodePool::new();
22559            pool.add_inode(raw, seen, t);
22560            if retire {
22561                pool.retire(raw);
22562                pool.forget_retired(raw);
22563                pool.forget_retired(RawInode::new(2049, 77));
22564            }
22565            [RawInode::new(2049, 20), RawInode::new(2049, 21)]
22566                .map(|raw| pool.add_inode(raw, seen, t).0)
22567        };
22568        assert_eq!(numbers(true), numbers(false));
22569    }
22570
22571    /// Whether a host inode is new to the pool depends on host state the guest
22572    /// does not control. In https://github.com/rrnewton/hermit/issues/2897,
22573    /// `sar` and its child `sadc` each opened one file; in one run another
22574    /// process replaced it between the two opens, so the second open saw a new
22575    /// host inode. Every later file, on every device, came out one number
22576    /// higher, and every `getdents64` chunk of a `/sys` listing differed. Only
22577    /// the file whose host identity differed may change number.
22578    #[test]
22579    fn a_host_dependent_new_inode_does_not_renumber_later_files() {
22580        use crate::types::DetInode;
22581        use crate::types::RawInode;
22582
22583        let t = LogicalTime::from_nanos(0);
22584        let seen = super::ObservedMtime::Unobserved;
22585        const ROOTFS: u64 = 2049;
22586        const SYSFS: u64 = 22;
22587        let later_files = |pool: &mut super::InodePool| -> Vec<DetInode> {
22588            [
22589                RawInode::new(SYSFS, 76_927),
22590                RawInode::new(SYSFS, 76_925),
22591                RawInode::new(SYSFS, 243_324),
22592                RawInode::new(ROOTFS, 1_311),
22593                RawInode::new(ROOTFS, 1_312),
22594            ]
22595            .into_iter()
22596            .map(|raw| pool.add_inode(raw, seen, t).0)
22597            .collect()
22598        };
22599
22600        // Both opens find the same host file.
22601        let mut unchanged = super::InodePool::new();
22602        unchanged.add_inode(RawInode::new(ROOTFS, 94_136_091), seen, t);
22603        unchanged.add_inode(RawInode::new(ROOTFS, 94_136_091), seen, t);
22604        // The file is replaced between the two opens.
22605        let mut replaced = super::InodePool::new();
22606        replaced.add_inode(RawInode::new(ROOTFS, 94_136_091), seen, t);
22607        replaced.add_inode(RawInode::new(ROOTFS, 94_154_661), seen, t);
22608        assert_eq!(
22609            later_files(&mut unchanged),
22610            later_files(&mut replaced),
22611            "a host-dependent new inode renumbered files minted after it"
22612        );
22613
22614        // The converse: two paths that are one host file (a hard link, or an
22615        // inode the filesystem reused) versus two separate host files.
22616        let mut linked = super::InodePool::new();
22617        linked.add_inode(RawInode::new(ROOTFS, 500), seen, t);
22618        linked.add_inode(RawInode::new(ROOTFS, 500), seen, t);
22619        let mut separate = super::InodePool::new();
22620        separate.add_inode(RawInode::new(ROOTFS, 500), seen, t);
22621        separate.add_inode(RawInode::new(ROOTFS, 501), seen, t);
22622        assert_eq!(
22623            later_files(&mut linked),
22624            later_files(&mut separate),
22625            "a host-dependent reused inode renumbered files minted after it"
22626        );
22627    }
22628
22629    /// Whether an mtime update reaches the pool depends on host state:
22630    /// `utimensat` skips it when the target's host identity changed during the
22631    /// call or the kernel stored an unexpected time. Setting an mtime must not
22632    /// consume a number, so a skipped update cannot renumber later files; and
22633    /// a file that gets its mtime before its number keeps that mtime.
22634    #[test]
22635    fn an_mtime_update_does_not_number_files() {
22636        use crate::types::DetInode;
22637        use crate::types::RawInode;
22638
22639        let epoch = LogicalTime::from_nanos(0);
22640        let set = LogicalTime::from_nanos(5_000_000_000);
22641        let seen = super::ObservedMtime::Unobserved;
22642        let touched = RawInode::new(2049, 77);
22643        let later_files = |pool: &mut super::InodePool| -> Vec<DetInode> {
22644            (1_000..1_003)
22645                .map(|ino| pool.add_inode(RawInode::new(2049, ino), seen, epoch).0)
22646                .collect()
22647        };
22648
22649        // The update reached the pool, before and after the file was numbered.
22650        let mut applied = super::InodePool::new();
22651        applied.set_mtime(touched, set);
22652        let (_, mtime) = applied.add_inode(touched, seen, epoch);
22653        assert_eq!(
22654            mtime, set,
22655            "a pending mtime is lost when the file is numbered"
22656        );
22657        applied.set_mtime(touched, set);
22658        // The same calls with the updates skipped.
22659        let mut skipped = super::InodePool::new();
22660        skipped.add_inode(touched, seen, epoch);
22661        assert_eq!(
22662            later_files(&mut applied),
22663            later_files(&mut skipped),
22664            "a host-dependent mtime update renumbered later files"
22665        );
22666    }
22667
22668    /// The serial grant follows the scheduler's real transitions: a granted
22669    /// thread holds it; filing its next request surrenders it; and a grant
22670    /// that releases the thread into a background call does not confer it,
22671    /// although that grant is the last one (`parked.running`).
22672    #[tokio::test]
22673    async fn the_serial_grant_ends_at_the_next_request_and_is_not_a_background_grant() {
22674        let (_config, state, tid, _) = cancellation_test_state();
22675        let first = Ivar::new();
22676        first.put(Ok(Resources::new(tid)));
22677        install_test_registration(&state, tid, first);
22678        assert!(!state.sched.lock().unwrap().holds_serial_grant(tid));
22679
22680        let grant = |state: &GlobalState| {
22681            let (chosen, request, response) =
22682                state.sched.lock().unwrap().select_test_turn().unwrap();
22683            assert_eq!(chosen, tid);
22684            crate::scheduler::finish_selected_turn(
22685                state.sched.clone(),
22686                state.global_time.clone(),
22687                chosen,
22688                request,
22689                response,
22690            )
22691        };
22692        grant(&state).await.unwrap();
22693        assert!(state.sched.lock().unwrap().holds_serial_grant(tid));
22694
22695        // The thread files its next request: a blocking external call.
22696        let next = state.sched.lock().unwrap().next_turns[&tid].req.clone();
22697        let mut background = Resources::new(tid);
22698        background.insert(
22699            ResourceID::BlockingExternalIO(ExternalOpId::new(tid, 1)),
22700            Permission::RW,
22701        );
22702        next.put(Ok(background));
22703        assert!(!state.sched.lock().unwrap().holds_serial_grant(tid));
22704
22705        // The scheduler releases it into the background. A background grant
22706        // unblocks the thread in step 4 and then skips the rest of the turn.
22707        assert!(grant(&state).await.is_err());
22708        let sched = state.sched.lock().unwrap();
22709        assert!(sched.blocked.external_io_blockers.contains_key(&tid));
22710        assert!(!sched.holds_serial_grant(tid));
22711    }
22712
22713    /// Controller mapping capture authenticates its task and procfs view,
22714    /// retains the existing serial grant, and returns no logical time.
22715    #[tokio::test]
22716    async fn controller_brk_capture_authenticates_the_view_without_spending_the_grant() {
22717        use std::os::unix::fs::MetadataExt;
22718
22719        use crate::syscalls::memory::BrkMappingCheck;
22720
22721        let config = Config {
22722            sequentialize_threads: true,
22723            ..Config::default()
22724        };
22725        let state = GlobalState::initialize(&config, false);
22726        let tid = DetTid::from_raw(37);
22727        let mm = MmId::initial(tid);
22728        let pid = std::process::id() as i32;
22729        let physical_tid = unsafe { libc::syscall(libc::SYS_gettid) as i32 };
22730        let procfs = std::fs::metadata("/proc").unwrap();
22731        let fact = reverie::ControllerCaptureFact {
22732            physical_pid: reverie::Pid::from_raw(pid),
22733            physical_tid: reverie::Pid::from_raw(physical_tid),
22734            procfs_device: procfs.dev(),
22735            procfs_inode: procfs.ino(),
22736        };
22737        {
22738            let mut sched = state.sched.lock().unwrap();
22739            sched.thread_tree.add_child(tid, tid, true);
22740            sched.record_thread_mm(tid, mm);
22741            sched.next_turns.insert(
22742                tid,
22743                ThreadNextTurn {
22744                    dettid: tid,
22745                    child_tid_addr: 0,
22746                    req: Ivar::new(),
22747                    resp: Ivar::new(),
22748                    protocol: Default::default(),
22749                },
22750            );
22751            sched
22752                .register_physical_thread(tid, mm, pid, physical_tid)
22753                .unwrap();
22754            sched.set_running_for_test(tid);
22755        }
22756        let before = state.global_time.lock().unwrap().as_nanos();
22757        // This real smaps capture must deny an overflowing growth, without
22758        // allocating a guest-visible snapshot or advancing the logical clock.
22759        let check = BrkMappingCheck::Grow {
22760            old_end: 0,
22761            new_end: u64::MAX,
22762        };
22763        let answer = state
22764            .receive_rpc(
22765                Tid::from_raw(tid.as_raw()),
22766                (
22767                    DetTime::new(&config),
22768                    mm,
22769                    GlobalRequest::CheckBrkMappings(check, fact),
22770                ),
22771            )
22772            .await;
22773        assert_eq!(
22774            answer,
22775            (None, GlobalResponse::CheckBrkMappings(Some(false)))
22776        );
22777        assert_eq!(state.global_time.lock().unwrap().as_nanos(), before);
22778        assert!(state.sched.lock().unwrap().holds_serial_grant(tid));
22779        assert_eq!(
22780            state.capture_brk_mappings(
22781                Tid::from_raw(tid.as_raw()),
22782                mm,
22783                check,
22784                reverie::ControllerCaptureFact {
22785                    procfs_inode: fact.procfs_inode + 1,
22786                    ..fact
22787                },
22788            ),
22789            None,
22790        );
22791        let address = &before as *const _ as u64;
22792        assert_eq!(
22793            state.capture_brk_mappings(
22794                Tid::from_raw(tid.as_raw()),
22795                mm,
22796                BrkMappingCheck::Shrink {
22797                    new_end: address,
22798                    old_end: address + 1
22799                },
22800                fact,
22801            ),
22802            Some(true),
22803        );
22804        let unsequentialized = GlobalState::initialize(
22805            &Config {
22806                sequentialize_threads: false,
22807                ..config
22808            },
22809            false,
22810        );
22811        assert_eq!(
22812            unsequentialized.capture_brk_mappings(Tid::from_raw(tid.as_raw()), mm, check, fact,),
22813            None
22814        );
22815    }
22816
22817    /// Only the thread holding the serial grant reaches a shared open file
22818    /// description. Its control messages move no clock, and the death of the
22819    /// lease holder's process ends the lease. A refused message fails the run.
22820    #[tokio::test]
22821    async fn shared_open_file_control_needs_the_serial_grant() {
22822        use nix::fcntl::OFlag;
22823
22824        use crate::fd::DetFd;
22825        use crate::fd::FdType;
22826        use crate::shared_open_files::OpenFileControl;
22827        use crate::shared_open_files::OpenFileControlReply;
22828        use crate::types::OpenFileId;
22829
22830        let config = Config {
22831            sequentialize_threads: true,
22832            ..Config::default()
22833        };
22834        let state = GlobalState::initialize(&config, false);
22835        let granted = DetTid::from_raw(21);
22836        let other = DetTid::from_raw(22);
22837        let id = OpenFileId::new(granted, 0);
22838        {
22839            let mut sched = state.sched.lock().unwrap();
22840            for tid in [granted, other] {
22841                sched.thread_tree.add_child(tid, tid, true);
22842                sched.next_turns.insert(
22843                    tid,
22844                    ThreadNextTurn {
22845                        dettid: tid,
22846                        child_tid_addr: 0,
22847                        req: Ivar::new(),
22848                        resp: Ivar::new(),
22849                        protocol: Default::default(),
22850                    },
22851                );
22852            }
22853            sched.set_running_for_test(granted);
22854        }
22855        state
22856            .shared_open_files
22857            .lock()
22858            .unwrap()
22859            .insert(DetFd::new(3, OFlag::O_RDWR, FdType::Regular, id).model_for_test())
22860            .unwrap();
22861        let global_before = state.global_time.lock().unwrap().as_nanos();
22862        let take = |tid: DetTid| {
22863            state.receive_rpc(
22864                reverie::Tid::from_raw(tid.as_raw()),
22865                (
22866                    DetTime::new(&config),
22867                    MmId::initial(tid),
22868                    GlobalRequest::SharedOpenFile(OpenFileControl::Take { id }),
22869                ),
22870            )
22871        };
22872
22873        assert!(matches!(
22874            take(granted).await,
22875            (
22876                None,
22877                GlobalResponse::SharedOpenFile(Ok(OpenFileControlReply::Taken { .. }))
22878            )
22879        ));
22880        assert!(state.shared_open_files.lock().unwrap().is_leased(id));
22881        {
22882            let global_time = state.global_time.lock().unwrap();
22883            assert_eq!(global_time.as_nanos(), global_before, "no clock accounting");
22884            assert!(!global_time.contains_thread(granted));
22885        }
22886
22887        state.complete_physical_process_exit(granted.as_raw());
22888        assert!(!state.shared_open_files.lock().unwrap().is_leased(id));
22889        assert!(!state.sched.lock().unwrap().backend_failed());
22890
22891        // A thread that does not hold the grant is refused, takes nothing,
22892        // and the refusal is a backend failure.
22893        assert!(matches!(
22894            take(other).await,
22895            (None, GlobalResponse::SharedOpenFile(Err(_)))
22896        ));
22897        assert!(!state.shared_open_files.lock().unwrap().is_leased(id));
22898        assert!(state.sched.lock().unwrap().backend_failed());
22899    }
22900
22901    /// A SIGALRM ledger control message is answered with no logical time and
22902    /// moves no clock, and its answer is the ledger's: a send to a process that
22903    /// handles SIGALRM is refused, one to another process is not.
22904    #[tokio::test]
22905    async fn sigalrm_control_is_answered_before_clock_accounting() {
22906        let config = Config {
22907            sequentialize_threads: true,
22908            ..Config::default()
22909        };
22910        let state = GlobalState::initialize(&config, false);
22911        let (handler, sender) = (DetTid::from_raw(21), DetTid::from_raw(22));
22912        {
22913            let mut sched = state.sched.lock().unwrap();
22914            for tid in [handler, sender] {
22915                sched.thread_tree.add_child(tid, tid, true);
22916                sched.next_turns.insert(
22917                    tid,
22918                    ThreadNextTurn {
22919                        dettid: tid,
22920                        child_tid_addr: 0,
22921                        req: Ivar::new(),
22922                        resp: Ivar::new(),
22923                        protocol: Default::default(),
22924                    },
22925                );
22926            }
22927            sched.set_sigalrm_handled(DetPid::from_raw(21), true);
22928            sched.set_running_for_test(sender);
22929        }
22930        let global_before = state.global_time.lock().unwrap().as_nanos();
22931        let send_to = |target: DetTid| {
22932            state.receive_rpc(
22933                reverie::Tid::from_raw(sender.as_raw()),
22934                (
22935                    DetTime::new(&config),
22936                    MmId::initial(sender),
22937                    GlobalRequest::Sigalrm(super::SigalrmControl::SendTo(target)),
22938                ),
22939            )
22940        };
22941
22942        assert_eq!(
22943            send_to(handler).await,
22944            (None, GlobalResponse::Sigalrm(true))
22945        );
22946        assert_eq!(
22947            send_to(sender).await,
22948            (None, GlobalResponse::Sigalrm(false))
22949        );
22950        let global_time = state.global_time.lock().unwrap();
22951        assert_eq!(global_time.as_nanos(), global_before, "no clock accounting");
22952        assert!(!global_time.contains_thread(sender));
22953    }
22954
22955    /// A run that replays a schedule whose recording names the host files its
22956    /// run opened reports each file whose first open now finds another file
22957    /// (here, unhashed, another identity), once, in its summary, and keeps its own host-input set in the
22958    /// summary and in the schedule it records. The comparison changes no
22959    /// clock (`host_input_observations_are_kept_by_the_run_and_written_at_its_end`).
22960    #[tokio::test]
22961    async fn a_schedule_replay_reports_host_inputs_changed_since_its_recording() {
22962        use detcore_model::host_input::HostFileIdentity;
22963        use detcore_model::host_input::HostInputChange;
22964        use detcore_model::host_input::HostInputRecord;
22965
22966        let identity = |ino: u64, size: i64| HostFileIdentity {
22967            dev: 33,
22968            ino,
22969            size,
22970            mtime_sec: 1_700_000_000,
22971            mtime_nsec: 5,
22972        };
22973        let open = |path: &str, ino: u64, size: i64| HostInputRecord {
22974            path: path.to_owned(),
22975            dtid: 3,
22976            syscall: 1,
22977            identity: identity(ino, size),
22978            content: None,
22979            exec: false,
22980            kernel_object: false,
22981        };
22982        let directory = tempfile::tempdir().unwrap();
22983        let recorded = directory.path().join("recorded.json");
22984        let mut writer = crate::preemptions::PreemptionWriter::new(Some(recorded.clone()));
22985        writer.set_host_inputs(crate::host_inputs::input_set(
22986            &[
22987                open("/etc/ld.so.cache", 9, 36887),
22988                open("/lib64/libc.so.6", 10, 2_000_000),
22989            ],
22990            &BTreeSet::new(),
22991        ));
22992        writer.flush().unwrap();
22993
22994        let rerecorded = directory.path().join("rerecorded.json");
22995        let summary = directory.path().join("summary.json");
22996        let config = Config {
22997            sequentialize_threads: true,
22998            record_host_inputs: true,
22999            replay_preemptions_from: Some(recorded),
23000            record_preemptions: true,
23001            record_preemptions_to: Some(rerecorded.clone()),
23002            ..Config::default()
23003        };
23004        let state = GlobalState::initialize(&config, false);
23005        let dettid = DetTid::from_raw(3);
23006        let sender_time = DetTime::new(&config);
23007        let global_before = state.global_time.lock().unwrap().as_nanos();
23008        for (path, ino, size) in [
23009            ("/etc/ld.so.cache", 11, 36839),
23010            ("/lib64/libc.so.6", 10, 2_000_000),
23011            // A second open of a changed path is not reported again.
23012            ("/etc/ld.so.cache", 12, 36887),
23013        ] {
23014            let response = state
23015                .receive_rpc(
23016                    reverie::Tid::from_raw(dettid.as_raw()),
23017                    (
23018                        sender_time.clone(),
23019                        MmId::initial(dettid),
23020                        GlobalRequest::RecordHostInput {
23021                            path: path.into(),
23022                            syscall: 1,
23023                            identity: identity(ino, size),
23024                            source: None,
23025                            regular: true,
23026                            exec: false,
23027                            kernel_object: false,
23028                        },
23029                    ),
23030                )
23031                .await;
23032            assert_eq!(response, (None, GlobalResponse::RecordHostInput(())));
23033        }
23034        assert_eq!(
23035            state.global_time.lock().unwrap().as_nanos(),
23036            global_before,
23037            "no clock accounting"
23038        );
23039
23040        state.clean_up(false, &Some(summary.clone())).await.unwrap();
23041        let summary: detcore_model::summary::RunSummary =
23042            serde_json::from_str(&std::fs::read_to_string(&summary).unwrap()).unwrap();
23043        assert_eq!(
23044            summary.host_inputs_changed,
23045            vec![HostInputChange {
23046                path: "/etc/ld.so.cache".into(),
23047                recorded: identity(9, 36887),
23048                now: identity(11, 36839),
23049            }]
23050        );
23051        let inputs = summary.host_inputs.expect("the run's host-input set");
23052        assert_eq!(
23053            inputs
23054                .files
23055                .iter()
23056                .map(|file| (file.path.as_str(), file.identity.ino))
23057                .collect::<Vec<_>>(),
23058            [("/etc/ld.so.cache", 11), ("/lib64/libc.so.6", 10)]
23059        );
23060        let rerecorded =
23061            crate::preemptions::boundary_recording::Recording::read(&rerecorded).unwrap();
23062        assert_eq!(rerecorded.host_inputs(), Some(&inputs));
23063    }
23064
23065    /// The bytes of a file the guest opened are read when the run has ended,
23066    /// not at the open (a read while the guest runs raises inotify events
23067    /// it can see), and only where the file still has the identity the open
23068    /// found; a replay's change that only the bytes can show is reported
23069    /// then, in the summary.
23070    #[tokio::test]
23071    async fn host_input_contents_are_read_when_the_run_has_ended() {
23072        use std::path::Path;
23073        use std::path::PathBuf;
23074
23075        use detcore_model::host_input::HostFileIdentity;
23076        use detcore_model::host_input::HostInputRecord;
23077
23078        let directory = tempfile::tempdir().unwrap();
23079        let kept = directory.path().join("kept");
23080        let edited = directory.path().join("edited");
23081        let replaced = directory.path().join("replaced");
23082        for file in [&kept, &edited, &replaced] {
23083            std::fs::write(file, "input\n").unwrap();
23084        }
23085        let identity_of =
23086            |path: &Path| HostFileIdentity::from_stat(&crate::util::raw_stat(path).unwrap());
23087        // `printf 'input\n' | sha256sum`
23088        let input = "7d3f9b6284c6f36e77b425cac882e8fbbcc97a4727ec20790853076d0f463453";
23089        // The recording read "input\n" at each path.
23090        let recorded = directory.path().join("recorded.json");
23091        let mut writer = crate::preemptions::PreemptionWriter::new(Some(recorded.clone()));
23092        writer.set_host_inputs(crate::host_inputs::input_set(
23093            &[&kept, &edited, &replaced].map(|file| HostInputRecord {
23094                path: file.to_str().unwrap().to_owned(),
23095                dtid: 3,
23096                syscall: 1,
23097                identity: identity_of(file),
23098                content: Some(input.to_owned()),
23099                exec: false,
23100                kernel_object: false,
23101            }),
23102            &BTreeSet::new(),
23103        ));
23104        writer.flush().unwrap();
23105        // This run finds other bytes of the same size at `edited`, and at
23106        // `replaced` under a new inode.
23107        std::fs::write(&edited, "other\n").unwrap();
23108        let new = directory.path().join("new");
23109        std::fs::write(&new, "other\n").unwrap();
23110        std::fs::rename(&new, &replaced).unwrap();
23111
23112        let summary = directory.path().join("summary.json");
23113        let config = Config {
23114            sequentialize_threads: true,
23115            record_host_inputs: true,
23116            replay_preemptions_from: Some(recorded),
23117            ..Config::default()
23118        };
23119        let state = GlobalState::initialize(&config, false);
23120        let dettid = DetTid::from_raw(3);
23121        let sender_time = DetTime::new(&config);
23122        let opened: Vec<(PathBuf, HostFileIdentity)> = [&kept, &edited, &replaced]
23123            .map(|file| (file.clone(), identity_of(file)))
23124            .into();
23125        for (file, identity) in &opened {
23126            let response = state
23127                .receive_rpc(
23128                    reverie::Tid::from_raw(dettid.as_raw()),
23129                    (
23130                        sender_time.clone(),
23131                        MmId::initial(dettid),
23132                        GlobalRequest::RecordHostInput {
23133                            path: file.to_str().unwrap().to_owned(),
23134                            syscall: 1,
23135                            identity: *identity,
23136                            source: Some(file.to_str().unwrap().to_owned()),
23137                            regular: true,
23138                            exec: false,
23139                            kernel_object: false,
23140                        },
23141                    ),
23142                )
23143                .await;
23144            assert_eq!(response, (None, GlobalResponse::RecordHostInput(())));
23145        }
23146        // Nothing was read at the opens: `replaced` is replaced again before
23147        // the run ends, so the end does not read it and its identity decides.
23148        std::fs::write(&new, "input\n").unwrap();
23149        std::fs::rename(&new, &replaced).unwrap();
23150
23151        state.clean_up(false, &Some(summary.clone())).await.unwrap();
23152        let summary: detcore_model::summary::RunSummary =
23153            serde_json::from_str(&std::fs::read_to_string(&summary).unwrap()).unwrap();
23154        let inputs = summary.host_inputs.expect("the run's host-input set");
23155        let content = |file: &Path| inputs.file(file.to_str().unwrap()).unwrap().content.clone();
23156        assert_eq!(content(&kept).as_deref(), Some(input));
23157        assert_ne!(content(&edited).as_deref(), Some(input));
23158        assert!(content(&edited).is_some());
23159        // Read at the open, it would hold the bytes of "other\n".
23160        assert_eq!(content(&replaced), None);
23161        let changed: Vec<&str> = summary
23162            .host_inputs_changed
23163            .iter()
23164            .map(|change| change.path.as_str())
23165            .collect();
23166        assert_eq!(
23167            changed,
23168            [edited.to_str().unwrap(), replaced.to_str().unwrap()]
23169        );
23170    }
23171
23172    #[tokio::test]
23173    async fn captured_host_inputs_publish_origins_only_for_authenticated_snapshot_bytes() {
23174        use std::path::Path;
23175
23176        use detcore_model::host_input::HostFileIdentity;
23177        use detcore_model::host_input::HostInputRecord;
23178        use detcore_model::host_input::HostInputSnapshot;
23179
23180        for case in [
23181            "kept",
23182            "alias",
23183            "wrong-object",
23184            "no-source",
23185            "replaced",
23186            "removed",
23187            "modified",
23188            "already-changed",
23189        ] {
23190            let directory = tempfile::tempdir().unwrap();
23191            let origin_path = directory.path().join("origin");
23192            let copy = directory.path().join("copy");
23193            let other = directory.path().join("other");
23194            for path in [&origin_path, &copy, &other] {
23195                std::fs::write(path, b"original").unwrap();
23196            }
23197            let alias = directory.path().join("alias");
23198            std::fs::hard_link(&copy, &alias).unwrap();
23199            let identity_of =
23200                |path: &Path| HostFileIdentity::from_stat(&crate::util::raw_stat(path).unwrap());
23201            let origin = identity_of(&origin_path);
23202            let physical = identity_of(&copy);
23203            let opened = if case == "wrong-object" {
23204                identity_of(&other)
23205            } else {
23206                physical
23207            };
23208            let guest_path = if case == "alias" {
23209                "/another-name"
23210            } else {
23211                "/etc/ld.so.cache"
23212            };
23213            let content = crate::Digest::new(b"original").to_string();
23214            let mut recorded_identity = origin;
23215            if case == "already-changed" {
23216                recorded_identity.size += 1;
23217            }
23218            let recorded = directory.path().join("recorded.json");
23219            let mut writer = crate::preemptions::PreemptionWriter::new(Some(recorded.clone()));
23220            writer.set_host_inputs(crate::host_inputs::input_set(
23221                &[HostInputRecord {
23222                    path: guest_path.into(),
23223                    dtid: 3,
23224                    syscall: 1,
23225                    identity: recorded_identity,
23226                    // Exercise the identity fallback, including a changed copy
23227                    // whose preserved stat would otherwise keep equal origins.
23228                    content: None,
23229                    exec: false,
23230                    kernel_object: false,
23231                }],
23232                &BTreeSet::new(),
23233            ));
23234            writer.flush().unwrap();
23235            let config = Config {
23236                sequentialize_threads: true,
23237                record_host_inputs: true,
23238                replay_preemptions_from: Some(recorded),
23239                host_input_snapshots: vec![HostInputSnapshot {
23240                    physical,
23241                    origin,
23242                    content: content.clone(),
23243                }],
23244                ..Config::default()
23245            };
23246            let state = GlobalState::initialize(&config, false);
23247            let dettid = DetTid::from_raw(3);
23248            let before = state.global_time.lock().unwrap().as_nanos();
23249            for syscall in [1, 2] {
23250                state
23251                    .receive_rpc(
23252                        reverie::Tid::from_raw(3),
23253                        (
23254                            DetTime::new(&config),
23255                            MmId::initial(dettid),
23256                            GlobalRequest::RecordHostInput {
23257                                path: guest_path.into(),
23258                                syscall,
23259                                identity: opened,
23260                                source: (case != "no-source").then(|| {
23261                                    match case {
23262                                        "wrong-object" => &other,
23263                                        "alias" => &alias,
23264                                        _ => &copy,
23265                                    }
23266                                    .to_str()
23267                                    .unwrap()
23268                                    .to_owned()
23269                                }),
23270                                regular: true,
23271                                exec: false,
23272                                kernel_object: false,
23273                            },
23274                        ),
23275                    )
23276                    .await;
23277            }
23278            assert_eq!(state.global_time.lock().unwrap().as_nanos(), before);
23279            let at_open = state.host_inputs.lock().unwrap()[0].identity;
23280            assert_eq!(
23281                at_open,
23282                if matches!(case, "wrong-object" | "no-source") {
23283                    opened
23284                } else {
23285                    origin
23286                },
23287                "{case}"
23288            );
23289            assert!(
23290                state.host_inputs.lock().unwrap()[0].content.is_none(),
23291                "no live-guest read"
23292            );
23293            if case == "replaced" {
23294                std::fs::rename(&other, &copy).unwrap();
23295            } else if case == "removed" {
23296                std::fs::remove_file(&copy).unwrap();
23297            } else if matches!(case, "modified" | "already-changed") {
23298                let mtime = std::fs::metadata(&copy).unwrap().modified().unwrap();
23299                std::fs::write(&copy, b"modified").unwrap();
23300                std::fs::File::options()
23301                    .write(true)
23302                    .open(&copy)
23303                    .unwrap()
23304                    .set_modified(mtime)
23305                    .unwrap();
23306                assert_eq!(
23307                    identity_of(&copy),
23308                    physical,
23309                    "the digest, not stat, must reject this copy"
23310                );
23311            }
23312            state.settle_host_inputs();
23313            let records = state.host_inputs.lock().unwrap();
23314            assert_eq!(records.len(), 2);
23315            let observed = &records[0];
23316            let trusted = matches!(case, "kept" | "alias");
23317            assert_eq!(
23318                observed.identity,
23319                if trusted { origin } else { opened },
23320                "{case}"
23321            );
23322            assert_eq!(records[1].identity, observed.identity, "later open: {case}");
23323            assert_eq!(records[1].content, None, "only the first open is hashed");
23324            match case {
23325                "no-source" | "replaced" | "removed" => {
23326                    assert_eq!(observed.content, None, "{case}")
23327                }
23328                "modified" | "already-changed" => assert_eq!(
23329                    observed.content,
23330                    Some(crate::Digest::new(b"modified").to_string())
23331                ),
23332                _ => assert_eq!(
23333                    observed.content.as_deref(),
23334                    Some(content.as_str()),
23335                    "{case}"
23336                ),
23337            }
23338            let replay = state.replay_host_inputs.as_ref().unwrap().lock().unwrap();
23339            let changes = replay.changes();
23340            assert_eq!(
23341                changes.len(),
23342                usize::from(!matches!(case, "kept" | "alias")),
23343                "{case}: {changes:?}"
23344            );
23345            if let Some(change) = changes.first() {
23346                assert_eq!(
23347                    change.now,
23348                    if case == "already-changed" {
23349                        origin
23350                    } else {
23351                        opened
23352                    },
23353                    "{case}"
23354                );
23355            }
23356        }
23357    }
23358
23359    /// A host-input observation, and a report of a path the guest rebound,
23360    /// belong to the run whose global state receives them. Each is answered
23361    /// with no logical time, before any clock accounting,
23362    /// so it cannot move the global clock or the sender's, and the run's
23363    /// records are written with their end line at normal clean-up. The same
23364    /// request reaches this state from every backend: the openat handler sends
23365    /// it over the backend's RPC whether Detcore runs in the tracer or inside
23366    /// the guest.
23367    #[tokio::test]
23368    async fn host_input_observations_are_kept_by_the_run_and_written_at_its_end() {
23369        use detcore_model::host_input::HostFileIdentity;
23370        use detcore_model::host_input::HostInputLogEnd;
23371        use detcore_model::host_input::HostInputRecord;
23372
23373        let directory = tempfile::tempdir().unwrap();
23374        let log = directory.path().join("host-inputs");
23375        let config = Config {
23376            sequentialize_threads: true,
23377            record_host_inputs: true,
23378            host_input_log: Some(log.clone()),
23379            ..Config::default()
23380        };
23381        let state = GlobalState::initialize(&config, false);
23382        let dettid = DetTid::from_raw(17);
23383        let identity = HostFileIdentity {
23384            dev: 33,
23385            ino: 7,
23386            size: 100,
23387            mtime_sec: 1_700_000_000,
23388            mtime_nsec: 5,
23389        };
23390        let mut sender_time = DetTime::new(&config);
23391        sender_time.add_syscall();
23392        sender_time.add_syscall();
23393        let global_before = state.global_time.lock().unwrap().as_nanos();
23394        let response = state
23395            .receive_rpc(
23396                reverie::Tid::from_raw(dettid.as_raw()),
23397                (
23398                    sender_time.clone(),
23399                    MmId::initial(dettid),
23400                    GlobalRequest::RecordHostInput {
23401                        path: "/etc/ld.so.cache".into(),
23402                        syscall: 12,
23403                        identity,
23404                        source: None,
23405                        regular: true,
23406                        exec: false,
23407                        kernel_object: false,
23408                    },
23409                ),
23410            )
23411            .await;
23412        assert_eq!(response, (None, GlobalResponse::RecordHostInput(())));
23413        for _ in 0..2 {
23414            let response = state
23415                .receive_rpc(
23416                    reverie::Tid::from_raw(dettid.as_raw()),
23417                    (
23418                        sender_time.clone(),
23419                        MmId::initial(dettid),
23420                        GlobalRequest::RecordHostMutation {
23421                            path: "/data/F".into(),
23422                        },
23423                    ),
23424                )
23425                .await;
23426            assert_eq!(response, (None, GlobalResponse::RecordHostMutation(())));
23427        }
23428        {
23429            let global_time = state.global_time.lock().unwrap();
23430            assert_eq!(global_time.as_nanos(), global_before, "no clock accounting");
23431            assert!(
23432                !global_time.contains_thread(dettid),
23433                "the sender's clock was not pushed"
23434            );
23435        }
23436        assert!(!log.exists(), "nothing is written while the run goes on");
23437
23438        state.clean_up(false, &None).await.unwrap();
23439        let text = std::fs::read_to_string(&log).unwrap();
23440        let lines: Vec<&str> = text.lines().collect();
23441        assert_eq!(lines.len(), 3, "{text}");
23442        assert_eq!(
23443            serde_json::from_str::<HostInputRecord>(lines[0]).unwrap(),
23444            HostInputRecord {
23445                path: "/etc/ld.so.cache".into(),
23446                dtid: 17,
23447                syscall: 12,
23448                identity,
23449                content: None,
23450                exec: false,
23451                kernel_object: false,
23452            }
23453        );
23454        // A path rebound twice is one line.
23455        assert_eq!(
23456            serde_json::from_str::<detcore_model::host_input::HostMutationRecord>(lines[1])
23457                .unwrap()
23458                .mutated,
23459            "/data/F"
23460        );
23461        assert_eq!(
23462            serde_json::from_str::<HostInputLogEnd>(lines[2]).unwrap(),
23463            HostInputLogEnd { records: 2 }
23464        );
23465    }
23466
23467    /// Two files on different filesystems can share an inode number; they
23468    /// are different files and must get different deterministic inodes, with
23469    /// first-seen mtimes of their own. Keyed by the number alone they shared
23470    /// one, so a guest comparing inode numbers took them for the same file,
23471    /// and the second file reported the first one's mtime.
23472    #[test]
23473    fn files_on_different_devices_with_one_inode_number_stay_distinct() {
23474        use super::ObservedMtime;
23475
23476        let epoch = LogicalTime::from_secs(1_798_761_600);
23477        let mut pool = super::InodePool::new();
23478        // 76927 was /sys/bus/i2c/devices on the host that motivated this.
23479        let sysfs = crate::types::RawInode::new(23, 76_927);
23480        let rootfs = crate::types::RawInode::new(33, 76_927);
23481
23482        let (on_sysfs, sysfs_mtime) = pool.add_inode(sysfs, ObservedMtime::HostSpecific, epoch);
23483        let (on_rootfs, rootfs_mtime) = pool.add_inode(
23484            rootfs,
23485            ObservedMtime::Canonical(LogicalTime::from_secs(1)),
23486            epoch,
23487        );
23488        assert_ne!(on_sysfs, on_rootfs, "different files, different det inodes");
23489        assert_eq!(sysfs_mtime, epoch);
23490        assert_eq!(rootfs_mtime, LogicalTime::from_secs(1), "its own mtime");
23491        assert_eq!(
23492            pool.add_inode(sysfs, ObservedMtime::Unobserved, epoch).0,
23493            on_sysfs
23494        );
23495        assert_eq!(
23496            pool.add_inode(rootfs, ObservedMtime::Unobserved, epoch).0,
23497            on_rootfs
23498        );
23499    }
23500
23501    /// Linux numbers a per-process procfs entry when it builds the entry's
23502    /// dentry, and the host can evict the dentry and build it again under a
23503    /// new number. Two `lsof` listings of `/proc/3/fd` found the same numbers
23504    /// in one `--verify` run and new ones in the other, where the host had
23505    /// rebuilt the entries between the listings. A rebuilt entry reached by
23506    /// its path keeps its number, so the guest sees the same numbers either
23507    /// way.
23508    #[test]
23509    fn a_rebuilt_procfs_entry_keeps_its_number() {
23510        use std::path::PathBuf;
23511
23512        use crate::procfs_inode::DirectoryInodes;
23513        use crate::procfs_inode::ProcfsDirectory;
23514        use crate::types::DetInode;
23515
23516        let t = LogicalTime::from_nanos(0);
23517        let seen = super::ObservedMtime::Unobserved;
23518        let proc_device = libc::makedev(0, 23);
23519        let names: [&[u8]; 4] = [b".", b"..", b"0", b"1"];
23520        // The guest numbers of two listings of `/proc/3/fd`, in which the
23521        // host reports the inode numbers `first` and then `second`, as
23522        // `recv_determinize_inode` numbers them.
23523        let listings = |with_paths: bool, first: [u64; 4], second: [u64; 4]| -> Vec<DetInode> {
23524            let mut pool = super::InodePool::new();
23525            let directory = with_paths.then(|| ProcfsDirectory::new(PathBuf::from("/3/fd")));
23526            let inodes = DirectoryInodes::new(proc_device, directory);
23527            let mut numbers = Vec::new();
23528            for host in [first, second] {
23529                for (name, ino) in names.iter().zip(host) {
23530                    let (raw, procfs) = inodes.entry_inode(name, ino);
23531                    if let Some(procfs) = procfs {
23532                        let path = pool.procfs_path_key(&procfs);
23533                        pool.procfs_key(raw, path);
23534                    }
23535                    let sighting = super::InodeSighting::Listed(0);
23536                    numbers.push(pool.add_sighted_inode(raw, seen, t, sighting).0);
23537                }
23538            }
23539            numbers
23540        };
23541        let cached = [4_100, 4_000, 4_101, 4_102];
23542        // The open directory pins `.` and `..`; the fd entries were rebuilt.
23543        let rebuilt = [4_100, 4_000, 4_901, 4_902];
23544        assert_eq!(
23545            listings(true, cached, cached),
23546            listings(true, cached, rebuilt),
23547            "a host rebuilding procfs dentries changed guest inode numbers"
23548        );
23549        // The control: by host inode alone, the rebuilt entries were renumbered.
23550        assert_ne!(
23551            listings(false, cached, cached),
23552            listings(false, cached, rebuilt)
23553        );
23554    }
23555
23556    /// One procfs entry reached by two paths, as `/proc/<pid>/net/tcp` and
23557    /// `/proc/<pid>/task/<pid>/net/tcp` are, is one host inode, so it keeps
23558    /// one number however it is reached, and after the host rebuilds it.
23559    #[test]
23560    fn procfs_aliases_share_one_number() {
23561        use std::path::Path;
23562
23563        use crate::procfs_inode::path_inode;
23564
23565        let t = LogicalTime::from_nanos(0);
23566        let seen = super::ObservedMtime::Unobserved;
23567        let device = libc::makedev(0, 23);
23568        let host = |ino| crate::types::RawInode::new(device, ino);
23569        let net = path_inode(device, Path::new("/3/net/tcp"));
23570        let task = path_inode(device, Path::new("/3/task/3/net/tcp"));
23571        let mut pool = super::InodePool::new();
23572        let mut number = |raw, path| {
23573            pool.procfs_key(raw, path);
23574            pool.add_inode(raw, seen, t).0
23575        };
23576        let listed = number(host(4_100), net);
23577        assert_eq!(number(host(4_100), task), listed, "an alias");
23578        assert_eq!(number(host(4_900), task), listed, "rebuilt, by the alias");
23579        assert_eq!(number(host(4_901), net), listed, "rebuilt again");
23580        let other = number(host(4_200), path_inode(device, Path::new("/3/net/udp")));
23581        assert_ne!(other, listed);
23582    }
23583
23584    /// A rebuilt procfs entry first seen without its path is new to the pool,
23585    /// as it was before paths were named; its path then leads to its new
23586    /// number, which it keeps across the next rebuild.
23587    #[test]
23588    fn a_numbered_host_inode_keeps_its_number_when_its_path_is_named() {
23589        use std::path::Path;
23590
23591        use crate::procfs_inode::path_inode;
23592
23593        let t = LogicalTime::from_nanos(0);
23594        let seen = super::ObservedMtime::Unobserved;
23595        let device = libc::makedev(0, 23);
23596        let host = |ino| crate::types::RawInode::new(device, ino);
23597        let path = path_inode(device, Path::new("/3/fd/0"));
23598        let mut pool = super::InodePool::new();
23599        pool.procfs_key(host(4_100), path);
23600        let first = pool.add_inode(host(4_100), seen, t).0;
23601        let without_path = pool.add_inode(host(4_900), seen, t).0;
23602        assert_ne!(without_path, first);
23603        pool.procfs_key(host(4_900), path);
23604        assert_eq!(pool.add_inode(host(4_900), seen, t).0, without_path);
23605        pool.procfs_key(host(4_901), path);
23606        assert_eq!(pool.add_inode(host(4_901), seen, t).0, without_path);
23607        assert_eq!(pool.add_inode(host(4_100), seen, t).0, first);
23608    }
23609
23610    /// A write to a rebuilt procfs entry, which names the host inode only,
23611    /// sets the mtime of the number the entry kept, whether the descriptor's
23612    /// open named the path before the write (`procfs_key` then `set_mtime`) or
23613    /// after it (a pending mtime).
23614    #[test]
23615    fn a_write_to_a_rebuilt_procfs_entry_sets_the_mtime_of_its_number() {
23616        use std::path::Path;
23617
23618        use crate::procfs_inode::path_inode;
23619
23620        let t = LogicalTime::from_nanos(0);
23621        let written = LogicalTime::from_secs(7);
23622        let seen = super::ObservedMtime::Unobserved;
23623        let device = libc::makedev(0, 23);
23624        let host = |ino| crate::types::RawInode::new(device, ino);
23625        let path = path_inode(device, Path::new("/3/comm"));
23626        for open_first in [true, false] {
23627            let mut pool = super::InodePool::new();
23628            pool.procfs_key(host(4_100), path);
23629            let (number, mtime) = pool.add_inode(host(4_100), seen, t);
23630            assert_ne!(mtime, written);
23631            if open_first {
23632                pool.procfs_key(host(4_900), path);
23633                pool.set_mtime(host(4_900), written);
23634            } else {
23635                pool.set_mtime(host(4_900), written);
23636                pool.procfs_key(host(4_900), path);
23637            }
23638            assert_eq!(
23639                pool.add_inode(host(4_100), seen, t),
23640                (number, written),
23641                "open_first={open_first}"
23642            );
23643            assert_eq!(pool.add_inode(host(4_900), seen, t), (number, written));
23644        }
23645    }
23646
23647    /// A process ID the kernel gives out again names a new process, whose
23648    /// procfs entries are new files: Linux drops the old process's entries
23649    /// when it exits. The new process's `/proc/<pid>` takes a number of its
23650    /// own and keeps it across rebuilds, while a descriptor still open on
23651    /// the old process's entry keeps the old number.
23652    #[test]
23653    fn a_reused_task_id_names_new_procfs_entries() {
23654        use std::path::Path;
23655
23656        use crate::procfs_inode::ProcfsName;
23657        use crate::types::RawInode;
23658
23659        let t = LogicalTime::from_nanos(0);
23660        let seen = super::ObservedMtime::Unobserved;
23661        let device = libc::makedev(0, 23);
23662        let host = |ino| RawInode::new(device, ino);
23663        let process = ProcfsName::new(device, Path::new("/7"));
23664        let number = |pool: &mut super::InodePool, raw: RawInode| {
23665            let path = pool.procfs_path_key(&process);
23666            pool.procfs_key(raw, path);
23667            pool.add_inode(raw, seen, t).0
23668        };
23669        let mut pool = super::InodePool::new();
23670        pool.count_incarnation(7);
23671        let first = number(&mut pool, host(4_100));
23672        assert_eq!(number(&mut pool, host(4_900)), first, "rebuilt");
23673        pool.count_incarnation(7);
23674        let second = number(&mut pool, host(5_000));
23675        assert_ne!(second, first, "a new process took the old one's number");
23676        assert_eq!(pool.add_inode(host(4_900), seen, t).0, first, "held");
23677        assert_eq!(number(&mut pool, host(5_001)), second, "rebuilt");
23678        // A thread's directory also names the thread's incarnation.
23679        let thread = ProcfsName::new(device, Path::new("/7/task/9/stat"));
23680        assert_eq!(thread.tasks, [7, 9]);
23681        let before = pool.procfs_path_key(&thread);
23682        pool.count_incarnation(9);
23683        assert_ne!(pool.procfs_path_key(&thread), before);
23684    }
23685
23686    /// A pid Detcore gives out again is a new task: each registration of a
23687    /// task ID counts one more incarnation (`count_incarnation`), which the
23688    /// `ProcfsTask` answer reports for the process and the thread, and a file
23689    /// or directory bound to the earlier incarnation no longer names the
23690    /// task holding the numbers (`ProcfsTaskAnswer::still_names`).
23691    #[test]
23692    fn a_reused_pid_is_a_new_incarnation_for_procfs_files() {
23693        use crate::procfs::TaskIncarnation;
23694        use crate::types::DetTid;
23695        let mut pool = super::InodePool::new();
23696        assert_eq!(pool.incarnation(7), 0, "an ID no task has held");
23697        pool.count_incarnation(7);
23698        pool.count_incarnation(9);
23699        assert_eq!((pool.incarnation(7), pool.incarnation(9)), (1, 1));
23700        let (seven, nine) = (DetTid::from_raw(7), DetTid::from_raw(9));
23701        let first = pool.procfs_task_answer(seven, Some(nine), (true, false));
23702        assert_eq!(
23703            first.incarnation(),
23704            TaskIncarnation {
23705                process: 1,
23706                thread: Some(1),
23707            }
23708        );
23709        let bound = first.incarnation();
23710        assert!(first.still_names(bound));
23711        // The process exits and is reaped; a new process takes pid 7.
23712        pool.count_incarnation(7);
23713        let reused = pool.procfs_task_answer(seven, Some(nine), (true, false));
23714        assert_eq!(reused.process_incarnation, 2);
23715        assert_eq!(reused.thread_incarnation, Some(1));
23716        assert!(!reused.still_names(bound), "the file of the old pid 7");
23717        assert!(reused.still_names(reused.incarnation()));
23718        // A thread ID given out again is a new thread of the same process.
23719        pool.count_incarnation(9);
23720        let thread_reused = pool.procfs_task_answer(seven, Some(nine), (true, false));
23721        assert!(!thread_reused.still_names(reused.incarnation()));
23722        // A task that is gone, or a zombie that was reaped, is no task,
23723        // whatever its incarnation.
23724        let gone = pool.procfs_task_answer(seven, None, (false, false));
23725        assert!(!gone.still_names(gone.incarnation()));
23726        // The process's own file names no thread.
23727        assert_eq!(gone.thread_incarnation, None);
23728    }
23729
23730    /// Reading a rendered file's state inserts nothing; a write keeps one
23731    /// entry for that inode only.
23732    #[test]
23733    fn a_read_of_rendered_file_state_keeps_nothing() {
23734        use crate::procfs::listing::RenderedFileChange;
23735        let mut pool = super::InodePool::new();
23736        let read = pool.rendered_file_state(5, RenderedFileChange::Read);
23737        assert_eq!(read, Default::default());
23738        assert!(pool.rendered_file_states.is_empty());
23739        let written = pool.rendered_file_state(5, RenderedFileChange::Mtime(3, 4));
23740        assert_eq!(written.mtime, Some((3, 4)));
23741        let hinted = pool.rendered_file_state(5, RenderedFileChange::WriteHint(2));
23742        assert_eq!((hinted.mtime, hinted.write_hint), (Some((3, 4)), 2));
23743        assert_eq!(
23744            pool.rendered_file_state(6, RenderedFileChange::Read),
23745            Default::default()
23746        );
23747        assert_eq!(pool.rendered_file_states.len(), 1);
23748        assert_eq!(
23749            pool.rendered_file_state(5, RenderedFileChange::Read),
23750            hinted
23751        );
23752    }
23753
23754    /// A directory snapshot returns its entries after it was taken, and a
23755    /// task it lists can exit, and a new thread take its ID, in between. The
23756    /// entry still names the task the snapshot listed: an entry of the old
23757    /// task rebuilt since keeps the old task's number, rather than adopting
23758    /// the new task's.
23759    #[test]
23760    fn a_snapshot_entry_names_the_task_that_held_its_id() {
23761        use std::path::Path;
23762
23763        use crate::procfs_inode::ProcfsName;
23764        use crate::types::RawInode;
23765
23766        let t = LogicalTime::from_nanos(0);
23767        let seen = super::ObservedMtime::Unobserved;
23768        let device = libc::makedev(0, 23);
23769        let host = |ino| RawInode::new(device, ino);
23770        let number = |pool: &mut super::InodePool, raw: RawInode, name: &ProcfsName| {
23771            let path = pool.procfs_path_key(name);
23772            pool.procfs_key(raw, path);
23773            pool.add_inode(raw, seen, t).0
23774        };
23775        let process = ProcfsName::new(device, Path::new("/7"));
23776        let mut pool = super::InodePool::new();
23777        pool.count_incarnation(7);
23778        let first = number(&mut pool, host(4_100), &process);
23779        let listed = process.clone().in_snapshot(pool.threads_counted);
23780        pool.count_incarnation(7);
23781        let second = number(&mut pool, host(5_000), &process);
23782        assert_ne!(second, first, "a new process took the old one's number");
23783        assert_eq!(
23784            number(&mut pool, host(4_900), &listed),
23785            first,
23786            "the listed process's entry, rebuilt"
23787        );
23788        // An ID no thread held when the snapshot was taken names no task.
23789        let unborn = ProcfsName::new(device, Path::new("/8"));
23790        let listed = unborn.clone().in_snapshot(pool.threads_counted);
23791        let unsalted = pool.procfs_path_key(&listed);
23792        assert_eq!(unsalted, pool.procfs_path_key(&unborn));
23793        pool.count_incarnation(8);
23794        assert_eq!(pool.procfs_path_key(&listed), unsalted);
23795        assert_ne!(pool.procfs_path_key(&unborn), unsalted);
23796    }
23797
23798    /// Only an exact whole-second 0 or 1 host mtime is canonical
23799    /// (https://github.com/rrnewton/hermit/issues/3639).
23800    #[test]
23801    fn only_exact_canonical_host_mtimes_are_kept() {
23802        use super::ObservedMtime;
23803
23804        assert_eq!(
23805            ObservedMtime::from_host_mtime(1, 0),
23806            ObservedMtime::Canonical(LogicalTime::from_secs(1))
23807        );
23808        assert_eq!(
23809            ObservedMtime::from_host_mtime(0, 0),
23810            ObservedMtime::Canonical(LogicalTime::from_secs(0))
23811        );
23812        for (secs, nanos) in [(1, 5), (0, 1), (2, 0), (-1, 0), (1_600_000_000, 0)] {
23813            assert_eq!(
23814                ObservedMtime::from_host_mtime(secs, nanos),
23815                ObservedMtime::HostSpecific,
23816                "{secs}.{nanos:09} is a real timestamp, not a canonical one"
23817            );
23818        }
23819    }
23820
23821    /// The first-seen policy is applied at the first stat, whichever syscall
23822    /// minted the inode, and is not re-applied once resolved.
23823    #[test]
23824    fn first_seen_mtime_is_resolved_by_the_first_stat() {
23825        use super::ObservedMtime;
23826
23827        let epoch = LogicalTime::from_secs(1_798_761_600);
23828        let canonical = ObservedMtime::Canonical(LogicalTime::from_secs(1));
23829        let mut pool = super::InodePool::new();
23830
23831        // An ordinary file reports the epoch.
23832        let (_, mtime) = pool.add_inode(
23833            crate::types::RawInode::new(1, 10),
23834            ObservedMtime::HostSpecific,
23835            epoch,
23836        );
23837        assert_eq!(mtime, epoch);
23838        // A canonical file keeps its mtime.
23839        let (_, mtime) = pool.add_inode(crate::types::RawInode::new(1, 11), canonical, epoch);
23840        assert_eq!(mtime, LogicalTime::from_secs(1));
23841
23842        // A read or getdents mints the inode first; the later stat decides.
23843        let (_, mtime) = pool.add_inode(
23844            crate::types::RawInode::new(1, 12),
23845            ObservedMtime::Unobserved,
23846            epoch,
23847        );
23848        assert_eq!(mtime, epoch, "an unresolved mtime reads as the epoch");
23849        let (_, mtime) = pool.add_inode(crate::types::RawInode::new(1, 12), canonical, epoch);
23850        assert_eq!(mtime, LogicalTime::from_secs(1));
23851
23852        // Once resolved, a later observation does not change it.
23853        let (_, mtime) = pool.add_inode(crate::types::RawInode::new(1, 10), canonical, epoch);
23854        assert_eq!(mtime, epoch);
23855        let (_, mtime) = pool.add_inode(
23856            crate::types::RawInode::new(1, 11),
23857            ObservedMtime::HostSpecific,
23858            epoch,
23859        );
23860        assert_eq!(mtime, LogicalTime::from_secs(1));
23861    }
23862    include!("tool_global/callback_history_tests.rs");
23863}
23864
23865#[cfg(test)]
23866mod robust_exit_clock_tests {
23867    use std::sync::Mutex;
23868    use std::task::Poll;
23869
23870    use nix::sys::signal::Signal;
23871    use reverie::ExitStatus;
23872    use reverie::GlobalRPC;
23873    use reverie::GlobalTool;
23874    use reverie::Tid;
23875    use reverie::Tool;
23876
23877    use super::GlobalRequest;
23878    use super::GlobalResponse;
23879    use super::GlobalState;
23880    use crate::Detcore;
23881    use crate::ThreadState;
23882    use crate::config::Config;
23883    use crate::ivar::Ivar;
23884    use crate::resources::Permission;
23885    use crate::resources::ResourceID;
23886    use crate::resources::Resources;
23887    use crate::scheduler::DEFAULT_PRIORITY;
23888    use crate::scheduler::SkipTurn;
23889    use crate::scheduler::ThreadNextTurn;
23890    use crate::tool_local::RobustListExit;
23891    use crate::tool_local::RobustListWake;
23892    use crate::types::*;
23893
23894    #[derive(Debug, PartialEq)]
23895    struct WakeObservation {
23896        wakes: Vec<(DetTid, FutexID)>,
23897        counts: Vec<u64>,
23898        clocks: serde_json::Value,
23899        turn: u64,
23900    }
23901
23902    #[derive(Debug)]
23903    struct RpcObservation {
23904        sender: DetTid,
23905        kind: &'static str,
23906        accepted: bool,
23907        clocks: serde_json::Value,
23908        queued: Vec<DetTid>,
23909        waiters: usize,
23910        turn: u64,
23911    }
23912
23913    // This is only a sender-bound transport, as in Reverie's WrappedFrom.
23914    // Replies, clock accounting, wakes and deregistration come from GlobalState.
23915    struct ExitRpc<'a> {
23916        state: &'a GlobalState,
23917        sender: DetTid,
23918        observations: &'a Mutex<Vec<WakeObservation>>,
23919        rpc_observations: &'a Mutex<Vec<RpcObservation>>,
23920    }
23921
23922    #[reverie::tool]
23923    impl GlobalRPC<GlobalState> for ExitRpc<'_> {
23924        async fn send_rpc(
23925            &self,
23926            request: <GlobalState as GlobalTool>::Request,
23927        ) -> <GlobalState as GlobalTool>::Response {
23928            let kind = match &request.2 {
23929                GlobalRequest::RobustListWakes(wakes) if wakes.is_empty() => "empty-wake",
23930                GlobalRequest::RobustListWakes(_) => "wake",
23931                GlobalRequest::DeregisterThread(_) => "deregister",
23932                // A SIGKILLed thread asks for a robust list its sender may have
23933                // recorded (`Detcore::sigkill_robust_list_wakes`).
23934                GlobalRequest::TakeSigkillRobustList => "take-sigkill-list",
23935                _ => panic!("unexpected exit RPC: {:?}", request.2),
23936            };
23937            let wakes = match &request.2 {
23938                GlobalRequest::RobustListWakes(wakes) if !wakes.is_empty() => Some(wakes.clone()),
23939                _ => None,
23940            };
23941            let response = self
23942                .state
23943                .receive_rpc(Tid::from_raw(self.sender.as_raw()), request)
23944                .await;
23945            let clocks = serde_json::to_value(&*self.state.global_time.lock().unwrap()).unwrap();
23946            {
23947                let sched = self.state.sched.lock().unwrap();
23948                self.rpc_observations.lock().unwrap().push(RpcObservation {
23949                    sender: self.sender,
23950                    kind,
23951                    accepted: !matches!(response.1, GlobalResponse::ThreadExited),
23952                    clocks: clocks.clone(),
23953                    queued: sched.run_queue.tids().copied().collect(),
23954                    waiters: sched
23955                        .blocked
23956                        .futex_waiters
23957                        .values()
23958                        .map(Vec::len)
23959                        .sum::<usize>(),
23960                    turn: sched.turn,
23961                });
23962            }
23963            if let Some(wakes) = wakes {
23964                let GlobalResponse::RobustListWakes(counts) = &response.1 else {
23965                    panic!("a complete admitted exit batch was refused: {response:?}");
23966                };
23967                let clocks =
23968                    serde_json::to_value(&*self.state.global_time.lock().unwrap()).unwrap();
23969                let turn = self.state.sched.lock().unwrap().turn;
23970                self.observations.lock().unwrap().push(WakeObservation {
23971                    wakes,
23972                    counts: counts.clone(),
23973                    clocks,
23974                    turn,
23975                });
23976            }
23977            response
23978        }
23979
23980        fn config(&self) -> &Config {
23981            &self.state.cfg
23982        }
23983    }
23984
23985    /// A send may complete before a host SIGKILL cancels its callback. The
23986    /// real exit hook must publish its pending export before dropping the
23987    /// owner, even when no later resource request carries that fact. The
23988    /// surviving reader is another process, so sender teardown cannot itself
23989    /// exempt the reader from the terminal deadlock check.
23990    #[tokio::test]
23991    async fn an_exit_publishes_pending_descriptor_exports_before_retirement() {
23992        for exported in [false, true] {
23993            let config = Config {
23994                sequentialize_threads: true,
23995                ..Config::default()
23996            };
23997            let state = GlobalState::initialize(&config, false);
23998            let reader = DetTid::from_raw(3);
23999            let sender = DetTid::from_raw(5);
24000            let tool: Detcore = Detcore::new(Tid::from_raw(sender.as_raw()), &config);
24001            let mut thread = ThreadState::new(sender, &config, ());
24002            thread.detpid = Some(sender);
24003            thread.passed_descriptors_out_unreported = exported;
24004            {
24005                let mut sched = state.sched.lock().unwrap();
24006                sched.thread_tree.add_child(reader, reader, true);
24007                sched.thread_tree.add_child(reader, sender, true);
24008                for tid in [reader, sender] {
24009                    sched.register_thread_for_test(tid);
24010                    sched.install_test_exec_incarnation(tid, MmId::initial(tid));
24011                }
24012            }
24013            let observations = Mutex::new(Vec::new());
24014            let rpc_observations = Mutex::new(Vec::new());
24015            let rpc = ExitRpc {
24016                state: &state,
24017                sender,
24018                observations: &observations,
24019                rpc_observations: &rpc_observations,
24020            };
24021            tool.on_exit_thread(
24022                Tid::from_raw(sender.as_raw()),
24023                &rpc,
24024                thread,
24025                ExitStatus::Signaled(Signal::SIGKILL, false),
24026            )
24027            .await
24028            .unwrap();
24029            {
24030                let sched = state.sched.lock().unwrap();
24031                assert!(!sched.next_turns.contains_key(&sender));
24032                assert!(sched.next_turns.contains_key(&reader));
24033                assert_eq!(sched.turn, 0, "exit publication must not take a turn");
24034                let rpcs = rpc_observations.lock().unwrap();
24035                // The export rides the deregistration itself: that is the
24036                // one exit notification for it (the SIGKILL robust-list take
24037                // is the chain's separate, pre-existing exit RPC).
24038                let deregisters: Vec<_> =
24039                    rpcs.iter().filter(|rpc| rpc.kind == "deregister").collect();
24040                assert_eq!(deregisters.len(), 1, "reuse the existing exit notification");
24041                assert!(deregisters[0].accepted);
24042                assert!(rpcs[0].accepted);
24043            }
24044            assert_reader_after_sender_exit(&state, reader, exported).await;
24045        }
24046    }
24047
24048    pub(super) async fn assert_reader_after_sender_exit(
24049        state: &GlobalState,
24050        reader: DetTid,
24051        exported: bool,
24052    ) {
24053        let mut poll = Resources::new(reader);
24054        poll.insert(ResourceID::InternalIOPolling, Permission::W);
24055        poll.polls_guest_state_only = true;
24056        poll.relies_on_guest_held_marks = true;
24057        // This export fixture has no signal handlers; record that fact
24058        // explicitly rather than relying on absent interruption metadata.
24059        poll.set_signal_interrupt_errno(reverie::syscalls::Errno::ERESTARTSYS);
24060        poll.signal_state_task = Some((reader.as_raw(), reader.as_raw()));
24061        {
24062            let mut sched = state.sched.lock().unwrap();
24063            sched.default_signal_state_for_test(reader);
24064            sched.queue_request_for_test(reader, poll.clone());
24065        }
24066        // One post-exit attempt fails; its next request retries the same
24067        // read after every possible guest action has completed.
24068        let first = crate::scheduler::do_a_turn_blocking(
24069            state.sched.clone(),
24070            state.global_time.clone(),
24071            &Err(SkipTurn),
24072        )
24073        .await;
24074        assert!(first.is_ok(), "the first read must get an attempt");
24075        poll.poll_attempt = 2;
24076        {
24077            let mut sched = state.sched.lock().unwrap();
24078            sched.next_turns[&reader].req.put(Ok(poll));
24079            if !sched.run_queue.contains_tid(reader) {
24080                sched.runqueue_push_back(reader);
24081            }
24082        }
24083        // A live polling retry first backs off, then is granted. A
24084        // terminal verdict never grants it. Bound the scripted drain
24085        // without a host clock or a timeout-based success condition.
24086        let mut last = first;
24087        let mut granted = false;
24088        for _ in 0..3 {
24089            last = crate::scheduler::do_a_turn_blocking(
24090                state.sched.clone(),
24091                state.global_time.clone(),
24092                &last,
24093            )
24094            .await;
24095            if last.is_ok() {
24096                granted = true;
24097                break;
24098            }
24099        }
24100        assert_eq!(
24101            granted, exported,
24102            "a host-held peer must keep the surviving reader polling; without an export it must diagnose the deadlock"
24103        );
24104        let sched = state.sched.lock().unwrap();
24105        let report = sched.terminal_deadlock_for_test();
24106        if exported {
24107            assert_eq!(report, None, "an exported peer prevents a terminal verdict");
24108        } else {
24109            let headline = if sched.blocked.futex_waiters.is_empty() {
24110                "Deadlock detected: thread(s) retrying a pipe or socket-pair call no remaining thread can satisfy, but no runnable threads left."
24111            } else {
24112                "Deadlock detected: thread(s) waiting on futex, and thread(s) retrying a pipe or socket-pair call no remaining thread can satisfy, but no runnable threads left."
24113            };
24114            assert_eq!(
24115                report.and_then(|report| report.lines().next()),
24116                Some(headline)
24117            );
24118        }
24119    }
24120
24121    struct Fixture {
24122        state: GlobalState,
24123        tool: Detcore,
24124        owners: [ThreadState<()>; 2],
24125        initial_clocks: [DetTime; 2],
24126        waiters: [DetTid; 2],
24127        peer: DetTid,
24128        futexes: [FutexID; 2],
24129        observations: Mutex<Vec<WakeObservation>>,
24130        rpc_observations: Mutex<Vec<RpcObservation>>,
24131    }
24132
24133    impl Fixture {
24134        fn new(
24135            reason: RobustListExit,
24136            equal_clocks: bool,
24137            empty_owner: Option<usize>,
24138            needs_rpc_cancellation: bool,
24139        ) -> Self {
24140            let config = Config {
24141                sequentialize_threads: true,
24142                ..Config::default().with_backend(|backend| {
24143                    backend.needs_killed_thread_rpc_cancellation = needs_rpc_cancellation
24144                })
24145            };
24146            let state = GlobalState::initialize(&config, false);
24147            let parent = DetTid::from_raw(1);
24148            let leader = DetTid::from_raw(17);
24149            let worker = DetTid::from_raw(18);
24150            let waiters = [DetTid::from_raw(21), DetTid::from_raw(23)];
24151            let peer = DetTid::from_raw(25);
24152            let mm = MmId::initial(leader);
24153            let mut first = ThreadState::new(leader, &config, ());
24154            first.detpid = Some(leader);
24155            let mut second = first.clone();
24156            second.dettid = worker;
24157            let mut inherited = DetTime::new(&config);
24158            inherited.add_syscall_with_cost(1_000);
24159            let first_initial = inherited.clone_for_child();
24160            inherited.add_syscall_with_cost(370);
24161            let second_initial = inherited.clone_for_child();
24162            first.thread_logical_time = first_initial.clone();
24163            second.thread_logical_time = second_initial.clone();
24164            first
24165                .thread_logical_time
24166                .add_syscall_with_cost(if equal_clocks { 407 } else { 37 });
24167            second.thread_logical_time.add_syscall_with_cost(37);
24168            first.record_robust_list_head(Some(0x404100));
24169            second.record_robust_list_head(Some(0x404200));
24170            let object = SharedMemoryObjectId::Anonymous {
24171                origin: MmId::initial(parent),
24172                sequence: 1,
24173            };
24174            let futexes = [FutexID::shared(object, 0), FutexID::shared(object, 8)];
24175            first.stage_robust_list_wakes(
24176                reason,
24177                vec![
24178                    (
24179                        worker,
24180                        if empty_owner == Some(1) {
24181                            Vec::new()
24182                        } else {
24183                            vec![RobustListWake { futex: futexes[1] }]
24184                        },
24185                    ),
24186                    (
24187                        leader,
24188                        if empty_owner == Some(0) {
24189                            Vec::new()
24190                        } else {
24191                            vec![RobustListWake { futex: futexes[0] }]
24192                        },
24193                    ),
24194                ],
24195            );
24196            {
24197                let mut sched = state.sched.lock().unwrap();
24198                sched.thread_tree.add_child(parent, parent, true);
24199                sched.thread_tree.add_child(parent, leader, true);
24200                sched.thread_tree.add_child(leader, worker, false);
24201                for tid in [waiters[0], waiters[1], peer] {
24202                    sched.thread_tree.add_child(parent, tid, true);
24203                }
24204                for tid in [leader, worker, waiters[0], waiters[1], peer] {
24205                    sched.priorities.insert(tid, DEFAULT_PRIORITY);
24206                    sched.next_turns.insert(
24207                        tid,
24208                        ThreadNextTurn {
24209                            dettid: tid,
24210                            child_tid_addr: 0,
24211                            req: Ivar::new(),
24212                            resp: Ivar::new(),
24213                            protocol: Default::default(),
24214                        },
24215                    );
24216                    sched.install_test_exec_incarnation(
24217                        tid,
24218                        if tid == leader || tid == worker {
24219                            mm
24220                        } else {
24221                            MmId::initial(tid)
24222                        },
24223                    );
24224                }
24225                // Both owners have returned from their last guest turn and
24226                // retain its empty next request until their exit callbacks.
24227                // The independent peer is ready, but cannot pass quiescence.
24228                sched.runqueue_push_back(leader);
24229                sched.runqueue_push_back(worker);
24230                sched.runqueue_push_back(peer);
24231                sched.next_turns[&peer].req.put(Ok(Resources::new(peer)));
24232                for (waiter, futex) in waiters.into_iter().zip(futexes) {
24233                    sched.sleep_futex_waiter(&waiter, futex, None, u32::MAX, None);
24234                }
24235            }
24236            {
24237                let mut time = state.global_time.lock().unwrap();
24238                for (tid, clock) in [(leader, &first_initial), (worker, &second_initial)] {
24239                    time.update_global_time(tid, clock.as_nanos(), clock.inherited_nanos());
24240                }
24241            }
24242            let tool = Detcore::new(Tid::from_raw(leader.as_raw()), &config);
24243            Self {
24244                state,
24245                tool,
24246                owners: [first, second],
24247                initial_clocks: [first_initial, second_initial],
24248                waiters,
24249                peer,
24250                futexes,
24251                observations: Mutex::new(Vec::new()),
24252                rpc_observations: Mutex::new(Vec::new()),
24253            }
24254        }
24255
24256        fn assert_clocks(&self, completed: &[usize]) -> serde_json::Value {
24257            let time = self.state.global_time.lock().unwrap();
24258            let snapshot = serde_json::to_value(&*time).unwrap();
24259            let epoch = DetTime::new(&self.state.cfg).as_nanos();
24260            let mut expected = epoch;
24261            for (index, owner) in self.owners.iter().enumerate() {
24262                let clock = if completed.contains(&index) {
24263                    &owner.thread_logical_time
24264                } else {
24265                    &self.initial_clocks[index]
24266                };
24267                assert_eq!(time.threads_time(owner.dettid), clock.as_nanos());
24268                assert_eq!(
24269                    snapshot["inherited_time"][owner.dettid.as_raw().to_string()],
24270                    serde_json::to_value(clock.inherited_nanos()).unwrap()
24271                );
24272                expected = expected + (clock.as_nanos() - epoch - clock.inherited_nanos());
24273            }
24274            assert_eq!(
24275                time.as_nanos(),
24276                expected,
24277                "only each owner's own uninherited work contributes"
24278            );
24279            snapshot
24280        }
24281
24282        async fn exit(&self, index: usize, status: ExitStatus) {
24283            self.exit_thread(self.owners[index].clone(), status).await;
24284        }
24285
24286        async fn exit_thread(&self, thread: ThreadState<()>, status: ExitStatus) {
24287            let rpc = ExitRpc {
24288                state: &self.state,
24289                sender: thread.dettid,
24290                observations: &self.observations,
24291                rpc_observations: &self.rpc_observations,
24292            };
24293            let exit =
24294                self.tool
24295                    .on_exit_thread(Tid::from_raw(rpc.sender.as_raw()), &rpc, thread, status);
24296            let mut exit = std::pin::pin!(exit);
24297            // On the CLI's current-thread ptrace route, Ready on the first
24298            // poll excludes a new queued-observer window inside this callback.
24299            // Multi-thread embeddings still admit concurrent scheduler reads;
24300            // the RPC observations separately check the actual global actions.
24301            assert!(
24302                matches!(futures::poll!(exit.as_mut()), Poll::Ready(Ok(()))),
24303                "exit callback yielded before its accounting and cleanup completed"
24304            );
24305        }
24306
24307        async fn nonmember_exit(&self, raw_tid: i32) {
24308            let mut thread = self.owners[0].clone();
24309            thread.dettid = DetTid::from_raw(raw_tid);
24310            thread.thread_logical_time = DetTime::new(&self.state.cfg);
24311            let tid = thread.dettid;
24312            {
24313                let mut sched = self.state.sched.lock().unwrap();
24314                sched
24315                    .thread_tree
24316                    .add_child(self.owners[0].dettid, tid, false);
24317                sched.priorities.insert(tid, DEFAULT_PRIORITY);
24318                sched.next_turns.insert(
24319                    tid,
24320                    ThreadNextTurn {
24321                        dettid: tid,
24322                        child_tid_addr: 0,
24323                        req: Ivar::new(),
24324                        resp: Ivar::new(),
24325                        protocol: Default::default(),
24326                    },
24327                );
24328                sched.install_test_exec_incarnation(tid, thread.mm_id);
24329                sched.runqueue_push_back(tid);
24330            }
24331            let before = self.rpc_observations.lock().unwrap().len();
24332            self.exit_thread(thread, ExitStatus::Exited(0)).await;
24333            let observations = self.rpc_observations.lock().unwrap();
24334            assert_eq!(
24335                observations.len(),
24336                before + 1,
24337                "a nonmember sent an acknowledgement or wake RPC"
24338            );
24339            assert_eq!(observations[before].sender, tid);
24340            assert_eq!(observations[before].kind, "deregister");
24341            assert!(observations[before].accepted);
24342        }
24343    }
24344
24345    #[tokio::test]
24346    async fn robust_exit_acknowledgements_preserve_global_action_order_and_eligibility() {
24347        for order in [[0, 1], [1, 0]] {
24348            for empty_owner in [0, 1] {
24349                let f = Fixture::new(RobustListExit::ExitGroup, false, Some(empty_owner), false);
24350                f.nonmember_exit(30).await;
24351                assert!(f.observations.lock().unwrap().is_empty());
24352                let queue_before_first: Vec<_> = f
24353                    .state
24354                    .sched
24355                    .lock()
24356                    .unwrap()
24357                    .run_queue
24358                    .tids()
24359                    .copied()
24360                    .collect();
24361                let first_start = f.rpc_observations.lock().unwrap().len();
24362                f.exit(order[0], ExitStatus::Exited(0)).await;
24363                let first_clocks = f.assert_clocks(&[order[0]]);
24364                {
24365                    let observations = f.rpc_observations.lock().unwrap();
24366                    let actions = &observations[first_start..];
24367                    assert_eq!(
24368                        actions.iter().map(|a| a.kind).collect::<Vec<_>>(),
24369                        ["empty-wake", "deregister"]
24370                    );
24371                    assert!(actions.iter().all(|a| a.accepted
24372                        && a.sender == f.owners[order[0]].dettid
24373                        && a.clocks == first_clocks
24374                        && a.waiters == 2
24375                        && a.turn == 0));
24376                    assert_eq!(
24377                        actions[0].queued, queue_before_first,
24378                        "empty acknowledgement changed scheduler eligibility"
24379                    );
24380                }
24381                f.nonmember_exit(31).await;
24382                f.assert_clocks(&[order[0]]);
24383                assert!(
24384                    f.observations.lock().unwrap().is_empty(),
24385                    "a nonmember completed the physical-exit barrier"
24386                );
24387                let queue_before_last: Vec<_> = f
24388                    .state
24389                    .sched
24390                    .lock()
24391                    .unwrap()
24392                    .run_queue
24393                    .tids()
24394                    .copied()
24395                    .collect();
24396                let last_start = f.rpc_observations.lock().unwrap().len();
24397                f.exit(order[1], ExitStatus::Exited(0)).await;
24398                let final_clocks = f.assert_clocks(&[0, 1]);
24399                {
24400                    let observations = f.rpc_observations.lock().unwrap();
24401                    let actions = &observations[last_start..];
24402                    assert_eq!(
24403                        actions.iter().map(|a| a.kind).collect::<Vec<_>>(),
24404                        ["empty-wake", "wake", "deregister"]
24405                    );
24406                    assert!(actions.iter().all(|a| a.accepted
24407                        && a.sender == f.owners[order[1]].dettid
24408                        && a.clocks == final_clocks
24409                        && a.turn == 0));
24410                    assert_eq!(actions[0].waiters, 2);
24411                    assert_eq!(actions[1].waiters, 1);
24412                    assert_eq!(actions[0].queued, queue_before_last);
24413                    assert_eq!(
24414                        actions[1].queued, queue_before_last,
24415                        "wake bypassed deferred admission"
24416                    );
24417                }
24418                f.nonmember_exit(32).await;
24419                f.assert_clocks(&[0, 1]);
24420                assert_eq!(f.observations.lock().unwrap().len(), 1);
24421                let observations = f.rpc_observations.lock().unwrap();
24422                for owner in &f.owners {
24423                    assert_eq!(
24424                        observations
24425                            .iter()
24426                            .filter(|a| a.sender == owner.dettid && a.kind == "empty-wake")
24427                            .count(),
24428                        1,
24429                        "each unique matching owner, including an empty-wake owner, must acknowledge before the batch clears"
24430                    );
24431                }
24432            }
24433        }
24434    }
24435
24436    #[tokio::test]
24437    async fn robust_exit_callbacks_preserve_owner_clocks_across_arrival_orders() {
24438        for reason in [
24439            RobustListExit::ExitGroup,
24440            RobustListExit::Signal(libc::SIGTERM),
24441        ] {
24442            for equal_clocks in [false, true] {
24443                for empty_owner in [None, Some(0), Some(1)] {
24444                    let mut results = Vec::new();
24445                    for order in [[0, 1], [1, 0]] {
24446                        let f = Fixture::new(reason, equal_clocks, empty_owner, false);
24447                        let status = match reason {
24448                            RobustListExit::ExitGroup => ExitStatus::Exited(0),
24449                            RobustListExit::Signal(_) => {
24450                                ExitStatus::Signaled(Signal::SIGTERM, false)
24451                            }
24452                        };
24453                        f.exit(order[0], status).await;
24454                        f.assert_clocks(&[order[0]]);
24455                        assert!(f.observations.lock().unwrap().is_empty());
24456                        {
24457                            let sched = f.state.sched.lock().unwrap();
24458                            assert_eq!(
24459                                sched
24460                                    .blocked
24461                                    .futex_waiters
24462                                    .values()
24463                                    .map(Vec::len)
24464                                    .sum::<usize>(),
24465                                2
24466                            );
24467                            assert_eq!(sched.turn, 0);
24468                        }
24469                        // An actually runnable peer must still wait for the
24470                        // remaining owner's outstanding next request.
24471                        {
24472                            let last = Err(SkipTurn);
24473                            let turn = crate::scheduler::do_a_turn_blocking(
24474                                f.state.sched.clone(),
24475                                f.state.global_time.clone(),
24476                                &last,
24477                            );
24478                            let mut turn = std::pin::pin!(turn);
24479                            assert!(matches!(futures::poll!(turn.as_mut()), Poll::Pending));
24480                        }
24481                        f.exit(order[0], status).await;
24482                        f.assert_clocks(&[order[0]]);
24483                        assert!(
24484                            f.observations.lock().unwrap().is_empty(),
24485                            "duplicate physical exit released an incomplete group"
24486                        );
24487                        f.exit(order[1], status).await;
24488                        let clocks = f.assert_clocks(&[0, 1]);
24489                        {
24490                            let observations = f.observations.lock().unwrap();
24491                            assert_eq!(observations.len(), 1);
24492                            let expected: Vec<_> = (0..2)
24493                                .filter(|i| Some(*i) != empty_owner)
24494                                .map(|i| (f.owners[i].dettid, f.futexes[i]))
24495                                .collect();
24496                            assert_eq!(observations[0].wakes, expected);
24497                            assert_eq!(observations[0].counts, vec![1; expected.len()]);
24498                            assert_eq!(
24499                                observations[0].clocks, clocks,
24500                                "all owner clocks must be accounted before wake admission"
24501                            );
24502                            assert_eq!(observations[0].turn, 0);
24503                        }
24504                        f.exit(order[1], status).await;
24505                        assert_eq!(f.assert_clocks(&[0, 1]), clocks);
24506                        assert_eq!(
24507                            f.observations.lock().unwrap().len(),
24508                            1,
24509                            "repeated cleanup emitted a second batch"
24510                        );
24511                        {
24512                            let sched = f.state.sched.lock().unwrap();
24513                            assert!(!sched.run_queue.contains_tid(f.waiters[0]));
24514                            assert!(!sched.run_queue.contains_tid(f.waiters[1]));
24515                        }
24516                        // Drive the real step1/step2 drain and one peer turn;
24517                        // no test-only scheduler implementation or reply shim.
24518                        let result = crate::scheduler::do_a_turn_blocking(
24519                            f.state.sched.clone(),
24520                            f.state.global_time.clone(),
24521                            &Err(SkipTurn),
24522                        )
24523                        .await;
24524                        assert!(result.is_ok());
24525                        let sched = f.state.sched.lock().unwrap();
24526                        assert_eq!(sched.turn, 1);
24527                        let queued: Vec<_> = sched.run_queue.tids().copied().collect();
24528                        for (i, waiter) in f.waiters.iter().enumerate() {
24529                            assert_eq!(queued.contains(waiter), Some(i) != empty_owner);
24530                        }
24531                        assert!(queued.contains(&f.peer));
24532                        results.push((
24533                            clocks,
24534                            queued,
24535                            std::mem::take(&mut *f.observations.lock().unwrap()),
24536                        ));
24537                    }
24538                    assert_eq!(
24539                        results[0], results[1],
24540                        "callback order changed final clocks, typed wake observations or the actual scheduler drain"
24541                    );
24542                }
24543            }
24544        }
24545    }
24546
24547    #[tokio::test]
24548    async fn robust_exit_callbacks_keep_rejected_or_mismatched_batches_incomplete() {
24549        for rejection in ["old-mm", "tombstone", "wrong-signal", "normal-exit"] {
24550            let f = Fixture::new(
24551                RobustListExit::Signal(libc::SIGTERM),
24552                false,
24553                None,
24554                rejection == "tombstone",
24555            );
24556            let rejected = f.owners[0].dettid;
24557            let mm = f.owners[0].mm_id;
24558            f.exit(1, ExitStatus::Signaled(Signal::SIGTERM, false))
24559                .await;
24560            let replacement_request = Ivar::new();
24561            if rejection == "old-mm" {
24562                let mut sched = f.state.sched.lock().unwrap();
24563                sched.install_test_exec_incarnation(rejected, mm.for_exec(rejected));
24564                sched.next_turns.insert(
24565                    rejected,
24566                    ThreadNextTurn {
24567                        dettid: rejected,
24568                        child_tid_addr: 0,
24569                        req: replacement_request.clone(),
24570                        resp: Ivar::new(),
24571                        protocol: Default::default(),
24572                    },
24573                );
24574                let mut replacement_time = f.owners[0].thread_logical_time.clone();
24575                replacement_time.add_syscall_with_cost(500);
24576                f.state.global_time.lock().unwrap().update_global_time(
24577                    rejected,
24578                    replacement_time.as_nanos(),
24579                    replacement_time.inherited_nanos(),
24580                );
24581            } else if rejection == "tombstone" {
24582                // Use the real cancelling backend gate; keep its matching Mm.
24583                let mut sched = f.state.sched.lock().unwrap();
24584                sched.logically_kill_thread(&rejected, &rejected, mm);
24585            }
24586            let before = serde_json::to_value(&*f.state.global_time.lock().unwrap()).unwrap();
24587            let status = match rejection {
24588                "wrong-signal" => ExitStatus::Signaled(Signal::SIGKILL, false),
24589                "normal-exit" => ExitStatus::Exited(0),
24590                _ => ExitStatus::Signaled(Signal::SIGTERM, false),
24591            };
24592            f.exit(0, status).await;
24593            assert!(
24594                f.observations.lock().unwrap().is_empty(),
24595                "{rejection} released a group"
24596            );
24597            if rejection == "old-mm" || rejection == "tombstone" {
24598                assert_eq!(
24599                    serde_json::to_value(&*f.state.global_time.lock().unwrap()).unwrap(),
24600                    before,
24601                    "rejected acknowledgement changed clock state"
24602                );
24603            }
24604            let sched = f.state.sched.lock().unwrap();
24605            assert_eq!(
24606                sched
24607                    .blocked
24608                    .futex_waiters
24609                    .values()
24610                    .map(Vec::len)
24611                    .sum::<usize>(),
24612                2,
24613                "rejected group lost a real waiter"
24614            );
24615            assert!(
24616                f.waiters
24617                    .iter()
24618                    .all(|tid| !sched.run_queue.contains_tid(*tid))
24619            );
24620            if rejection == "old-mm" {
24621                assert!(sched.rpc_incarnation_matches(rejected, mm.for_exec(rejected)));
24622                assert_eq!(
24623                    sched.next_turns[&rejected].req, replacement_request,
24624                    "old cleanup destroyed replacement registration"
24625                );
24626            }
24627        }
24628    }
24629
24630    #[tokio::test]
24631    #[should_panic(expected = "Attempted to update tid 17 time")]
24632    async fn robust_exit_clock_ack_still_refuses_a_backwards_owner_sample() {
24633        let f = Fixture::new(RobustListExit::ExitGroup, false, None, false);
24634        let owner = &f.owners[0];
24635        let mut later = owner.thread_logical_time.clone();
24636        later.add_syscall_with_cost(1);
24637        f.state.global_time.lock().unwrap().update_global_time(
24638            owner.dettid,
24639            later.as_nanos(),
24640            later.inherited_nanos(),
24641        );
24642        f.exit(0, ExitStatus::Exited(0)).await;
24643    }
24644
24645    #[tokio::test]
24646    async fn backend_failure_preserves_consuming_robust_exit_clock_accounting() {
24647        for order in [[0, 1], [1, 0]] {
24648            let f = Fixture::new(RobustListExit::ExitGroup, false, None, false);
24649            let selected = {
24650                let mut sched = f.state.sched.lock().unwrap();
24651                sched.next_turns.get_mut(&f.peer).unwrap().req = Ivar::new();
24652                sched.select_test_turn().unwrap()
24653            };
24654            let responses = {
24655                let sched = f.state.sched.lock().unwrap();
24656                f.waiters.map(|tid| sched.next_turns[&tid].resp.clone())
24657            };
24658            let mut turn = std::pin::pin!(crate::scheduler::finish_selected_turn(
24659                f.state.sched.clone(),
24660                f.state.global_time.clone(),
24661                selected.0,
24662                selected.1,
24663                selected.2,
24664            ));
24665            assert!(futures::poll!(turn.as_mut()).is_pending());
24666            f.state.report_backend_failure(reverie::BackendFailure {
24667                pid: Tid::from_raw(17),
24668                tid: Tid::from_raw(18),
24669                phase: "native robust cleanup control",
24670            });
24671            for index in order {
24672                f.exit(index, ExitStatus::Exited(0)).await;
24673            }
24674            assert!(matches!(futures::poll!(turn.as_mut()), Poll::Ready(Err(_))));
24675            f.assert_clocks(&[0, 1]);
24676            let observations = f.observations.lock().unwrap();
24677            assert_eq!(observations.len(), 1, "one complete batch");
24678            assert_eq!(observations[0].counts, vec![1, 1]);
24679            assert!(
24680                responses
24681                    .iter()
24682                    .all(|response| response.try_read().is_none())
24683            );
24684            let mut sched = f.state.sched.lock().unwrap();
24685            assert_eq!(sched.turn, 0);
24686            for owner in &f.owners {
24687                assert!(!sched.next_turns.contains_key(&owner.dettid));
24688                assert!(!sched.note_deregistration_accounted(owner.dettid));
24689            }
24690        }
24691    }
24692}
24693
24694#[cfg(test)]
24695mod replay_departure_tests {
24696    use super::*;
24697    use crate::preemptions::PreemptionRecord;
24698    use crate::preemptions::PreemptionWriter;
24699
24700    #[test]
24701    fn edited_history_rerecording_inherits_the_consumed_history_departure() {
24702        let directory = tempfile::tempdir().unwrap();
24703        let path = directory.path().join("history.json");
24704        for marker in [None, Some(0), Some(71)] {
24705            let mut writer = PreemptionWriter::new(None);
24706            if let Some(event) = marker {
24707                writer.mark_replay_departed(event);
24708            }
24709            std::fs::write(&path, writer.into_string()).unwrap();
24710            let history = HistoryReader::read(&path).unwrap();
24711            history.require_replay_mode(true).unwrap();
24712            assert_eq!(
24713                history.require_replay_mode(false).is_err(),
24714                marker.is_some()
24715            );
24716            let config = Config {
24717                replay_preemptions_from: Some(path.clone()),
24718                replay_edited_preemptions: true,
24719                record_preemptions: true,
24720                ..Config::default()
24721            };
24722            let global = GlobalState::for_tests(&config);
24723            let text = global
24724                .sched
24725                .lock()
24726                .unwrap()
24727                .preemption_writer
24728                .take()
24729                .unwrap()
24730                .into_string();
24731            let written: PreemptionRecord = serde_json::from_str(&text).unwrap();
24732            assert_eq!(written.replay_departed_at(), marker);
24733        }
24734    }
24735}