Skip to main content

detcore/
tool_local.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//! The process-local portion of the Detcore Reverie-tool.
10
11use std::collections::BTreeMap;
12use std::collections::BTreeSet;
13use std::collections::HashMap;
14use std::os::fd::BorrowedFd;
15use std::path::Path;
16use std::sync::Arc;
17use std::sync::Mutex;
18use std::sync::MutexGuard;
19use std::time::Duration;
20
21use detcore_model::pedigree::Pedigree;
22use detcore_model::summary::TimesliceStats;
23use detcore_model::syscall_histogram::SyscallHistogram;
24use nix::fcntl::OFlag;
25use nix::sys::stat;
26use nix::unistd::Pid;
27use rand::Rng as _;
28use rand::RngExt as _;
29use rand::SeedableRng;
30use rand_distr::Distribution;
31use rand_distr::Exp;
32use rand_pcg::Pcg64Mcg;
33use reverie::Errno;
34use reverie::Error;
35use reverie::Guest;
36use reverie::syscalls::CloneFlags;
37use reverie::syscalls::Syscall;
38use reverie::syscalls::Sysno;
39use serde::Deserialize;
40use serde::Serialize;
41use sha2::Digest as _;
42use sha2::Sha256;
43use tracing::debug;
44use tracing::info;
45
46use crate::config::Config;
47use crate::detlog;
48use crate::fd::*;
49use crate::memory::MemoryMetadata;
50use crate::preemptions::ThreadHistoryIterator;
51use crate::record_or_replay::NoopTool;
52use crate::record_or_replay::RecordOrReplay;
53use crate::resources::ChaosEpochTransition;
54use crate::resources::Device;
55use crate::resources::ExternalOpId;
56use crate::resources::Permission;
57use crate::resources::ResourceID;
58use crate::resources::Resources;
59use crate::scheduler::Priority;
60use crate::stat::*;
61use crate::types::*;
62
63/// The detcore tool and its per-process state.
64#[derive(Debug, Serialize, Deserialize)]
65pub struct Detcore<T = NoopTool> {
66    //
67    // TODO:
68    //  - Add Pid cache here.
69    //
70    /// The detpid of this process.
71    pub(crate) detpid: DetPid,
72
73    /// Cached copy of the tool Config.  Immutable over the lifetime of the program.
74    pub(crate) cfg: Config,
75
76    /// The record or replay sub-tool. Any events that cannot be made
77    /// deterministic are forwarded to this tool. Thus, Detcore acts as a
78    /// filter-map for syscalls.
79    pub(crate) record_or_replay: T,
80}
81
82/// The metadata associated with the file system view of a particular *process*.
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct FileMetadata {
85    /// Identity of the Linux descriptor table represented by `file_handles`.
86    pub(crate) files_id: FilesId,
87    /// Sequence used to allocate open file descriptions observed through this table.
88    next_open_file_sequence: u64,
89    /// Socket-only sequence used for backend-independent socket cookies.
90    #[serde(default)]
91    next_socket_open_file_sequence: u64,
92    /// Random-device-only sequence that names each random-device open file's
93    /// stream (`crate::fd::RandomDeviceStream`).
94    #[serde(default)]
95    next_random_device_sequence: u64,
96    /// A random-device or timerfd OFD from this table has been promoted.
97    /// Retain this history after its final close so exec cannot reuse a
98    /// canonical identity. (Serialized under its earlier name, which the
99    /// field had while only random devices were promoted.)
100    #[serde(default, rename = "has_promoted_random_files")]
101    has_promoted_shared_files: bool,
102    /// A description from this table has kept its record's sequence in
103    /// Detcore's global state (`ProcfsFile::share_sequence`, at a fork or a
104    /// send), keyed by its identity: so an exec on a backend that rebuilds
105    /// the table carries this table's identity numbering, and no description
106    /// of the new image takes an identity whose sequence another process may
107    /// still hold (`ThreadState::procfs_records_for_exec`). Kept after the
108    /// description closes, and by `fork_for` and `for_exec`.
109    #[serde(default)]
110    has_shared_procfs_sequences: bool,
111    /// Track what file handles actually point to (e.g. after dup2).
112    /// This includes both the identifying resource (usually inode) and the deterministic file handle.
113    /// Deserialization restores aliasing (`crate::fd::intern_open_files`).
114    #[serde(deserialize_with = "deserialize_interned_file_handles")]
115    pub(crate) file_handles: HashMap<RawFd, DetFd>,
116    /// Blocking accepts waiting in this table, each as (operation, listener):
117    /// a `close` of a listener here is refused while an accept waits on it
118    /// (`crate::syscalls::accept_in_turn`). Waits never cross a fork or an
119    /// exec: a fork's child and an exec'd task start with none, and each wait
120    /// removes only its own entry, from the table it registered in.
121    // TODO-HUMAN-REVIEW(PR-3908)
122    #[serde(skip)]
123    accept_waits: Vec<(u64, RawFd)>,
124    /// The next accept-wait operation number in this table.
125    #[serde(skip)]
126    next_accept_wait: u64,
127    /// Whether the session renders `/proc/cpuinfo`, so that a memfd named as
128    /// its placeholder may be one when this table rediscovers descriptors
129    /// without a guest at hand (an in-guest backend's image after `exec`):
130    /// the session's own answer (`Detcore::rendered_files`), taken at the
131    /// image's first guest callback (`decide_cpuinfo_placeholders`), and kept
132    /// by `fork_for` and `for_exec`. `None` until then, and when absent from
133    /// an older serialized table. Detcore makes no such placeholder in a
134    /// session that does not render the file, so there a memfd of that name
135    /// stays an ordinary memfd.
136    #[serde(default)]
137    cpuinfo_placeholders: Option<bool>,
138    /// Descriptors rediscovered before [`Self::cpuinfo_placeholders`] was
139    /// decided whose memfd is named as a placeholder for a file rendered only
140    /// where `/proc/cpuinfo` is (the file itself, or a task's `arch_status`):
141    /// kept as the ordinary descriptors they look like until the session's
142    /// answer says whether they are placeholders
143    /// (`decide_cpuinfo_placeholders`).
144    #[serde(default)]
145    pending_cpuinfo_placeholders: Vec<RawFd>,
146    /// The run's `--proc` level, so that a memfd named as a placeholder for
147    /// a file the level omits stays the memfd it is when this table
148    /// discovers it (`discover_fd_from_current_process`): Detcore makes no
149    /// placeholder for such a file, whose open fails with ENOENT. Noted as
150    /// the table is made for a thread (`ThreadState::new`) and again at each
151    /// image's first guest callback (`decide_cpuinfo_placeholders`), kept by
152    /// `fork_for` and `for_exec`, and serialized with the table, so a table
153    /// carried across a fork or an `exec` knows it before it rediscovers a
154    /// descriptor. `None` until noted (a table no thread made) and when absent
155    /// from an older serialized table, where no name is refused for its
156    /// level.
157    #[serde(default)]
158    proc_level: Option<detcore_model::config::ProcLevel>,
159    /// Descriptors open in this table that Detcore keeps no description of,
160    /// with whether each is close-on-exec: a received anonymous inode, or a
161    /// received descriptor whose facts could not be taken
162    /// (`Detcore::record_received_descriptors`). Listed in the table's
163    /// descriptor directory (`syscalls::files::descriptor_directory`), so a
164    /// close-all loop reaches them; forgotten when the number is closed or
165    /// replaced.
166    #[serde(default)]
167    present_unmodelled: BTreeMap<RawFd, bool>,
168    /// Whether a receive may have installed descriptors in this table whose
169    /// numbers Detcore does not know (a receive whose layout could not be
170    /// trusted, one on the background path, or one without sequentialized
171    /// threads): then a listing of the table cannot come from Detcore's
172    /// model.
173    #[serde(default)]
174    unaccounted_receipts: bool,
175    /// The capacity Linux's descriptor table has (`max_fds`): 64
176    /// (`NR_OPEN_DEFAULT`) until a descriptor at or above it is installed,
177    /// then the next power of two above the highest one installed
178    /// (`expand_files`, `alloc_fdtable`); a fork's copy is sized from the
179    /// descriptors open at the fork (`dup_fd`, `sane_fdtable_size`); `exec`
180    /// keeps the table. The end of a descriptor directory's listing.
181    #[serde(default = "default_descriptor_capacity")]
182    descriptor_capacity: u64,
183    /// Whether the exec that made this table dropped a close-on-exec
184    /// description whose locks Detcore follows (`DetFd::is_lock_tracked`):
185    /// the post-exec handler then names the surviving ones to
186    /// `procfs::locks::LockLedger` even where none survived, so that the
187    /// ledger releases a placeholder `flock` the exec closed
188    /// (`LockLedger::exec_kept`).
189    #[serde(default)]
190    exec_dropped_lock_tracked: bool,
191    /// Descriptors queued over `SCM_RIGHTS` that Linux released in the
192    /// current call (the queue of a socket whose last reference this table
193    /// dropped, `FileMetadata::release_detfd`, or what a receive or a purge
194    /// handed over), until the call completes and releases what they held
195    /// (`Detcore::settle_released_rights`). Never serialized, and never
196    /// copied.
197    #[serde(skip)]
198    released_rights: ReleasedRights,
199    /// The processes whose threads hold this table, and how many threads of
200    /// each, counted by guest calls alone: its first holder; each thread or
201    /// process made with `CLONE_FILES`, counted as its creator's `clone`
202    /// makes it, before either runs on; and each holder's exit, a thread's
203    /// `exit` counting one out and an `exit_group` its whole process
204    /// ([`Self::leave`]). The count of thread states that share the table
205    /// cannot say which exit is its last: the backend drops an exited
206    /// thread's state at no particular point of the schedule. Empty for a
207    /// table whose holders are not known (one carried across a snapshot),
208    /// which no exit releases.
209    #[serde(default)]
210    holders: BTreeMap<DetPid, usize>,
211    /// The table the exec that made this one replaced, and the exec'ing
212    /// process (`Self::keep_replaced_table`): kept from the exec's entry
213    /// (`Detcore::handle_execveat`) until the new image's first callback,
214    /// where that process leaves the replaced table, and where it was the
215    /// table's last holder, the table's descriptors go, the close-on-exec ones
216    /// as Linux closes them at the exec
217    /// (`ThreadState::release_exec_dropped_descriptors`). A failed exec drops
218    /// this table, and the replaced one with it. Never serialized, and never
219    /// copied.
220    #[serde(skip)]
221    exec_replaced: ReplacedTable,
222}
223
224/// The table an exec replaced, and the exec'ing process
225/// ([`FileMetadata::exec_replaced`]). A copy of the table that holds it
226/// starts with none: the exec's own table leaves the replaced one, once.
227#[derive(Default)]
228struct ReplacedTable(Option<(Arc<Mutex<FileMetadata>>, DetPid)>);
229
230impl Clone for ReplacedTable {
231    fn clone(&self) -> Self {
232        Self::default()
233    }
234}
235
236impl std::fmt::Debug for ReplacedTable {
237    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
238        f.debug_tuple("ReplacedTable")
239            .field(&self.0.is_some())
240            .finish()
241    }
242}
243
244/// What the death of a process by a fatal signal did with one of the tables
245/// its threads held ([`FileMetadata::release_at_death`]).
246#[derive(Debug)]
247pub(crate) enum TableAtDeath {
248    /// No holder is left, and every descriptor went: what the queues of the
249    /// sockets whose last reference went held, for the death to release
250    /// (`Detcore::settle_released_rights`'s rule).
251    Released(Vec<SentDescriptor>),
252    /// Another process still holds the table, which keeps every descriptor.
253    Kept,
254    /// The table's holders are not known, or do not name the process, so
255    /// whether its death released the table cannot be decided.
256    Undecided,
257}
258
259/// The descriptors of messages queued to sockets a table released
260/// ([`FileMetadata::released_rights`]). A copy of the table, for a fork or an
261/// exec, starts with none: what one table released is released once.
262#[derive(Debug, Default)]
263struct ReleasedRights(Vec<SentDescriptor>);
264
265impl Clone for ReleasedRights {
266    fn clone(&self) -> Self {
267        Self::default()
268    }
269}
270
271/// `NR_OPEN_DEFAULT` on 64-bit Linux.
272fn default_descriptor_capacity() -> u64 {
273    64
274}
275
276/// Deserializes a descriptor table with its aliases restored, so a table
277/// carried across fork (or any other state migration) keeps one open file
278/// description object per `OpenFileId`.
279fn deserialize_interned_file_handles<'de, D>(
280    deserializer: D,
281) -> Result<HashMap<RawFd, DetFd>, D::Error>
282where
283    D: serde::Deserializer<'de>,
284{
285    let mut handles = <HashMap<RawFd, DetFd> as Deserialize>::deserialize(deserializer)?;
286    crate::fd::intern_open_files(&mut handles);
287    Ok(handles)
288}
289
290/// A descriptor identity held across an awaited syscall.
291///
292/// The clone retains the source open-file description until the operation
293/// finishes. `files_id` binds it to the table in which the syscall began.
294#[derive(Debug)]
295pub(crate) struct CapturedDetFd {
296    files_id: FilesId,
297    detfd: DetFd,
298}
299
300/// A captured descriptor could not be installed into the current descriptor
301/// table. The capture is returned so the caller can release any resource held
302/// alive solely by the in-flight operation.
303#[derive(Debug)]
304pub(crate) struct CapturedDetFdInstallError {
305    pub(crate) expected_files_id: FilesId,
306    pub(crate) actual_files_id: FilesId,
307    returned_fd: RawFd,
308    pub(crate) captured: CapturedDetFd,
309}
310
311/// Cleanup required after the kernel created a descriptor but Detcore could not
312/// install its captured open-file-description identity in the current table.
313#[derive(Debug)]
314pub(crate) struct CapturedDetFdInstallCleanup {
315    /// The exact kernel-returned descriptor that must be closed.
316    pub(crate) close_fd: RawFd,
317    /// A scheduler resource whose final modeled OFD reference was the capture.
318    pub(crate) release_open_file: Option<OpenFileId>,
319    /// Where that capture was a receiving socket's last reference, the
320    /// messages queued to it, which go with it as at a close
321    /// ([`FileMetadata::release_detfd`]), for the call to release
322    /// (`ThreadState::defer_released_rights`).
323    pub(crate) released_rights: Vec<SentDescriptor>,
324}
325
326impl CapturedDetFdInstallError {
327    /// Abandon the failed capture and preserve both cleanup obligations.
328    pub(crate) fn into_cleanup(self) -> CapturedDetFdInstallCleanup {
329        let last = self.captured.detfd.is_last_reference();
330        let release_open_file = last.then(|| self.captured.detfd.open_file_id());
331        let released_rights = if last {
332            self.captured.detfd.take_all_queued_rights()
333        } else {
334            Vec::new()
335        };
336        drop(self.captured);
337        CapturedDetFdInstallCleanup {
338            close_fd: self.returned_fd,
339            release_open_file,
340            released_rights,
341        }
342    }
343}
344
345/// Whether a pidfd target is proven to use the caller's exact descriptor table.
346///
347/// Linux pidfds name a specific task, and `pidfd_open(2)` accepts a thread-group
348/// leader. Equal TGIDs alone are not enough: a `CLONE_THREAD` child may omit
349/// `CLONE_FILES` and therefore have a different `files_struct`. Restrict the
350/// modeled path to the leader itself, where `target == getpid() == gettid()`
351/// proves that the task named by the pidfd and the caller are identical.
352pub(crate) fn pidfd_getfd_targets_calling_task(
353    target: Option<DetPid>,
354    current_tgid: DetPid,
355    current_tid: DetTid,
356) -> bool {
357    target == Some(current_tgid) && current_tid == current_tgid
358}
359
360// AUTONOMOUS-BOT-IMPLEMENTED
361// TODO-HUMAN-REVIEW(PR-1154): Review SaBRe exec descriptor-status handoff state.
362/// Descriptor numbers that Detcore keeps physically nonblocking while presenting them as
363/// blocking to the guest. SaBRe carries this narrow status set across an exec plugin reload.
364pub type ExecFdBlockingOverrides = BTreeSet<RawFd>;
365
366/// Metadata carried by the existing exec preparation/completion exchange.
367/// Descriptor numbers are authorized by that exchange's process/image fence,
368/// not by rediscovering whichever object later occupies a numeric stdio slot.
369#[derive(Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
370pub struct ExecFdState {
371    pub(crate) blocking: ExecFdBlockingOverrides,
372    pub(crate) inherited_reads: BTreeMap<RawFd, crate::fd::InheritedExternalRead>,
373    /// Set only by the global first-image completion, never by a preparation.
374    pub(crate) initialize_inherited_stdio: bool,
375    /// Offered only to the registered root's initial image by its launcher.
376    #[serde(default)]
377    pub(crate) initial_input: Option<reverie::InheritedInputIdentity>,
378    /// The owner-death wakes a successful exec makes Linux deliver, read
379    /// before the exec where the backend runs the native execve outside
380    /// Detcore's handler (DBT). The coordinator delivers them at the new
381    /// image's reconnect, or when the caller's thread ends without one (an
382    /// exec that ends its group), and drops them with an exec cancelled before
383    /// its point of no return (<https://github.com/rrnewton/hermit/issues/4268>).
384    #[serde(default)]
385    pub(crate) robust_wakes: Vec<(DetTid, FutexID)>,
386    /// Promoted random-device handles and allocator state of a reloaded tool.
387    /// Nonrandom descriptors retain their existing exec restoration paths.
388    #[serde(default)]
389    pub(crate) open_files: Option<ExecOpenFiles>,
390    /// Maps descriptions surviving a reload retain their opened target image,
391    /// aliases and any cached bytes. Kernel EOF must not erase this authority.
392    #[serde(default)]
393    pub(crate) maps: Option<ExecMapsFiles>,
394    /// On a backend that rebuilds its descriptor table after exec: the
395    /// surviving descriptors of per-process files that follow their record's
396    /// sequence, with their open files' identities, models and sequences, and
397    /// the old table's identity numbering, where it has either
398    /// (`ThreadState::procfs_records_for_exec`).
399    #[serde(default)]
400    pub(crate) procfs_records: Option<ExecProcfsRecords>,
401    /// Under `--pipe-wake-latency`, on a backend that rediscovers its
402    /// descriptors after exec: the channel end each surviving descriptor is,
403    /// reattached after rediscovery (`crate::pipe_wake`).
404    #[serde(default)]
405    pub(crate) channels: BTreeMap<RawFd, crate::pipe_wake::ChannelEnd>,
406    /// Under `--pipe-wake-latency`, on such a backend, at every exec: the old
407    /// table's open-file creator and next sequence, reserved before any
408    /// rediscovery, so that no description of the new image reuses an id a
409    /// channel elsewhere may still carry.
410    #[serde(default)]
411    pub(crate) open_file_namespace: Option<(DetTid, u64)>,
412    /// The old table's count of random-device opens. A backend that
413    /// rebuilds its descriptor table after exec starts the new one from it,
414    /// so no open after the exec reuses a stream's name
415    /// (`crate::fd::RandomDeviceStream`).
416    #[serde(default)]
417    pub(crate) random_device_sequence: u64,
418    /// The image the exec runs, as Detcore resolved it from the exec's
419    /// arguments before the exec ([`crate::exec_record`]), or for the
420    /// registered root's first image the one its launch named
421    /// (`Config::launch_image`), once the coordinator checked it against the
422    /// kernel's link. `None` leaves the new image without a record.
423    #[serde(default)]
424    pub(crate) exe: Option<crate::exec_record::ExecRecord>,
425}
426
427/// Written out, not derived, so that the `--pipe-wake-latency` fields are
428/// printed only when set: an exec without the option traces as before.
429impl std::fmt::Debug for ExecFdState {
430    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
431        let mut debug = f.debug_struct("ExecFdState");
432        debug
433            .field("blocking", &self.blocking)
434            .field("inherited_reads", &self.inherited_reads)
435            .field(
436                "initialize_inherited_stdio",
437                &self.initialize_inherited_stdio,
438            )
439            .field("initial_input", &self.initial_input)
440            .field("robust_wakes", &self.robust_wakes)
441            .field("open_files", &self.open_files);
442        if let Some(maps) = &self.maps {
443            // Native authority tokens and kernel descriptor identities are
444            // transport-only; they must not enter canonical guest logs.
445            debug.field("maps_survivors", &maps.handles.len());
446        }
447        if let Some(records) = &self.procfs_records {
448            debug.field("procfs_record_survivors", &records.handles.len());
449        }
450        if !self.channels.is_empty() {
451            debug.field("channels", &self.channels);
452        }
453        if let Some(namespace) = &self.open_file_namespace {
454            debug.field("open_file_namespace", namespace);
455        }
456        if self.random_device_sequence != 0 {
457            debug.field("random_device_sequence", &self.random_device_sequence);
458        }
459        if let Some(exe) = &self.exe {
460            debug.field("exe", &exe.exe);
461        }
462        debug.finish()
463    }
464}
465
466#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
467pub(crate) struct ExecOpenFiles {
468    pub(crate) creator: DetTid,
469    next_sequence: u64,
470    next_socket_sequence: u64,
471    handles: BTreeMap<RawFd, ExecOpenFile>,
472}
473
474#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
475pub(crate) struct ExecMapsFiles {
476    creator: DetTid,
477    next_sequence: u64,
478    handles: BTreeMap<RawFd, ExecMapsFile>,
479}
480
481#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
482struct ExecMapsFile {
483    identity: ExecOpenFile,
484    procfs: crate::procfs::ProcfsFile,
485    flags: i32,
486    path: Option<std::path::PathBuf>,
487}
488
489/// The descriptors of per-process files that follow their record's sequence
490/// (`ProcfsFile::follows_record_sequence`) surviving an exec on a backend
491/// that rebuilds its descriptor table after exec, and the old table's open
492/// file creator and next identity (`ThreadState::procfs_records_for_exec`).
493#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
494pub(crate) struct ExecProcfsRecords {
495    creator: DetTid,
496    next_sequence: u64,
497    handles: BTreeMap<RawFd, ExecProcfsRecord>,
498}
499
500/// One surviving descriptor of [`ExecProcfsRecords`]: its open file's
501/// identity, authenticated against the descriptor after the exec, its model,
502/// its record's sequence whole (the model does not serialize where the
503/// sequence is, `ProcfsFile::read_pos`), its status flags and path, and
504/// whether that sequence is one Detcore cannot know
505/// (`DetFd::record_sequence_unknown`).
506#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
507struct ExecProcfsRecord {
508    identity: ExecOpenFile,
509    procfs: crate::procfs::ProcfsFile,
510    sequence: crate::procfs::ProcfsSequence,
511    flags: i32,
512    path: Option<std::path::PathBuf>,
513    sequence_unknown: bool,
514}
515
516#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
517struct ExecOpenFile {
518    id: OpenFileId,
519    dev: u64,
520    ino: u64,
521    rdev: u64,
522    file_type: u32,
523    access: i32,
524}
525
526impl std::fmt::Debug for ExecOpenFile {
527    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
528        f.debug_struct("ExecOpenFile")
529            .field("id", &self.id)
530            .finish_non_exhaustive()
531    }
532}
533
534impl ExecOpenFile {
535    fn capture(fd: RawFd, id: OpenFileId) -> Result<Self, Errno> {
536        let mut stat: libc::stat = unsafe { std::mem::zeroed() };
537        // Runtime-owned read-only inspection of this process's descriptor table.
538        let result = unsafe {
539            crate::util::raw_syscall(
540                libc::SYS_fstat,
541                [fd as u64, (&raw mut stat) as u64, 0, 0, 0, 0],
542            )
543        };
544        if result < 0 {
545            return Err(Errno::new(-result as i32));
546        }
547        let flags = unsafe {
548            crate::util::raw_syscall(
549                libc::SYS_fcntl,
550                [fd as u64, libc::F_GETFL as u64, 0, 0, 0, 0],
551            )
552        };
553        if flags < 0 {
554            return Err(Errno::new(-flags as i32));
555        }
556        Ok(Self {
557            id,
558            dev: stat.st_dev,
559            ino: stat.st_ino,
560            rdev: stat.st_rdev,
561            file_type: stat.st_mode & libc::S_IFMT,
562            access: flags as i32 & libc::O_ACCMODE,
563        })
564    }
565}
566
567impl ExecFdState {
568    pub(crate) fn open_file_creator(&self) -> Option<DetTid> {
569        self.open_files
570            .as_ref()
571            .map(|files| files.creator)
572            .or_else(|| self.open_file_namespace.map(|(creator, _)| creator))
573            .or_else(|| self.maps.as_ref().map(|maps| maps.creator))
574            .or_else(|| self.procfs_records.as_ref().map(|records| records.creator))
575    }
576
577    pub(crate) fn is_empty(&self) -> bool {
578        self.blocking.is_empty()
579            && self.inherited_reads.is_empty()
580            && !self.initialize_inherited_stdio
581            && self.initial_input.is_none()
582            && self.open_files.is_none()
583            && self.maps.is_none()
584            && self.procfs_records.is_none()
585            && self.channels.is_empty()
586            && self.open_file_namespace.is_none()
587            && self.random_device_sequence == 0
588            && self.exe.is_none()
589    }
590}
591
592impl From<ExecFdBlockingOverrides> for ExecFdState {
593    fn from(blocking: ExecFdBlockingOverrides) -> Self {
594        Self {
595            blocking,
596            ..Self::default()
597        }
598    }
599}
600
601/// The wakes an exec's walk of each owner's list found, in delivery order:
602/// owners in walk order, each owner's words in futex-key order.
603pub(crate) fn flatten_exec_robust_list_wakes(
604    wakes: Vec<(DetTid, Vec<RobustListWake>)>,
605) -> Vec<(DetTid, RobustListWake)> {
606    wakes
607        .into_iter()
608        .flat_map(|(owner, mut wakes)| {
609            wakes.sort_by_key(|wake| format!("{:?}", wake.futex));
610            wakes.into_iter().map(move |wake| (owner, wake))
611        })
612        .collect()
613}
614
615/// A single POSIX per-process interval timer created by `timer_create(2)`.
616///
617/// Detcore records arming against virtual time and schedules supported
618/// `SIGEV_SIGNAL` notifications through the deterministic scheduler.
619// AUTONOMOUS-BOT-IMPLEMENTED
620// TODO-HUMAN-REVIEW(#869)
621#[derive(Debug, Clone, Serialize, Deserialize)]
622struct PosixTimer {
623    /// Reload interval for periodic timers, in nanoseconds (0 => one-shot).
624    interval_ns: u64,
625    /// Absolute virtual-time deadline of the next expiration, or `None` when the
626    /// timer is disarmed (`it_value == 0`).
627    deadline: Option<LogicalTime>,
628    /// Signal number configured by `timer_create`, or `None` for notifications
629    /// that Detcore cannot deliver through its scheduler.
630    signal: Option<i32>,
631    /// The thread a `SIGEV_THREAD_ID` timer notifies, or `None` for a
632    /// process-directed one.
633    #[serde(default)]
634    thread: Option<DetTid>,
635    /// The `si_value` of its signal: `sigev_value`, or `sival_int = id`
636    /// for a timer created with no `sigevent` (`do_timer_create`).
637    #[serde(default)]
638    value: u64,
639    /// Created on a CPU clock (`crate::cpu_clock`). Its relative armings
640    /// count down on the virtual clock like any other timer's.
641    #[serde(default)]
642    cpu_clock: bool,
643}
644
645/// The set of POSIX timers owned by a *process*.
646///
647/// Timers are shared among all threads of a process and, per POSIX, are **not**
648/// inherited across `fork(2)`. Detcore therefore shares this table on
649/// `CLONE_THREAD` and starts a fresh, empty table for every new process (see
650/// `init_thread_state`). Successful exec deletes the timers as well.
651#[derive(Debug, Clone, Default, Serialize, Deserialize)]
652pub struct PosixTimers {
653    /// Deterministic id allocator. Kernel `timer_t`s are opaque, so we hand out
654    /// ids as 0, 1, 2, ... in creation order to keep them reproducible.
655    next_id: i32,
656    timers: HashMap<i32, PosixTimer>,
657}
658
659impl PosixTimers {
660    // AUTONOMOUS-BOT-IMPLEMENTED
661    // TODO-HUMAN-REVIEW(#869)
662    /// Allocate a new (disarmed) timer, returning its deterministic id.
663    /// `thread` is the thread a `SIGEV_THREAD_ID` timer notifies, and
664    /// `value` its `sigev_value`, or `None` for a timer created with no
665    /// `sigevent`, whose `si_value` is its id. `cpu_clock` says the timer was
666    /// created on a CPU clock.
667    pub(crate) fn create(
668        &mut self,
669        signal: Option<i32>,
670        thread: Option<DetTid>,
671        value: Option<u64>,
672        cpu_clock: bool,
673    ) -> i32 {
674        let id = self.next_id;
675        self.next_id += 1;
676        self.timers.insert(
677            id,
678            PosixTimer {
679                interval_ns: 0,
680                deadline: None,
681                signal,
682                thread,
683                value: value.unwrap_or(id as u32 as u64),
684                cpu_clock,
685            },
686        );
687        id
688    }
689
690    /// Whether timer `id` exists and was created on a CPU clock.
691    pub(crate) fn on_cpu_clock(&self, id: i32) -> bool {
692        self.timers.get(&id).is_some_and(|timer| timer.cpu_clock)
693    }
694
695    /// The `si_value` of timer `id`'s signal.
696    pub(crate) fn value(&self, id: i32) -> Option<u64> {
697        self.timers.get(&id).map(|timer| timer.value)
698    }
699
700    /// Arm or disarm timer `id`. `interval_ns` is the periodic reload and
701    /// `deadline` the absolute virtual-time expiration (the caller derives it
702    /// from the request flags and the current virtual clock; `None` disarms).
703    /// Returns the previous `(remaining_ns, interval_ns)` for `old_value`, or
704    /// `None` if the id is unknown.
705    pub(crate) fn settime(
706        &mut self,
707        id: i32,
708        interval_ns: u64,
709        deadline: Option<LogicalTime>,
710        now: LogicalTime,
711    ) -> Option<(u64, u64)> {
712        let timer = self.timers.get_mut(&id)?;
713        let old = (
714            remaining_ns(timer.deadline, timer.interval_ns, now),
715            timer.interval_ns,
716        );
717        timer.interval_ns = interval_ns;
718        timer.deadline = deadline;
719        Some(old)
720    }
721
722    /// Report the current `(remaining_ns, interval_ns)` for `timer_gettime`, or
723    /// `None` if the id is unknown.
724    pub(crate) fn gettime(&self, id: i32, now: LogicalTime) -> Option<(u64, u64)> {
725        let timer = self.timers.get(&id)?;
726        Some((
727            remaining_ns(timer.deadline, timer.interval_ns, now),
728            timer.interval_ns,
729        ))
730    }
731
732    /// Whether a timer with this id currently exists.
733    pub(crate) fn contains(&self, id: i32) -> bool {
734        self.timers.contains_key(&id)
735    }
736
737    // AUTONOMOUS-BOT-IMPLEMENTED
738    // TODO-HUMAN-REVIEW(#869)
739    pub(crate) fn signal(&self, id: i32) -> Option<Option<i32>> {
740        self.timers.get(&id).map(|timer| timer.signal)
741    }
742
743    /// The thread timer `id` notifies, if it is a `SIGEV_THREAD_ID` timer.
744    pub(crate) fn thread(&self, id: i32) -> Option<DetTid> {
745        self.timers.get(&id).and_then(|timer| timer.thread)
746    }
747
748    /// Remove a timer; returns whether it existed.
749    pub(crate) fn remove(&mut self, id: i32) -> bool {
750        self.timers.remove(&id).is_some()
751    }
752
753    /// Linux deletes all POSIX timers on successful exec. Keep the process's
754    /// ID allocator, but invalidate armed, disarmed and non-notifying timers.
755    pub(crate) fn clear_for_exec(&mut self) {
756        self.timers.clear();
757    }
758}
759
760/// One virtualized resource limit, represented in the `prlimit64` ABI's units.
761#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
762pub(crate) struct ResourceLimit {
763    pub(crate) current: u64,
764    pub(crate) maximum: u64,
765}
766
767/// Deterministic resource limits owned by one guest process.
768#[derive(Debug, Clone, Serialize, Deserialize)]
769pub(crate) struct ResourceLimits {
770    limits: Vec<ResourceLimit>,
771}
772
773impl Default for ResourceLimits {
774    fn default() -> Self {
775        let unlimited = ResourceLimit {
776            current: libc::RLIM64_INFINITY,
777            maximum: libc::RLIM64_INFINITY,
778        };
779        let mut limits = vec![unlimited; reverie::syscalls::linux_abi::RLIMIT_RTTIME as usize + 1];
780        limits[reverie::syscalls::linux_abi::RLIMIT_STACK as usize] = ResourceLimit {
781            current: 8 * 1024 * 1024,
782            maximum: libc::RLIM64_INFINITY,
783        };
784        limits[reverie::syscalls::linux_abi::RLIMIT_NOFILE as usize] = ResourceLimit {
785            current: 1_048_576,
786            maximum: 1_048_576,
787        };
788        Self { limits }
789    }
790}
791
792/// The limit every guest process starts with for `resource`, as
793/// `(current, maximum)` in the `prlimit64` ABI's units, or `None` for an
794/// invalid resource number.
795///
796/// A backend whose guest reads a limit before Detcore runs answers it with
797/// this. SaBRe is one: its guest's dynamic loader and libc startup run before
798/// the plugin is initialized, and libc sizes default thread stacks from
799/// `RLIMIT_STACK` there.
800pub fn initial_resource_limit(resource: u32) -> Option<(u64, u64)> {
801    ResourceLimits::default()
802        .get(resource)
803        .map(|limit| (limit.current, limit.maximum))
804}
805
806impl ResourceLimits {
807    /// Return a limit when `resource` is a valid Linux resource number.
808    pub(crate) fn get(&self, resource: u32) -> Option<ResourceLimit> {
809        self.limits.get(resource as usize).copied()
810    }
811
812    /// Replace a previously validated resource limit.
813    pub(crate) fn set(&mut self, resource: u32, limit: ResourceLimit) {
814        self.limits[resource as usize] = limit;
815    }
816}
817
818/// Nanoseconds remaining until `deadline` relative to `now`, saturating at 0.
819/// A disarmed timer (`None`) or an elapsed one-shot reports 0. Periodic timers
820/// advance arithmetically to their next virtual deadline.
821fn remaining_ns(deadline: Option<LogicalTime>, interval_ns: u64, now: LogicalTime) -> u64 {
822    match deadline {
823        Some(d) if d > now => d.as_nanos() - now.as_nanos(),
824        Some(d) if interval_ns != 0 => {
825            let elapsed = now.as_nanos() - d.as_nanos();
826            interval_ns - (elapsed % interval_ns)
827        }
828        Some(_) => 0,
829        None => 0,
830    }
831}
832
833impl<T> Default for Detcore<T> {
834    fn default() -> Self {
835        // TODO(T77816673): eventually we want to remove this requirement.
836        // In the meantime... just don't call this.
837        // Instead see the new() method defined in lib.rs
838        panic!("Detcore Default impl should not be called");
839    }
840}
841
842impl<T: RecordOrReplay> AsRef<T> for Detcore<T> {
843    fn as_ref(&self) -> &T {
844        &self.record_or_replay
845    }
846}
847
848impl<T: RecordOrReplay> AsMut<T> for Detcore<T> {
849    fn as_mut(&mut self) -> &mut T {
850        &mut self.record_or_replay
851    }
852}
853
854impl<T: RecordOrReplay> Detcore<T> {
855    /// Delegate to the record/replay tool without collapsing a tool failure
856    /// into a guest-visible errno. Syscall errors remain [`Error::Errno`], while
857    /// failures of the record/replay machinery retain their non-errno variant.
858    pub(crate) async fn record_or_replay_preserving_tool_errors<G, S>(
859        &self,
860        guest: &mut G,
861        syscall: S,
862    ) -> Result<i64, Error>
863    where
864        G: Guest<Self>,
865        S: Into<Syscall>,
866    {
867        preserve_record_or_replay_error(
868            self.translated_record_or_replay(guest, syscall.into())
869                .await,
870        )?
871        .map_err(Error::from)
872    }
873
874    /// The record-or-replay tool's handling of `syscall`, with a `/proc`
875    /// path that names a process or thread by the guest's number reaching
876    /// the kernel's directory for it (DBT; see
877    /// `syscalls::files::procfs_numbers`).
878    async fn translated_record_or_replay<G: Guest<Self>>(
879        &self,
880        guest: &mut G,
881        syscall: Syscall,
882    ) -> Result<i64, Error> {
883        // Only a call that looks up a path, on a thread whose `/proc` numbers
884        // are translated (DBT), can have anything to translate;
885        // `procfs_translated_call` answers `None` at once for every other
886        // call. Asking first keeps its futures, and their size, out of every
887        // forwarded syscall's future, and the box is allocated only for a
888        // call that then reads its path from the guest anyway.
889        let translated = if Self::may_translate_procfs_call(guest, &syscall) {
890            Box::pin(Self::procfs_translated_call(guest, &syscall)).await?
891        } else {
892            None
893        };
894        let call = translated
895            .as_ref()
896            .map_or(syscall, |translated| translated.call);
897        crate::reserved_input::before_native_call(guest, call)?;
898        let result = if crate::record_or_replay::injects_only::<T>() {
899            // What the tool's handler would have done (`NoopTool`), here:
900            // the same single injection of the same call, awaited at the
901            // same point, with its errno converted the same way.
902            guest.inject(call).await.map_err(Error::from)
903        } else {
904            self.record_or_replay
905                .handle_syscall_event(&mut guest.into_guest(), call)
906                .await
907        };
908        if let Some(translated) = translated {
909            Box::pin(Self::finish_translated_call(guest, translated)).await;
910        }
911        result
912    }
913
914    /// Helper function for delegating the injection of a syscall to the
915    /// record_or_replay tool.
916    ///
917    /// It is important to classify the cases where we need to call `inject`. We
918    /// have three main choices to make when handling a syscall:
919    ///  1. Fully determinize the syscall. In this case, it doesn't need to call
920    ///     `inject` at all.
921    ///  2. Partially determinize the syscall. In this case, it can't fully
922    ///     determinize a syscall but only part of it. For example, a `stat` syscall
923    ///     is ultimately non-deterministic because the file may not always exist.
924    ///     However, the mtime or inode numbers can be made deterministic.
925    ///  3. The syscall cannot be determinized at all. For example, a call to
926    ///     `recvfrom` cannot be made deterministic.
927    ///
928    /// Thus, this is called whenever `inject` would be called for non-bookkeeping
929    /// operations.
930    pub(crate) async fn record_or_replay<G, S>(
931        &self,
932        guest: &mut G,
933        syscall: S,
934    ) -> Result<i64, Errno>
935    where
936        G: Guest<Self>,
937        S: Into<Syscall>,
938    {
939        self.translated_record_or_replay(guest, syscall.into())
940            .await
941            // TODO: Get rid of this and make this whole function use the Error type.
942            .map_err(|err| err.into_errno().unwrap())
943    }
944}
945
946fn preserve_record_or_replay_error(
947    result: Result<i64, Error>,
948) -> Result<Result<i64, Errno>, Error> {
949    match result {
950        Ok(value) => Ok(Ok(value)),
951        Err(Error::Errno(error)) => Ok(Err(error)),
952        Err(error) => Err(error),
953    }
954}
955
956/// Linux reports a positive byte count when a later guest-visible write error
957/// follows partial progress. An error in the record/replay tool is different:
958/// replay itself failed, so it must remain fatal even after earlier bytes were
959/// accepted.
960pub(crate) fn finish_partial_record_or_replay_write(
961    written: i64,
962    error: Error,
963) -> Result<i64, Error> {
964    match error {
965        Error::Errno(_) if written > 0 => Ok(written),
966        error => Err(error),
967    }
968}
969
970#[cfg(test)]
971mod record_or_replay_error_tests {
972    use super::*;
973
974    #[test]
975    fn errno_remains_a_guest_syscall_result() {
976        let result = preserve_record_or_replay_error(Err(Error::Errno(Errno::EBADF)))
977            .expect("guest errno must not abort the tool");
978        assert_eq!(result, Err(Errno::EBADF));
979    }
980
981    #[test]
982    fn tool_failure_remains_an_outer_error() {
983        let failure =
984            preserve_record_or_replay_error(Err(Error::Tool(anyhow::anyhow!("restore failed"))))
985                .expect_err("tool failure must abort replay");
986        assert!(matches!(failure.into_errno(), Err(Error::Tool(_))));
987    }
988
989    #[test]
990    fn partial_write_suppresses_only_guest_errno() {
991        assert_eq!(
992            finish_partial_record_or_replay_write(7, Error::Errno(Errno::EPIPE)).unwrap(),
993            7
994        );
995
996        let io_failure = finish_partial_record_or_replay_write(
997            7,
998            Error::Io(std::io::Error::from_raw_os_error(libc::ENOSPC)),
999        )
1000        .expect_err("host replay-output failure must not become a partial guest write");
1001        assert!(matches!(io_failure.into_errno(), Err(Error::Io(_))));
1002
1003        let tool_failure = finish_partial_record_or_replay_write(
1004            7,
1005            Error::Tool(anyhow::anyhow!("replay endpoint disappeared")),
1006        )
1007        .expect_err("tool failure must not become a partial guest write");
1008        assert!(matches!(tool_failure.into_errno(), Err(Error::Tool(_))));
1009
1010        let guest_failure = finish_partial_record_or_replay_write(0, Error::Errno(Errno::EPIPE))
1011            .expect_err("zero-progress guest failure must remain guest-visible");
1012        assert_eq!(guest_failure.into_errno().unwrap(), Errno::EPIPE);
1013    }
1014}
1015
1016impl FileMetadata {
1017    /// create an empty file metadata
1018    pub(crate) fn new(owner: DetTid) -> Self {
1019        FileMetadata {
1020            files_id: FilesId::initial(owner),
1021            next_open_file_sequence: 0,
1022            next_socket_open_file_sequence: 0,
1023            next_random_device_sequence: 0,
1024            has_promoted_shared_files: false,
1025            has_shared_procfs_sequences: false,
1026            file_handles: HashMap::new(),
1027            accept_waits: Vec::new(),
1028            next_accept_wait: 0,
1029            cpuinfo_placeholders: None,
1030            pending_cpuinfo_placeholders: Vec::new(),
1031            proc_level: None,
1032            present_unmodelled: BTreeMap::new(),
1033            unaccounted_receipts: false,
1034            descriptor_capacity: default_descriptor_capacity(),
1035            exec_dropped_lock_tracked: false,
1036            released_rights: ReleasedRights::default(),
1037            holders: BTreeMap::from([(owner, 1)]),
1038            exec_replaced: ReplacedTable::default(),
1039        }
1040    }
1041
1042    /// Note that descriptor `fd` was installed in this table: it is no
1043    /// longer an unmodelled one, and the table grows to hold it as Linux's
1044    /// does ([`Self::descriptor_capacity`]).
1045    fn note_installed(&mut self, fd: RawFd) {
1046        self.present_unmodelled.remove(&fd);
1047        self.grow_to_hold(fd);
1048    }
1049
1050    /// Grow the table to hold descriptor `fd`, as Linux's `expand_files`
1051    /// does ([`Self::descriptor_capacity`]): for an installed descriptor, and
1052    /// for the target of a `dup2` or `dup3` that then fails because its old
1053    /// descriptor is not open, which `ksys_dup3` expands the table for first
1054    /// (<https://github.com/torvalds/linux/blob/v6.17/fs/file.c#L1408-L1415>).
1055    fn grow_to_hold(&mut self, fd: RawFd) {
1056        if let Ok(fd) = u64::try_from(fd)
1057            && fd >= self.descriptor_capacity
1058        {
1059            self.descriptor_capacity = (fd + 1)
1060                .checked_next_power_of_two()
1061                .unwrap_or(u64::MAX / 2)
1062                .max(default_descriptor_capacity());
1063        }
1064    }
1065
1066    /// Set the close-on-exec flag of descriptor `fd` if it is one this table
1067    /// lists without modelling ([`Self::present_unmodelled`]); whether it is.
1068    fn set_unmodelled_cloexec(&mut self, fd: RawFd, cloexec: bool) -> bool {
1069        match self.present_unmodelled.get_mut(&fd) {
1070            Some(flag) => {
1071                *flag = cloexec;
1072                true
1073            }
1074            None => false,
1075        }
1076    }
1077
1078    /// The descriptors open in this table that it knows of, ascending:
1079    /// those it models and those it does not ([`Self::present_unmodelled`]).
1080    fn descriptor_numbers(&self) -> Vec<RawFd> {
1081        let mut numbers: Vec<RawFd> = self
1082            .file_handles
1083            .keys()
1084            .copied()
1085            .chain(self.present_unmodelled.keys().copied())
1086            .collect();
1087        numbers.sort_unstable();
1088        numbers.dedup();
1089        numbers
1090    }
1091
1092    /// The capacity a fork's copy of this table gets: `dup_fd` sizes it from
1093    /// the highest descriptor open, rounded up to a multiple of
1094    /// `BITS_PER_LONG` (`sane_fdtable_size`), and allocates a larger table
1095    /// only past `NR_OPEN_DEFAULT`, to the next power of two
1096    /// (`alloc_fdtable`).
1097    fn forked_descriptor_capacity(&self) -> u64 {
1098        let open = self
1099            .descriptor_numbers()
1100            .last()
1101            .and_then(|fd| u64::try_from(*fd).ok())
1102            .map_or(0, |fd| (fd + 1).next_multiple_of(64));
1103        if open <= default_descriptor_capacity() {
1104            default_descriptor_capacity()
1105        } else {
1106            open.checked_next_power_of_two().unwrap_or(u64::MAX / 2)
1107        }
1108    }
1109
1110    /// Note the run's `--proc` level ([`Self::proc_level`]).
1111    pub(crate) fn note_proc_level(&mut self, level: detcore_model::config::ProcLevel) {
1112        self.proc_level = Some(level);
1113    }
1114
1115    /// The placeholder `found` names, unless it names a file outside the
1116    /// run's `--proc` level ([`Self::proc_level`]), for which Detcore makes
1117    /// no placeholder.
1118    fn within_proc_level(
1119        &self,
1120        found: Option<(&'static std::path::Path, Vec<u8>)>,
1121    ) -> Option<(&'static std::path::Path, Vec<u8>)> {
1122        found.filter(|(path, _)| {
1123            self.proc_level.is_none_or(|level| {
1124                !crate::syscalls::rendered_procfs::named_file_outside_level(path, level)
1125            })
1126        })
1127    }
1128
1129    /// Take the session's answer to whether it renders `/proc/cpuinfo`
1130    /// (`rendered`: `Detcore::rendered_files(guest).cpuinfo`), at the first
1131    /// guest callback of this image, before guest code runs. A descriptor
1132    /// rediscovered before it whose memfd is named as a placeholder for a
1133    /// file rendered only where `/proc/cpuinfo` is (the file itself, or a
1134    /// task's `arch_status`) becomes the placeholder it is where the session
1135    /// renders the file, and stays an ordinary memfd where it does not. Later
1136    /// answers of the same session are the same and change nothing.
1137    pub(crate) fn decide_cpuinfo_placeholders(&mut self, rendered: bool) -> Result<(), Errno> {
1138        if self.cpuinfo_placeholders.is_some() {
1139            return Ok(());
1140        }
1141        self.cpuinfo_placeholders = Some(rendered);
1142        let pending = std::mem::take(&mut self.pending_cpuinfo_placeholders);
1143        if !rendered {
1144            return Ok(());
1145        }
1146        for fd in pending {
1147            if !self.file_handles.contains_key(&fd) {
1148                continue;
1149            }
1150            let raw_stat =
1151                stat::fstat(unsafe { BorrowedFd::borrow_raw(fd) }).map_err(|_| Errno::last())?;
1152            let fd_flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };
1153            if let Some((path, snapshot)) =
1154                self.within_proc_level(placeholder_behind(fd, crate::procfs::RenderedFiles::ALL))
1155            {
1156                self.adopt_placeholder(fd, &raw_stat, fd_flags, path, snapshot)?;
1157            }
1158        }
1159        Ok(())
1160    }
1161
1162    /// Register a blocking accept waiting on `listener` and return the
1163    /// operation number that removes it again.
1164    pub(crate) fn add_accept_wait(&mut self, listener: RawFd) -> u64 {
1165        let operation = self.next_accept_wait;
1166        self.next_accept_wait += 1;
1167        self.accept_waits.push((operation, listener));
1168        operation
1169    }
1170
1171    /// Remove the accept wait registered as `operation`, if it is here.
1172    pub(crate) fn remove_accept_wait(&mut self, operation: u64) {
1173        self.accept_waits
1174            .retain(|(waiting, _)| *waiting != operation);
1175    }
1176
1177    /// The lowest descriptor in `fds` that a blocking accept waits on.
1178    pub(crate) fn waited_listener_in(&self, fds: std::ops::RangeInclusive<u32>) -> Option<RawFd> {
1179        self.accept_waits
1180            .iter()
1181            .map(|(_, listener)| *listener)
1182            .filter(|listener| u32::try_from(*listener).is_ok_and(|fd| fds.contains(&fd)))
1183            .min()
1184    }
1185
1186    fn allocate_open_file_id(&mut self, creator: DetTid, ty: FdType) -> OpenFileId {
1187        if ty == FdType::Socket {
1188            let id = OpenFileId::new_socket(creator, self.next_socket_open_file_sequence);
1189            self.next_socket_open_file_sequence += 1;
1190            id
1191        } else {
1192            let id = OpenFileId::new(creator, self.next_open_file_sequence);
1193            self.next_open_file_sequence += 1;
1194            id
1195        }
1196    }
1197
1198    fn count_open_files_at_paths(&self, paths: &[&Path]) -> usize {
1199        self.file_handles
1200            .values()
1201            .filter(|fd| {
1202                fd.path()
1203                    .is_some_and(|path| paths.iter().any(|candidate| path == *candidate))
1204            })
1205            .map(DetFd::open_file_id)
1206            .collect::<BTreeSet<_>>()
1207            .len()
1208    }
1209
1210    fn has_loopback_peer(&self) -> bool {
1211        self.file_handles.values().any(DetFd::is_loopback_peer)
1212    }
1213
1214    // TODO-HUMAN-REVIEW(#2373)
1215    /// True when `vfork` must be refused because some open file description
1216    /// either holds a lock or carries a cached claim that may now be wrong.
1217    ///
1218    /// ⚠️ NEVER-OBSERVED IS DELIBERATELY NOT UNSAFE. The previous form refused
1219    /// on `!= Some(None)`, which also caught descriptors Detcore had simply
1220    /// never observed. stdin, stdout and stderr are marked unobserved at
1221    /// container setup because they predate Detcore, so that form refused
1222    /// `vfork` for EVERY guest that reached it -- measured: fds 0, 1 and 2 were
1223    /// the sole reason `run_dbt_virtualizes_process_identities` was refused,
1224    /// and that guest never calls `flock` at all. A guard that cannot pass is
1225    /// not conservative, it is broken.
1226    ///
1227    /// The hazard is a STALE CACHE: a copied runtime mutating a lock that this
1228    /// side still has a claim about. A description Detcore never observed
1229    /// carries no claim, so there is nothing about it that can go stale.
1230    ///
1231    /// ⚠️ AN INVALIDATED DESCRIPTION WITH NO GRANT THROUGH IT IS NOT UNSAFE
1232    /// EITHER. Every ordinary fork invalidates every description the process
1233    /// created, so refusing on invalidation alone refused `vfork` in every
1234    /// process that had forked once while holding any descriptor at all --
1235    /// measured: a pipe, a fork whose child exits, then `vfork` was refused,
1236    /// and that refusal stopped every DBT `make -j4` cell in run 1, although
1237    /// `make` never calls `flock`. An invalidated description is refused only
1238    /// when this process's Detcore granted a lock through it
1239    /// (`DetFd::flock_may_have_been_granted`), before or after the fork.
1240    ///
1241    /// The invariant is "no process holds a known mode for an admitted
1242    /// description", not "an admitted description holds no lock": a sibling
1243    /// may have locked it through its own split copy of the model, and the
1244    /// copied child may change that lock natively. That is Linux behaviour
1245    /// and no Detcore flock cache can go stale from it. The routes by which
1246    /// a copied child could reach a description another process DOES hold a
1247    /// claim about, or change an OFD lock the `/proc/locks` ledger records,
1248    /// are refused in the copied child itself, whatever the
1249    /// unsupported-syscall policy: the run ends by name under the default
1250    /// fail-closed policy and the call answers an errno without executing
1251    /// under `--allow-unsupported-syscalls` (`pidfd_getfd`, `recvmsg` and
1252    /// `recvmmsg`, which can import a description over `SCM_RIGHTS`, and OFD
1253    /// `fcntl` lock changes: `reverie_dbt_runtime_copied_syscall` in
1254    /// detcore-dbt). A
1255    /// native flock change can leave the ledger's recorded flock stale, but
1256    /// that record admits no `/proc/locks` row: flock rows are kept only by
1257    /// their taker's pid (`procfs::locks`).
1258    fn has_unsafe_vfork_flock_state(&self) -> bool {
1259        self.file_handles.values().any(|detfd| {
1260            matches!(detfd.known_flock_mode(), Some(Some(_)))
1261                || (detfd.flock_mode_may_be_stale() && detfd.flock_may_have_been_granted())
1262        })
1263    }
1264
1265    fn has_unsafe_vfork_ofd_state(&self) -> bool {
1266        self.file_handles
1267            .values()
1268            .any(|detfd| detfd.may_hold_ofd_lock())
1269    }
1270
1271    fn forget_flock_modes(&self) {
1272        for detfd in self.file_handles.values() {
1273            detfd.forget_local_flock_mode_at_fork();
1274        }
1275    }
1276
1277    // TODO-HUMAN-REVIEW(#2373)
1278    /// Fork the file table for `child`.
1279    ///
1280    /// Local flock caches retain the existing conservative invalidation.
1281    /// A promoted description has one canonical model, so copying a table
1282    /// neither invalidates that model nor contacts it before child admission.
1283    /// Same-process aliases keep their lock; a separately hosted runtime
1284    /// resets copied locks at its physical child boundary.
1285    pub(crate) fn fork_for(&self, child: DetTid) -> Self {
1286        self.forget_flock_modes();
1287        Self {
1288            files_id: FilesId::forked(child),
1289            next_open_file_sequence: self.next_open_file_sequence,
1290            next_socket_open_file_sequence: self.next_socket_open_file_sequence,
1291            next_random_device_sequence: self.next_random_device_sequence,
1292            has_promoted_shared_files: self.has_promoted_shared_files,
1293            has_shared_procfs_sequences: self.has_shared_procfs_sequences,
1294            file_handles: self.file_handles.clone(),
1295            accept_waits: Vec::new(),
1296            next_accept_wait: 0,
1297            cpuinfo_placeholders: self.cpuinfo_placeholders,
1298            pending_cpuinfo_placeholders: self.pending_cpuinfo_placeholders.clone(),
1299            proc_level: self.proc_level,
1300            present_unmodelled: self.present_unmodelled.clone(),
1301            unaccounted_receipts: self.unaccounted_receipts,
1302            descriptor_capacity: self.forked_descriptor_capacity(),
1303            exec_dropped_lock_tracked: false,
1304            released_rights: ReleasedRights::default(),
1305            holders: BTreeMap::from([(child, 1)]),
1306            exec_replaced: ReplacedTable::default(),
1307        }
1308    }
1309
1310    pub(crate) fn for_exec(&self, task: DetTid) -> Self {
1311        Self {
1312            files_id: self.files_id.for_exec(task),
1313            next_open_file_sequence: self.next_open_file_sequence,
1314            next_socket_open_file_sequence: self.next_socket_open_file_sequence,
1315            next_random_device_sequence: self.next_random_device_sequence,
1316            has_promoted_shared_files: self.has_promoted_shared_files,
1317            has_shared_procfs_sequences: self.has_shared_procfs_sequences,
1318            file_handles: self
1319                .file_handles
1320                .iter()
1321                .filter_map(|(&fd, detfd)| (!detfd.is_cloexec()).then_some((fd, detfd.clone())))
1322                .collect(),
1323            accept_waits: Vec::new(),
1324            next_accept_wait: 0,
1325            cpuinfo_placeholders: self.cpuinfo_placeholders,
1326            pending_cpuinfo_placeholders: self.pending_cpuinfo_placeholders.clone(),
1327            proc_level: self.proc_level,
1328            present_unmodelled: self
1329                .present_unmodelled
1330                .iter()
1331                .filter(|(_, cloexec)| !**cloexec)
1332                .map(|(fd, cloexec)| (*fd, *cloexec))
1333                .collect(),
1334            unaccounted_receipts: self.unaccounted_receipts,
1335            descriptor_capacity: self.descriptor_capacity,
1336            exec_dropped_lock_tracked: self
1337                .file_handles
1338                .values()
1339                .any(|detfd| detfd.is_cloexec() && detfd.is_lock_tracked()),
1340            released_rights: ReleasedRights::default(),
1341            holders: BTreeMap::from([(task, 1)]),
1342            exec_replaced: ReplacedTable::default(),
1343        }
1344    }
1345
1346    /// A thread of `process` made with `CLONE_FILES` holds this table too
1347    /// ([`Self::holders`]); a table whose holders are not known stays so.
1348    pub(crate) fn join_holder(&mut self, process: DetPid) {
1349        if !self.holders.is_empty() {
1350            *self.holders.entry(process).or_insert(0) += 1;
1351        }
1352    }
1353
1354    /// One thread of `process` holds this table, and nothing else does
1355    /// ([`Self::holders`]): a copy made for it, by a `clone` without
1356    /// `CLONE_FILES` or by an `exec`.
1357    pub(crate) fn hold_alone(&mut self, process: DetPid) {
1358        self.holders = BTreeMap::from([(process, 1)]);
1359    }
1360
1361    /// A thread of `process` that holds this table exits, with its whole
1362    /// process where `whole_process` (an `exit_group`, which ends every
1363    /// thread of it). Returns whether that left the table to no holder: then
1364    /// it goes with this exit, as Linux releases a table with its last
1365    /// holder's exit (`exit_files`). Never for a table whose holders are not
1366    /// known, nor for a process that does not hold it.
1367    pub(crate) fn leave(&mut self, process: DetPid, whole_process: bool) -> bool {
1368        let Some(count) = self.holders.get_mut(&process) else {
1369            return false;
1370        };
1371        if whole_process || *count <= 1 {
1372            self.holders.remove(&process);
1373        } else {
1374            *count -= 1;
1375        }
1376        self.holders.is_empty()
1377    }
1378
1379    /// `process` has ended, by the default action of a fatal signal, as the
1380    /// terminal receipt of its death says (`GlobalState::release_tables_at_death`),
1381    /// and this is one of the tables its threads held. Every thread of it
1382    /// leaves the table at once ([`Self::leave`]); where that leaves the
1383    /// table to no holder, every descriptor of it goes, in order, as Linux
1384    /// releases a table with its last holder's exit (`exit_files`), and a
1385    /// socket whose last reference goes takes its queue ([`Self::release_detfd`]).
1386    /// Where another process still holds the table (`CLONE_FILES` without
1387    /// `CLONE_THREAD`), it keeps every descriptor. Where its holders are not
1388    /// known, or do not name `process`, nothing is decided.
1389    pub(crate) fn release_at_death(&mut self, process: DetPid) -> TableAtDeath {
1390        if !self.holders.contains_key(&process) {
1391            return TableAtDeath::Undecided;
1392        }
1393        if !self.leave(process, true) {
1394            return TableAtDeath::Kept;
1395        }
1396        self.release_every_descriptor();
1397        TableAtDeath::Released(self.take_released_rights())
1398    }
1399
1400    /// Keep `replaced`, the table an exec of `process` replaces with this
1401    /// one, until the new image's first callback ([`Self::exec_replaced`]).
1402    pub(crate) fn keep_replaced_table(
1403        &mut self,
1404        replaced: Arc<Mutex<FileMetadata>>,
1405        process: DetPid,
1406    ) {
1407        self.exec_replaced = ReplacedTable(Some((replaced, process)));
1408    }
1409
1410    /// Read the live table incarnation for an independently owned exec cut.
1411    pub fn native_exec_files_id(&self) -> FilesId {
1412        self.files_id
1413    }
1414
1415    /// Validate before and after preparation: `for_exec` intentionally drops
1416    /// waits and CLOEXEC-only descriptions, so checking only its result would
1417    /// lose evidence about their skipped runtime capabilities.
1418    pub(crate) fn validate_native_exec_cut(
1419        &self,
1420        expected: FilesId,
1421        prepared: bool,
1422    ) -> Result<(), crate::native_exec::NativeExecCutError> {
1423        use crate::native_exec::NativeExecCutError;
1424
1425        if self.files_id != expected {
1426            return Err(NativeExecCutError::Identity("files_id"));
1427        }
1428        if !self.accept_waits.is_empty() {
1429            return Err(NativeExecCutError::LiveCapability("accept_waits"));
1430        }
1431        if prepared && self.next_accept_wait != 0 {
1432            return Err(NativeExecCutError::PreparedState("next_accept_wait"));
1433        }
1434        // The native final cut repeats this check after freezing counters.
1435        // Compare prior descriptors directly so validation neither allocates a
1436        // scratch map nor changes the carried Tool heap. The state transport
1437        // separately bounds this first, quiescent single-thread domain.
1438        for (index, (&slot, fd)) in self.file_handles.iter().enumerate() {
1439            if slot < 0 || slot != fd.fd {
1440                return Err(NativeExecCutError::Descriptor("slot number"));
1441            }
1442            if prepared && fd.is_cloexec() {
1443                return Err(NativeExecCutError::PreparedState("CLOEXEC slot"));
1444            }
1445            let (id, owner) = fd.validate_native_exec_runtime_state()?;
1446            for (_, previous) in self.file_handles.iter().take(index) {
1447                let (previous_id, previous_owner) =
1448                    previous.validate_native_exec_runtime_state()?;
1449                if (id == previous_id) != (owner == previous_owner) {
1450                    return Err(NativeExecCutError::Descriptor("OFD alias identity"));
1451                }
1452            }
1453        }
1454        Ok(())
1455    }
1456
1457    pub(crate) fn exec_blocking_overrides(&self) -> ExecFdBlockingOverrides {
1458        self.file_handles
1459            .iter()
1460            .filter_map(|(&fd, detfd)| {
1461                (!detfd.is_cloexec() && detfd.physically_nonblocking() && !detfd.is_nonblocking())
1462                    .then_some(fd)
1463            })
1464            .collect()
1465    }
1466
1467    pub(crate) fn apply_exec_blocking_overrides(
1468        &mut self,
1469        owner: DetTid,
1470        overrides: ExecFdBlockingOverrides,
1471    ) {
1472        for fd in overrides {
1473            tracing::trace!(
1474                "[detcore, dtid {}] restoring descriptor {} as logically blocking after exec",
1475                owner,
1476                fd,
1477            );
1478            match self.discover_fd_from_current_process(owner, fd) {
1479                Ok(()) => {
1480                    self.with_detfd(fd, |detfd| detfd.set_logical_nonblocking(false))
1481                        .expect("a just-discovered descriptor must remain registered");
1482                }
1483                Err(error) => {
1484                    tracing::warn!(
1485                        "[detcore, dtid {}] unable to restore inherited descriptor {} after exec: {}",
1486                        owner,
1487                        fd,
1488                        error,
1489                    );
1490                }
1491            }
1492        }
1493    }
1494
1495    fn inherited_reads_for_exec(&self) -> BTreeMap<RawFd, crate::fd::InheritedExternalRead> {
1496        self.file_handles
1497            .iter()
1498            .filter(|(_, detfd)| !detfd.is_cloexec())
1499            .filter_map(|(&fd, detfd)| {
1500                detfd
1501                    .inherited_external_read()
1502                    .map(|terminal| (fd, terminal))
1503            })
1504            .collect()
1505    }
1506
1507    /// Called only in the actual shared guest descriptor table, after the
1508    /// global state confirms this is the container's first image. TCGETS is
1509    /// read-only: neither terminal settings nor OFD flags are changed.
1510    fn discover_inherited_terminals(&mut self, descriptors: impl IntoIterator<Item = RawFd>) {
1511        for fd in descriptors {
1512            let Some(detfd) = self.file_handles.get(&fd) else {
1513                continue;
1514            };
1515            let Some(stat) = detfd.stat() else { continue };
1516            if stat.mode & libc::S_IFMT != libc::S_IFCHR {
1517                continue;
1518            }
1519            let mut settings: libc::termios = unsafe { std::mem::zeroed() };
1520            // SAFETY: TCGETS writes only to this live, adequately sized buffer.
1521            if unsafe { libc::ioctl(fd, libc::TCGETS, &raw mut settings) } != 0 {
1522                continue;
1523            }
1524            detfd.set_inherited_external_read(crate::fd::InheritedExternalRead {
1525                origin: detfd.open_file_id(),
1526                dev: stat.dev,
1527                inode: stat.inode,
1528                rdev: stat.rdev,
1529                file_type: libc::S_IFCHR,
1530                access_mode: detfd.status_flags() & libc::O_ACCMODE,
1531                resource: detfd.resource(),
1532            });
1533        }
1534    }
1535
1536    /// Called only in the actual shared guest descriptor table, after the
1537    /// global state confirms this is the container's first image, beside
1538    /// `discover_inherited_terminals`. The guest has not run yet, so a pipe
1539    /// end the first image holds came from outside the run: a readable one is
1540    /// an inherited external read, as a terminal or a launcher-bound input is
1541    /// (https://github.com/rrnewton/hermit/issues/4447), and its reads take
1542    /// the same external-input path theirs do. That holds even when the image
1543    /// also holds the pipe's write end: such a pipe was not made by Detcore's
1544    /// `pipe2`, so it is not physically nonblocking and cannot take the
1545    /// internal-pipe path, whose invariant (`record_or_replay_blocking`) a
1546    /// blocking read of it would otherwise trip. The decision reads only what
1547    /// discovery recorded for the descriptor (its type, the kernel's stat and
1548    /// its access mode), so no later probe can change it; a description a
1549    /// launcher already bound keeps its binding. Inherited descriptors above 2
1550    /// are not discovered here.
1551    fn discover_inherited_pipes(&mut self, descriptors: impl IntoIterator<Item = RawFd>) {
1552        for fd in descriptors {
1553            let Some(detfd) = self.file_handles.get(&fd) else {
1554                continue;
1555            };
1556            let access_mode = detfd.status_flags() & libc::O_ACCMODE;
1557            if detfd.ty() != FdType::Pipe
1558                || detfd.is_socketpair_endpoint()
1559                || access_mode == libc::O_WRONLY
1560                || detfd.inherited_external_read().is_some()
1561            {
1562                continue;
1563            }
1564            let Some(stat) = detfd.stat() else { continue };
1565            detfd.set_inherited_external_read(crate::fd::InheritedExternalRead {
1566                origin: detfd.open_file_id(),
1567                dev: stat.dev,
1568                inode: stat.inode,
1569                rdev: stat.rdev,
1570                file_type: libc::S_IFIFO,
1571                access_mode,
1572                resource: detfd.resource(),
1573            });
1574        }
1575    }
1576
1577    /// Restore only external-read descriptions proven to survive the prepared exec. Unknown
1578    /// descriptors and guest-created PTYs are not promoted by rediscovery.
1579    fn restore_inherited_reads(
1580        &mut self,
1581        owner: DetTid,
1582        inherited_reads: BTreeMap<RawFd, crate::fd::InheritedExternalRead>,
1583    ) -> Result<(), Errno> {
1584        let mut aliases: HashMap<OpenFileId, DetFd> = HashMap::new();
1585        for (fd, terminal) in inherited_reads {
1586            self.discover_fd_from_current_process(owner, fd)?;
1587            let detfd = self.file_handles.get_mut(&fd).ok_or(Errno::EBADF)?;
1588            let stat =
1589                stat::fstat(unsafe { BorrowedFd::borrow_raw(fd) }).map_err(|_| Errno::last())?;
1590            if stat.st_mode & libc::S_IFMT != terminal.file_type
1591                || (stat.st_dev, stat.st_ino, stat.st_rdev)
1592                    != (terminal.dev, terminal.inode, terminal.rdev)
1593                || detfd.status_flags() & libc::O_ACCMODE != terminal.access_mode
1594            {
1595                return Err(Errno::EIO);
1596            }
1597            // Do not repeat TCGETS here: a hung-up terminal may return EIO
1598            // from that query while still being the inherited open file.
1599            if let Some(alias) = aliases.get(&terminal.origin) {
1600                if detfd.status_flags() != alias.status_flags() {
1601                    return Err(Errno::EIO);
1602                }
1603                detfd.share_open_file_with(alias);
1604            } else {
1605                detfd.set_resource(terminal.resource.clone());
1606                detfd.set_inherited_external_read(terminal.clone());
1607                aliases.insert(terminal.origin, detfd.clone());
1608            }
1609        }
1610        Ok(())
1611    }
1612
1613    fn install_inherited_input(
1614        &mut self,
1615        owner: DetTid,
1616        input: reverie::InheritedInputIdentity,
1617    ) -> Result<(), Errno> {
1618        let fd = input.guest_fd();
1619        self.discover_fd_from_current_process(owner, fd)?;
1620        let stat = stat::fstat(unsafe { BorrowedFd::borrow_raw(fd) }).map_err(|_| Errno::last())?;
1621        let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
1622        let detfd = self.file_handles.get(&fd).ok_or(Errno::EBADF)?;
1623        if !input.matches(&stat, flags) || detfd.ty() != FdType::Pipe {
1624            return Err(Errno::EIO);
1625        }
1626        detfd.set_inherited_external_read(crate::fd::InheritedExternalRead {
1627            origin: detfd.open_file_id(),
1628            dev: input.dev(),
1629            inode: input.inode(),
1630            rdev: input.rdev(),
1631            file_type: libc::S_IFIFO,
1632            access_mode: input.access_mode(),
1633            resource: detfd.resource(),
1634        });
1635        Ok(())
1636    }
1637
1638    pub(crate) fn apply_exec_fd_state(
1639        &mut self,
1640        owner: DetTid,
1641        state: ExecFdState,
1642        shared_guest_table: bool,
1643    ) -> Result<(), Errno> {
1644        if shared_guest_table && let Some(maps) = &state.maps {
1645            // The existing exec exchange authorizes these survivors. Validate
1646            // every slot before installing any carried maps authority, and
1647            // reserve its old ID space before lazy discovery allocates IDs.
1648            for (&fd, survivor) in &maps.handles {
1649                if ExecOpenFile::capture(fd, survivor.identity.id)? != survivor.identity {
1650                    return Err(Errno::EIO);
1651                }
1652            }
1653            if state
1654                .open_files
1655                .as_ref()
1656                .is_some_and(|files| files.creator != maps.creator)
1657                || state
1658                    .open_file_namespace
1659                    .is_some_and(|(creator, _)| creator != maps.creator)
1660            {
1661                return Err(Errno::EIO);
1662            }
1663            self.next_open_file_sequence = self.next_open_file_sequence.max(maps.next_sequence);
1664        }
1665        if shared_guest_table && let Some(records) = &state.procfs_records {
1666            // The same for the per-process files carried with their record's
1667            // sequence, and their table's numbering, reserved before any
1668            // rediscovery allocates an identity whose sequence another
1669            // process may still hold.
1670            for (&fd, survivor) in &records.handles {
1671                if ExecOpenFile::capture(fd, survivor.identity.id)? != survivor.identity {
1672                    return Err(Errno::EIO);
1673                }
1674            }
1675            if state
1676                .open_files
1677                .as_ref()
1678                .is_some_and(|files| files.creator != records.creator)
1679                || state
1680                    .open_file_namespace
1681                    .is_some_and(|(creator, _)| creator != records.creator)
1682                || state
1683                    .maps
1684                    .as_ref()
1685                    .is_some_and(|maps| maps.creator != records.creator)
1686            {
1687                return Err(Errno::EIO);
1688            }
1689            self.next_open_file_sequence = self.next_open_file_sequence.max(records.next_sequence);
1690            self.has_shared_procfs_sequences = true;
1691        }
1692        // Legacy discovery also allocates IDs. Reserve the complete old
1693        // namespace before it can create any nonrandom descriptions, including
1694        // same-leader exec where the creator did not change.
1695        let owner = if shared_guest_table && let Some(open_files) = &state.open_files {
1696            self.next_open_file_sequence =
1697                self.next_open_file_sequence.max(open_files.next_sequence);
1698            self.next_socket_open_file_sequence = self
1699                .next_socket_open_file_sequence
1700                .max(open_files.next_socket_sequence);
1701            open_files.creator
1702        } else if shared_guest_table
1703            && let Some((creator, next_sequence)) = state.open_file_namespace
1704        {
1705            // `--pipe-wake-latency`: no new description may take an id that a
1706            // carried channel names (`crate::pipe_wake`).
1707            self.next_open_file_sequence = self.next_open_file_sequence.max(next_sequence);
1708            creator
1709        } else if shared_guest_table && let Some(maps) = &state.maps {
1710            maps.creator
1711        } else if shared_guest_table && let Some(records) = &state.procfs_records {
1712            records.creator
1713        } else {
1714            owner
1715        };
1716        if shared_guest_table {
1717            self.next_random_device_sequence = self
1718                .next_random_device_sequence
1719                .max(state.random_device_sequence);
1720        }
1721        self.apply_exec_blocking_overrides(owner, state.blocking);
1722        if !shared_guest_table {
1723            return Ok(());
1724        }
1725        if let Some(input) = state.initial_input {
1726            self.install_inherited_input(owner, input)?;
1727        }
1728        if state.initialize_inherited_stdio {
1729            self.discover_inherited_terminals(0..=2);
1730            self.discover_inherited_pipes(0..=2);
1731        } else {
1732            self.restore_inherited_reads(owner, state.inherited_reads)?;
1733        }
1734        if let Some(open_files) = state.open_files {
1735            let mut handles = HashMap::new();
1736            for (fd, survivor) in open_files.handles {
1737                if ExecOpenFile::capture(fd, survivor.id)? != survivor {
1738                    return Err(Errno::EIO);
1739                }
1740                handles.insert(fd, DetFd::shared_survivor(fd, survivor.id));
1741            }
1742            crate::fd::intern_open_files(&mut handles);
1743            // Replace only the authenticated random-device and timerfd survivors. Pipes,
1744            // sockets and other descriptors retain the legacy restoration above;
1745            // in particular, do not reconnect an old socket's port association.
1746            self.file_handles.extend(handles);
1747            self.has_promoted_shared_files = true;
1748        }
1749        // Last, so that no restoration above can replace a description after
1750        // its channel is set: every carried channel end, discovering a
1751        // descriptor not yet rediscovered (one the guest left `O_NONBLOCK`).
1752        for (fd, end) in state.channels {
1753            match self.discover_fd_from_current_process(owner, fd) {
1754                Ok(()) => {
1755                    self.with_detfd(fd, |detfd| detfd.set_channel(end))
1756                        .expect("a just-discovered descriptor must remain registered");
1757                }
1758                Err(error) => {
1759                    tracing::warn!(
1760                        "[detcore, dtid {}] descriptor {} carried a pipe channel across exec \
1761                         but cannot be rediscovered; it keeps none: {}",
1762                        owner,
1763                        fd,
1764                        error,
1765                    );
1766                }
1767            }
1768        }
1769        if let Some(maps) = state.maps {
1770            let mut handles = HashMap::new();
1771            for (fd, survivor) in maps.handles {
1772                let raw_stat = stat::fstat(unsafe { BorrowedFd::borrow_raw(fd) })
1773                    .map_err(|error| Errno::new(error as i32))?;
1774                let handle = DetFd::new(
1775                    fd,
1776                    OFlag::from_bits_truncate(survivor.flags),
1777                    FdType::Regular,
1778                    survivor.identity.id,
1779                )
1780                .with_stat(DetStat::from(raw_stat));
1781                if let Some(path) = survivor.path {
1782                    handle.set_path(path);
1783                }
1784                handle.set_procfs(survivor.procfs);
1785                handle.mark_flock_mode_unobserved();
1786                handles.insert(fd, handle);
1787            }
1788            // Aliases retain one old snapshot/cursor. Independent opens of the
1789            // same proc inode remain separate because their IDs differ.
1790            crate::fd::intern_open_files(&mut handles);
1791            self.file_handles.extend(handles);
1792        }
1793        if let Some(records) = state.procfs_records {
1794            self.restore_procfs_records(records)?;
1795        }
1796        Ok(())
1797    }
1798
1799    /// Restore the per-process files carried across exec with their open
1800    /// files' identities and record sequences (`ExecProcfsRecords`), already
1801    /// authenticated against the descriptors (`apply_exec_fd_state`): the
1802    /// new image's table names the same open files as every other process
1803    /// holding them, takes and stores a sequence a fork or a send moved to
1804    /// Detcore's global state, and keeps one it alone held. A rendered file's
1805    /// placeholder is registered again, under the open file it belongs to,
1806    /// as its open registered it (`rendered_procfs::register_placeholder_open`).
1807    fn restore_procfs_records(&mut self, records: ExecProcfsRecords) -> Result<(), Errno> {
1808        let mut handles = HashMap::new();
1809        let mut placeholders = Vec::new();
1810        for (fd, survivor) in records.handles {
1811            let raw_stat = stat::fstat(unsafe { BorrowedFd::borrow_raw(fd) })
1812                .map_err(|error| Errno::new(error as i32))?;
1813            let handle = DetFd::new(
1814                fd,
1815                OFlag::from_bits_truncate(survivor.flags),
1816                FdType::Regular,
1817                survivor.identity.id,
1818            )
1819            .with_stat(DetStat::from(raw_stat));
1820            if let Some(path) = survivor.path {
1821                handle.set_path(path);
1822            }
1823            let mut procfs = survivor.procfs;
1824            procfs.adopt_sequence(survivor.sequence);
1825            if let Some(rendered) = procfs.opened_path() {
1826                placeholders.push((fd, (survivor.identity.dev, survivor.identity.ino), rendered));
1827            }
1828            handle.set_procfs(procfs);
1829            handle.mark_flock_mode_unobserved();
1830            if survivor.sequence_unknown {
1831                handle.mark_record_sequence_unknown();
1832            }
1833            handles.insert(fd, handle);
1834        }
1835        crate::fd::intern_open_files(&mut handles);
1836        self.file_handles.extend(handles);
1837        for (fd, identity, rendered) in placeholders {
1838            crate::syscalls::rendered_procfs::register_placeholder(
1839                identity.0, identity.1, rendered,
1840            );
1841            let handle = &self.file_handles[&fd];
1842            crate::syscalls::rendered_procfs::register_placeholder_open(
1843                identity,
1844                handle.open_file_handle(),
1845                handle.open_file_id(),
1846                Vec::new(),
1847            );
1848        }
1849        Ok(())
1850    }
1851
1852    pub(crate) fn open_files_closed_on_exec(&self, table_is_shared: bool) -> Vec<OpenFileId> {
1853        if table_is_shared {
1854            return Vec::new();
1855        }
1856
1857        let mut open_files = HashMap::new();
1858        for detfd in self.file_handles.values() {
1859            let id = detfd.open_file_id();
1860            let total_aliases = detfd.open_file_alias_count();
1861            // A copy in flight keeps the description open, whatever holds the
1862            // copy now (`DetFd::is_last_reference`).
1863            let in_flight = detfd.copies_in_flight() > 0;
1864            let entry = open_files
1865                .entry(id)
1866                .or_insert((0, total_aliases, !in_flight));
1867            debug_assert_eq!(entry.1, total_aliases);
1868            entry.0 += 1;
1869            entry.2 &= detfd.is_cloexec();
1870        }
1871
1872        let mut closed: Vec<_> = open_files
1873            .into_iter()
1874            .filter_map(|(id, (table_aliases, total_aliases, all_cloexec))| {
1875                (all_cloexec && table_aliases == total_aliases).then_some(id)
1876            })
1877            .collect();
1878        closed.sort();
1879        closed
1880    }
1881
1882    /// set default fds
1883    ///
1884    /// `aliases`: for each of the three, the lowest of them that is one open
1885    /// file description with it, where the launcher knows
1886    /// (`Config::standard_stream_aliases`); only `procfs::locks::LockLedger`
1887    /// reads the groups.
1888    fn setup_stdio(mut self, _pid: Pid, owner: DetTid, aliases: Option<[u8; 3]>) -> Self {
1889        // guest stdio can be a pipe, which make things difficult
1890        // hence use a dummy stat here.
1891        // The raw syscall, not libc's interposable fstat: this runs inside the
1892        // guest under in-guest LiteInst (see crate::util::raw_syscall).
1893        let mut metadata: libc::stat = unsafe { std::mem::zeroed() };
1894        // SAFETY: fstat writes only into `metadata`.
1895        let status = unsafe {
1896            crate::util::raw_syscall(libc::SYS_fstat, [0, (&raw mut metadata) as u64, 0, 0, 0, 0])
1897        };
1898        if let Err(error) = crate::util::raw_zero_result(status) {
1899            panic!(
1900                "fstat of the guest's stdin failed (errno {})",
1901                error.raw_os_error().unwrap_or(0)
1902            );
1903        }
1904        let stat: DetStat = metadata.into();
1905        let stdin = DetFd::new(
1906            0,
1907            OFlag::empty(),
1908            FdType::Regular,
1909            self.allocate_open_file_id(owner, FdType::Regular),
1910        )
1911        .with_stat(stat)
1912        .with_resource(ResourceID::Device(Device::ContainerStdin));
1913        let stdout = DetFd::new(
1914            1,
1915            OFlag::empty(),
1916            FdType::Regular,
1917            self.allocate_open_file_id(owner, FdType::Regular),
1918        )
1919        .with_stat(stat)
1920        .with_resource(ResourceID::Device(Device::ContainerStdout));
1921        let stderr = DetFd::new(
1922            2,
1923            OFlag::empty(),
1924            FdType::Regular,
1925            self.allocate_open_file_id(owner, FdType::Regular),
1926        )
1927        .with_stat(stat)
1928        .with_resource(ResourceID::Device(Device::ContainerStderr));
1929
1930        // These descriptors existed before Detcore began observing the guest,
1931        // so they may already carry flock state that we cannot query.
1932        stdin.mark_flock_mode_unobserved();
1933        stdout.mark_flock_mode_unobserved();
1934        stderr.mark_flock_mode_unobserved();
1935
1936        // Which of the three are one open file description, as the launcher
1937        // found with kcmp(KCMP_FILE) on the descriptors it handed the guest
1938        // (`Config::standard_stream_aliases`): it may pass one description on
1939        // several (`<>file >&0`, `>log 2>&1`), each of which Detcore models
1940        // with an identity of its own, and the lock ledger then takes the
1941        // group as the one description it is. Where the launcher does not
1942        // know, each stream is a description of its own (a named residual).
1943        if let Some(aliases) = aliases {
1944            let streams = [&stdin, &stdout, &stderr];
1945            for (later, earlier) in aliases.iter().enumerate() {
1946                let earlier = usize::from(*earlier);
1947                if earlier < later {
1948                    let group = streams[earlier]
1949                        .stdio_alias_group()
1950                        .unwrap_or_else(|| streams[earlier].open_file_id());
1951                    streams[earlier].set_stdio_alias_group(group);
1952                    streams[later].set_stdio_alias_group(group);
1953                }
1954            }
1955        }
1956
1957        self.add_detfd(stdin);
1958        self.add_detfd(stdout);
1959        self.add_detfd(stderr);
1960
1961        self
1962    }
1963
1964    // AUTONOMOUS-BOT-IMPLEMENTED
1965    // TODO-HUMAN-REVIEW(PR-845): Review SaBRe on-demand inherited descriptor discovery.
1966    fn discover_fd_from_current_process(&mut self, owner: DetTid, fd: RawFd) -> Result<(), Errno> {
1967        if self.file_handles.contains_key(&fd) {
1968            return Ok(());
1969        }
1970
1971        let fd_flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };
1972        let status_flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
1973        if fd_flags == -1 || status_flags == -1 {
1974            return Err(Errno::last());
1975        }
1976        let raw_stat =
1977            stat::fstat(unsafe { BorrowedFd::borrow_raw(fd) }).map_err(|_| Errno::last())?;
1978        // An anonymous inode has no file type; its link names what it is.
1979        let link = (raw_stat.st_mode & libc::S_IFMT == 0)
1980            .then(|| own_fd_link(fd))
1981            .flatten();
1982        self.add_discovered_fd(
1983            owner,
1984            fd,
1985            fd_flags,
1986            status_flags,
1987            &raw_stat,
1988            link.as_deref(),
1989        )?;
1990        // A placeholder for a procfs file Detcore renders, which the backend
1991        // rediscovers after `exec`: the file again, at the position the old
1992        // image left and with the snapshot it took, as a backend that keeps
1993        // Detcore's state across `exec` has them (`syscalls::files::rendered_procfs`).
1994        // The snapshot is the one its name carries (`procfs::placeholder_name`).
1995        // Only an empty regular file with no link can be one.
1996        let may_be_placeholder = raw_stat.st_mode & libc::S_IFMT == libc::S_IFREG
1997            && raw_stat.st_nlink == 0
1998            && raw_stat.st_size == 0;
1999        // A `/proc/cpuinfo` placeholder only once the session's answer says
2000        // it renders the file (`decide_cpuinfo_placeholders`); before that
2001        // answer, one waits for it as the ordinary descriptor it looks like.
2002        // So does a placeholder for a task's `arch_status`, which Detcore
2003        // renders exactly where it renders `/proc/cpuinfo`.
2004        let decided = self.cpuinfo_placeholders;
2005        // A process's `mountinfo` or `mounts` has a placeholder only in a
2006        // session with a container mount table, the only one that renders it.
2007        let files = crate::procfs::RenderedFiles {
2008            cpuinfo: decided == Some(true),
2009            mounts: true,
2010            // Rendered only where the tool runs outside the guest (KVM), which
2011            // never rediscovers its descriptors.
2012            locks: false,
2013        };
2014        let found = self.within_proc_level(
2015            may_be_placeholder
2016                .then(|| placeholder_behind(fd, files))
2017                .flatten(),
2018        );
2019        // Before the answer, `files` differs from every rendered file only in
2020        // the files rendered where `/proc/cpuinfo` is: a placeholder it does
2021        // not find but every rendered file does is one of those.
2022        if found.is_none()
2023            && decided.is_none()
2024            && may_be_placeholder
2025            && self
2026                .within_proc_level(placeholder_behind(fd, crate::procfs::RenderedFiles::ALL))
2027                .is_some()
2028        {
2029            self.pending_cpuinfo_placeholders.push(fd);
2030        }
2031        if let Some((path, snapshot)) = found {
2032            if self.adopt_placeholder(fd, &raw_stat, fd_flags, path, snapshot)? {
2033                return Ok(());
2034            }
2035        }
2036        // A fixed procfs file the kernel opened (through a symbolic link),
2037        // which the old image read from Detcore's rendering: the file again,
2038        // at the position Detcore kept in the kernel
2039        // (`DetFd::procfs_keeps_kernel_position`), so the new image does not
2040        // read the host's file. procfs files report size 0.
2041        else if raw_stat.st_mode & libc::S_IFMT == libc::S_IFREG
2042            && raw_stat.st_size == 0
2043            && let Some(procfs) = fixed_procfs_behind(fd)
2044        {
2045            // SAFETY: lseek only reads the descriptor's position.
2046            let offset = unsafe {
2047                crate::util::raw_syscall(
2048                    libc::SYS_lseek,
2049                    [fd as u64, 0, libc::SEEK_CUR as u64, 0, 0, 0],
2050                )
2051            };
2052            let offset = usize::try_from(offset).unwrap_or(0);
2053            let path = procfs
2054                .rendered_path(crate::procfs::RenderedFiles::WITHOUT_CPUINFO)
2055                .expect("a fixed procfs file has a path");
2056            self.with_detfd(fd, |detfd| {
2057                detfd.set_path(path);
2058                detfd.set_procfs(procfs.clone());
2059                detfd.set_procfs_offset(offset);
2060            })?;
2061        }
2062        // A per-process file that follows its record's sequence, which the
2063        // kernel opened in another process and sent here over `SCM_RIGHTS`:
2064        // where that sequence is and the record it buffered belong to the open
2065        // file, which every holder shares and the sender's Detcore keeps,
2066        // and which this one cannot know, so every read and seek through it
2067        // is refused by name (`Detcore::refuse_unknown_record_sequence`; on
2068        // DBT, through its runtime's exit path; on SaBRe, where a refusal that
2069        // ended the run would strand it, with EOPNOTSUPP), so no read reaches
2070        // the kernel's file. Before, it read the kernel's file, where the
2071        // sender's captures had left it (end of file), unsanitized (Claude's
2072        // narrow check of the kvm lane's slice N3, round 5; on SaBRe until its
2073        // final check, which saw the host's own statm). One the exec that
2074        // made this image kept is carried with its sequence instead
2075        // (`ExecProcfsRecords`).
2076        else if let Some(path) =
2077            own_fd_link(fd).and_then(|link| kernel_record_file_link(&raw_stat, &link))
2078        {
2079            self.with_detfd(fd, |detfd| {
2080                detfd.set_path(path.clone());
2081                detfd.mark_record_sequence_unknown();
2082            })?;
2083        }
2084        if let Some(resource) = stdio_resource(fd) {
2085            self.with_detfd(fd, |detfd| detfd.set_resource(resource.clone()))?;
2086        }
2087        Ok(())
2088    }
2089
2090    /// Record descriptor `fd`, whose creation Detcore did not see, from the
2091    /// kernel's facts about it: its descriptor flags `fd_flags` (`F_GETFD`),
2092    /// status flags `status_flags` (`F_GETFL`) and `raw_stat` (`fstat`). Its
2093    /// type is the file's (pipe, socket, random device or regular), or, for
2094    /// an anonymous inode whose descriptor link (`link`) names an inotify
2095    /// instance, inotify; its
2096    /// flags are the kernel's (an observed `O_NONBLOCK` stays guest-visible:
2097    /// the provenance is unknown), and its lock state is unobserved. Shared by
2098    /// the in-guest discovery ([`Self::discover_fd_from_current_process`])
2099    /// and by the record of a descriptor received over `SCM_RIGHTS`
2100    /// (`Detcore::record_received_descriptors`), so both make the same
2101    /// description from the same facts.
2102    fn add_discovered_fd(
2103        &mut self,
2104        owner: DetTid,
2105        fd: RawFd,
2106        fd_flags: libc::c_int,
2107        status_flags: libc::c_int,
2108        raw_stat: &libc::stat,
2109        link: Option<&[u8]>,
2110    ) -> Result<(), Errno> {
2111        let file_type = stat::SFlag::from_bits_truncate(raw_stat.st_mode);
2112        let ty = if raw_stat.st_mode & libc::S_IFMT == 0
2113            && link == Some(crate::syscalls::inotify::INOTIFY_LINK)
2114        {
2115            // Its reads renumber rename cookies (`crate::syscalls::inotify`).
2116            FdType::Inotify
2117        } else if file_type.contains(stat::SFlag::S_IFIFO) {
2118            FdType::Pipe
2119        } else if file_type.contains(stat::SFlag::S_IFSOCK) {
2120            FdType::Socket
2121        } else if status_flags & libc::O_PATH == 0
2122            && crate::fd::is_random_device(raw_stat.st_mode, raw_stat.st_rdev)
2123        {
2124            // Linux's random devices by device number, as an open classifies
2125            // them (`Detcore::handle_openat`).
2126            FdType::Rng
2127        } else {
2128            FdType::Regular
2129        };
2130        let mut flags = OFlag::from_bits_truncate(status_flags);
2131        let physically_nonblocking = flags.contains(OFlag::O_NONBLOCK);
2132        // Discovered descriptors have unknown provenance, so an observed
2133        // O_NONBLOCK bit must remain guest-visible. Detcore-created scheduler
2134        // pipes are registered when created and do not reach this fallback.
2135        if fd_flags & libc::FD_CLOEXEC != 0 {
2136            flags.insert(OFlag::O_CLOEXEC);
2137        }
2138        self.add_fd(owner, fd, flags, ty, Some((*raw_stat).into()))?;
2139        // This newly discovered description predates our observation. Its
2140        // kernel lock may be held, but no cached lock claim existed to go stale.
2141        self.with_detfd(fd, |detfd| detfd.mark_flock_mode_unobserved())?;
2142        if ty == FdType::Rng {
2143            let stream = crate::fd::RandomDeviceStream::discovered(owner, fd);
2144            self.with_detfd(fd, |detfd| detfd.set_random_device(stream))?;
2145        }
2146        if ty == FdType::Pipe && physically_nonblocking {
2147            self.with_detfd(fd, |detfd| detfd.set_physically_nonblocking())?;
2148        }
2149        Ok(())
2150    }
2151
2152    /// Make descriptor `fd`, already in this table, the placeholder for the
2153    /// rendered file `path` that its memfd (`raw_stat`, descriptor flags
2154    /// `fd_flags`) is, with the snapshot its name carries. Returns whether it
2155    /// joined another descriptor's open file (an alias), whose state it then
2156    /// shares.
2157    ///
2158    /// Otherwise the open file is rebuilt. A per-process file that follows
2159    /// its record's sequence (`ProcfsFile::follows_record_sequence`) needs
2160    /// more than its placeholder keeps: where its sequence is and the record
2161    /// it buffered, which Linux keeps in the one open file every holder
2162    /// shares. Where this process's registry names the open file the
2163    /// placeholder belongs to (this process, or an ancestor before a fork,
2164    /// opened it: `rendered_procfs::placeholder_open_id`), the descriptor
2165    /// takes that open file's identity, and its sequence from Detcore's global
2166    /// state, where every holder keeps it once the open file has left a
2167    /// process (`ThreadState::share_procfs_sequences` at a fork,
2168    /// `Detcore::share_sent_record_sequences` at a send). Otherwise (a
2169    /// descriptor another process sent to one that never held the open file)
2170    /// the sequence cannot be known, and every read and seek through it is
2171    /// refused by name (`DetFd::mark_record_sequence_unknown`), on DBT through
2172    /// its runtime's exit path, and on SaBRe, where a refusal that ended the
2173    /// run would strand it, with EOPNOTSUPP. An open file
2174    /// surviving `exec` is carried with its sequence instead
2175    /// (`ExecProcfsRecords`), before this rediscovery could reach it.
2176    fn adopt_placeholder(
2177        &mut self,
2178        fd: RawFd,
2179        raw_stat: &libc::stat,
2180        fd_flags: libc::c_int,
2181        path: &'static std::path::Path,
2182        snapshot: Vec<u8>,
2183    ) -> Result<bool, Errno> {
2184        crate::syscalls::rendered_procfs::register_placeholder(
2185            raw_stat.st_dev,
2186            raw_stat.st_ino,
2187            path,
2188        );
2189        // Every open makes its own placeholder, so another descriptor on
2190        // the same one is the same open file: one cursor, as before.
2191        if let Some(alias) = self.rendered_alias(fd, raw_stat.st_dev, raw_stat.st_ino) {
2192            let mut alias = alias.with_fd(fd);
2193            alias.set_cloexec(fd_flags & libc::FD_CLOEXEC != 0);
2194            self.add_detfd(alias);
2195            return Ok(true);
2196        }
2197        // SAFETY: lseek only reads the descriptor's position.
2198        let offset = unsafe {
2199            crate::util::raw_syscall(
2200                libc::SYS_lseek,
2201                [fd as u64, 0, libc::SEEK_CUR as u64, 0, 0, 0],
2202            )
2203        };
2204        let offset = usize::try_from(offset).unwrap_or(0);
2205        let procfs = crate::procfs::ProcfsFile::from_rendered_path(path)
2206            .expect("a rendered path is a procfs file")
2207            .into_rendered();
2208        let mut procfs = if snapshot.is_empty() {
2209            procfs
2210        } else {
2211            procfs.with_rendered_snapshot(snapshot)
2212        };
2213        let follows = procfs.follows_record_sequence();
2214        let joined = follows
2215            .then(|| {
2216                crate::syscalls::rendered_procfs::placeholder_open_id((
2217                    raw_stat.st_dev,
2218                    raw_stat.st_ino,
2219                ))
2220            })
2221            .flatten();
2222        if joined.is_some() {
2223            procfs.share_sequence();
2224            self.has_shared_procfs_sequences = true;
2225        }
2226        self.with_detfd(fd, |detfd| {
2227            if let Some(id) = joined {
2228                detfd.rebind_open_file_id(id);
2229            }
2230            detfd.set_path(path);
2231            detfd.set_procfs(procfs.clone());
2232            detfd.set_procfs_offset(offset);
2233            if follows && joined.is_none() {
2234                detfd.mark_record_sequence_unknown();
2235            }
2236        })?;
2237        Ok(false)
2238    }
2239
2240    /// Another descriptor of this process open on the placeholder `(dev,
2241    /// ino)`, which `fd` is too.
2242    fn rendered_alias(&self, fd: RawFd, dev: u64, ino: u64) -> Option<DetFd> {
2243        self.file_handles
2244            .iter()
2245            .filter(|(other, detfd)| **other != fd && detfd.procfs_rendered_path().is_some())
2246            .find(|(other, _)| {
2247                stat::fstat(unsafe { BorrowedFd::borrow_raw(**other) })
2248                    .is_ok_and(|stat| stat.st_dev == dev && stat.st_ino == ino)
2249            })
2250            .map(|(_, detfd)| detfd.clone())
2251    }
2252
2253    /// get detfd from rawfd, rawfd must be added or dup-ed first.
2254    fn with_detfd<F, U>(&mut self, fd: RawFd, mut f: F) -> Result<U, Errno>
2255    where
2256        F: FnMut(&mut DetFd) -> U,
2257    {
2258        let detfd = self.file_handles.get_mut(&fd).ok_or(Errno::EBADF)?;
2259        Ok(f(detfd))
2260    }
2261
2262    /// add a detfd
2263    fn add_detfd(&mut self, detfd: DetFd) {
2264        let fd = detfd.fd;
2265        self.note_installed(fd);
2266        self.file_handles.insert(fd, detfd);
2267    }
2268
2269    /// add a raw fd
2270    fn add_fd(
2271        &mut self,
2272        creator: DetTid,
2273        fd: RawFd,
2274        flags: OFlag,
2275        ty: FdType,
2276        stat: Option<DetStat>,
2277    ) -> Result<(), Errno> {
2278        let id = self.allocate_open_file_id(creator, ty);
2279        let detfd = DetFd::new(fd, flags, ty, id).with_stat(stat);
2280        self.add_detfd(detfd);
2281        Ok(())
2282    }
2283
2284    /// Make `fd`, which `creator` just opened, a random device that reads a
2285    /// stream named by the opener and the table's count of random-device
2286    /// opens (`crate::fd::RandomDeviceStream`).
2287    fn classify_random_device(&mut self, creator: DetTid, fd: RawFd) -> Result<(), Errno> {
2288        let stream = crate::fd::RandomDeviceStream {
2289            creator,
2290            sequence: self.next_random_device_sequence,
2291        };
2292        self.with_detfd(fd, |detfd| detfd.set_random_device(stream))?;
2293        self.next_random_device_sequence += 1;
2294        Ok(())
2295    }
2296
2297    /// The access trace's note of descriptor `fd` changing by `op`: the
2298    /// open file it names now, and for an open, the path it was opened by.
2299    fn slot_note(&self, fd: RawFd, op: FdNoteOp) -> crate::access_trace::FdNote {
2300        use detcore_model::access_trace::FdOp;
2301        let detfd = self.file_handles.get(&fd);
2302        let op = match op {
2303            FdNoteOp::Open => FdOp::Open {
2304                path: detfd
2305                    .and_then(DetFd::path)
2306                    .map(|path| path.to_string_lossy().into_owned()),
2307            },
2308            FdNoteOp::Dup(source) => FdOp::Dup { source },
2309        };
2310        crate::access_trace::FdNote {
2311            slot: detcore_model::fd::FdSlot {
2312                files: self.files_id,
2313                fd,
2314            },
2315            open_file: detfd.map(DetFd::open_file_id),
2316            op,
2317            released: None,
2318        }
2319    }
2320
2321    /// The access trace's note that `fd` is about to be released, with what
2322    /// it named; `None` when it names nothing Detcore models.
2323    fn close_note(&self, fd: RawFd) -> Option<crate::access_trace::FdNote> {
2324        let detfd = self.file_handles.get(&fd)?;
2325        Some(crate::access_trace::FdNote {
2326            slot: detcore_model::fd::FdSlot {
2327                files: self.files_id,
2328                fd,
2329            },
2330            open_file: Some(detfd.open_file_id()),
2331            op: detcore_model::access_trace::FdOp::Close,
2332            released: Some(crate::access_trace::FdInfo::of(detfd)),
2333        })
2334    }
2335
2336    /// remove a rawfd
2337    fn remove_fd(&mut self, fd: RawFd) -> Option<OpenFileId> {
2338        self.present_unmodelled.remove(&fd);
2339        let detfd = self.file_handles.remove(&fd)?;
2340        self.release_detfd(detfd)
2341    }
2342
2343    /// `detfd`, gone from this table: the description it names, if this was
2344    /// its last reference ([`DetFd::is_last_reference`]: no other modeled
2345    /// descriptor, no copy in flight), for the caller to release its port. A
2346    /// socket's queued `SCM_RIGHTS` messages go with its last reference, as
2347    /// Linux purges a released socket's receive queue (`unix_release_sock`),
2348    /// and wait in [`Self::released_rights`] for the call to release what
2349    /// they held (`Detcore::settle_released_rights`). A description with a
2350    /// copy in flight stays, port and queue included, until a guest call
2351    /// receives or releases the copy.
2352    fn release_detfd(&mut self, detfd: DetFd) -> Option<OpenFileId> {
2353        detfd.is_last_reference().then(|| {
2354            self.released_rights
2355                .0
2356                .extend(detfd.take_all_queued_rights());
2357            detfd.open_file_id()
2358        })
2359    }
2360
2361    /// Let go of every descriptor of this table, in order, as its last
2362    /// holder's exit does (`ThreadState::release_table_at_exit`), or the
2363    /// exec that replaced it (`ThreadState::release_exec_dropped_descriptors`):
2364    /// a socket whose last reference goes takes its queue
2365    /// ([`Self::release_detfd`]). No port is released here.
2366    fn release_every_descriptor(&mut self) {
2367        let mut fds: Vec<RawFd> = self.file_handles.keys().copied().collect();
2368        fds.sort_unstable();
2369        for fd in fds {
2370            let _ = self.remove_fd(fd);
2371        }
2372    }
2373
2374    /// What closes in this table took from receiving sockets' queues
2375    /// ([`Self::release_detfd`]).
2376    fn take_released_rights(&mut self) -> Vec<SentDescriptor> {
2377        std::mem::take(&mut self.released_rights.0)
2378    }
2379
2380    /// Remove every modeled descriptor in an inclusive close_range interval.
2381    fn remove_fd_range(&mut self, first: u32, last: u32) -> Vec<OpenFileId> {
2382        self.present_unmodelled
2383            .retain(|fd, _| !(*fd >= 0 && first <= *fd as u32 && *fd as u32 <= last));
2384        let mut descriptors: Vec<_> = self
2385            .file_handles
2386            .keys()
2387            .copied()
2388            .filter(|fd| *fd >= 0 && first <= *fd as u32 && *fd as u32 <= last)
2389            .collect();
2390        descriptors.sort_unstable();
2391        descriptors
2392            .into_iter()
2393            .filter_map(|fd| self.remove_fd(fd))
2394            .collect()
2395    }
2396
2397    /// dup raw fds.
2398    fn dup_fd(
2399        &mut self,
2400        oldfd: RawFd,
2401        newfd: RawFd,
2402        flags: OFlag,
2403    ) -> Result<Option<OpenFileId>, Errno> {
2404        if oldfd == newfd {
2405            if self.present_unmodelled.contains_key(&oldfd) {
2406                return Ok(None);
2407            }
2408            self.with_detfd(oldfd, |_| ())?;
2409            return Ok(None);
2410        }
2411        // A copy of a descriptor Detcore keeps no description of is another
2412        // such descriptor, which the kernel has installed.
2413        if !self.file_handles.contains_key(&oldfd) && self.present_unmodelled.contains_key(&oldfd) {
2414            self.note_installed(newfd);
2415            self.present_unmodelled
2416                .insert(newfd, flags.contains(OFlag::O_CLOEXEC));
2417            let replaced = self.file_handles.remove(&newfd);
2418            return Ok(replaced.and_then(|detfd| self.release_detfd(detfd)));
2419        }
2420
2421        let detfd = self.with_detfd(oldfd, |old_detfd| {
2422            old_detfd.clone().with_fd(newfd).with_fd_flags(flags)
2423        })?;
2424        self.note_installed(newfd);
2425        let replaced = self.file_handles.insert(newfd, detfd);
2426        Ok(replaced.and_then(|detfd| self.release_detfd(detfd)))
2427    }
2428
2429    /// Capture the exact open-file description currently named by `fd`.
2430    fn capture_fd(&mut self, fd: RawFd) -> Result<CapturedDetFd, Errno> {
2431        let detfd = self.with_detfd(fd, |detfd| detfd.clone())?;
2432        Ok(CapturedDetFd {
2433            files_id: self.files_id,
2434            detfd,
2435        })
2436    }
2437
2438    /// Install a previously captured open-file description as `newfd`.
2439    ///
2440    /// Unlike [`Self::dup_fd`], this never resolves the source fd number again.
2441    /// It succeeds only if the destination still belongs to the descriptor
2442    /// table in which the source was captured.
2443    fn install_captured_fd(
2444        &mut self,
2445        captured: CapturedDetFd,
2446        newfd: RawFd,
2447        flags: OFlag,
2448    ) -> Result<Option<OpenFileId>, CapturedDetFdInstallError> {
2449        if captured.files_id != self.files_id {
2450            return Err(CapturedDetFdInstallError {
2451                expected_files_id: captured.files_id,
2452                actual_files_id: self.files_id,
2453                returned_fd: newfd,
2454                captured,
2455            });
2456        }
2457
2458        let detfd = captured.detfd.with_fd(newfd).with_fd_flags(flags);
2459        self.note_installed(newfd);
2460        let replaced = self.file_handles.insert(newfd, detfd);
2461        Ok(replaced.and_then(|detfd| self.release_detfd(detfd)))
2462    }
2463
2464    /// Drop a capture that was not installed and report whether it retained the
2465    /// final reference to the open-file description.
2466    fn abandon_captured_fd(&mut self, captured: CapturedDetFd) -> Option<OpenFileId> {
2467        self.release_detfd(captured.detfd)
2468    }
2469}
2470
2471/// How a descriptor slot changed, for [`FileMetadata::slot_note`].
2472#[derive(Clone, Copy)]
2473enum FdNoteOp {
2474    Open,
2475    Dup(RawFd),
2476}
2477
2478fn stdio_resource(fd: RawFd) -> Option<ResourceID> {
2479    match fd {
2480        0 => Some(ResourceID::Device(Device::ContainerStdin)),
2481        1 => Some(ResourceID::Device(Device::ContainerStdout)),
2482        2 => Some(ResourceID::Device(Device::ContainerStderr)),
2483        _ => None,
2484    }
2485}
2486
2487#[cfg(test)]
2488mod posix_timers_tests {
2489    use super::*;
2490
2491    fn t(ns: u64) -> LogicalTime {
2492        LogicalTime::from_nanos(ns)
2493    }
2494
2495    #[test]
2496    fn ids_are_deterministic_and_sequential() {
2497        let mut timers = PosixTimers::default();
2498        assert_eq!(timers.create(None, None, None, false), 0);
2499        assert_eq!(timers.create(Some(libc::SIGALRM), None, None, false), 1);
2500        assert_eq!(timers.create(None, None, None, false), 2);
2501    }
2502
2503    #[test]
2504    fn settime_reports_previous_arming_and_remaining_uses_virtual_clock() {
2505        let mut timers = PosixTimers::default();
2506        let id = timers.create(None, None, None, false);
2507
2508        // Arm a one-shot timer for 100ns at t=0. A freshly created timer was
2509        // disarmed, so the reported old value is zero.
2510        let old = timers.settime(id, 0, Some(t(100)), t(0)).expect("known id");
2511        assert_eq!(old, (0, 0));
2512
2513        // At t=40 there should be 60ns remaining and no interval.
2514        assert_eq!(timers.gettime(id, t(40)), Some((60, 0)));
2515        // Past the deadline the remaining time saturates at 0.
2516        assert_eq!(timers.gettime(id, t(150)), Some((0, 0)));
2517    }
2518
2519    #[test]
2520    fn resetting_reports_old_remaining() {
2521        let mut timers = PosixTimers::default();
2522        let id = timers.create(None, None, None, false);
2523        timers.settime(id, 0, Some(t(100)), t(0));
2524        // Re-arm at t=30 (70ns remained) with a periodic 50ns timer.
2525        let old = timers
2526            .settime(id, 50, Some(t(200)), t(30))
2527            .expect("known id");
2528        assert_eq!(old, (70, 0));
2529        assert_eq!(timers.gettime(id, t(30)), Some((170, 50)));
2530    }
2531
2532    // AUTONOMOUS-BOT-IMPLEMENTED
2533    // TODO-HUMAN-REVIEW(#869)
2534    #[test]
2535    fn periodic_remaining_advances_past_each_deadline() {
2536        let mut timers = PosixTimers::default();
2537        let id = timers.create(Some(libc::SIGALRM), None, None, false);
2538        timers.settime(id, 50, Some(t(100)), t(0));
2539
2540        assert_eq!(timers.gettime(id, t(100)), Some((50, 50)));
2541        assert_eq!(timers.gettime(id, t(125)), Some((25, 50)));
2542        assert_eq!(timers.gettime(id, t(150)), Some((50, 50)));
2543        assert_eq!(timers.signal(id), Some(Some(libc::SIGALRM)));
2544    }
2545
2546    #[test]
2547    fn disarm_and_unknown_ids() {
2548        let mut timers = PosixTimers::default();
2549        let id = timers.create(None, None, None, false);
2550        timers.settime(id, 0, Some(t(100)), t(0));
2551        // Disarm: value of 0 -> deadline None -> remaining 0.
2552        timers.settime(id, 0, None, t(10));
2553        assert_eq!(timers.gettime(id, t(10)), Some((0, 0)));
2554
2555        // Unknown ids are rejected.
2556        assert_eq!(timers.settime(99, 0, Some(t(1)), t(0)), None);
2557        assert_eq!(timers.gettime(99, t(0)), None);
2558        assert!(!timers.contains(99));
2559    }
2560
2561    #[test]
2562    fn delete_removes_timer() {
2563        let mut timers = PosixTimers::default();
2564        let id = timers.create(None, None, None, false);
2565        assert!(timers.contains(id));
2566        assert!(timers.remove(id));
2567        assert!(!timers.contains(id));
2568        // Deleting again fails.
2569        assert!(!timers.remove(id));
2570    }
2571}
2572
2573#[cfg(test)]
2574mod resource_limits_tests {
2575    use super::*;
2576
2577    #[test]
2578    fn defaults_are_fixed_and_cover_linux_resources() {
2579        let limits = ResourceLimits::default();
2580        assert_eq!(
2581            limits.get(reverie::syscalls::linux_abi::RLIMIT_STACK),
2582            Some(ResourceLimit {
2583                current: 8 * 1024 * 1024,
2584                maximum: libc::RLIM64_INFINITY,
2585            })
2586        );
2587        assert_eq!(
2588            limits.get(reverie::syscalls::linux_abi::RLIMIT_NOFILE),
2589            Some(ResourceLimit {
2590                current: 1_048_576,
2591                maximum: 1_048_576,
2592            })
2593        );
2594        assert_eq!(
2595            limits.get(reverie::syscalls::linux_abi::RLIMIT_CORE),
2596            Some(ResourceLimit {
2597                current: libc::RLIM64_INFINITY,
2598                maximum: libc::RLIM64_INFINITY,
2599            })
2600        );
2601        assert_eq!(
2602            limits.get(reverie::syscalls::linux_abi::RLIMIT_RTTIME + 1),
2603            None
2604        );
2605    }
2606
2607    #[test]
2608    fn cloned_process_state_changes_independently() {
2609        let parent = ResourceLimits::default();
2610        let mut child = parent.clone();
2611        let lowered = ResourceLimit {
2612            current: 1024,
2613            maximum: 1_048_576,
2614        };
2615        child.set(reverie::syscalls::linux_abi::RLIMIT_NOFILE, lowered);
2616
2617        assert_eq!(
2618            child.get(reverie::syscalls::linux_abi::RLIMIT_NOFILE),
2619            Some(lowered)
2620        );
2621        assert_eq!(
2622            parent.get(reverie::syscalls::linux_abi::RLIMIT_NOFILE),
2623            Some(ResourceLimit {
2624                current: 1_048_576,
2625                maximum: 1_048_576,
2626            })
2627        );
2628    }
2629}
2630
2631#[cfg(test)]
2632mod file_metadata_tests {
2633    mod inherited_input;
2634    use std::os::fd::AsRawFd;
2635
2636    use super::*;
2637    use crate::pipe_wake::ChannelEnd;
2638    use crate::pipe_wake::ChannelId;
2639
2640    /// Run the test named `case` in an isolated child process, returning
2641    /// true when this invocation is the parent and the caller must return
2642    /// at once. Tests here key model tables by this process's real
2643    /// descriptor numbers; under plain `cargo test` a sibling thread can
2644    /// be handed a number this test is using as a model-only alias, or
2645    /// reuse a number this test just closed, so the model observes a
2646    /// descriptor that is not its own. A child process has no sibling
2647    /// threads allocating descriptors.
2648    fn isolated_child(case: &str) -> bool {
2649        const CHILD: &str = "HERMIT_FILE_METADATA_CHILD";
2650        if std::env::var_os(CHILD).as_deref() == Some(std::ffi::OsStr::new(case)) {
2651            return false;
2652        }
2653        let output = std::process::Command::new(std::env::current_exe().unwrap())
2654            .args(["--exact", case, "--nocapture", "--test-threads=1"])
2655            .env(CHILD, case)
2656            .output()
2657            .unwrap();
2658        assert!(output.status.success(), "{output:?}");
2659        assert!(
2660            String::from_utf8_lossy(&output.stdout).contains("1 passed"),
2661            "{output:?}"
2662        );
2663        true
2664    }
2665
2666    #[test]
2667    fn maps_exec_survivors_keep_opened_target_aliases_and_cursor() {
2668        use crate::procfs::BoundTask;
2669        use crate::procfs::MapsTarget;
2670        use crate::procfs::ProcfsFile;
2671        use crate::procfs::ProcfsSnapshotContext;
2672        use crate::procfs::TaskIncarnation;
2673
2674        let owner = DetTid::from_raw(9021);
2675        let config = Config::default();
2676        let mut parent = ThreadState::new(owner, &config, ());
2677        let maps = std::fs::File::open("/proc/self/maps").unwrap();
2678        let alias = maps.try_clone().unwrap();
2679        let closed = maps.try_clone().unwrap();
2680        let unread = std::fs::File::open("/proc/self/maps").unwrap();
2681        let fd = maps.as_raw_fd();
2682        let alias_fd = alias.as_raw_fd();
2683        let closed_fd = closed.as_raw_fd();
2684        let unread_fd = unread.as_raw_fd();
2685        let target = MapsTarget {
2686            task: BoundTask {
2687                process: 9021,
2688                thread: None,
2689            },
2690            incarnation: TaskIncarnation {
2691                process: 1,
2692                thread: None,
2693            },
2694            mm: MmId::initial(owner),
2695            private: reverie::RuntimePrivateMemorySnapshot {
2696                owner: [1, 2, 3, 4],
2697                version: 7,
2698                regions: vec![reverie::RuntimePrivateMemoryRegion {
2699                    start: 0x1000,
2700                    end: 0x2000,
2701                }],
2702            },
2703        };
2704        for number in [fd, unread_fd] {
2705            parent
2706                .add_fd(
2707                    number,
2708                    OFlag::O_RDONLY | OFlag::O_NONBLOCK,
2709                    FdType::Regular,
2710                    None,
2711                )
2712                .unwrap();
2713            let mut procfs = ProcfsFile::from_path(Path::new("/proc/self/maps")).unwrap();
2714            procfs.bind_maps_target(target.clone());
2715            parent
2716                .with_detfd(number, |handle| {
2717                    handle.set_path("/proc/self/maps");
2718                    handle.set_procfs(procfs.clone());
2719                })
2720                .unwrap();
2721        }
2722        parent.dup_fd(fd, alias_fd, OFlag::empty()).unwrap();
2723        parent.dup_fd(fd, closed_fd, OFlag::O_CLOEXEC).unwrap();
2724        parent
2725            .with_detfd(fd, |handle| {
2726                handle.initialize_procfs(
2727                    b"3000-4000 rw-p 00000000 00:00 0 \n".to_vec(),
2728                    ProcfsSnapshotContext::default(),
2729                );
2730                handle.commit_procfs_read(0, 7);
2731            })
2732            .unwrap();
2733        assert!(
2734            parent.maps_for_exec(None).unwrap().is_none(),
2735            "exec maps carry requires an advertised private-memory provider"
2736        );
2737        let transfer = parent
2738            .maps_for_exec(Some(&reverie::RuntimePrivateMemory::Unavailable))
2739            .unwrap()
2740            .unwrap();
2741        let ready_transfer = parent
2742            .maps_for_exec(Some(&reverie::RuntimePrivateMemory::Ready(
2743                target.private.clone(),
2744            )))
2745            .unwrap()
2746            .unwrap();
2747        assert!(ready_transfer == transfer);
2748        let original_preview = parent
2749            .with_detfd(fd, |h| h.preview_procfs(3))
2750            .unwrap()
2751            .unwrap();
2752        assert!(!transfer.handles.contains_key(&closed_fd));
2753        assert_eq!(
2754            transfer.handles[&fd].identity.id,
2755            transfer.handles[&alias_fd].identity.id
2756        );
2757        assert_ne!(
2758            transfer.handles[&fd].identity.id,
2759            transfer.handles[&unread_fd].identity.id
2760        );
2761        let old_id = transfer.handles[&fd].identity.id;
2762        let old_unread_id = transfer.handles[&unread_fd].identity.id;
2763        let next_sequence = transfer.next_sequence;
2764        let state = ExecFdState {
2765            maps: Some(transfer),
2766            ..ExecFdState::default()
2767        };
2768        assert!(!state.is_empty());
2769        // Exercise the actual serialized exec preparation/completion payload.
2770        let state: ExecFdState =
2771            serde_json::from_slice(&serde_json::to_vec(&state).unwrap()).unwrap();
2772        drop(state.clone()); // cancelled preparation leaves the original model alone
2773        assert_eq!(
2774            parent
2775                .with_detfd(fd, |h| h.procfs_position())
2776                .unwrap()
2777                .unwrap()
2778                .0,
2779            7
2780        );
2781        let receiving_tid = DetTid::from_raw(9022);
2782        let mut successor_thread = ThreadState::new(receiving_tid, &config, ());
2783        // The same creator choice used by the real exec-completion handler.
2784        assert_eq!(state.open_file_creator(), Some(owner));
2785        successor_thread.set_open_file_creator(state.open_file_creator().unwrap());
2786        let mut successor = successor_thread.metadata();
2787        successor
2788            .apply_exec_fd_state(receiving_tid, state.clone(), true)
2789            .unwrap();
2790        assert!(
2791            successor.file_handles.contains_key(&fd),
2792            "exec must retain maps provenance before lazy discovery"
2793        );
2794        assert!(!successor.file_handles.contains_key(&closed_fd));
2795        for number in [fd, alias_fd, unread_fd] {
2796            assert_eq!(
2797                successor.file_handles[&number].procfs_maps_target(),
2798                Some(target.clone())
2799            );
2800            assert!(successor.file_handles[&number].is_nonblocking());
2801        }
2802        assert_eq!(successor.file_handles[&fd].open_file_id(), old_id);
2803        assert_eq!(
2804            successor.file_handles[&unread_fd].open_file_id(),
2805            old_unread_id
2806        );
2807        assert_eq!(successor.file_handles[&fd].procfs_position().unwrap().0, 7);
2808        assert!(successor.file_handles[&unread_fd].procfs_needs_snapshot());
2809        let (offset, bytes) = successor.file_handles[&alias_fd].preview_procfs(3).unwrap();
2810        assert_eq!((offset, bytes.clone()), original_preview);
2811        successor.file_handles[&alias_fd].commit_procfs_read(offset, bytes.len());
2812        assert_eq!(successor.file_handles[&fd].procfs_position().unwrap().0, 10);
2813        assert_eq!(
2814            parent
2815                .with_detfd(fd, |h| h.procfs_position())
2816                .unwrap()
2817                .unwrap()
2818                .0,
2819            7
2820        );
2821        assert!(successor.next_open_file_sequence >= parent.metadata().next_open_file_sequence);
2822        drop(successor);
2823        let next = tempfile::tempfile().unwrap();
2824        successor_thread
2825            .add_fd(next.as_raw_fd(), OFlag::O_RDWR, FdType::Regular, None)
2826            .unwrap();
2827        assert_eq!(
2828            successor_thread
2829                .with_detfd(next.as_raw_fd(), |h| h.open_file_id())
2830                .unwrap(),
2831            OpenFileId::new(owner, next_sequence),
2832        );
2833        // A reused numeric slot cannot acquire the old target's authority.
2834        let replacement = tempfile::tempfile().unwrap();
2835        assert_eq!(
2836            unsafe { libc::dup2(replacement.as_raw_fd(), alias_fd) },
2837            alias_fd
2838        );
2839        assert_eq!(
2840            FileMetadata::new(owner).apply_exec_fd_state(owner, state, true),
2841            Err(Errno::EIO)
2842        );
2843    }
2844
2845    #[test]
2846    fn shared_exec_survivors_keep_aliases_and_allocator_after_closed_high_ids() {
2847        crate::fd::install_test_shared_open_file_channel();
2848        let creator = DetTid::from_raw(9011);
2849        let leader = DetTid::from_raw(9012);
2850        let config = Config::default();
2851        let mut parent = ThreadState::new(creator, &config, ());
2852        parent.set_open_file_creator(creator);
2853        parent.metadata().file_handles.clear();
2854        let file = std::fs::File::open("/dev/urandom").unwrap();
2855        let alias = nix::unistd::dup(&file).unwrap();
2856        let distinct = std::fs::File::open("/dev/urandom").unwrap();
2857        let cloexec = nix::unistd::dup(&file).unwrap();
2858        let (socket, _peer) = std::os::unix::net::UnixStream::pair().unwrap();
2859        let (fd, alias_fd, distinct_fd, closed_fd, socket_fd) = (
2860            file.as_raw_fd(),
2861            alias.as_raw_fd(),
2862            distinct.as_raw_fd(),
2863            cloexec.as_raw_fd(),
2864            socket.as_raw_fd(),
2865        );
2866        for fd in [fd, alias_fd, distinct_fd, socket_fd] {
2867            assert_eq!(unsafe { libc::fcntl(fd, libc::F_SETFD, 0) }, 0);
2868        }
2869        parent
2870            .add_fd(fd, OFlag::O_RDONLY, FdType::Rng, None)
2871            .unwrap();
2872        parent.classify_random_device(fd).unwrap();
2873        parent.dup_fd(fd, alias_fd, OFlag::empty()).unwrap();
2874        parent.dup_fd(fd, closed_fd, OFlag::O_CLOEXEC).unwrap();
2875        parent
2876            .add_fd(distinct_fd, OFlag::O_RDONLY, FdType::Rng, None)
2877            .unwrap();
2878        parent.classify_random_device(distinct_fd).unwrap();
2879        parent
2880            .add_fd(socket_fd, OFlag::O_RDWR, FdType::Socket, None)
2881            .unwrap();
2882        let first_id = parent.metadata().file_handles[&fd].open_file_id();
2883        let distinct_id = parent.metadata().file_handles[&distinct_fd].open_file_id();
2884        let old_socket_id = parent.metadata().file_handles[&socket_fd].open_file_id();
2885        assert_ne!(
2886            first_id, distinct_id,
2887            "same inode does not identify one OFD"
2888        );
2889        parent.metadata().file_handles[&fd]
2890            .with_random_device_stream::<Errno>(|offset| {
2891                assert_eq!(offset, 0);
2892                Ok(7)
2893            })
2894            .unwrap();
2895        // Closed and CLOEXEC descriptions still consume allocator identities.
2896        let high = parent
2897            .metadata()
2898            .allocate_open_file_id(creator, FdType::Rng);
2899        let high_socket = parent
2900            .metadata()
2901            .allocate_open_file_id(creator, FdType::Socket);
2902        parent.promote_shared_open_files().unwrap();
2903        assert!(!parent.metadata().file_handles[&socket_fd].is_shared());
2904        let transfer = parent.shared_open_files_for_exec().unwrap().unwrap();
2905        assert_eq!(transfer.creator, creator);
2906        assert!(!transfer.handles.contains_key(&closed_fd));
2907        assert!(!transfer.handles.contains_key(&socket_fd));
2908        assert_eq!(transfer.handles[&fd].id, transfer.handles[&alias_fd].id);
2909        // Dropping a cancelled preparation cannot change any cursor.
2910        drop(transfer.clone());
2911        parent.metadata().file_handles[&fd]
2912            .with_random_device_stream::<Errno>(|offset| {
2913                assert_eq!(offset, 7);
2914                Ok(0)
2915            })
2916            .unwrap();
2917
2918        // A genuinely replaced descriptor must not acquire a survivor's ID.
2919        // This owns only a duplicate, never one of the test process's stdio FDs.
2920        let replacement = tempfile::tempfile().unwrap();
2921        assert_eq!(
2922            unsafe { libc::dup2(replacement.as_raw_fd(), alias_fd) },
2923            alias_fd
2924        );
2925        assert_eq!(
2926            FileMetadata::new(leader).apply_exec_fd_state(
2927                leader,
2928                ExecFdState {
2929                    open_files: Some(transfer.clone()),
2930                    ..ExecFdState::default()
2931                },
2932                true,
2933            ),
2934            Err(Errno::EIO),
2935        );
2936        assert_eq!(unsafe { libc::dup2(fd, alias_fd) }, alias_fd);
2937
2938        let mut successor = ThreadState::new(leader, &config, ());
2939        // Preserve the new image's existing nonrandom association. The handover
2940        // must not restore the old socket ID used by ReleasePort bookkeeping.
2941        successor
2942            .add_fd(socket_fd, OFlag::O_RDWR, FdType::Socket, None)
2943            .unwrap();
2944        let new_socket_id = successor.metadata().file_handles[&socket_fd].open_file_id();
2945        assert_ne!(old_socket_id, new_socket_id);
2946        let stdio_ids: Vec<_> = (0..=2)
2947            .map(|fd| successor.metadata().file_handles[&fd].open_file_id())
2948            .collect();
2949        successor.set_open_file_creator(transfer.creator);
2950        successor
2951            .metadata()
2952            .apply_exec_fd_state(
2953                creator,
2954                ExecFdState {
2955                    open_files: Some(transfer),
2956                    random_device_sequence: parent.random_device_sequence_for_exec(),
2957                    ..ExecFdState::default()
2958                },
2959                true,
2960            )
2961            .unwrap();
2962        assert!(!successor.metadata().file_handles.contains_key(&closed_fd));
2963        assert_eq!(
2964            successor.metadata().file_handles[&socket_fd].open_file_id(),
2965            new_socket_id
2966        );
2967        assert!(!successor.metadata().file_handles[&socket_fd].is_shared());
2968        assert_eq!(
2969            (0..=2)
2970                .map(|fd| successor.metadata().file_handles[&fd].open_file_id())
2971                .collect::<Vec<_>>(),
2972            stdio_ids
2973        );
2974        successor.metadata().file_handles[&alias_fd]
2975            .with_random_device_stream::<Errno>(|offset| {
2976                assert_eq!(offset, 7);
2977                Ok(5)
2978            })
2979            .unwrap();
2980        parent.metadata().file_handles[&fd]
2981            .with_random_device_stream::<Errno>(|offset| {
2982                assert_eq!(offset, 12);
2983                Ok(0)
2984            })
2985            .unwrap();
2986        successor.metadata().file_handles[&distinct_fd]
2987            .with_random_device_stream::<Errno>(|offset| {
2988                assert_eq!(offset, 0);
2989                Ok(0)
2990            })
2991            .unwrap();
2992        successor.remove_fd(fd);
2993        successor
2994            .add_fd(fd, OFlag::O_RDONLY, FdType::Rng, None)
2995            .unwrap();
2996        successor.classify_random_device(fd).unwrap();
2997        let reopened = successor.metadata().file_handles[&fd].open_file_id();
2998        assert_ne!(reopened, first_id);
2999        assert_ne!(reopened, high);
3000        assert_eq!(
3001            reopened,
3002            OpenFileId::new(creator, successor.metadata().next_open_file_sequence - 1)
3003        );
3004        // A survivor keeps its stream, and the exec exchange carries the count
3005        // of random-device opens, so the new image's first open reads a new one.
3006        let stream_of = |fd| successor.metadata().file_handles[&fd].random_device_stream();
3007        let named = |sequence| crate::fd::RandomDeviceStream { creator, sequence };
3008        assert_eq!(stream_of(alias_fd), named(0));
3009        assert_eq!(stream_of(distinct_fd), named(1));
3010        assert_eq!(stream_of(fd), named(2));
3011        assert_ne!(
3012            successor
3013                .metadata()
3014                .allocate_open_file_id(creator, FdType::Socket),
3015            high_socket
3016        );
3017        successor.promote_shared_open_files().unwrap();
3018        let second = successor.shared_open_files_for_exec().unwrap().unwrap();
3019        assert_eq!(
3020            second.creator, creator,
3021            "nonleader creator survives repeated exec"
3022        );
3023        let mut again = FileMetadata::new(leader);
3024        again
3025            .apply_exec_fd_state(
3026                creator,
3027                ExecFdState {
3028                    open_files: Some(second),
3029                    ..ExecFdState::default()
3030                },
3031                true,
3032            )
3033            .unwrap();
3034        assert_eq!(again.file_handles[&fd].open_file_id(), reopened);
3035        again.file_handles[&alias_fd]
3036            .with_random_device_stream::<Errno>(|offset| {
3037                assert_eq!(offset, 12);
3038                Ok(0)
3039            })
3040            .unwrap();
3041
3042        // No surviving RNG descriptor does not mean its old identities are free.
3043        for fd in [fd, alias_fd, distinct_fd] {
3044            successor.remove_fd(fd);
3045        }
3046        let empty = successor.shared_open_files_for_exec().unwrap().unwrap();
3047        assert!(empty.handles.is_empty());
3048        let next_sequence = empty.next_sequence;
3049        let mut after_close = ThreadState::new(leader, &config, ());
3050        after_close.set_open_file_creator(empty.creator);
3051        after_close
3052            .metadata()
3053            .apply_exec_fd_state(
3054                creator,
3055                ExecFdState {
3056                    open_files: Some(empty),
3057                    ..ExecFdState::default()
3058                },
3059                true,
3060            )
3061            .unwrap();
3062        after_close
3063            .add_fd(fd, OFlag::O_RDONLY, FdType::Rng, None)
3064            .unwrap();
3065        assert_eq!(
3066            after_close.metadata().file_handles[&fd].open_file_id(),
3067            OpenFileId::new(creator, next_sequence)
3068        );
3069        // The canonical store still owns all old models: reusing an ID refuses.
3070        after_close.promote_shared_open_files().unwrap();
3071    }
3072
3073    #[test]
3074    fn shared_random_promotion_leaves_nonrandom_tables_local() {
3075        let state = ThreadState::new(DetTid::from_raw(9013), &Config::default(), ());
3076        let regular = tempfile::tempfile().unwrap();
3077        let (socket, _peer) = std::os::unix::net::UnixStream::pair().unwrap();
3078        state
3079            .add_fd(regular.as_raw_fd(), OFlag::O_RDWR, FdType::Regular, None)
3080            .unwrap();
3081        state
3082            .add_fd(socket.as_raw_fd(), OFlag::O_RDWR, FdType::Socket, None)
3083            .unwrap();
3084        let before: HashMap<_, _> = state
3085            .metadata()
3086            .file_handles
3087            .iter()
3088            .map(|(&fd, handle)| (fd, handle.open_file_id()))
3089            .collect();
3090        state.promote_shared_open_files().unwrap();
3091        assert!(state.shared_open_files_for_exec().unwrap().is_none());
3092        for (fd, id) in before {
3093            let metadata = state.metadata();
3094            let handle = &metadata.file_handles[&fd];
3095            assert!(!handle.is_shared());
3096            assert_eq!(handle.open_file_id(), id);
3097        }
3098    }
3099
3100    #[test]
3101    fn shared_exec_restores_allocator_before_legacy_pipe_discovery() {
3102        crate::fd::install_test_shared_open_file_channel();
3103        let owner = DetTid::from_raw(9014);
3104        let config = Config::default();
3105        let parent = ThreadState::new(owner, &config, ());
3106        let random = std::fs::File::open("/dev/random").unwrap();
3107        let fd = random.as_raw_fd();
3108        assert_eq!(unsafe { libc::fcntl(fd, libc::F_SETFD, 0) }, 0);
3109        parent
3110            .add_fd(fd, OFlag::O_RDONLY, FdType::Rng, None)
3111            .unwrap();
3112        parent.promote_shared_open_files().unwrap();
3113        let transfer = parent.shared_open_files_for_exec().unwrap().unwrap();
3114        let random_id = transfer.handles[&fd].id;
3115        let old_next = transfer.next_sequence;
3116        let (pipe, _writer) = nix::unistd::pipe2(OFlag::O_NONBLOCK).unwrap();
3117        let pipe_fd = pipe.as_raw_fd();
3118        let successor = ThreadState::new(owner, &config, ());
3119        successor
3120            .metadata()
3121            .apply_exec_fd_state(
3122                owner,
3123                ExecFdState {
3124                    blocking: [pipe_fd].into_iter().collect(),
3125                    open_files: Some(transfer),
3126                    ..ExecFdState::default()
3127                },
3128                true,
3129            )
3130            .unwrap();
3131        let mut metadata = successor.metadata();
3132        let pipe_id = metadata.file_handles[&pipe_fd].open_file_id();
3133        assert_eq!(pipe_id, OpenFileId::new(owner, old_next));
3134        assert_ne!(pipe_id, random_id);
3135        assert_eq!(metadata.file_handles[&pipe_fd].ty(), FdType::Pipe);
3136        assert!(!metadata.file_handles[&pipe_fd].is_nonblocking());
3137        assert!(metadata.file_handles[&pipe_fd].physically_nonblocking());
3138        assert!(!metadata.file_handles[&pipe_fd].is_shared());
3139        assert_eq!(metadata.file_handles[&fd].open_file_id(), random_id);
3140        assert_ne!(
3141            metadata.allocate_open_file_id(owner, FdType::Regular),
3142            pipe_id
3143        );
3144        // Interning must never merge the unrelated restored pipe and RNG.
3145        crate::fd::intern_open_files(&mut metadata.file_handles);
3146        assert_eq!(metadata.file_handles[&pipe_fd].ty(), FdType::Pipe);
3147        assert_eq!(metadata.file_handles[&fd].ty(), FdType::Rng);
3148    }
3149
3150    /// The channel end of a pipe made through `ThreadState::add_fd`, as
3151    /// `handle_pipe2` stores it on the read end under `--pipe-wake-latency`.
3152    fn pipe_with_channel(
3153        state: &ThreadState<()>,
3154    ) -> (std::os::fd::OwnedFd, std::os::fd::OwnedFd, ChannelEnd) {
3155        let (reader, writer) = nix::unistd::pipe2(OFlag::empty()).unwrap();
3156        state
3157            .add_fd(reader.as_raw_fd(), OFlag::O_RDONLY, FdType::Pipe, None)
3158            .unwrap();
3159        let id = state.metadata().file_handles[&reader.as_raw_fd()].open_file_id();
3160        let end = ChannelEnd {
3161            channel: ChannelId::Pipe(id),
3162            reads: true,
3163            writes: false,
3164        };
3165        state
3166            .with_detfd(reader.as_raw_fd(), |detfd| detfd.set_channel(end))
3167            .unwrap();
3168        (reader, writer, end)
3169    }
3170
3171    /// A channel lives on the open file description, so a `dup` and a forked
3172    /// child's copy of the table carry it with no copy step, while a
3173    /// descriptor Detcore discovers afresh, as it does one it did not create
3174    /// (one received over `SCM_RIGHTS`), has none
3175    /// (<https://github.com/rrnewton/hermit/issues/4380>).
3176    /// The capacity a descriptor directory's end follows is Linux's: it
3177    /// grows to the next power of two above an installed descriptor, keeps
3178    /// it when the descriptor closes, and a fork's copy is sized from what
3179    /// is open (measured: `dup2(0, 100); close(100)` ends at 130, and a
3180    /// fork's child after it at 66).
3181    #[test]
3182    fn the_descriptor_capacity_follows_linuxs_allocation() {
3183        let owner = DetTid::from_raw(9031);
3184        let mut table = FileMetadata::new(owner);
3185        assert_eq!(table.descriptor_capacity, 64);
3186        table
3187            .add_fd(owner, 100, OFlag::empty(), FdType::Regular, None)
3188            .unwrap();
3189        assert_eq!(table.descriptor_capacity, 128);
3190        table.remove_fd(100);
3191        assert_eq!(table.descriptor_capacity, 128);
3192        assert_eq!(
3193            table.fork_for(DetTid::from_raw(9032)).descriptor_capacity,
3194            64
3195        );
3196        table
3197            .add_fd(owner, 70, OFlag::empty(), FdType::Regular, None)
3198            .unwrap();
3199        assert_eq!(
3200            table.fork_for(DetTid::from_raw(9033)).descriptor_capacity,
3201            128
3202        );
3203        assert_eq!(table.for_exec(owner).descriptor_capacity, 128);
3204    }
3205
3206    /// A `dup2` or `dup3` to 100 whose old descriptor is not open grows the
3207    /// table to 128 slots, as `ksys_dup3` expands it before it looks the old
3208    /// descriptor up (measured: `dup2(-1, 100)` fails with EBADF and a
3209    /// listing then ends at 130); a target below the capacity changes
3210    /// nothing.
3211    #[test]
3212    fn a_failed_dup_grows_the_table_for_its_target() {
3213        let owner = DetTid::from_raw(9041);
3214        let mut table = FileMetadata::new(owner);
3215        table.grow_to_hold(10);
3216        assert_eq!(table.descriptor_capacity, 64);
3217        table.grow_to_hold(100);
3218        assert_eq!(table.descriptor_capacity, 128);
3219        assert!(table.descriptor_numbers().is_empty());
3220        table.grow_to_hold(-1);
3221        assert_eq!(table.descriptor_capacity, 128);
3222    }
3223
3224    /// A descriptor listed without modelling keeps the close-on-exec flag
3225    /// `F_SETFD`, `FIOCLEX` and `FIONCLEX` give it, which decides whether it
3226    /// survives `exec`; any other descriptor is not one.
3227    #[test]
3228    fn an_unmodelled_descriptor_keeps_its_close_on_exec_changes() {
3229        let owner = DetTid::from_raw(9042);
3230        let mut table = FileMetadata::new(owner);
3231        table.present_unmodelled.insert(5, false);
3232        table.present_unmodelled.insert(6, true);
3233        assert!(table.set_unmodelled_cloexec(5, true));
3234        assert!(table.set_unmodelled_cloexec(6, false));
3235        assert!(!table.set_unmodelled_cloexec(7, true));
3236        let after = table.for_exec(owner);
3237        assert!(!after.present_unmodelled.contains_key(&5));
3238        assert_eq!(after.present_unmodelled.get(&6), Some(&false));
3239    }
3240
3241    /// A descriptor open in the table that Detcore does not model (a
3242    /// received anonymous inode) is listed until it is closed or replaced,
3243    /// survives `exec` unless it is close-on-exec, and a `dup` of it is
3244    /// another such descriptor.
3245    #[test]
3246    fn an_unmodelled_descriptor_is_listed_until_it_goes() {
3247        let owner = DetTid::from_raw(9034);
3248        let mut table = FileMetadata::new(owner);
3249        table
3250            .add_fd(owner, 0, OFlag::empty(), FdType::Regular, None)
3251            .unwrap();
3252        table.present_unmodelled.insert(5, false);
3253        table.present_unmodelled.insert(6, true);
3254        assert_eq!(table.descriptor_numbers(), vec![0, 5, 6]);
3255        assert_eq!(table.for_exec(owner).descriptor_numbers(), vec![0, 5]);
3256        assert_eq!(table.dup_fd(5, 9, OFlag::O_CLOEXEC).unwrap(), None);
3257        assert_eq!(table.present_unmodelled.get(&9), Some(&true));
3258        table.remove_fd(5);
3259        table
3260            .add_fd(owner, 6, OFlag::empty(), FdType::Regular, None)
3261            .unwrap();
3262        assert_eq!(table.descriptor_numbers(), vec![0, 6, 9]);
3263        assert!(!table.present_unmodelled.contains_key(&6));
3264        table.remove_fd_range(0, 100);
3265        assert!(table.descriptor_numbers().is_empty());
3266    }
3267
3268    /// The close that drops a socket's last descriptor takes the messages
3269    /// queued to it, for the call to release what they held
3270    /// (`FileMetadata::release_detfd`), as Linux purges a released socket's
3271    /// receive queue; one that leaves another descriptor of it takes
3272    /// nothing, and a copy of the table has nothing to release.
3273    #[test]
3274    fn the_last_close_of_a_socket_takes_its_queue() {
3275        let owner = DetTid::from_raw(9043);
3276        let mut table = FileMetadata::new(owner);
3277        for (fd, ty) in [
3278            (3, FdType::Socket),
3279            (4, FdType::Socket),
3280            (5, FdType::Regular),
3281        ] {
3282            table.add_fd(owner, fd, OFlag::empty(), ty, None).unwrap();
3283        }
3284        let left = table.with_detfd(3, |detfd| detfd.clone()).unwrap();
3285        let right = table.with_detfd(4, |detfd| detfd.clone()).unwrap();
3286        left.pair_sockets(&right, false);
3287        let carried = table
3288            .with_detfd(5, |detfd| detfd.in_flight_open_file(true))
3289            .unwrap();
3290        assert!(matches!(
3291            left.queue_rights_to_peer(
3292                vec![SentDescriptor {
3293                    file: (1, 5),
3294                    open_file: Some(carried),
3295                    lock_copy: None,
3296                }],
3297                1
3298            ),
3299            Ok(())
3300        ));
3301        drop((left, right));
3302        table.dup_fd(4, 6, OFlag::empty()).unwrap();
3303        assert_eq!(table.remove_fd(4), None, "another descriptor of it remains");
3304        assert!(table.take_released_rights().is_empty());
3305        let id = table.with_detfd(6, |detfd| detfd.open_file_id()).unwrap();
3306        assert_eq!(table.remove_fd(6), Some(id), "its last descriptor");
3307        let copy = table.clone();
3308        assert_eq!(table.take_released_rights().len(), 1, "its queue, taken");
3309        assert!(
3310            copy.released_rights.0.is_empty(),
3311            "a copy of the table releases nothing"
3312        );
3313    }
3314
3315    /// A table let go at its holder's exit takes the queue of each socket
3316    /// whose last reference it held, so what those queues carried is
3317    /// released by that exit (`ThreadState::release_table_at_exit`), whenever
3318    /// the backend tears the exited thread's state down: here a socket the
3319    /// parent sent and closed, queued to a socket only the child held, which
3320    /// is then the last reference to it.
3321    #[test]
3322    fn a_table_let_go_at_exit_takes_its_last_sockets_queues() {
3323        let parent_id = DetTid::from_raw(9044);
3324        let mut parent = FileMetadata::new(parent_id);
3325        for fd in [3, 4] {
3326            parent
3327                .add_fd(parent_id, fd, OFlag::empty(), FdType::Socket, None)
3328                .unwrap();
3329        }
3330        let left = parent.with_detfd(3, |detfd| detfd.clone()).unwrap();
3331        let right = parent.with_detfd(4, |detfd| detfd.clone()).unwrap();
3332        left.pair_sockets(&right, false);
3333        drop(right);
3334        let mut child = parent.fork_for(DetTid::from_raw(9045));
3335        assert_eq!(parent.remove_fd(4), None, "the child holds the receiver");
3336        assert_eq!(child.remove_fd(3), None, "the parent holds the sender");
3337        parent
3338            .add_fd(parent_id, 5, OFlag::empty(), FdType::Socket, None)
3339            .unwrap();
3340        let carried = parent
3341            .with_detfd(5, |detfd| detfd.in_flight_open_file(true))
3342            .unwrap();
3343        let sent_id = parent.with_detfd(5, |detfd| detfd.open_file_id()).unwrap();
3344        assert!(matches!(
3345            left.queue_rights_to_peer(
3346                vec![SentDescriptor {
3347                    file: (1, 5),
3348                    open_file: Some(carried),
3349                    lock_copy: None,
3350                }],
3351                1
3352            ),
3353            Ok(())
3354        ));
3355        assert_eq!(parent.remove_fd(5), None, "a copy is in flight");
3356        child.release_every_descriptor();
3357        assert!(child.file_handles.is_empty());
3358        let released: Vec<_> = child
3359            .take_released_rights()
3360            .into_iter()
3361            .filter_map(|sent| {
3362                sent.open_file
3363                    .and_then(crate::fd::InFlightOpenFile::release)
3364            })
3365            .map(|(id, ty, _)| (id, ty))
3366            .collect();
3367        assert_eq!(
3368            released,
3369            [(sent_id, FdType::Socket)],
3370            "the exit released the queued socket, whose last reference it was"
3371        );
3372        drop(left);
3373    }
3374
3375    /// The exit that leaves a table to no holder lets it go
3376    /// (`FileMetadata::leave`), as guest calls alone count its holders: a
3377    /// thread's `exit` counts one out and an `exit_group` its whole process;
3378    /// a process made with `CLONE_FILES` holds the table too; a copy made for
3379    /// a fork or an exec has one holder of its own. A table whose holders are
3380    /// not known, or a process that does not hold it, never goes this way.
3381    #[test]
3382    fn a_table_goes_with_the_exit_that_leaves_it_no_holder() {
3383        let process = DetTid::from_raw(9050);
3384        let mut table = FileMetadata::new(process);
3385        table.join_holder(process);
3386        assert!(!table.leave(process, false), "one thread of two exits");
3387        assert!(table.leave(process, false), "and then the other");
3388
3389        let mut table = FileMetadata::new(process);
3390        table.join_holder(process);
3391        table.join_holder(process);
3392        assert!(table.leave(process, true), "an exit_group ends all three");
3393
3394        let sharer = DetTid::from_raw(9051);
3395        let mut table = FileMetadata::new(process);
3396        table.join_holder(sharer);
3397        assert!(
3398            !table.leave(process, true),
3399            "a process made with CLONE_FILES"
3400        );
3401        assert!(
3402            !table.leave(process, false),
3403            "a process that left holds none"
3404        );
3405        assert!(table.leave(sharer, false));
3406
3407        let mut copy = FileMetadata::new(process).fork_for(sharer);
3408        assert!(
3409            copy.leave(sharer, false),
3410            "a fork's copy, its child's alone"
3411        );
3412        let mut copy = FileMetadata::new(process).for_exec(sharer);
3413        copy.hold_alone(process);
3414        assert!(!copy.leave(sharer, false));
3415        assert!(copy.leave(process, false), "an exec's copy, its process's");
3416
3417        let mut unknown = FileMetadata::new(process);
3418        unknown.holders.clear();
3419        unknown.join_holder(process);
3420        assert!(!unknown.leave(process, true), "holders not known");
3421    }
3422
3423    /// An exec leaves the table it replaces, all of the exec'ing process's
3424    /// threads at once (`ThreadState::release_exec_dropped_descriptors`), and
3425    /// drops that table's descriptors only where it was the last holder:
3426    /// Linux copies a table another process still holds (`unshare_files`),
3427    /// which keeps every descriptor, and that process's own exit then lets the
3428    /// table go, which the exec'ing process no longer counts against. A table
3429    /// whose holders are not known is left as it is (Codex's notes on K8-h
3430    /// reb7: the exec'ing process stayed a holder of the shared table, so the
3431    /// other process's exit never let it go).
3432    #[test]
3433    fn an_exec_leaves_a_table_another_process_still_holds() {
3434        let process = DetTid::from_raw(9052);
3435        let sharer = DetTid::from_raw(9053);
3436        let exec_of = |table: FileMetadata| {
3437            let replaced = Arc::new(Mutex::new(table));
3438            let mut new = replaced.lock().unwrap().for_exec(process);
3439            new.hold_alone(process);
3440            new.keep_replaced_table(Arc::clone(&replaced), process);
3441            let state = ThreadState::new(process, &Config::default(), ());
3442            *state.file_metadata.lock().unwrap() = new;
3443            let released = state.release_exec_dropped_descriptors();
3444            (replaced, released)
3445        };
3446
3447        let mut shared = FileMetadata::new(process);
3448        shared.join_holder(process);
3449        shared.join_holder(sharer);
3450        shared
3451            .add_fd(process, 3, OFlag::O_CLOEXEC, FdType::Regular, None)
3452            .unwrap();
3453        let (shared, released) = exec_of(shared);
3454        assert!(!released, "a process made with CLONE_FILES still holds it");
3455        let mut shared = shared.lock().unwrap();
3456        assert_eq!(shared.file_handles.len(), 1, "it keeps every descriptor");
3457        assert_eq!(shared.holders, BTreeMap::from([(sharer, 1)]));
3458        assert!(shared.leave(sharer, true), "its exit lets the table go");
3459
3460        let mut unknown = FileMetadata::new(process);
3461        unknown.holders.clear();
3462        unknown
3463            .add_fd(process, 3, OFlag::O_CLOEXEC, FdType::Regular, None)
3464            .unwrap();
3465        let (unknown, released) = exec_of(unknown);
3466        assert!(!released);
3467        assert_eq!(
3468            unknown.lock().unwrap().file_handles.len(),
3469            1,
3470            "holders not known"
3471        );
3472    }
3473
3474    /// The exec of a process whose table only it holds lets go of that
3475    /// table's descriptors at the new image's first callback
3476    /// (`ThreadState::release_exec_dropped_descriptors`): here a receiving
3477    /// socket whose last descriptor, in the child, is close-on-exec, queued a
3478    /// description the parent sent and closed, so the exec releases the copy
3479    /// in flight, as Linux releases it when the exec closes the socket
3480    /// (Claude's narrow check of K8-h reb6, finding 1: before, the copy, and
3481    /// with it the lock ledger's count of its locks, stayed for the rest of
3482    /// the run). The replaced table names nothing afterwards, and a second
3483    /// call releases nothing.
3484    #[test]
3485    fn an_exec_releases_what_its_close_on_exec_receiving_socket_held() {
3486        let parent_id = DetTid::from_raw(9054);
3487        let child_id = DetTid::from_raw(9055);
3488        let mut parent = FileMetadata::new(parent_id);
3489        parent
3490            .add_fd(parent_id, 3, OFlag::empty(), FdType::Socket, None)
3491            .unwrap();
3492        parent
3493            .add_fd(parent_id, 4, OFlag::O_CLOEXEC, FdType::Socket, None)
3494            .unwrap();
3495        let left = parent.with_detfd(3, |detfd| detfd.clone()).unwrap();
3496        let right = parent.with_detfd(4, |detfd| detfd.clone()).unwrap();
3497        left.pair_sockets(&right, false);
3498        drop(right);
3499        let mut child = parent.fork_for(child_id);
3500        assert_eq!(parent.remove_fd(4), None, "the child holds the receiver");
3501        assert_eq!(child.remove_fd(3), None, "the parent holds the sender");
3502        parent
3503            .add_fd(parent_id, 5, OFlag::empty(), FdType::Regular, None)
3504            .unwrap();
3505        let carried = parent
3506            .with_detfd(5, |detfd| detfd.in_flight_open_file(true))
3507            .unwrap();
3508        let sent_id = parent.with_detfd(5, |detfd| detfd.open_file_id()).unwrap();
3509        assert!(matches!(
3510            left.queue_rights_to_peer(
3511                vec![SentDescriptor {
3512                    file: (1, 5),
3513                    open_file: Some(carried),
3514                    lock_copy: Some(sent_id),
3515                }],
3516                1
3517            ),
3518            Ok(())
3519        ));
3520        assert_eq!(parent.remove_fd(5), None, "a copy is in flight");
3521
3522        child.join_holder(child_id);
3523        let replaced = Arc::new(Mutex::new(child));
3524        let mut new = replaced.lock().unwrap().for_exec(child_id);
3525        new.hold_alone(child_id);
3526        assert!(
3527            new.file_handles.is_empty(),
3528            "the receiver was close-on-exec"
3529        );
3530        new.keep_replaced_table(Arc::clone(&replaced), child_id);
3531        let state = ThreadState::new(child_id, &Config::default(), ());
3532        *state.file_metadata.lock().unwrap() = new;
3533
3534        assert!(state.release_exec_dropped_descriptors());
3535        let released: Vec<_> = state
3536            .take_released_rights()
3537            .into_iter()
3538            .map(|sent| {
3539                (
3540                    sent.lock_copy,
3541                    sent.open_file
3542                        .and_then(crate::fd::InFlightOpenFile::release)
3543                        .map(|(id, ty, _)| (id, ty)),
3544                )
3545            })
3546            .collect();
3547        assert_eq!(
3548            released,
3549            [(Some(sent_id), Some((sent_id, FdType::Regular)))],
3550            "the exec released the copy in flight, its description's last reference"
3551        );
3552        assert!(
3553            replaced.lock().unwrap().file_handles.is_empty(),
3554            "the replaced table names nothing"
3555        );
3556        assert!(!state.release_exec_dropped_descriptors(), "released once");
3557        drop(left);
3558    }
3559
3560    /// The death of a process by a fatal signal's default action, which its
3561    /// terminal receipt reports (`GlobalState::release_tables_at_death`), lets
3562    /// go of a table only that process held (`FileMetadata::release_at_death`):
3563    /// here a receiving socket whose last descriptor the child holds queued
3564    /// a description the parent sent and closed, so the death releases the
3565    /// copy in flight, the description's last reference, as Linux's
3566    /// `exit_files` releases it before the parent can wait (Codex's narrow
3567    /// check of K8-h reb8: before, KVM listed its lock after the child died
3568    /// by its own SIGTERM, abort() or SIGSEGV). Every thread of the process
3569    /// leaves at once. A table another process still holds keeps every
3570    /// descriptor, and a table whose holders are not known, or do not name
3571    /// the process, is left undecided.
3572    #[test]
3573    fn a_fatal_signals_death_releases_a_table_only_its_process_held() {
3574        let parent_id = DetTid::from_raw(9056);
3575        let child_id = DetTid::from_raw(9057);
3576        let sharer = DetTid::from_raw(9058);
3577        let mut parent = FileMetadata::new(parent_id);
3578        parent
3579            .add_fd(parent_id, 3, OFlag::empty(), FdType::Socket, None)
3580            .unwrap();
3581        parent
3582            .add_fd(parent_id, 4, OFlag::empty(), FdType::Socket, None)
3583            .unwrap();
3584        let left = parent.with_detfd(3, |detfd| detfd.clone()).unwrap();
3585        let right = parent.with_detfd(4, |detfd| detfd.clone()).unwrap();
3586        left.pair_sockets(&right, false);
3587        drop(right);
3588        let mut child = parent.fork_for(child_id);
3589        child.hold_alone(child_id);
3590        assert_eq!(parent.remove_fd(4), None, "the child holds the receiver");
3591        assert_eq!(child.remove_fd(3), None, "the parent holds the sender");
3592        parent
3593            .add_fd(parent_id, 5, OFlag::empty(), FdType::Regular, None)
3594            .unwrap();
3595        let carried = parent
3596            .with_detfd(5, |detfd| detfd.in_flight_open_file(true))
3597            .unwrap();
3598        let sent_id = parent.with_detfd(5, |detfd| detfd.open_file_id()).unwrap();
3599        assert!(matches!(
3600            left.queue_rights_to_peer(
3601                vec![SentDescriptor {
3602                    file: (1, 5),
3603                    open_file: Some(carried),
3604                    lock_copy: Some(sent_id),
3605                }],
3606                1
3607            ),
3608            Ok(())
3609        ));
3610        assert_eq!(parent.remove_fd(5), None, "a copy is in flight");
3611
3612        // A second thread of the child holds the table too.
3613        child.join_holder(child_id);
3614        let TableAtDeath::Released(rights) = child.release_at_death(child_id) else {
3615            panic!("the child alone held its table");
3616        };
3617        assert_eq!(
3618            crate::syscalls::scm_rights::settle(rights),
3619            crate::syscalls::scm_rights::Settled {
3620                lock_copies: vec![sent_id],
3621                ports: Vec::new(),
3622            },
3623            "the death released the copy in flight, its description's last reference"
3624        );
3625        assert!(child.file_handles.is_empty(), "every descriptor went");
3626        assert!(child.holders.is_empty(), "both threads left");
3627        assert!(
3628            matches!(child.release_at_death(child_id), TableAtDeath::Undecided),
3629            "released once"
3630        );
3631
3632        let mut shared = FileMetadata::new(child_id);
3633        shared.join_holder(sharer);
3634        shared
3635            .add_fd(child_id, 3, OFlag::empty(), FdType::Regular, None)
3636            .unwrap();
3637        assert!(matches!(
3638            shared.release_at_death(child_id),
3639            TableAtDeath::Kept
3640        ));
3641        assert_eq!(shared.file_handles.len(), 1, "it keeps every descriptor");
3642        assert_eq!(shared.holders, BTreeMap::from([(sharer, 1)]));
3643
3644        let mut unknown = FileMetadata::new(child_id);
3645        unknown.holders.clear();
3646        unknown
3647            .add_fd(child_id, 3, OFlag::empty(), FdType::Regular, None)
3648            .unwrap();
3649        assert!(matches!(
3650            unknown.release_at_death(child_id),
3651            TableAtDeath::Undecided
3652        ));
3653        assert_eq!(unknown.file_handles.len(), 1, "holders not known");
3654        let mut another = FileMetadata::new(sharer);
3655        another
3656            .add_fd(sharer, 3, OFlag::empty(), FdType::Regular, None)
3657            .unwrap();
3658        assert!(matches!(
3659            another.release_at_death(child_id),
3660            TableAtDeath::Undecided
3661        ));
3662        assert_eq!(another.file_handles.len(), 1, "not the process's table");
3663        drop(left);
3664    }
3665
3666    #[test]
3667    fn a_pipe_channel_follows_its_description_through_dup_and_fork() {
3668        if isolated_child(
3669            "tool_local::file_metadata_tests::a_pipe_channel_follows_its_description_through_dup_and_fork",
3670        ) {
3671            return;
3672        }
3673        let owner = DetTid::from_raw(9018);
3674        let mut parent = FileMetadata::new(owner);
3675        let (reader, _writer) = nix::unistd::pipe2(OFlag::empty()).unwrap();
3676        let fd = reader.as_raw_fd();
3677        parent
3678            .add_fd(owner, fd, OFlag::empty(), FdType::Pipe, None)
3679            .unwrap();
3680        let end = ChannelEnd {
3681            channel: ChannelId::Pipe(parent.with_detfd(fd, |detfd| detfd.open_file_id()).unwrap()),
3682            reads: true,
3683            writes: false,
3684        };
3685        parent
3686            .with_detfd(fd, |detfd| detfd.set_channel(end))
3687            .unwrap();
3688        let alias = fd + 100;
3689        parent.dup_fd(fd, alias, OFlag::empty()).unwrap();
3690        assert_eq!(
3691            parent.with_detfd(alias, |detfd| detfd.channel()).unwrap(),
3692            Some(end)
3693        );
3694        let mut child = parent.fork_for(DetTid::from_raw(9019));
3695        for fd in [fd, alias] {
3696            assert_eq!(
3697                child.with_detfd(fd, |detfd| detfd.channel()).unwrap(),
3698                Some(end)
3699            );
3700        }
3701        let (received, _peer) = nix::unistd::pipe2(OFlag::empty()).unwrap();
3702        parent
3703            .discover_fd_from_current_process(owner, received.as_raw_fd())
3704            .unwrap();
3705        assert_eq!(
3706            parent
3707                .with_detfd(received.as_raw_fd(), |detfd| detfd.channel())
3708                .unwrap(),
3709            None
3710        );
3711    }
3712
3713    /// A descriptor received over `SCM_RIGHTS` on a backend whose tool does
3714    /// not share the guest's table is recorded from injected facts
3715    /// (`add_discovered_fd`); an in-guest backend discovers the same
3716    /// descriptor in process. Both make the same description: type, status
3717    /// flags, close-on-exec, physical nonblocking and an unobserved lock.
3718    #[test]
3719    fn a_received_descriptor_is_recorded_as_the_in_guest_discovery_records_it() {
3720        let owner = DetTid::from_raw(9031);
3721        let facts = |fd: RawFd| {
3722            // SAFETY: fcntl and fstat only read this test's descriptor.
3723            let fd_flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };
3724            let status = unsafe { libc::fcntl(fd, libc::F_GETFL) };
3725            let mut stat: libc::stat = unsafe { std::mem::zeroed() };
3726            assert_eq!(unsafe { libc::fstat(fd, &mut stat) }, 0);
3727            (fd_flags, status, stat)
3728        };
3729        let shape = |metadata: &FileMetadata, fd: RawFd| {
3730            let detfd = &metadata.file_handles[&fd];
3731            (
3732                detfd.ty(),
3733                detfd.status_flags(),
3734                detfd.is_cloexec(),
3735                detfd.physically_nonblocking(),
3736                detfd.is_nonblocking(),
3737                detfd.known_flock_mode(),
3738                detfd.flock_may_have_been_granted(),
3739                detfd.stat().map(|stat| stat.raw_inode()),
3740            )
3741        };
3742        let (pipe_read, _pipe_write) =
3743            nix::unistd::pipe2(OFlag::O_NONBLOCK | OFlag::O_CLOEXEC).unwrap();
3744        let (socket, _peer) = nix::sys::socket::socketpair(
3745            nix::sys::socket::AddressFamily::Unix,
3746            nix::sys::socket::SockType::Stream,
3747            None,
3748            nix::sys::socket::SockFlag::empty(),
3749        )
3750        .unwrap();
3751        let file = std::fs::File::open("/proc/self/exe").unwrap();
3752        let random = std::fs::File::open("/dev/urandom").unwrap();
3753        // SAFETY: inotify_init1 takes no pointer; the descriptor is owned here.
3754        let inotify = unsafe {
3755            <std::os::fd::OwnedFd as std::os::fd::FromRawFd>::from_raw_fd(libc::inotify_init1(
3756                libc::IN_NONBLOCK | libc::IN_CLOEXEC,
3757            ))
3758        };
3759        let cases = [
3760            (pipe_read.as_raw_fd(), FdType::Pipe),
3761            (socket.as_raw_fd(), FdType::Socket),
3762            (file.as_raw_fd(), FdType::Regular),
3763            (random.as_raw_fd(), FdType::Rng),
3764            (inotify.as_raw_fd(), FdType::Inotify),
3765        ];
3766        for (fd, ty) in cases {
3767            let mut recorded = FileMetadata::new(owner);
3768            let (fd_flags, status, stat) = facts(fd);
3769            // As the receipt reads it: only an anonymous inode's link.
3770            let link = (stat.st_mode & libc::S_IFMT == 0)
3771                .then(|| super::own_fd_link(fd))
3772                .flatten();
3773            recorded
3774                .add_discovered_fd(owner, fd, fd_flags, status, &stat, link.as_deref())
3775                .unwrap();
3776            let mut discovered = FileMetadata::new(owner);
3777            discovered
3778                .discover_fd_from_current_process(owner, fd)
3779                .unwrap();
3780            assert_eq!(shape(&recorded, fd), shape(&discovered, fd), "fd {fd}");
3781            assert_eq!(recorded.file_handles[&fd].ty(), ty, "fd {fd}");
3782            // Recorded unobserved: no lock claim, and one may be held.
3783            assert_eq!(recorded.file_handles[&fd].known_flock_mode(), None);
3784        }
3785        let pipe = pipe_read.as_raw_fd();
3786        let mut recorded = FileMetadata::new(owner);
3787        let (fd_flags, status, stat) = facts(pipe);
3788        recorded
3789            .add_discovered_fd(owner, pipe, fd_flags, status, &stat, None)
3790            .unwrap();
3791        let detfd = &recorded.file_handles[&pipe];
3792        // The kernel's flags are what the record knows: O_NONBLOCK stays
3793        // visible and physical, and FD_CLOEXEC is the slot's.
3794        assert!(detfd.is_cloexec() && detfd.is_nonblocking() && detfd.physically_nonblocking());
3795    }
3796
3797    /// Under `--pipe-wake-latency` a rediscovering exec carries each surviving
3798    /// descriptor's channel end and the old numbering. The same leader, whose
3799    /// new image restarts its open-file sequence, rediscovers pipe A with A's
3800    /// channel; a pipe B it then makes, past every id the old table allocated,
3801    /// has another channel value. A descriptor left `O_NONBLOCK`, outside the
3802    /// blocking set, gets its channel too
3803    /// (https://github.com/rrnewton/hermit/issues/4380).
3804    #[test]
3805    fn a_rediscovering_exec_keeps_pipe_channels_apart() {
3806        let leader = DetTid::from_raw(9015);
3807        let config = Config::default();
3808        let before = ThreadState::new(leader, &config, ());
3809        let (a_reader, _a_writer, a_end) = pipe_with_channel(&before);
3810        assert_eq!(
3811            unsafe { libc::fcntl(a_reader.as_raw_fd(), libc::F_SETFL, libc::O_NONBLOCK) },
3812            0
3813        );
3814        let channels = before.pipe_channels_for_exec();
3815        assert_eq!(channels, BTreeMap::from([(a_reader.as_raw_fd(), a_end)]));
3816        let namespace = before.open_file_namespace_for_exec();
3817        assert_eq!(
3818            namespace,
3819            (leader, before.metadata().next_open_file_sequence)
3820        );
3821
3822        let after = ThreadState::new(leader, &config, ());
3823        after
3824            .metadata()
3825            .apply_exec_fd_state(
3826                leader,
3827                ExecFdState {
3828                    channels,
3829                    open_file_namespace: Some(namespace),
3830                    ..ExecFdState::default()
3831                },
3832                true,
3833            )
3834            .unwrap();
3835        assert_eq!(
3836            after
3837                .with_detfd(a_reader.as_raw_fd(), |detfd| detfd.channel())
3838                .unwrap(),
3839            Some(a_end),
3840            "the carried channel survives rediscovery"
3841        );
3842        let (b_reader, _b_writer, b_end) = pipe_with_channel(&after);
3843        assert_ne!(b_end.channel, a_end.channel);
3844        let metadata = after.metadata();
3845        assert_ne!(
3846            metadata.file_handles[&b_reader.as_raw_fd()].open_file_id(),
3847            metadata.file_handles[&a_reader.as_raw_fd()].open_file_id()
3848        );
3849    }
3850
3851    /// A channel held by another process is kept apart too: the old image
3852    /// made pipe A, a child holds it, and the exec carries no channel at all;
3853    /// the old numbering is still reserved, so a pipe the new image makes
3854    /// cannot take A's id.
3855    #[test]
3856    fn an_exec_carrying_no_channel_still_reserves_the_old_numbering() {
3857        let leader = DetTid::from_raw(9016);
3858        let config = Config::default();
3859        let before = ThreadState::new(leader, &config, ());
3860        let (a_reader, _a_writer, a_end) = pipe_with_channel(&before);
3861        let namespace = before.open_file_namespace_for_exec();
3862        drop(a_reader);
3863
3864        let after = ThreadState::new(leader, &config, ());
3865        let state = ExecFdState {
3866            open_file_namespace: Some(namespace),
3867            ..ExecFdState::default()
3868        };
3869        assert!(
3870            !state.is_empty(),
3871            "a namespace-only state survives reconnect"
3872        );
3873        after
3874            .metadata()
3875            .apply_exec_fd_state(leader, state, true)
3876            .unwrap();
3877        let (_b_reader, _b_writer, b_end) = pipe_with_channel(&after);
3878        assert_ne!(b_end.channel, a_end.channel);
3879    }
3880
3881    /// With the option off the exec state carries neither field: nothing is
3882    /// reserved, the new image numbers as a fresh one does, and the state's
3883    /// trace prints as it did before the fields existed. A channel-only state
3884    /// is not empty.
3885    #[test]
3886    fn an_exec_without_the_pipe_wake_latency_is_unchanged() {
3887        let leader = DetTid::from_raw(9017);
3888        let config = Config::default();
3889        let after = ThreadState::new(leader, &config, ());
3890        let state = ExecFdState::default();
3891        assert!(state.is_empty());
3892        assert_eq!(
3893            format!("{state:?}"),
3894            "ExecFdState { blocking: {}, inherited_reads: {}, \
3895             initialize_inherited_stdio: false, initial_input: None, \
3896             robust_wakes: [], open_files: None }"
3897        );
3898        after
3899            .metadata()
3900            .apply_exec_fd_state(leader, state, true)
3901            .unwrap();
3902        let (reader, _writer, _) = pipe_with_channel(&after);
3903        let fresh = ThreadState::new(leader, &config, ());
3904        let (fresh_reader, _fresh_writer, _) = pipe_with_channel(&fresh);
3905        assert_eq!(
3906            after.metadata().file_handles[&reader.as_raw_fd()].open_file_id(),
3907            fresh.metadata().file_handles[&fresh_reader.as_raw_fd()].open_file_id()
3908        );
3909        let channel_only = ExecFdState {
3910            channels: BTreeMap::from([(
3911                reader.as_raw_fd(),
3912                ChannelEnd {
3913                    channel: ChannelId::Pipe(OpenFileId::new(leader, 0)),
3914                    reads: true,
3915                    writes: false,
3916                },
3917            )]),
3918            ..ExecFdState::default()
3919        };
3920        assert!(!channel_only.is_empty());
3921        assert!(format!("{channel_only:?}").contains("channels: {"));
3922
3923        // A description prints its channel only when it has one.
3924        let (plain, _plain_writer) = nix::unistd::pipe2(OFlag::empty()).unwrap();
3925        after
3926            .add_fd(plain.as_raw_fd(), OFlag::O_RDONLY, FdType::Pipe, None)
3927            .unwrap();
3928        let printed = format!("{:?}", after.metadata().file_handles[&plain.as_raw_fd()]);
3929        assert!(!printed.contains(" channel:"), "{printed}");
3930        assert!(printed.contains("timerfd: None }"), "{printed}");
3931        let printed = format!("{:?}", after.metadata().file_handles[&reader.as_raw_fd()]);
3932        assert!(
3933            printed.contains("timerfd: None, channel: ChannelEnd {"),
3934            "{printed}"
3935        );
3936    }
3937
3938    fn terminal_pair() -> (std::os::fd::OwnedFd, std::os::fd::OwnedFd) {
3939        use std::os::fd::FromRawFd;
3940        let (mut master, mut slave) = (-1, -1);
3941        assert_eq!(
3942            unsafe {
3943                libc::openpty(
3944                    &raw mut master,
3945                    &raw mut slave,
3946                    std::ptr::null_mut(),
3947                    std::ptr::null(),
3948                    std::ptr::null(),
3949                )
3950            },
3951            0
3952        );
3953        unsafe {
3954            (
3955                std::os::fd::OwnedFd::from_raw_fd(master),
3956                std::os::fd::OwnedFd::from_raw_fd(slave),
3957            )
3958        }
3959    }
3960
3961    #[test]
3962    fn inherited_terminal_discovery_keeps_kernel_flags_and_nonterminal_types() {
3963        let owner = DetTid::from_raw(9);
3964        let (_master, slave) = terminal_pair();
3965        let regular = tempfile::tempfile().unwrap();
3966        let zero = std::fs::File::open("/dev/zero").unwrap();
3967        let null = std::fs::File::open("/dev/null").unwrap();
3968        let (pipe, _writer) = nix::unistd::pipe().unwrap();
3969        let fds = [
3970            slave.as_raw_fd(),
3971            regular.as_raw_fd(),
3972            zero.as_raw_fd(),
3973            null.as_raw_fd(),
3974            pipe.as_raw_fd(),
3975        ];
3976        let before: Vec<_> = fds
3977            .iter()
3978            .map(|fd| unsafe {
3979                (
3980                    libc::fcntl(*fd, libc::F_GETFL),
3981                    libc::fcntl(*fd, libc::F_GETFD),
3982                )
3983            })
3984            .collect();
3985        let mut metadata = FileMetadata::new(owner);
3986        for fd in fds {
3987            metadata
3988                .discover_fd_from_current_process(owner, fd)
3989                .unwrap();
3990        }
3991        metadata.discover_inherited_terminals(fds);
3992        for (index, fd) in fds.into_iter().enumerate() {
3993            let detfd = &metadata.file_handles[&fd];
3994            assert_eq!(detfd.inherited_external_read().is_some(), index == 0);
3995            assert_eq!(
3996                detfd.ty(),
3997                if index == 4 {
3998                    FdType::Pipe
3999                } else {
4000                    FdType::Regular
4001                }
4002            );
4003            assert_eq!(
4004                unsafe {
4005                    (
4006                        libc::fcntl(fd, libc::F_GETFL),
4007                        libc::fcntl(fd, libc::F_GETFD),
4008                    )
4009                },
4010                before[index]
4011            );
4012        }
4013    }
4014
4015    /// A memfd named and sealed as a `/proc/cpuinfo` placeholder, met by a
4016    /// table that rediscovers descriptors before the session's answer (an
4017    /// in-guest backend's image after `exec`), is a placeholder only once the
4018    /// session says it renders the file. With CPUID virtualized but not
4019    /// intercepted (SaBRe), `Detcore::rendered_files` says it does not, and
4020    /// the memfd stays the ordinary memfd it is (Codex and
4021    /// deterministic-scheduling review of
4022    /// https://github.com/rrnewton/hermit/issues/4213).
4023    #[test]
4024    fn a_cpuinfo_placeholder_waits_for_the_sessions_answer() {
4025        let owner = DetTid::from_raw(9);
4026        let forged = || {
4027            // SAFETY: the name is NUL-terminated; the result is checked.
4028            let raw = unsafe {
4029                libc::memfd_create(
4030                    c"hermit-procfs:/proc/cpuinfo@".as_ptr(),
4031                    libc::MFD_ALLOW_SEALING,
4032                )
4033            };
4034            assert!(
4035                raw >= 0,
4036                "memfd_create: {}",
4037                std::io::Error::last_os_error()
4038            );
4039            // SAFETY: `raw` is a descriptor this test just made and owns.
4040            let fd = unsafe { <std::os::fd::OwnedFd as std::os::fd::FromRawFd>::from_raw_fd(raw) };
4041            let seals = crate::syscalls::rendered_procfs::PLACEHOLDER_SEALS;
4042            assert_eq!(unsafe { libc::fcntl(raw, libc::F_ADD_SEALS, seals) }, 0);
4043            fd
4044        };
4045        let ordinary = |metadata: &FileMetadata, fd: RawFd| {
4046            metadata.file_handles[&fd].procfs_rendered_path().is_none()
4047        };
4048        // The session does not render /proc/cpuinfo (CPUID virtualized by
4049        // the configuration, but the backend delivers no CPUID to Detcore).
4050        let fd = forged();
4051        let mut metadata = FileMetadata::new(owner);
4052        metadata
4053            .discover_fd_from_current_process(owner, fd.as_raw_fd())
4054            .unwrap();
4055        assert!(ordinary(&metadata, fd.as_raw_fd()));
4056        metadata.decide_cpuinfo_placeholders(false).unwrap();
4057        assert!(ordinary(&metadata, fd.as_raw_fd()));
4058        // Once decided, a later descriptor is not one either.
4059        let later = forged();
4060        metadata
4061            .discover_fd_from_current_process(owner, later.as_raw_fd())
4062            .unwrap();
4063        assert!(ordinary(&metadata, later.as_raw_fd()));
4064        // A later answer of the same session changes nothing.
4065        metadata.decide_cpuinfo_placeholders(true).unwrap();
4066        assert!(ordinary(&metadata, fd.as_raw_fd()));
4067        // The session renders it: the waiting descriptor becomes the
4068        // placeholder, and so is one met after the answer.
4069        let mut metadata = FileMetadata::new(owner);
4070        metadata
4071            .discover_fd_from_current_process(owner, fd.as_raw_fd())
4072            .unwrap();
4073        assert!(ordinary(&metadata, fd.as_raw_fd()));
4074        metadata.decide_cpuinfo_placeholders(true).unwrap();
4075        assert_eq!(
4076            metadata.file_handles[&fd.as_raw_fd()].procfs_rendered_path(),
4077            Some(std::path::Path::new("/proc/cpuinfo"))
4078        );
4079        let after = forged();
4080        metadata
4081            .discover_fd_from_current_process(owner, after.as_raw_fd())
4082            .unwrap();
4083        assert_eq!(
4084            metadata.file_handles[&after.as_raw_fd()].procfs_rendered_path(),
4085            Some(std::path::Path::new("/proc/cpuinfo"))
4086        );
4087    }
4088
4089    /// A task's `arch_status` is rendered exactly where `/proc/cpuinfo` is,
4090    /// so a memfd named and sealed as its placeholder, met before the
4091    /// session's answer, waits for that answer as a `/proc/cpuinfo`
4092    /// placeholder does, and becomes the placeholder it is once the session
4093    /// renders the files (Claude review of the kvm lane's slice N3: it was
4094    /// neither adopted nor queued, and stayed an ordinary memfd).
4095    #[test]
4096    fn an_arch_status_placeholder_waits_for_the_sessions_answer() {
4097        let owner = DetTid::from_raw(9);
4098        let stand_in = || {
4099            // SAFETY: the name is NUL-terminated; the result is checked.
4100            let raw = unsafe {
4101                libc::memfd_create(
4102                    c"hermit-procfs:/proc/3/arch_status#1@".as_ptr(),
4103                    libc::MFD_ALLOW_SEALING,
4104                )
4105            };
4106            assert!(
4107                raw >= 0,
4108                "memfd_create: {}",
4109                std::io::Error::last_os_error()
4110            );
4111            // SAFETY: `raw` is a descriptor this test just made and owns.
4112            let fd = unsafe { <std::os::fd::OwnedFd as std::os::fd::FromRawFd>::from_raw_fd(raw) };
4113            let seals = crate::syscalls::rendered_procfs::PLACEHOLDER_SEALS;
4114            assert_eq!(unsafe { libc::fcntl(raw, libc::F_ADD_SEALS, seals) }, 0);
4115            fd
4116        };
4117        // The session does not render the files: it stays an ordinary memfd.
4118        let fd = stand_in();
4119        let mut metadata = FileMetadata::new(owner);
4120        metadata
4121            .discover_fd_from_current_process(owner, fd.as_raw_fd())
4122            .unwrap();
4123        assert!(
4124            metadata.file_handles[&fd.as_raw_fd()]
4125                .procfs_rendered_path()
4126                .is_none()
4127        );
4128        metadata.decide_cpuinfo_placeholders(false).unwrap();
4129        assert!(
4130            metadata.file_handles[&fd.as_raw_fd()]
4131                .procfs_rendered_path()
4132                .is_none()
4133        );
4134        // The session renders them: the waiting descriptor becomes the
4135        // task's arch_status.
4136        let mut metadata = FileMetadata::new(owner);
4137        metadata
4138            .discover_fd_from_current_process(owner, fd.as_raw_fd())
4139            .unwrap();
4140        assert!(
4141            metadata.file_handles[&fd.as_raw_fd()]
4142                .procfs_rendered_path()
4143                .is_none()
4144        );
4145        metadata.decide_cpuinfo_placeholders(true).unwrap();
4146        assert_eq!(
4147            metadata.file_handles[&fd.as_raw_fd()].procfs_rendered_path(),
4148            Some(std::path::Path::new("/proc/3/arch_status"))
4149        );
4150    }
4151
4152    /// A memfd named and sealed as the placeholder of a file the run's
4153    /// `--proc` level omits stays the ordinary memfd it is when a table
4154    /// discovers it: Detcore makes no placeholder for such a file, whose open
4155    /// fails with ENOENT (Claude review of the kvm lane's slice N3, round 2).
4156    /// At a level that has the file it is the placeholder, as before.
4157    #[test]
4158    fn a_memfd_with_a_stand_in_name_outside_the_proc_level_stays_a_memfd() {
4159        use detcore_model::config::ProcLevel;
4160        let owner = DetTid::from_raw(9);
4161        // SAFETY: the name is NUL-terminated; the result is checked.
4162        let raw = unsafe {
4163            libc::memfd_create(
4164                c"hermit-procfs:/proc/diskstats@".as_ptr(),
4165                libc::MFD_ALLOW_SEALING,
4166            )
4167        };
4168        assert!(
4169            raw >= 0,
4170            "memfd_create: {}",
4171            std::io::Error::last_os_error()
4172        );
4173        // SAFETY: `raw` is a descriptor this test just made and owns.
4174        let fd = unsafe { <std::os::fd::OwnedFd as std::os::fd::FromRawFd>::from_raw_fd(raw) };
4175        let seals = crate::syscalls::rendered_procfs::PLACEHOLDER_SEALS;
4176        assert_eq!(unsafe { libc::fcntl(raw, libc::F_ADD_SEALS, seals) }, 0);
4177        let diskstats = std::path::Path::new("/proc/diskstats");
4178        assert!(crate::syscalls::rendered_procfs::named_file_outside_level(
4179            diskstats,
4180            ProcLevel::Minimal
4181        ));
4182        assert!(!crate::syscalls::rendered_procfs::named_file_outside_level(
4183            diskstats,
4184            ProcLevel::Standard
4185        ));
4186        let mut noted = FileMetadata::new(owner);
4187        noted.note_proc_level(ProcLevel::Minimal);
4188        // Carried through serialization, as a table is across a fork or an
4189        // exec on an in-guest backend, before it rediscovers a descriptor.
4190        let mut minimal: FileMetadata =
4191            serde_json::from_slice(&serde_json::to_vec(&noted.for_exec(owner)).unwrap()).unwrap();
4192        minimal
4193            .discover_fd_from_current_process(owner, fd.as_raw_fd())
4194            .unwrap();
4195        assert!(
4196            minimal.file_handles[&fd.as_raw_fd()]
4197                .procfs_rendered_path()
4198                .is_none()
4199        );
4200        let mut standard = FileMetadata::new(owner);
4201        standard.note_proc_level(ProcLevel::Standard);
4202        standard
4203            .discover_fd_from_current_process(owner, fd.as_raw_fd())
4204            .unwrap();
4205        assert_eq!(
4206            standard.file_handles[&fd.as_raw_fd()].procfs_rendered_path(),
4207            Some(diskstats)
4208        );
4209    }
4210
4211    #[test]
4212    fn inherited_terminal_aliases_survive_fork_serialization_and_exec_handoff() {
4213        use std::os::fd::FromRawFd;
4214        let owner = DetTid::from_raw(9);
4215        let (_master, slave) = terminal_pair();
4216        let fd = slave.as_raw_fd();
4217        let duplicate = unsafe { libc::fcntl(fd, libc::F_DUPFD_CLOEXEC, 3) };
4218        assert!(duplicate >= 0);
4219        let duplicate = unsafe { std::os::fd::OwnedFd::from_raw_fd(duplicate) };
4220        let alias = duplicate.as_raw_fd();
4221        // Make this alias survive the real descriptor boundary as the model does.
4222        assert_eq!(unsafe { libc::fcntl(alias, libc::F_SETFD, 0) }, 0);
4223        let mut metadata = FileMetadata::new(owner);
4224        metadata
4225            .discover_fd_from_current_process(owner, fd)
4226            .unwrap();
4227        metadata.file_handles[&fd].set_resource(ResourceID::Device(Device::ContainerStdin));
4228        metadata.discover_inherited_terminals([fd]);
4229        metadata.dup_fd(fd, alias, OFlag::empty()).unwrap();
4230        metadata.dup_fd(fd, 100_000, OFlag::O_CLOEXEC).unwrap();
4231        let child = metadata.fork_for(DetTid::from_raw(10));
4232        let encoded = serde_json::to_vec(&child).unwrap();
4233        let child: FileMetadata = serde_json::from_slice(&encoded).unwrap();
4234        let state = ExecFdState {
4235            inherited_reads: child.inherited_reads_for_exec(),
4236            ..Default::default()
4237        };
4238        assert_eq!(
4239            state.inherited_reads.len(),
4240            2,
4241            "CLOEXEC alias is not transferred"
4242        );
4243        assert_eq!(state.inherited_reads[&fd], state.inherited_reads[&alias]);
4244        let mut restored = FileMetadata::new(owner);
4245        restored
4246            .apply_exec_fd_state(
4247                owner,
4248                serde_json::from_slice(&serde_json::to_vec(&state).unwrap()).unwrap(),
4249                true,
4250            )
4251            .unwrap();
4252        assert_eq!(
4253            restored.file_handles[&fd].open_file_id(),
4254            restored.file_handles[&alias].open_file_id()
4255        );
4256        assert_eq!(
4257            restored.file_handles[&alias].resource(),
4258            Some(ResourceID::Device(Device::ContainerStdin))
4259        );
4260        let flags = unsafe { libc::fcntl(alias, libc::F_GETFL) };
4261        assert_eq!(
4262            unsafe { libc::fcntl(alias, libc::F_SETFL, flags | libc::O_NONBLOCK) },
4263            0
4264        );
4265        restored.file_handles[&alias].set_status_flags(flags | libc::O_NONBLOCK);
4266        assert!(restored.file_handles[&fd].is_nonblocking());
4267        assert!(restored.file_handles[&fd].physically_nonblocking());
4268        assert_eq!(
4269            unsafe { libc::fcntl(fd, libc::F_GETFL) },
4270            flags | libc::O_NONBLOCK
4271        );
4272        let mut byte = 0_u8;
4273        assert_eq!(unsafe { libc::read(fd, (&raw mut byte).cast(), 1) }, -1);
4274        assert_eq!(Errno::last(), Errno::EAGAIN);
4275    }
4276
4277    #[test]
4278    fn guest_created_terminal_and_reused_descriptor_do_not_gain_inherited_provenance() {
4279        let owner = DetTid::from_raw(9);
4280        let (_master, slave) = terminal_pair();
4281        let fd = slave.as_raw_fd();
4282        let mut guest_created = FileMetadata::new(owner);
4283        guest_created
4284            .discover_fd_from_current_process(owner, fd)
4285            .unwrap();
4286        guest_created.dup_fd(fd, 0, OFlag::empty()).unwrap();
4287        assert!(guest_created.inherited_reads_for_exec().is_empty());
4288        let mut fresh = FileMetadata::new(owner);
4289        fresh
4290            .apply_exec_fd_state(owner, ExecFdState::default(), true)
4291            .unwrap();
4292        fresh.discover_fd_from_current_process(owner, fd).unwrap();
4293        assert!(fresh.file_handles[&fd].inherited_external_read().is_none());
4294
4295        guest_created.discover_inherited_terminals([fd]);
4296        let state = ExecFdState {
4297            inherited_reads: guest_created.inherited_reads_for_exec(),
4298            ..Default::default()
4299        };
4300        // Remove the synthetic slot; only the actual owned fd enters this check.
4301        let state = ExecFdState {
4302            inherited_reads: state
4303                .inherited_reads
4304                .into_iter()
4305                .filter(|(number, _)| *number == fd)
4306                .collect(),
4307            ..Default::default()
4308        };
4309        guest_created.remove_fd(fd);
4310        let zero = std::fs::File::open("/dev/zero").unwrap();
4311        assert_eq!(unsafe { libc::dup2(zero.as_raw_fd(), fd) }, fd);
4312        let mut reused = FileMetadata::new(owner);
4313        assert_eq!(
4314            reused.apply_exec_fd_state(owner, state, true),
4315            Err(Errno::EIO)
4316        );
4317        assert!(reused.file_handles[&fd].inherited_external_read().is_none());
4318    }
4319
4320    #[test]
4321    fn on_demand_discovery_finds_a_live_descriptor() {
4322        let owner = DetTid::from_raw(9);
4323        let file = tempfile::NamedTempFile::new().expect("create test descriptor");
4324        let fd = file.as_raw_fd();
4325        assert_eq!(unsafe { libc::flock(fd, libc::LOCK_SH | libc::LOCK_NB) }, 0);
4326        let mut metadata = FileMetadata::new(owner);
4327
4328        assert_eq!(metadata.with_detfd(fd, |_| ()), Err(Errno::EBADF));
4329        metadata
4330            .discover_fd_from_current_process(owner, fd)
4331            .expect("live descriptor should be discovered");
4332        assert_eq!(
4333            metadata
4334                .with_detfd(fd, |detfd| detfd.ty())
4335                .expect("discovered descriptor should be tracked"),
4336            FdType::Regular
4337        );
4338        assert_eq!(
4339            metadata
4340                .with_detfd(fd, |detfd| detfd.known_flock_mode())
4341                .expect("discovered descriptor should be tracked"),
4342            None,
4343            "a live descriptor may already hold a flock that Detcore did not observe"
4344        );
4345        assert!(
4346            !metadata
4347                .with_detfd(fd, |detfd| detfd.flock_mode_may_be_stale())
4348                .expect("discovered descriptor should be tracked"),
4349            "first discovery must not invent a previously known lock state"
4350        );
4351        assert!(
4352            !metadata.has_unsafe_vfork_flock_state(),
4353            "never-observed history is distinct from a held or invalidated cached claim"
4354        );
4355
4356        // The metadata repair must not query by converting or dropping the
4357        // inherited lock. A separate open must still be excluded by it.
4358        let separate = std::fs::OpenOptions::new()
4359            .read(true)
4360            .write(true)
4361            .open(file.path())
4362            .expect("open an independent file description");
4363        assert_eq!(
4364            unsafe { libc::flock(separate.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) },
4365            -1
4366        );
4367        assert_eq!(Errno::last(), Errno::EWOULDBLOCK);
4368
4369        metadata
4370            .with_detfd(fd, |detfd| detfd.set_flock_mode(None))
4371            .expect("discovered descriptor should remain tracked");
4372        assert_eq!(metadata.file_handles[&fd].known_flock_mode(), None);
4373        assert!(!metadata.file_handles[&fd].flock_mode_may_be_stale());
4374    }
4375
4376    #[test]
4377    fn discovery_classifies_random_devices_by_device_number() {
4378        let owner = DetTid::from_raw(9);
4379        let random = std::fs::File::open("/dev/random").unwrap();
4380        let urandom = std::fs::File::open("/dev/urandom").unwrap();
4381        let null = std::fs::File::open("/dev/null").unwrap();
4382        let path_only = nix::fcntl::open(
4383            "/dev/urandom",
4384            OFlag::O_PATH | OFlag::O_CLOEXEC,
4385            nix::sys::stat::Mode::empty(),
4386        )
4387        .unwrap();
4388        let mut metadata = FileMetadata::new(owner);
4389        for (fd, expected) in [
4390            (random.as_raw_fd(), FdType::Rng),
4391            (urandom.as_raw_fd(), FdType::Rng),
4392            // Another character device, and an `O_PATH` descriptor of a
4393            // random device, which reads nothing, stay `Regular`.
4394            (null.as_raw_fd(), FdType::Regular),
4395            (path_only.as_raw_fd(), FdType::Regular),
4396        ] {
4397            metadata
4398                .discover_fd_from_current_process(owner, fd)
4399                .expect("live descriptor should be discovered");
4400            assert_eq!(metadata.file_handles[&fd].ty(), expected, "fd {fd}");
4401        }
4402        // A discovered random device is named by its descriptor, apart from
4403        // the count of opens, which discovery leaves alone.
4404        for fd in [random.as_raw_fd(), urandom.as_raw_fd()] {
4405            assert_eq!(
4406                metadata.file_handles[&fd].random_device_stream(),
4407                crate::fd::RandomDeviceStream::discovered(owner, fd)
4408            );
4409        }
4410        assert_eq!(metadata.next_random_device_sequence, 0);
4411    }
4412
4413    #[test]
4414    fn rediscovery_preserves_known_and_invalidated_flock_history() {
4415        let owner = DetTid::from_raw(9);
4416        let file = tempfile::tempfile().expect("create test descriptor");
4417        let fd = file.as_raw_fd();
4418        let mut metadata = FileMetadata::new(owner);
4419        metadata
4420            .add_fd(owner, fd, OFlag::O_RDWR, FdType::Regular, None)
4421            .expect("register an observed open");
4422        let description = metadata.file_handles[&fd].open_file_id();
4423        assert_eq!(metadata.file_handles[&fd].known_flock_mode(), Some(None));
4424        metadata.file_handles[&fd].set_flock_mode(Some(libc::LOCK_SH));
4425
4426        metadata
4427            .discover_fd_from_current_process(owner, fd)
4428            .expect("rediscover an existing descriptor");
4429        assert_eq!(metadata.file_handles[&fd].open_file_id(), description);
4430        assert_eq!(
4431            metadata.file_handles[&fd].known_flock_mode(),
4432            Some(Some(libc::LOCK_SH))
4433        );
4434        assert!(metadata.has_unsafe_vfork_flock_state());
4435
4436        metadata.file_handles[&fd].forget_flock_mode();
4437        metadata
4438            .discover_fd_from_current_process(owner, fd)
4439            .expect("rediscover an invalidated descriptor");
4440        assert_eq!(metadata.file_handles[&fd].open_file_id(), description);
4441        assert_eq!(metadata.file_handles[&fd].known_flock_mode(), None);
4442        assert!(metadata.file_handles[&fd].flock_mode_may_be_stale());
4443        assert!(metadata.file_handles[&fd].flock_may_have_been_granted());
4444        assert!(metadata.has_unsafe_vfork_flock_state());
4445    }
4446
4447    /// The converse of the case above: a description Detcore created, never
4448    /// locked through Detcore, then invalidated by a fork (a `make` job pipe)
4449    /// has no Detcore claim in any process that a copied `vfork` child's
4450    /// native lock call could make stale, so it must not refuse `vfork`.
4451    #[test]
4452    fn invalidated_description_never_locked_does_not_refuse_vfork() {
4453        let owner = DetTid::from_raw(9);
4454        let mut metadata = FileMetadata::new(owner);
4455        metadata
4456            .add_fd(owner, 3, OFlag::empty(), FdType::Pipe, None)
4457            .expect("register a pipe end");
4458        assert!(!metadata.file_handles[&3].flock_may_have_been_granted());
4459
4460        let child = metadata.fork_for(DetTid::from_raw(10));
4461        for table in [&metadata, &child] {
4462            let detfd = &table.file_handles[&3];
4463            assert_eq!(detfd.known_flock_mode(), None);
4464            assert!(detfd.flock_mode_may_be_stale());
4465            assert!(!detfd.flock_may_have_been_granted());
4466            assert!(
4467                !table.has_unsafe_vfork_flock_state(),
4468                "an invalidated description with no grant through it holds no Detcore claim"
4469            );
4470        }
4471
4472        // A grant after the invalidation is still a grant: the mode stays
4473        // unknown, but the description may now hold a lock.
4474        metadata.file_handles[&3].set_flock_mode(Some(libc::LOCK_EX));
4475        assert_eq!(metadata.file_handles[&3].known_flock_mode(), None);
4476        assert!(metadata.file_handles[&3].flock_may_have_been_granted());
4477        assert!(metadata.has_unsafe_vfork_flock_state());
4478    }
4479
4480    /// A serialized description that predates `flock_ever_granted` reads it
4481    /// as true in a self-describing format, so an invalidated description
4482    /// from such a table still refuses `vfork`.
4483    #[test]
4484    fn a_description_serialized_without_the_grant_fact_reads_it_as_granted() {
4485        let owner = DetTid::from_raw(9);
4486        let mut metadata = FileMetadata::new(owner);
4487        metadata
4488            .add_fd(owner, 3, OFlag::empty(), FdType::Pipe, None)
4489            .expect("register a pipe end");
4490        let child = metadata.fork_for(DetTid::from_raw(10));
4491        assert!(!child.has_unsafe_vfork_flock_state());
4492        let encoded = serde_json::to_string(&child).expect("serialize the table");
4493        assert_eq!(encoded.matches("\"flock_ever_granted\":false").count(), 1);
4494        let without = encoded.replace("\"flock_ever_granted\":false,", "");
4495        assert_ne!(without, encoded, "the field must have been removed");
4496        let restored: FileMetadata = serde_json::from_str(&without).expect("deserialize the table");
4497        assert!(restored.file_handles[&3].flock_mode_may_be_stale());
4498        assert!(restored.file_handles[&3].flock_may_have_been_granted());
4499        assert!(restored.has_unsafe_vfork_flock_state());
4500    }
4501
4502    /// A lock granted before a fork travels with the description: both
4503    /// tables refuse `vfork` after the fork. The fact is deliberately sticky,
4504    /// so a lock released before the fork still counts: the guard stays a
4505    /// single monotone fact rather than a second lock-state cache.
4506    #[test]
4507    fn lock_granted_before_fork_still_refuses_vfork_after_it() {
4508        let owner = DetTid::from_raw(9);
4509        let mut metadata = FileMetadata::new(owner);
4510        metadata
4511            .add_fd(owner, 3, OFlag::empty(), FdType::Regular, None)
4512            .expect("register descriptor");
4513        metadata.file_handles[&3].set_flock_mode(Some(libc::LOCK_SH));
4514        metadata.file_handles[&3].set_flock_mode(None);
4515        assert_eq!(metadata.file_handles[&3].known_flock_mode(), Some(None));
4516        assert!(
4517            !metadata.has_unsafe_vfork_flock_state(),
4518            "a known-unlocked description is safe while its mode is known"
4519        );
4520
4521        let child = metadata.fork_for(DetTid::from_raw(10));
4522        for table in [&metadata, &child] {
4523            assert!(table.file_handles[&3].flock_may_have_been_granted());
4524            assert!(table.has_unsafe_vfork_flock_state());
4525        }
4526    }
4527
4528    /// A table carried through serialization (in-guest LiteInst's fork) keeps
4529    /// its aliases: two descriptors that shared one open file description
4530    /// before still share one object after, while a separate open stays
4531    /// separate and descriptor flags stay per descriptor.
4532    #[test]
4533    fn a_deserialized_table_keeps_dup_aliases_as_one_open_file() {
4534        let owner = DetTid::from_raw(9);
4535        let mut metadata = FileMetadata::new(owner);
4536        metadata
4537            .add_fd(owner, 3, OFlag::empty(), FdType::Rng, None)
4538            .expect("register descriptor");
4539        metadata
4540            .dup_fd(3, 4, OFlag::O_CLOEXEC)
4541            .expect("dup the descriptor");
4542        metadata
4543            .add_fd(owner, 5, OFlag::empty(), FdType::Rng, None)
4544            .expect("register a separate open");
4545        let encoded = serde_json::to_string(&metadata).expect("serialize the table");
4546        let restored: FileMetadata = serde_json::from_str(&encoded).expect("deserialize the table");
4547        let three = restored.file_handles[&3].clone();
4548        let four = restored.file_handles[&4].clone();
4549        let five = restored.file_handles[&5].clone();
4550        assert_eq!(three.open_file_id(), four.open_file_id());
4551        assert_ne!(three.open_file_id(), five.open_file_id());
4552        // One object again: a read through one alias moves the other's cursor.
4553        four.advance_random_device_offset(50);
4554        assert_eq!(three.random_device_offset(), 50);
4555        assert_eq!(five.random_device_offset(), 0);
4556        // The table plus the two local handles above.
4557        assert_eq!(three.open_file_alias_count(), 4);
4558        // Descriptor flags stay per descriptor.
4559        assert!(!three.is_cloexec());
4560        assert!(four.is_cloexec());
4561    }
4562
4563    /// Each open of a random device reads a stream of its own, named by its
4564    /// opener and the table's count of earlier random-device opens; a dup
4565    /// alias shares it. Other opens leave the count alone. Fork and exec
4566    /// carry it, and so does the exec exchange of a backend that rebuilds its
4567    /// table, so no later open in the table or a copy of it reuses a name.
4568    #[test]
4569    fn each_random_device_open_reads_its_own_stream() {
4570        use crate::fd::RandomDeviceStream;
4571        let owner = DetTid::from_raw(9);
4572        let thread = DetTid::from_raw(11);
4573        let stream = |table: &mut FileMetadata, fd| {
4574            table
4575                .with_detfd(fd, |detfd| detfd.random_device_stream())
4576                .expect("registered descriptor")
4577        };
4578        let open = |table: &mut FileMetadata, creator, fd| {
4579            table
4580                .add_fd(creator, fd, OFlag::empty(), FdType::Regular, None)
4581                .expect("register the open");
4582            table
4583                .classify_random_device(creator, fd)
4584                .expect("classify it");
4585        };
4586        let mut metadata = FileMetadata::new(owner);
4587        open(&mut metadata, owner, 3);
4588        metadata.dup_fd(3, 4, OFlag::empty()).expect("dup it");
4589        metadata
4590            .add_fd(owner, 5, OFlag::empty(), FdType::Regular, None)
4591            .expect("open a regular file");
4592        open(&mut metadata, thread, 6);
4593        assert_eq!(
4594            metadata.classify_random_device(thread, 7),
4595            Err(Errno::EBADF)
4596        );
4597        let child = DetTid::from_raw(10);
4598        let mut forked = metadata.fork_for(child);
4599        open(&mut forked, child, 7);
4600        let mut exec = metadata.for_exec(owner);
4601        open(&mut exec, owner, 7);
4602        let mut rebuilt = FileMetadata::new(owner);
4603        rebuilt
4604            .apply_exec_fd_state(
4605                owner,
4606                ExecFdState {
4607                    random_device_sequence: 2,
4608                    ..ExecFdState::default()
4609                },
4610                true,
4611            )
4612            .expect("apply the exec exchange");
4613        open(&mut rebuilt, owner, 3);
4614
4615        let named = |creator, sequence| RandomDeviceStream { creator, sequence };
4616        assert_eq!(stream(&mut metadata, 3), named(owner, 0));
4617        assert_eq!(stream(&mut metadata, 4), named(owner, 0));
4618        assert_eq!(stream(&mut metadata, 6), named(thread, 1));
4619        assert_eq!(stream(&mut forked, 3), named(owner, 0));
4620        assert_eq!(stream(&mut forked, 7), named(child, 2));
4621        assert_eq!(stream(&mut exec, 7), named(owner, 2));
4622        assert_eq!(stream(&mut rebuilt, 3), named(owner, 2));
4623    }
4624
4625    #[test]
4626    fn fork_makes_inherited_flock_state_unknown_in_parent_and_child() {
4627        let owner = DetTid::from_raw(9);
4628        let child = DetTid::from_raw(10);
4629        let mut metadata = FileMetadata::new(owner);
4630        metadata
4631            .add_fd(owner, 3, OFlag::empty(), FdType::Regular, None)
4632            .expect("register descriptor");
4633        metadata
4634            .with_detfd(3, |detfd| detfd.set_flock_mode(Some(libc::LOCK_SH)))
4635            .expect("registered descriptor");
4636
4637        let mut child_metadata = metadata.fork_for(child);
4638        assert_eq!(
4639            metadata
4640                .with_detfd(3, |detfd| detfd.known_flock_mode())
4641                .expect("parent descriptor should remain tracked"),
4642            None,
4643            "parent cache must not become stale when the child changes the shared open file description"
4644        );
4645        assert_eq!(
4646            child_metadata
4647                .with_detfd(3, |detfd| detfd.known_flock_mode())
4648                .expect("child descriptor should be inherited"),
4649            None,
4650            "child starts with the same deliberately unknown shared state"
4651        );
4652    }
4653
4654    #[test]
4655    fn exec_preserves_known_flock_mode_for_surviving_descriptors() {
4656        let owner = DetTid::from_raw(9);
4657        let mut metadata = FileMetadata::new(owner);
4658        metadata
4659            .add_fd(owner, 3, OFlag::empty(), FdType::Regular, None)
4660            .expect("register descriptor");
4661        metadata
4662            .with_detfd(3, |detfd| detfd.set_flock_mode(Some(libc::LOCK_SH)))
4663            .expect("registered descriptor");
4664
4665        let mut after_exec = metadata.for_exec(DetTid::from_raw(10));
4666        assert_eq!(
4667            after_exec
4668                .with_detfd(3, |detfd| detfd.known_flock_mode())
4669                .expect("non-cloexec descriptor should survive"),
4670            Some(Some(libc::LOCK_SH)),
4671            "exec must preserve known flock state for a surviving open file description"
4672        );
4673    }
4674
4675    #[test]
4676    fn discovered_stdio_uses_container_wide_resources() {
4677        let owner = DetTid::from_raw(9);
4678        let mut metadata = FileMetadata::new(owner);
4679
4680        metadata
4681            .discover_fd_from_current_process(owner, libc::STDOUT_FILENO)
4682            .expect("live stdout should be discovered");
4683
4684        assert_eq!(
4685            metadata
4686                .with_detfd(libc::STDOUT_FILENO, |detfd| detfd.resource())
4687                .expect("discovered stdout should be tracked"),
4688            Some(ResourceID::Device(Device::ContainerStdout))
4689        );
4690
4691        // Observe real metadata transitions without closing the test process's
4692        // stdio. The narrow repair preserves slot identities for inherited
4693        // resources and leaves aliases above fd 2 in the ordinary inode pool.
4694        use crate::syscalls::deterministic_stdio_inode_for_resource;
4695
4696        let inherited_stat = metadata
4697            .with_detfd(libc::STDOUT_FILENO, |detfd| detfd.stat().unwrap())
4698            .unwrap();
4699        metadata.dup_fd(1, 7, OFlag::empty()).unwrap();
4700        assert_eq!(
4701            metadata.with_detfd(7, |detfd| detfd.resource()).unwrap(),
4702            Some(ResourceID::Device(Device::ContainerStdout))
4703        );
4704        assert_eq!(
4705            metadata
4706                .with_detfd(7, |detfd| {
4707                    deterministic_stdio_inode_for_resource(7, detfd.resource())
4708                })
4709                .unwrap(),
4710            None
4711        );
4712        for fd in 0..=2 {
4713            metadata.remove_fd(fd);
4714            let mut ordinary = inherited_stat;
4715            ordinary.inode = 123 + fd as u64;
4716            metadata
4717                .add_fd(owner, fd, OFlag::empty(), FdType::Regular, Some(ordinary))
4718                .unwrap();
4719            metadata.dup_fd(fd, 8, OFlag::empty()).unwrap();
4720            for ordinary_fd in [fd, 8] {
4721                assert_eq!(
4722                    metadata
4723                        .with_detfd(ordinary_fd, |detfd| {
4724                            deterministic_stdio_inode_for_resource(ordinary_fd, detfd.resource())
4725                        })
4726                        .unwrap(),
4727                    None
4728                );
4729            }
4730            assert_eq!(
4731                metadata
4732                    .with_detfd(fd, |detfd| detfd.open_file_id())
4733                    .unwrap(),
4734                metadata
4735                    .with_detfd(8, |detfd| detfd.open_file_id())
4736                    .unwrap()
4737            );
4738            metadata.dup_fd(7, fd, OFlag::empty()).unwrap();
4739            assert_eq!(
4740                metadata
4741                    .with_detfd(fd, |detfd| {
4742                        deterministic_stdio_inode_for_resource(fd, detfd.resource())
4743                    })
4744                    .unwrap(),
4745                Some(DetInode::mint(1000 + fd as u64)),
4746                "inherited streams retain the existing numeric-slot outcome"
4747            );
4748        }
4749        metadata.dup_fd(7, 9, OFlag::O_CLOEXEC).unwrap();
4750        let child = metadata.fork_for(DetTid::from_raw(10));
4751        let after_exec = child.for_exec(DetTid::from_raw(10));
4752        assert!(!after_exec.file_handles.contains_key(&9));
4753        assert!(after_exec.file_handles.contains_key(&7));
4754    }
4755
4756    #[test]
4757    fn discovered_pipe_preserves_inherited_nonblocking() {
4758        let owner = DetTid::from_raw(9);
4759        let mut fds = [-1; 2];
4760        assert_eq!(
4761            unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_NONBLOCK) },
4762            0
4763        );
4764        let mut metadata = FileMetadata::new(owner);
4765
4766        metadata
4767            .discover_fd_from_current_process(owner, fds[0])
4768            .expect("live pipe should be discovered");
4769        let flags = metadata
4770            .with_detfd(fds[0], |detfd| {
4771                (detfd.is_nonblocking(), detfd.physically_nonblocking())
4772            })
4773            .expect("discovered pipe should be tracked");
4774
4775        assert_eq!(flags, (true, true));
4776        unsafe {
4777            libc::close(fds[0]);
4778            libc::close(fds[1]);
4779        }
4780    }
4781
4782    #[test]
4783    fn exec_handoff_restores_scheduler_pipe_as_logically_blocking() {
4784        let owner = DetTid::from_raw(9);
4785        let mut fds = [-1; 2];
4786        assert_eq!(
4787            unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_NONBLOCK) },
4788            0
4789        );
4790        let mut before_exec = FileMetadata::new(owner);
4791        before_exec
4792            .add_fd(owner, fds[0], OFlag::empty(), FdType::Pipe, None)
4793            .expect("scheduler pipe should be registered");
4794        before_exec
4795            .with_detfd(fds[0], |detfd| detfd.set_physically_nonblocking())
4796            .expect("scheduler pipe should remain registered");
4797
4798        let overrides = before_exec.exec_blocking_overrides();
4799        assert_eq!(overrides, BTreeSet::from([fds[0]]));
4800
4801        let mut after_exec = FileMetadata::new(owner);
4802        after_exec.apply_exec_blocking_overrides(owner, overrides);
4803        assert_eq!(
4804            after_exec
4805                .with_detfd(fds[0], |detfd| {
4806                    (detfd.is_nonblocking(), detfd.physically_nonblocking())
4807                })
4808                .expect("inherited pipe should be rediscovered"),
4809            (false, true)
4810        );
4811
4812        unsafe {
4813            libc::close(fds[0]);
4814            libc::close(fds[1]);
4815        }
4816    }
4817
4818    #[test]
4819    fn fork_copies_slots_but_preserves_open_file_aliases() {
4820        let parent_tid = DetTid::from_raw(10);
4821        let child_tid = DetTid::from_raw(11);
4822        let mut parent = FileMetadata::new(parent_tid);
4823        parent
4824            .add_fd(parent_tid, 3, OFlag::O_NONBLOCK, FdType::Socket, None)
4825            .expect("parent fd should be inserted");
4826        parent
4827            .dup_fd(3, 4, OFlag::O_CLOEXEC)
4828            .expect("dup should succeed");
4829
4830        let parent_open = parent
4831            .with_detfd(3, |fd| fd.open_file_id())
4832            .expect("parent fd should exist");
4833        let duplicate_open = parent
4834            .with_detfd(4, |fd| fd.open_file_id())
4835            .expect("duplicate fd should exist");
4836        assert_eq!(parent_open, duplicate_open);
4837
4838        let initial_timestamp = LogicalTime::from_nanos(1_234_567_890);
4839        parent
4840            .with_detfd(3, |fd| fd.set_socket_receive_timestamp(initial_timestamp))
4841            .expect("parent socket should accept a receive timestamp");
4842
4843        let mut child = parent.fork_for(child_tid);
4844        assert_ne!(parent.files_id, child.files_id);
4845        assert_ne!(
4846            FdSlot {
4847                files: parent.files_id,
4848                fd: 3,
4849            },
4850            FdSlot {
4851                files: child.files_id,
4852                fd: 3,
4853            }
4854        );
4855        assert_eq!(
4856            parent_open,
4857            child
4858                .with_detfd(3, |fd| fd.open_file_id())
4859                .expect("forked fd should retain its open file identity")
4860        );
4861        assert_eq!(
4862            child
4863                .with_detfd(3, |fd| fd.socket_receive_timestamp())
4864                .expect("forked fd should retain its receive timestamp"),
4865            Some(initial_timestamp)
4866        );
4867        let child_timestamp = LogicalTime::from_nanos(2_345_678_901);
4868        child
4869            .with_detfd(3, |fd| fd.set_socket_receive_timestamp(child_timestamp))
4870            .expect("child socket should update the shared receive timestamp");
4871        assert_eq!(
4872            parent
4873                .with_detfd(4, |fd| fd.socket_receive_timestamp())
4874                .expect("parent duplicate should see the child update"),
4875            Some(child_timestamp)
4876        );
4877
4878        parent
4879            .add_fd(parent_tid, 5, OFlag::empty(), FdType::Regular, None)
4880            .expect("new parent fd should be inserted");
4881        child
4882            .add_fd(child_tid, 5, OFlag::empty(), FdType::Regular, None)
4883            .expect("new child fd should be inserted");
4884        assert_ne!(
4885            parent
4886                .with_detfd(5, |fd| fd.open_file_id())
4887                .expect("new parent fd should exist"),
4888            child
4889                .with_detfd(5, |fd| fd.open_file_id())
4890                .expect("new child fd should exist"),
4891            "separate opens after fork must not alias"
4892        );
4893    }
4894
4895    #[test]
4896    fn pidfd_getfd_target_requires_the_calling_leader() {
4897        let leader = DetPid::from_raw(31);
4898        assert!(pidfd_getfd_targets_calling_task(
4899            Some(leader),
4900            leader,
4901            DetTid::from_raw(31),
4902        ));
4903        assert!(!pidfd_getfd_targets_calling_task(
4904            Some(leader),
4905            leader,
4906            DetTid::from_raw(32),
4907        ));
4908        assert!(!pidfd_getfd_targets_calling_task(
4909            Some(DetPid::from_raw(32)),
4910            leader,
4911            DetTid::from_raw(31),
4912        ));
4913    }
4914
4915    #[test]
4916    fn captured_fd_installs_when_the_descriptor_table_is_unchanged() {
4917        let owner = DetTid::from_raw(32);
4918        let mut metadata = FileMetadata::new(owner);
4919        metadata
4920            .add_fd(owner, 3, OFlag::empty(), FdType::Regular, None)
4921            .expect("source should be inserted");
4922        let source_id = metadata
4923            .with_detfd(3, |fd| fd.open_file_id())
4924            .expect("source should exist");
4925        let captured = metadata.capture_fd(3).expect("source should be captured");
4926
4927        assert_eq!(
4928            metadata
4929                .install_captured_fd(captured, 4, OFlag::O_CLOEXEC)
4930                .expect("an unchanged table should accept the captured alias"),
4931            None
4932        );
4933        assert_eq!(
4934            metadata
4935                .with_detfd(4, |fd| (fd.open_file_id(), fd.is_cloexec()))
4936                .expect("captured alias should be installed"),
4937            (source_id, true)
4938        );
4939    }
4940
4941    #[test]
4942    fn failed_captured_fd_install_preserves_cleanup_obligations() {
4943        let owner = DetTid::from_raw(36);
4944        let mut original = FileMetadata::new(owner);
4945        original
4946            .add_fd(owner, 3, OFlag::empty(), FdType::Socket, None)
4947            .expect("source should be inserted");
4948        let source_id = original
4949            .with_detfd(3, |fd| fd.open_file_id())
4950            .expect("source should exist");
4951        let captured = original.capture_fd(3).expect("source should be captured");
4952        assert_eq!(
4953            original.remove_fd(3),
4954            None,
4955            "the capture defers final-OFD cleanup while the syscall is in flight"
4956        );
4957
4958        let mut replacement_table = FileMetadata::new(DetTid::from_raw(37));
4959        let failure = replacement_table
4960            .install_captured_fd(captured, 41, OFlag::O_CLOEXEC)
4961            .expect_err("a capture must not cross descriptor-table identity");
4962        assert_ne!(failure.expected_files_id, failure.actual_files_id);
4963        let cleanup = failure.into_cleanup();
4964        assert_eq!(cleanup.close_fd, 41);
4965        assert_eq!(cleanup.release_open_file, Some(source_id));
4966    }
4967
4968    #[test]
4969    fn equal_fd_dup_preserves_descriptor_flags() {
4970        let owner = DetTid::from_raw(20);
4971        let mut metadata = FileMetadata::new(owner);
4972        metadata
4973            .add_fd(owner, 3, OFlag::O_CLOEXEC, FdType::Regular, None)
4974            .expect("fd should be inserted");
4975
4976        assert_eq!(
4977            metadata
4978                .dup_fd(3, 3, OFlag::empty())
4979                .expect("equal-fd dup should validate the source"),
4980            None
4981        );
4982        assert!(
4983            metadata
4984                .with_detfd(3, |fd| fd.is_cloexec())
4985                .expect("fd should remain present"),
4986            "dup2(fd, fd) must not clear close-on-exec"
4987        );
4988    }
4989
4990    #[test]
4991    fn last_open_file_alias_survives_dup_and_fork() {
4992        let parent_tid = DetTid::from_raw(30);
4993        let child_tid = DetTid::from_raw(31);
4994        let mut parent = FileMetadata::new(parent_tid);
4995        parent
4996            .add_fd(parent_tid, 3, OFlag::empty(), FdType::Socket, None)
4997            .expect("socket should be inserted");
4998        let open_file_id = parent
4999            .with_detfd(3, |fd| fd.open_file_id())
5000            .expect("socket should exist");
5001        assert_eq!(
5002            parent
5003                .dup_fd(3, 4, OFlag::empty())
5004                .expect("dup should succeed"),
5005            None
5006        );
5007        assert_eq!(parent.remove_fd(3), None, "duplicate retains the OFD");
5008
5009        let mut child = parent.fork_for(child_tid);
5010        assert_eq!(parent.remove_fd(4), None, "forked child retains the OFD");
5011        assert_eq!(
5012            child.remove_fd(4),
5013            Some(open_file_id),
5014            "only the final alias releases the OFD"
5015        );
5016
5017        let mut replacement = FileMetadata::new(parent_tid);
5018        replacement
5019            .add_fd(parent_tid, 3, OFlag::empty(), FdType::Socket, None)
5020            .expect("source should be inserted");
5021        replacement
5022            .add_fd(parent_tid, 4, OFlag::empty(), FdType::Socket, None)
5023            .expect("target should be inserted");
5024        let target_id = replacement
5025            .with_detfd(4, |fd| fd.open_file_id())
5026            .expect("target should exist");
5027        assert_eq!(
5028            replacement
5029                .dup_fd(3, 4, OFlag::empty())
5030                .expect("dup replacement should succeed"),
5031            Some(target_id),
5032            "replacing the target must release its last OFD alias"
5033        );
5034    }
5035
5036    #[test]
5037    fn close_range_removes_selected_slots_and_releases_final_aliases() {
5038        let owner = DetTid::from_raw(35);
5039        let mut metadata = FileMetadata::new(owner);
5040        metadata
5041            .add_fd(owner, 3, OFlag::empty(), FdType::Regular, None)
5042            .expect("source should be inserted");
5043        metadata
5044            .dup_fd(3, 4, OFlag::empty())
5045            .expect("duplicate should be inserted");
5046        metadata
5047            .add_fd(owner, 100, OFlag::empty(), FdType::Regular, None)
5048            .expect("high fd should be inserted");
5049        let high_id = metadata
5050            .with_detfd(100, |fd| fd.open_file_id())
5051            .expect("high fd should exist");
5052
5053        assert_eq!(metadata.remove_fd_range(4, 100), [high_id]);
5054        assert!(metadata.with_detfd(3, |_| ()).is_ok());
5055        assert_eq!(metadata.with_detfd(4, |_| ()), Err(Errno::EBADF));
5056        assert_eq!(metadata.with_detfd(100, |_| ()), Err(Errno::EBADF));
5057    }
5058
5059    #[test]
5060    fn exec_reports_only_cloexec_open_files_with_no_other_aliases() {
5061        let owner = DetTid::from_raw(40);
5062        let child_tid = DetTid::from_raw(41);
5063        let mut metadata = FileMetadata::new(owner);
5064        metadata
5065            .add_fd(owner, 3, OFlag::O_CLOEXEC, FdType::Socket, None)
5066            .expect("socket should be inserted");
5067        let open_file_id = metadata
5068            .with_detfd(3, |fd| fd.open_file_id())
5069            .expect("socket should exist");
5070
5071        assert_eq!(metadata.open_files_closed_on_exec(false), [open_file_id]);
5072        assert!(
5073            metadata.open_files_closed_on_exec(true).is_empty(),
5074            "a shared descriptor table retains the original slot"
5075        );
5076
5077        let child = metadata.fork_for(child_tid);
5078        assert!(
5079            metadata.open_files_closed_on_exec(false).is_empty(),
5080            "a copied table retains an OFD alias"
5081        );
5082        drop(child);
5083
5084        metadata
5085            .dup_fd(3, 4, OFlag::empty())
5086            .expect("non-CLOEXEC alias should be created");
5087        assert!(
5088            metadata.open_files_closed_on_exec(false).is_empty(),
5089            "a non-CLOEXEC alias keeps the OFD live across exec"
5090        );
5091    }
5092
5093    /// A rendered per-process file `spelling` (`/proc/<pid>/<name>`) bound to
5094    /// a task, as a rendered open makes one.
5095    fn rendered_record_file(spelling: &str) -> crate::procfs::ProcfsFile {
5096        let mut file = crate::procfs::ProcfsFile::from_path(Path::new(spelling)).unwrap();
5097        file.bind_task(3, 5);
5098        file.set_task_incarnation(crate::procfs::TaskIncarnation {
5099            process: 1,
5100            thread: None,
5101        });
5102        file.into_rendered()
5103    }
5104
5105    /// A per-process file that follows its record's sequence survives an
5106    /// exec on a backend that rebuilds its descriptor table with its open
5107    /// file's identity, its model and its sequence (the cursor, where the
5108    /// sequence is, the buffered record), aliases kept and a close-on-exec
5109    /// copy dropped, and with the old table's numbering: so the new image
5110    /// continues the record where every holder's sequence is (Claude's
5111    /// narrow check of the kvm lane's slice N3, round 5: before, the new image
5112    /// rebuilt it with no sequence) and allocates no identity a sequence in
5113    /// Detcore's global state may still hold (Codex's, round 5: before, it
5114    /// numbered from 0 again), also where the only such description closed
5115    /// at the exec after a fork shared its sequence. A survivor whose sequence
5116    /// Detcore cannot know stays so, and a slot reused since the preparation
5117    /// is refused (EIO).
5118    #[test]
5119    fn record_sequence_exec_survivors_keep_identity_sequence_and_numbering() {
5120        if isolated_child(
5121            "tool_local::file_metadata_tests::record_sequence_exec_survivors_keep_identity_sequence_and_numbering",
5122        ) {
5123            return;
5124        }
5125        use crate::procfs::ProcfsFile;
5126        use crate::procfs::ProcfsSnapshotContext;
5127
5128        let owner = DetTid::from_raw(9031);
5129        let config = Config::default();
5130        let mut parent = ThreadState::new(owner, &config, ());
5131        let statm = std::fs::File::open("/proc/self/statm").unwrap();
5132        let alias = statm.try_clone().unwrap();
5133        let closed = statm.try_clone().unwrap();
5134        let received = std::fs::File::open("/proc/self/statm").unwrap();
5135        let (fd, alias_fd, closed_fd, received_fd) = (
5136            statm.as_raw_fd(),
5137            alias.as_raw_fd(),
5138            closed.as_raw_fd(),
5139            received.as_raw_fd(),
5140        );
5141        for number in [fd, received_fd] {
5142            parent
5143                .add_fd(number, OFlag::O_RDONLY, FdType::Regular, None)
5144                .unwrap();
5145            parent
5146                .with_detfd(number, |handle| {
5147                    handle.set_path("/proc/self/statm");
5148                    handle
5149                        .set_procfs(ProcfsFile::from_path(Path::new("/proc/self/statm")).unwrap());
5150                })
5151                .unwrap();
5152        }
5153        parent.dup_fd(fd, alias_fd, OFlag::empty()).unwrap();
5154        parent.dup_fd(fd, closed_fd, OFlag::O_CLOEXEC).unwrap();
5155        let record = b"1 2 3 4 5 6 7\n".to_vec();
5156        parent
5157            .with_detfd(fd, |handle| {
5158                handle.initialize_procfs(record.clone(), ProcfsSnapshotContext::default());
5159                handle.commit_procfs_read(0, 4);
5160                handle.procfs_note_read_end(4);
5161            })
5162            .unwrap();
5163        parent
5164            .with_detfd(received_fd, |handle| handle.mark_record_sequence_unknown())
5165            .unwrap();
5166        // A fork marks the sequences shared.
5167        parent.share_procfs_sequences();
5168        let old_id = parent.with_detfd(fd, |h| h.open_file_id()).unwrap();
5169        let sequence = parent
5170            .with_detfd(fd, |h| h.procfs_sequence())
5171            .unwrap()
5172            .unwrap();
5173        let records = parent.procfs_records_for_exec().unwrap().unwrap();
5174        assert!(!records.handles.contains_key(&closed_fd));
5175        assert_eq!(
5176            records.handles[&fd].identity.id,
5177            records.handles[&alias_fd].identity.id
5178        );
5179        assert!(records.handles[&received_fd].sequence_unknown);
5180        let next_sequence = records.next_sequence;
5181        let state = ExecFdState {
5182            procfs_records: Some(records),
5183            ..ExecFdState::default()
5184        };
5185        assert!(!state.is_empty());
5186        assert_eq!(state.open_file_creator(), Some(owner));
5187        // The serialized exec preparation and completion payload.
5188        let state: ExecFdState =
5189            serde_json::from_slice(&serde_json::to_vec(&state).unwrap()).unwrap();
5190        let receiving = DetTid::from_raw(9032);
5191        let mut successor_thread = ThreadState::new(receiving, &config, ());
5192        // The creator choice the exec-completion handler makes.
5193        successor_thread.set_open_file_creator(state.open_file_creator().unwrap());
5194        let mut successor = successor_thread.metadata();
5195        successor
5196            .apply_exec_fd_state(receiving, state.clone(), true)
5197            .unwrap();
5198        assert!(!successor.file_handles.contains_key(&closed_fd));
5199        for number in [fd, alias_fd] {
5200            let handle = &successor.file_handles[&number];
5201            assert_eq!(handle.open_file_id(), old_id);
5202            assert_eq!(handle.procfs_shared_sequence(), Some(old_id));
5203            assert_eq!(handle.procfs_sequence(), Some(sequence.clone()));
5204            assert!(!handle.record_sequence_unknown());
5205        }
5206        assert!(successor.file_handles[&received_fd].record_sequence_unknown());
5207        // One open file: a read through the alias moves the other's cursor.
5208        successor.file_handles[&alias_fd].commit_procfs_read(4, 2);
5209        assert_eq!(successor.file_handles[&fd].procfs_position().unwrap().0, 6);
5210        assert!(successor.next_open_file_sequence >= next_sequence);
5211        assert!(successor.has_shared_procfs_sequences);
5212        drop(successor);
5213        let next = tempfile::tempfile().unwrap();
5214        successor_thread
5215            .add_fd(next.as_raw_fd(), OFlag::O_RDWR, FdType::Regular, None)
5216            .unwrap();
5217        assert_eq!(
5218            successor_thread
5219                .with_detfd(next.as_raw_fd(), |h| h.open_file_id())
5220                .unwrap(),
5221            OpenFileId::new(owner, next_sequence),
5222        );
5223        // A table whose shared description closed at the exec still carries
5224        // its numbering.
5225        let lone = ThreadState::new(owner, &config, ());
5226        let cloexec = std::fs::File::open("/proc/self/statm").unwrap();
5227        lone.add_fd(cloexec.as_raw_fd(), OFlag::O_CLOEXEC, FdType::Regular, None)
5228            .unwrap();
5229        lone.with_detfd(cloexec.as_raw_fd(), |handle| {
5230            handle.set_procfs(ProcfsFile::from_path(Path::new("/proc/self/statm")).unwrap());
5231        })
5232        .unwrap();
5233        assert!(lone.procfs_records_for_exec().unwrap().is_none());
5234        lone.share_procfs_sequences();
5235        let numbering = lone.procfs_records_for_exec().unwrap().unwrap();
5236        assert!(numbering.handles.is_empty());
5237        assert_eq!(
5238            numbering.next_sequence,
5239            lone.metadata().next_open_file_sequence
5240        );
5241        assert!(numbering.next_sequence > 0);
5242        // A reused numeric slot cannot take the old open file's identity.
5243        let replacement = tempfile::tempfile().unwrap();
5244        assert_eq!(
5245            unsafe { libc::dup2(replacement.as_raw_fd(), alias_fd) },
5246            alias_fd
5247        );
5248        assert_eq!(
5249            FileMetadata::new(owner).apply_exec_fd_state(owner, state, true),
5250            Err(Errno::EIO)
5251        );
5252    }
5253
5254    /// A descriptor's link names a per-process file that follows its record's
5255    /// sequence as the kernel opens one only for an empty regular file with a
5256    /// link (never a placeholder, which has none) whose link names a task's
5257    /// `stat`, `statm`, `io`, `arch_status` or `cgroup`: not its `comm`, which
5258    /// the kernel's own open file serves, nor any other file.
5259    #[test]
5260    fn a_kernel_record_file_link_names_only_a_sequence_file() {
5261        let mut stat: libc::stat = unsafe { std::mem::zeroed() };
5262        stat.st_mode = libc::S_IFREG | 0o444;
5263        stat.st_nlink = 1;
5264        for name in ["stat", "statm", "io", "arch_status", "cgroup"] {
5265            let link = format!("/proc/12/{name}");
5266            assert_eq!(
5267                kernel_record_file_link(&stat, link.as_bytes()),
5268                Some(std::path::PathBuf::from(&link)),
5269                "{name}"
5270            );
5271        }
5272        assert_eq!(
5273            kernel_record_file_link(&stat, b"/proc/12/task/13/statm"),
5274            Some(std::path::PathBuf::from("/proc/12/task/13/statm"))
5275        );
5276        for link in [
5277            "/proc/12/comm",
5278            "/proc/12/status",
5279            "/proc/12/maps",
5280            "/proc/uptime",
5281            "/tmp/statm",
5282        ] {
5283            assert_eq!(
5284                kernel_record_file_link(&stat, link.as_bytes()),
5285                None,
5286                "{link}"
5287            );
5288        }
5289        let mut placeholder = stat;
5290        placeholder.st_nlink = 0;
5291        assert_eq!(
5292            kernel_record_file_link(&placeholder, b"/proc/12/statm"),
5293            None
5294        );
5295        let mut sized = stat;
5296        sized.st_size = 1;
5297        assert_eq!(kernel_record_file_link(&sized, b"/proc/12/statm"), None);
5298        let mut directory = stat;
5299        directory.st_mode = libc::S_IFDIR | 0o555;
5300        assert_eq!(kernel_record_file_link(&directory, b"/proc/12/statm"), None);
5301    }
5302
5303    /// A rendered per-process file that follows its record's sequence keeps
5304    /// that sequence in Detcore's global state once a send passes its
5305    /// descriptor over `SCM_RIGHTS` (`ThreadState::share_sent_procfs_sequence`),
5306    /// as a fork's copies do: marked, under its open file's identity, and
5307    /// noted in the table's history. Not a file the kernel opened (a receiver
5308    /// cannot name its open file), not one whose sequence Detcore cannot know
5309    /// here, and not a file that follows no record's sequence.
5310    #[test]
5311    fn a_sent_rendered_record_file_shares_its_sequence() {
5312        let owner = DetTid::from_raw(9033);
5313        let config = Config::default();
5314        let state = ThreadState::new(owner, &config, ());
5315        state.metadata().file_handles.clear();
5316        for (fd, spelling, rendered) in [
5317            (40, "/proc/7/statm", true),
5318            (41, "/proc/7/statm", false),
5319            (42, "/proc/7/statm", true),
5320            (43, "/proc/7/auxv", true),
5321        ] {
5322            state
5323                .add_fd(fd, OFlag::O_RDONLY, FdType::Regular, None)
5324                .unwrap();
5325            let procfs = if rendered {
5326                rendered_record_file(spelling)
5327            } else {
5328                crate::procfs::ProcfsFile::from_path(Path::new(spelling)).unwrap()
5329            };
5330            state
5331                .with_detfd(fd, |handle| handle.set_procfs(procfs.clone()))
5332                .unwrap();
5333        }
5334        state
5335            .with_detfd(42, |handle| handle.mark_record_sequence_unknown())
5336            .unwrap();
5337        let id = state
5338            .with_detfd(40, |handle| handle.open_file_id())
5339            .unwrap();
5340        assert!(!state.metadata().has_shared_procfs_sequences);
5341        assert_eq!(state.share_sent_procfs_sequence(40), Some(id));
5342        assert_eq!(
5343            state
5344                .with_detfd(40, |handle| handle.procfs_shared_sequence())
5345                .unwrap(),
5346            Some(id)
5347        );
5348        assert!(state.metadata().has_shared_procfs_sequences);
5349        // Sent again: the same identity, the mark kept.
5350        assert_eq!(state.share_sent_procfs_sequence(40), Some(id));
5351        for fd in [41, 42, 43, 44] {
5352            assert_eq!(state.share_sent_procfs_sequence(fd), None, "{fd}");
5353        }
5354        assert_eq!(
5355            state
5356                .with_detfd(41, |handle| handle.procfs_shared_sequence())
5357                .unwrap(),
5358            None
5359        );
5360        assert_eq!(state.rendered_descriptors(), vec![40, 42, 43]);
5361    }
5362}
5363
5364/// Various measurements of one guest thread's execution. This is useful for printing
5365/// context in logs as we go and printing a final summary.
5366#[derive(Debug, Serialize, Deserialize, Clone, Default)]
5367pub struct ThreadStats {
5368    /// A simple count of how many syscalls have been handled on this thread.
5369    pub syscall_count: u64,
5370
5371    /// This thread's syscall entries by number, with the outcome of each
5372    /// completed one (`crate::syscall_histogram`). Its total is
5373    /// `syscall_count`. Handed to the scheduler when the thread deregisters.
5374    pub syscall_histogram: SyscallHistogram,
5375
5376    /// A Detcore refusal policy answered the current syscall
5377    /// (`crate::syscall_histogram::mark_refused`). Cleared at each syscall
5378    /// entry.
5379    pub syscall_refused: bool,
5380
5381    /// This thread's happens-before counting state: per (syscall, fd, futex
5382    /// operation) a syscall-occurrence anchor names, how many of this thread's
5383    /// syscall entries matched, and the origin of each relative anchor whose
5384    /// base this thread reached (`HappensBeforeProgram::count_syscall_occurrences`).
5385    /// Empty unless the run has such an anchor; empty for every new thread,
5386    /// like `syscall_count`.
5387    pub hb_occurrences: detcore_model::happens_before::HbThreadCounters,
5388
5389    /// The posthook happens-before anchors the current syscall entry reached
5390    /// (`Anchor::fires_at_posthook`): counted at its prehook, they check in
5391    /// with the scheduler when the call completes. Empty between syscalls.
5392    pub hb_pending_posthook: Vec<String>,
5393
5394    /// How many register-file samples this thread has CONSIDERED for `--detlog-regs`.
5395    ///
5396    /// This is the cadence index, and it exists because no pre-existing counter is a clean
5397    /// zero-based count of control points: the logged syscall ordinal starts at 2, and
5398    /// `syscall_count` also counts points this sampler never reaches. Keying the cadence on either
5399    /// let a short guest whose points never landed on a multiple of N emit ZERO samples while the
5400    /// run still reported PASS -- a spot-tier green backed by nothing. Counting the samples
5401    /// themselves makes index 0 the first control point of every thread, so a spot-tier run always
5402    /// samples at least once.
5403    pub regs_sample_index: u64,
5404
5405    /// A count of how many signals have arrived at this thread, total.
5406    pub signal_count: u64,
5407
5408    /// How many syscalls this time slice (since last preemption)?
5409    pub timeslice_syscall_count: u64,
5410
5411    /// How many signals this time slice (since last preemption)?
5412    pub timeslice_signal_count: u64,
5413
5414    /// How many logical timeslices have we completed before the current one?
5415    /// These correspond to when we are preempted at the `end_of_timeslice`.
5416    pub timeslice_count: u64,
5417
5418    /// The timeslice_count for the timeslice which was the last one that had a recorded end time in
5419    /// the `--replay-preemptions-from` log.
5420    pub last_recorded_slice: Option<u64>,
5421
5422    /// Distribution (min/max/sum/count) of completed timeslice durations for this
5423    /// thread, in virtual nanoseconds. A slice's duration is the delta of
5424    /// `thread_logical_time` between two consecutive `next_timeslice` resets.
5425    pub timeslice_stats: TimesliceStats,
5426
5427    /// The per-thread logical time (virtual ns) at which the current timeslice
5428    /// began. `None` until the first slice is opened. Used to compute the
5429    /// duration of a slice when the next reset occurs.
5430    pub timeslice_start_ns: Option<LogicalTime>,
5431}
5432
5433impl ThreadStats {
5434    /// Create a new thread stats with zero counters.
5435    pub fn new() -> Self {
5436        Default::default()
5437    }
5438
5439    /// Count one entry of `sysno`.
5440    pub fn count_syscall(&mut self, sysno: Sysno) {
5441        self.syscall_count += 1;
5442        self.timeslice_syscall_count += 1;
5443        self.syscall_histogram.enter(sysno);
5444        self.syscall_refused = false;
5445    }
5446
5447    /// Increment the count of signals.
5448    pub fn count_signal(&mut self) {
5449        self.signal_count += 1;
5450        self.timeslice_signal_count += 1;
5451    }
5452
5453    /// Reset counters for a new timeslice.
5454    /// Increases the count of completed timeslices.
5455    pub(crate) fn reset_timeslice(&mut self) {
5456        self.timeslice_syscall_count = 0;
5457        self.timeslice_signal_count = 0;
5458        self.timeslice_count += 1;
5459    }
5460
5461    /// Close the final, in-progress timeslice at thread exit, recording its
5462    /// virtual-ns duration. This captures short-lived or I/O-bound threads that
5463    /// exit (or block until exit) before ever exhausting a slice, so they still
5464    /// contribute one sample. Idempotent: consumes `timeslice_start_ns`.
5465    pub fn close_final_timeslice(&mut self, now: LogicalTime) {
5466        if let Some(start) = self.timeslice_start_ns.take()
5467            && now >= start
5468        {
5469            self.timeslice_stats.record((now - start).as_nanos());
5470        }
5471    }
5472}
5473
5474/// Information inherited by a `CLONE_VFORK` child so it can register itself
5475/// while its parent is blocked inside the kernel.
5476#[derive(Debug, Clone, Serialize, Deserialize)]
5477pub struct PendingVfork {
5478    pub parent_dettid: DetTid,
5479    /// The parent's `BlockingVfork` operation, which identifies this vfork's
5480    /// scheduler barrier.
5481    pub op: ExternalOpId,
5482    pub parent_detpid: DetPid,
5483    pub child_tid_addr: usize,
5484    pub flags: CloneFlags,
5485    pub exit_signal: libc::c_int,
5486    pub child_priority_entropy: Option<u64>,
5487    /// With `CLONE_NEWUSER`: the creator's effective user ID may not have
5488    /// been its real one, read before the clone
5489    /// (`Detcore::user_namespace_owner_may_differ`). The child notes it for
5490    /// the namespace the clone made when it registers itself, once the clone
5491    /// has succeeded.
5492    #[serde(default)]
5493    pub user_namespace_owner_may_differ: bool,
5494}
5495
5496// TODO-HUMAN-REVIEW(#797): Review process-wide logical CPU aggregation.
5497#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize)]
5498pub(crate) struct ProcessCpuSnapshot {
5499    pub user: LogicalTime,
5500    pub system: LogicalTime,
5501    pub children_user: LogicalTime,
5502    pub children_system: LogicalTime,
5503}
5504
5505/// Runtime-only readiness for one child birth. A retired or replaced token can
5506/// never be made ready again; cancelled waiters leave only weak references.
5507#[derive(Debug)]
5508pub(crate) struct ChildCpuPublication(Mutex<ChildCpuPublicationState>);
5509
5510#[derive(Debug)]
5511enum ChildCpuPublicationState {
5512    Pending(Vec<std::sync::Weak<futures::task::AtomicWaker>>),
5513    Published,
5514    Invalidated,
5515}
5516
5517impl ChildCpuPublication {
5518    fn pending() -> Arc<Self> {
5519        Arc::new(Self(Mutex::new(ChildCpuPublicationState::Pending(
5520            Vec::new(),
5521        ))))
5522    }
5523
5524    fn finish(&self, published: bool) {
5525        let waiters = {
5526            let mut state = self.0.lock().expect("child CPU publication mutex poisoned");
5527            if matches!(*state, ChildCpuPublicationState::Invalidated)
5528                || published && matches!(*state, ChildCpuPublicationState::Published)
5529            {
5530                return;
5531            }
5532            let next = if published {
5533                ChildCpuPublicationState::Published
5534            } else {
5535                ChildCpuPublicationState::Invalidated
5536            };
5537            match std::mem::replace(&mut *state, next) {
5538                ChildCpuPublicationState::Pending(waiters) => waiters,
5539                _ => Vec::new(),
5540            }
5541        };
5542        // Do not call an arbitrary task waker under either accounting lock.
5543        for waiter in waiters.into_iter().filter_map(|waiter| waiter.upgrade()) {
5544            waiter.wake();
5545        }
5546    }
5547
5548    async fn wait(&self) -> bool {
5549        let waiter = Arc::new(futures::task::AtomicWaker::new());
5550        {
5551            let mut state = self.0.lock().expect("child CPU publication mutex poisoned");
5552            match &mut *state {
5553                ChildCpuPublicationState::Pending(waiters) => {
5554                    waiters.retain(|waiter| waiter.strong_count() != 0);
5555                    waiters.push(Arc::downgrade(&waiter));
5556                }
5557                ChildCpuPublicationState::Published => return true,
5558                ChildCpuPublicationState::Invalidated => return false,
5559            }
5560        }
5561        futures::future::poll_fn(|cx| {
5562            waiter.register(cx.waker());
5563            let state = self.0.lock().expect("child CPU publication mutex poisoned");
5564            match *state {
5565                ChildCpuPublicationState::Pending(_) => std::task::Poll::Pending,
5566                ChildCpuPublicationState::Published => std::task::Poll::Ready(true),
5567                ChildCpuPublicationState::Invalidated => std::task::Poll::Ready(false),
5568            }
5569        })
5570        .await
5571    }
5572}
5573
5574#[derive(Debug, Default, Clone, Serialize, Deserialize)]
5575pub(crate) struct ProcessCpuTime {
5576    snapshot: ProcessCpuSnapshot,
5577    exited_children: BTreeMap<DetPid, ProcessCpuSnapshot>,
5578    /// Each thread's own (user, system) time as of its last accounting, which
5579    /// another thread's CPU clock of it reads (`crate::cpu_clock`). An entry
5580    /// leaves once its thread has exited and left the scheduler, unless the
5581    /// thread leads the process: a leader that exits before its threads stays
5582    /// a zombie whose clock still reads its final time, as on Linux.
5583    #[serde(default)]
5584    threads: BTreeMap<DetTid, (LogicalTime, LogicalTime)>,
5585    // Runtime ownership, not record-format state. ThreadState clones retain the
5586    // same parent accounting Arc; notification tokens also retain their identity.
5587    #[serde(skip)]
5588    child_cpu_publications: BTreeMap<DetPid, Arc<ChildCpuPublication>>,
5589}
5590
5591impl ProcessCpuTime {
5592    fn validate_native_exec_cut(&self) -> Result<(), crate::native_exec::NativeExecCutError> {
5593        if !self.child_cpu_publications.is_empty() {
5594            return Err(crate::native_exec::NativeExecCutError::LiveCapability(
5595                "child_cpu_publications",
5596            ));
5597        }
5598        Ok(())
5599    }
5600
5601    fn add_thread_delta(&mut self, user: LogicalTime, system: LogicalTime) {
5602        self.snapshot.user = self.snapshot.user + user;
5603        self.snapshot.system = self.snapshot.system + system;
5604    }
5605
5606    /// Record `thread`'s own (user, system) time, which its process total now
5607    /// includes.
5608    fn record_thread(&mut self, thread: DetTid, own: (LogicalTime, LogicalTime)) {
5609        self.threads.insert(thread, own);
5610    }
5611
5612    /// `thread`'s own (user, system) time as of its last accounting. A thread
5613    /// that has not been accounted yet has not run, and has used none.
5614    fn thread_time(&self, thread: DetTid) -> (LogicalTime, LogicalTime) {
5615        self.threads
5616            .get(&thread)
5617            .copied()
5618            .unwrap_or((LogicalTime::ZERO, LogicalTime::ZERO))
5619    }
5620
5621    /// `thread` exited: its time stays in the process total, as an exited
5622    /// thread's stays in Linux's (`thread_group_cputime`).
5623    fn forget_thread(&mut self, thread: DetTid) {
5624        self.threads.remove(&thread);
5625    }
5626
5627    fn record_exited_child(&mut self, pid: DetPid, child: ProcessCpuSnapshot) {
5628        self.exited_children
5629            .entry(pid)
5630            .and_modify(|previous| {
5631                previous.user = previous.user.max(child.user);
5632                previous.system = previous.system.max(child.system);
5633                previous.children_user = previous.children_user.max(child.children_user);
5634                previous.children_system = previous.children_system.max(child.children_system);
5635            })
5636            .or_insert(child);
5637    }
5638
5639    fn reap_child(&mut self, pid: DetPid) {
5640        let Some(child) = self.exited_children.remove(&pid) else {
5641            return;
5642        };
5643        self.snapshot.children_user =
5644            self.snapshot.children_user + child.user + child.children_user;
5645        self.snapshot.children_system =
5646            self.snapshot.children_system + child.system + child.children_system;
5647    }
5648
5649    fn prepare_child(&mut self, pid: DetPid) {
5650        self.exited_children.remove(&pid);
5651    }
5652}
5653
5654/// Guest-visible logical clock shared by an entire process tree.
5655///
5656/// The scheduler's raw logical time already includes the configured epoch and
5657/// is the clock used to judge absolute deadlines. Guest time must therefore
5658/// track it directly: subtracting a per-process or per-exec origin can put a
5659/// newly computed absolute deadline in the scheduler's past. The shared floor
5660/// preserves monotonicity if a backend supplies a stale local observation.
5661#[derive(Debug, Default, Serialize, Deserialize)]
5662pub(crate) struct GuestClock {
5663    now: LogicalTime,
5664}
5665
5666/// One modeled robust-futex wake that must wait until Linux has completed the
5667/// corresponding task-exit cleanup.
5668#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
5669pub(crate) struct RobustListWake {
5670    pub(crate) futex: FutexID,
5671}
5672
5673/// The physical-exit condition under which staged robust-futex wakes become
5674/// safe to deliver.
5675#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
5676pub(crate) enum RobustListExit {
5677    ExitGroup,
5678    Signal(i32),
5679}
5680
5681/// Wakes collected while guest memory is still readable, keyed by the owner
5682/// whose physical exit makes them safe.
5683#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5684pub(crate) struct PendingRobustListWakes {
5685    pub(crate) reason: RobustListExit,
5686    pub(crate) wakes: Vec<RobustListWake>,
5687}
5688
5689impl PendingRobustListWakes {
5690    fn matches_exit(&self, exit_signal: Option<i32>) -> bool {
5691        match self.reason {
5692            RobustListExit::ExitGroup => true,
5693            RobustListExit::Signal(expected) => exit_signal == Some(expected),
5694        }
5695    }
5696}
5697
5698/// How long a thread an exit_group killed waits for its robust-list walk
5699/// ([`ThreadState::wait_for_exit_group_robust_walk`]).
5700pub(crate) const EXIT_GROUP_ROBUST_WALK_WAIT: std::time::Duration =
5701    std::time::Duration::from_secs(10);
5702
5703/// A registered robust list: its owner and head.
5704#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5705pub(crate) struct RobustListOwner {
5706    pub(crate) dettid: DetTid,
5707    pub(crate) head: usize,
5708}
5709
5710/// Robust-list registrations and staged wakes shared by one Linux thread
5711/// group. A forked process starts with an empty instance; `CLONE_THREAD`
5712/// members share it.
5713#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5714pub(crate) struct RobustListProcessState {
5715    heads: BTreeMap<DetTid, usize>,
5716    /// Threads whose own exit Detcore has already walked on DBT
5717    /// ([`crate::syscalls::threads`]'s `OwnerDeathPath::Barrier`).
5718    #[serde(default)]
5719    tool_walked: BTreeSet<DetTid>,
5720    /// An exit_group on DBT (`OwnerDeathPath::Barrier`) has been filed: an
5721    /// exiting thread keeps its head here for that exit_group's walk
5722    /// ([`ThreadState::drop_robust_list_head_at_exit`]).
5723    #[serde(default)]
5724    exit_group_filed: bool,
5725    /// The threads whose filed exit_group is still pending: filed, and
5726    /// neither granted nor cancelled by the thread's own death.
5727    #[serde(default)]
5728    exit_group_filers: BTreeSet<DetTid>,
5729    /// The thread whose filed exit_group was granted and is walking the
5730    /// group's lists ([`ThreadState::begin_exit_group_robust_walk`]).
5731    #[serde(default)]
5732    exit_group_walker: Option<DetTid>,
5733    /// That exit_group's walk has finished
5734    /// ([`ThreadState::finish_exit_group_robust_walk`]).
5735    #[serde(default)]
5736    exit_group_walked: bool,
5737    pending: BTreeMap<DetTid, PendingRobustListWakes>,
5738    matched_physical_exits: BTreeMap<DetTid, DetTime>,
5739    /// Wakes read at an `execve`'s entry, delivered only if it succeeds.
5740    exec_pending: Vec<(DetTid, RobustListWake)>,
5741}
5742
5743impl GuestClock {
5744    fn observe(&mut self, raw: LogicalTime) -> LogicalTime {
5745        self.now = self.now.max(raw);
5746        self.now
5747    }
5748}
5749
5750/// Linux's initial per-task timer slack, in nanoseconds.
5751///
5752/// Detcore uses the kernel default rather than inheriting the launcher's
5753/// physical timer slack.  Guest writes are kept in virtual per-thread state so
5754/// they cannot perturb the host-timed waits Detcore still uses internally.
5755pub(crate) const DEFAULT_TIMER_SLACK_NS: u64 = 50_000;
5756
5757fn default_timer_slack_ns() -> u64 {
5758    DEFAULT_TIMER_SLACK_NS
5759}
5760
5761/// The most deliveries in a row that continue their thread's own
5762/// `rt_sigreturn` (`ThreadState::consecutive_continuations`). A chosen,
5763/// bounded preference budget: as many as Linux has standard signals, so any
5764/// set of distinct standard signals pending when a run begins is delivered
5765/// before other threads run. A cap of 1 cut short a second pending signal
5766/// and restarted the cycle <https://github.com/rrnewton/hermit/issues/4505>
5767/// fixes (Claude re-check of https://github.com/rrnewton/hermit/pull/4509).
5768/// It is not a promise that every backlog drains first: realtime signals
5769/// queue several instances and a periodic timer can re-arm during a run
5770/// (v6.12 kernel/signal.c:1070 and :695). Reaching the cap only drops the
5771/// preference; no signal is lost, and the next delivery waits its turn.
5772pub(crate) const MAX_SIGRETURN_CONTINUATIONS: u8 = 32;
5773
5774/// What a signal delivery knows before anything of it can hand the thread
5775/// to the scheduler (`Detcore::inbound_stop`).
5776#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
5777pub(crate) struct InboundStop {
5778    /// The stop follows a reported `rt_sigreturn` (`returned_from_sigreturn`).
5779    pub(crate) reported: bool,
5780    /// The thread's scheduler grants at the stop.
5781    pub(crate) grants_at_stop: u64,
5782}
5783
5784/// What a delivery's turn leaves for what the granted delivery settles
5785/// (`Detcore::inbound_turn`, `Detcore::inbound_delivered`).
5786#[derive(Clone, Debug, Serialize, Deserialize)]
5787pub(crate) struct InboundTurn {
5788    /// The delivering thread.
5789    pub(crate) dettid: DetTid,
5790    /// The thread's signal count before this delivery.
5791    pub(crate) mycount: u64,
5792    /// Actual callback identity retained through an announcement's await.
5793    #[serde(default)]
5794    pub(crate) boundary_token: Option<crate::preemptions::callback_history::Token>,
5795}
5796
5797/// One delivery announced to `Tool::announce_signal_event` and not yet
5798/// settled.
5799#[derive(Clone, Debug, Serialize, Deserialize)]
5800pub(crate) struct SignalAnnouncementState {
5801    /// The announcement's token.
5802    pub(crate) token: u64,
5803    /// The raw signal.
5804    pub(crate) signal: i32,
5805    /// What its turn left for its commit.
5806    pub(crate) turn: InboundTurn,
5807}
5808
5809/// The Detcore per-thread state.
5810#[derive(Serialize, Deserialize, Clone)]
5811pub struct ThreadState<T> {
5812    /// The deterministic thread ID of the this thread.
5813    pub dettid: DetTid,
5814    /// The deterministic process ID of the this thread.
5815    pub detpid: Option<DetTid>,
5816
5817    /// Whether the backend entered the thread-start callback. Construction
5818    /// alone does not imply that this thread was admitted to the scheduler.
5819    pub(crate) thread_start_entered: bool,
5820
5821    /// The parent's `BlockingVfork` operation, if a `CLONE_VFORK` call
5822    /// created this thread. A vfork child that is torn down before its
5823    /// scheduler registration completes reports it in its deregistration, so
5824    /// the barrier of exactly that vfork ends
5825    /// ([`crate::scheduler::Scheduler::record_unregistered_vfork_child_exit`]).
5826    pub(crate) vfork_origin: Option<ExternalOpId>,
5827
5828    /// Host thread ID supplied by a backend whose scheduler identity is
5829    /// virtual. `None` keeps the existing direct-ID behavior for other
5830    /// backends.
5831    #[serde(default)]
5832    pub physical_tid: Option<i32>,
5833
5834    /// Backend lifetime retained for consuming signal effects after retirement.
5835    #[serde(default)]
5836    pub(crate) signal_task_identity: Option<reverie::SignalTaskIdentity>,
5837
5838    // AUTONOMOUS-BOT-IMPLEMENTED
5839    // TODO-HUMAN-REVIEW(PR-1063): Review backend-supplied open-file creator identity.
5840    /// Stable identity used when allocating deterministic open-file descriptions.
5841    #[serde(default)]
5842    pub(crate) open_file_creator: Option<DetTid>,
5843
5844    /// The input geometry of the `recvmsg` or `recvmmsg` this thread is
5845    /// running, while Detcore needs it to name the descriptors it receives:
5846    /// `Some` from the handler's entry, re-read before each attempt of the
5847    /// internal polling loop so that it is the geometry of the attempt that
5848    /// succeeds (`retry_nonblocking_syscall_helper`), taken when the call
5849    /// returns. Never carried across a fork or a migration.
5850    #[serde(skip)]
5851    pub(crate) receive_geometry: Option<Option<crate::syscalls::scm_rights::ReceiveGeometry>>,
5852
5853    /// Retained metadata for the vectored call's observers. Original entry
5854    /// regions preserve the existing outbound and exit-dependency overlays.
5855    /// When buffer logging is observed, generic readv/recvmsg also retain a
5856    /// separate complete-geometry result from the successful actual attempt;
5857    /// their buffer observer hashes returned guest bytes without an overlay.
5858    /// Set as internal polling ends and taken as the call returns. A failed
5859    /// final capture cannot certify an earlier attempt's geometry.
5860    #[serde(skip)]
5861    pub(crate) entry_import: Option<Box<crate::syscalls::entry_import::EntryImport>>,
5862
5863    /// The `sendmsg` or `sendmmsg` this thread is running, while its
5864    /// `SCM_RIGHTS` descriptors matter to the `/proc` directories Detcore
5865    /// serves on placeholders: `Some` from the handler's inspection
5866    /// (`Detcore::inspect_placeholder_send`), read before each attempt of the
5867    /// internal polling loop, which refuses by name an attempt whose messages
5868    /// pass other placeholders than were inspected
5869    /// (`retry_nonblocking_syscall_helper`), taken when the call returns.
5870    /// Never carried across a fork or a migration.
5871    #[serde(skip)]
5872    pub(crate) send_inspection: Option<crate::syscalls::SendInspection>,
5873    /// The inotify read this thread is running, while the bytes queued on
5874    /// its instance before the attempt that completes say what that attempt
5875    /// took (`crate::syscalls::inotify::QueueCount`): `Some` from the read
5876    /// handler's entry, measured again before each attempt of the internal
5877    /// polling loop (`retry_nonblocking_syscall_helper`), so that it is the
5878    /// count before the attempt that completed, and taken when the call
5879    /// returns. Never carried across a fork or a migration.
5880    #[serde(skip)]
5881    pub(crate) inotify_queue: Option<crate::syscalls::inotify::QueueCount>,
5882    /// The call this thread runs in parts (a readv reading on into a later
5883    /// iovec, `Detcore::readv_inotify`), set around the whole call: whether
5884    /// the part now running follows one that transferred bytes, and a signal
5885    /// an earlier part completed with pending
5886    /// (`crate::syscalls::helpers::CallParts`). Never carried across a fork
5887    /// or a migration.
5888    #[serde(skip)]
5889    pub(crate) call_parts: Option<crate::syscalls::helpers::CallParts>,
5890    /// The syscall in progress, while the run records an access trace: the
5891    /// descriptors it named and the slot changes it made
5892    /// (`crate::access_trace`). Never carried across a fork or a migration.
5893    #[serde(skip)]
5894    pub(crate) traced_call: crate::access_trace::TracedCall,
5895
5896    /// Linux memory address space shared by tasks created with `CLONE_VM`.
5897    pub mm_id: MmId,
5898
5899    /// Last acknowledged ownership publication. A new mm or changed provider
5900    /// result is published before ordinary RPC can transfer the turn.
5901    #[serde(skip)]
5902    pub(crate) published_runtime_private_memory: Option<(MmId, reverie::RuntimePrivateMemory)>,
5903
5904    /// Shared memory mappings used to resolve process-shared futex keys.
5905    pub(crate) memory_metadata: Arc<Mutex<MemoryMetadata>>,
5906
5907    /// This threads path within the thread/process ancestry tree. (The terminology comes from
5908    /// Cilk.)
5909    pub pedigree: Pedigree,
5910
5911    /// Counting various events.
5912    pub stats: ThreadStats,
5913
5914    /// In chaos mode with --replay-preemptions-from, we hold a list of our future preemption points.
5915    pub preemption_points: Option<ThreadHistoryIterator>,
5916
5917    /// User defined interruption points
5918    pub interrupt_at: BTreeSet<u64>,
5919
5920    /// clone flags when SYS_clone is called.
5921    ///
5922    /// This is just a place to stash the value temporarily, where it can be read out by
5923    /// the child thread upon `init_thread_state`.  After that point, it is consumed by
5924    /// the child and becomes `None` again.
5925    ///
5926    /// Stated differently, this is just for message-passing communication.
5927    pub clone_flags: Option<CloneFlags>,
5928
5929    /// Registration metadata for a child whose parent cannot resume until the
5930    /// backend finishes the child. The child consumes this in
5931    /// `handle_thread_start`; the parent clears its copy when injection returns.
5932    pub pending_vfork: Option<PendingVfork>,
5933
5934    /// The ID this thread's restarted clone-family call took when Detcore
5935    /// chooses the IDs (`GuestIds::Chosen`), and the call it is for: the call
5936    /// comes back after the signal that interrupted it, and keeps its ID
5937    /// (`Detcore::plan_clone_id`). Any other call retires it.
5938    pub(crate) restarted_clone_id: Option<crate::syscalls::chosen_ids::RestartedCloneId>,
5939
5940    /// The open file description whose record sequence this thread took from
5941    /// Detcore's global state for the read, positioned read or seek it is
5942    /// handling, which stores it back when it ends
5943    /// (`Detcore::take_procfs_sequence`); `None` outside one. The handler it
5944    /// runs again sees it and takes nothing more.
5945    #[serde(skip)]
5946    pub(crate) procfs_sequence_taken: Option<OpenFileId>,
5947
5948    /// Shared file metadata among all threads in the same process.
5949    /// Initialized for new threads (shared or fresh), and then overwritten again on `execve`.
5950    pub file_metadata: Arc<Mutex<FileMetadata>>,
5951
5952    // AUTONOMOUS-BOT-IMPLEMENTED
5953    // TODO-HUMAN-REVIEW(PR-845): Review backend-gated live descriptor discovery state.
5954    /// Whether the tool runs in the guest's process and shares its descriptor
5955    /// table, so a descriptor detcore has not seen yet may be inspected live
5956    /// (copied from `BackendCapabilities::tool_shares_guest_descriptor_table`).
5957    #[serde(default)]
5958    pub(crate) tool_shares_guest_descriptor_table: bool,
5959
5960    // AUTONOMOUS-BOT-IMPLEMENTED
5961    // TODO-HUMAN-REVIEW(PR-2150): Review the per-thread timer-slack state
5962    // and the creator-current-as-child-default inheritance rule.
5963    /// Virtualized Linux timer slack for this thread.  Both prctl(2) and the
5964    /// top-level `/proc/<tid>/timerslack_ns` interface use these fields; the
5965    /// physical tracee value remains untouched.
5966    #[serde(default = "default_timer_slack_ns")]
5967    pub(crate) timer_slack_ns: u64,
5968    /// Value restored by a zero timer-slack write.  Linux gives a new task the
5969    /// creating thread's current value as both its current and its default.
5970    #[serde(default = "default_timer_slack_ns")]
5971    pub(crate) default_timer_slack_ns: u64,
5972
5973    /// This thread lives below the container's PID namespace: it, or an
5974    /// ancestor, was created with `CLONE_NEWPID`. Its pids are those of its
5975    /// own namespace, which Detcore translates
5976    /// (`scheduler::pid_namespaces`); at depth 0 nothing is asked.
5977    #[serde(default)]
5978    pub(crate) nested_pid_namespace: bool,
5979
5980    /// This task's root directory may not be the container's: a task it shares
5981    /// its root with (`CLONE_FS`), or one it inherited the root from, changed
5982    /// it with `chroot`. Detcore then leaves whether a procfs file exists to
5983    /// the kernel (`rendered_procfs`). Shared like the root itself: every task
5984    /// that shares the creator's filesystem information shares this flag.
5985    #[serde(default)]
5986    pub(crate) changed_root: Arc<std::sync::atomic::AtomicBool>,
5987
5988    /// This thread's working and root directories may not be its
5989    /// thread-group leader's: it, or the thread it was created by, was
5990    /// created as a thread without `CLONE_FS`, so it holds filesystem
5991    /// information of its own. Its process's `/proc/<pid>/cwd` and
5992    /// `/proc/<pid>/root` links, which Linux reads from the leader, are then
5993    /// not the thread's own `getcwd` (`Detcore::readlink_own_task_link`). A
5994    /// leader, a new process, and a thread after its exec (which makes it
5995    /// the leader) hold their own.
5996    #[serde(default)]
5997    pub(crate) fs_apart_from_leader: bool,
5998
5999    /// The image this thread's process executes ([`crate::exec_record`]),
6000    /// which answers the process's `/proc/<pid>/exe` link on KVM while it
6001    /// names the image exactly (`Detcore::readlink_own_task_link`), and in a
6002    /// launched run (`Detcore::launched_exe`); `None` leaves the link to the
6003    /// path it took before. A new task starts with its creator's, and a
6004    /// successful exec replaces it (`tool_global::mark_past_first_execve`).
6005    #[serde(default)]
6006    pub(crate) exe: Option<Arc<crate::exec_record::ExecRecord>>,
6007
6008    /// The identity of the descriptor table this thread's process holds:
6009    /// the one its thread-group leader holds now, which procfs reads for
6010    /// the process's descriptor links (`/proc/self/fd/N`, `/proc/<pid>/fd/N`,
6011    /// `/dev/fd/N`). A new process sets it to the table it starts with (its
6012    /// creator's under `CLONE_FILES`, a copy otherwise), and an exec to the
6013    /// table exec gives it; `unshare` is refused, so a leader's table
6014    /// changes at nothing else. A thread (`CLONE_THREAD`) copies its
6015    /// creator's. A plain copy suffices: it changes only where the group has
6016    /// one thread (a new process, a successful exec), and a failed exec
6017    /// restores the exec'ing thread's own copy. `None` where it is not
6018    /// known, which only a state serialized before this field restores.
6019    #[serde(default)]
6020    pub(crate) process_files: Option<FilesId>,
6021
6022    /// POSIX per-process timers created via `timer_create(2)`. Shared among the
6023    /// threads of a process (`CLONE_THREAD`) and not inherited across `fork`.
6024    pub(crate) posix_timers: Arc<Mutex<PosixTimers>>,
6025
6026    /// Resource limits shared by threads and copied when a new process forks.
6027    pub(crate) resource_limits: Arc<Mutex<ResourceLimits>>,
6028
6029    /// Logical CPU accounting shared by all threads in this process.
6030    pub(crate) process_cpu_time: Arc<Mutex<ProcessCpuTime>>,
6031
6032    /// Guest-visible logical clock shared by every task in this process tree.
6033    #[serde(default)]
6034    pub(crate) guest_clock: Arc<Mutex<GuestClock>>,
6035
6036    /// The `AF_UNIX` connections waiting for an `accept`, shared by every
6037    /// task in this process tree, as the guest clock is
6038    /// (`crate::syscalls::scm_rights::UnixConnections`). It names
6039    /// descriptions by handles that reach them only in the tool process that
6040    /// serves every guest (ptrace, KVM), so it is never serialized.
6041    #[serde(skip)]
6042    pub(crate) unix_connections: Arc<Mutex<crate::syscalls::scm_rights::UnixConnections>>,
6043
6044    /// Parent process accounting notified when this process leader exits.
6045    pub(crate) parent_process_cpu_time: Option<Arc<Mutex<ProcessCpuTime>>>,
6046
6047    /// Actual sequential KVM child's birth-owned publication capability.
6048    #[serde(skip)]
6049    pub(crate) parent_cpu_publication: Option<Arc<ChildCpuPublication>>,
6050
6051    /// The record of a wait this thread's last syscall ended with
6052    /// `ERESTART_RESTARTBLOCK`, for the `restart_syscall` that resumes it
6053    /// (`syscalls::helpers::RestartBlock`). The next syscall takes it
6054    /// (`Detcore::handle_syscall_event`).
6055    #[serde(skip)]
6056    pub(crate) restart_block: Option<crate::syscalls::helpers::RestartBlock>,
6057
6058    /// The calling thread's working directory in the guest's view, read for
6059    /// the syscall it is running where the tool cannot read it from host
6060    /// procfs (`procfs_inode::tool_can_read_guest_cwd`: KVM): its path where
6061    /// an injected `getcwd` answered one, else, where `getcwd` rendered none
6062    /// (a removed directory, or one whose path is longer than `PATH_MAX`),
6063    /// the directories the syscall's spellings climb to out of it with `..`
6064    /// (`syscalls::SyscallCwd`); `None` until it is read, at most
6065    /// once per syscall (`Detcore::prepare_guest_cwd`). Each syscall starts
6066    /// without it (`Detcore::handle_syscall_event`), so a later syscall never
6067    /// decides by a directory read before it. Never carried across a fork or
6068    /// a migration.
6069    #[serde(skip)]
6070    pub(crate) syscall_cwd: Option<crate::syscalls::SyscallCwd>,
6071
6072    /// Whether this thread's process has a guest handler for SIGALRM, so that
6073    /// signal phase 1's table (`sigalrm_phase1`) gates its syscalls. Set by the
6074    /// runtime's handler admission (phase 1 step I3), in the same turn it
6075    /// publishes the disposition to the scheduler's ledger; until then only
6076    /// tests set it.
6077    pub(crate) sigalrm_handled: bool,
6078
6079    /// Detcore's own copy of this thread's signal dispositions and masks
6080    /// (audit step A5: recorded from the calls that change them, checked
6081    /// against the kernel where it can be read, and read by the decisions
6082    /// step 2 converted). `None` until the first image seeds it, and wherever it
6083    /// cannot be seeded: `None` means unknown, never default actions.
6084    #[serde(default)]
6085    pub(crate) signal_shadow: Option<crate::signal_state::SignalShadow>,
6086
6087    /// Whether this thread, or the thread that created it, lost its signal
6088    /// state copy after a divergence: a lost copy is never seeded again.
6089    #[serde(default)]
6090    pub(crate) signal_shadow_lost: bool,
6091
6092    /// What this thread was doing at its last guest event, for checking a
6093    /// signal delivery against the kernel's syscall registers (A5 step 3.2,
6094    /// `signal_shadow::check_delivery`): the syscall it entered, the result
6095    /// Detcore finished it with, a handler frame just approved, or no
6096    /// syscall. `None` is unknown (a new thread or image), and the check then
6097    /// takes the registers' own answer.
6098    #[serde(skip)]
6099    pub(crate) delivery_interruption: Option<reverie::Interruption>,
6100
6101    /// Under the in-guest delivery gate, what ended the current syscall's wait
6102    /// for a signal (A5 step 3c.1c, design D40): set by the path that ends
6103    /// the wait, and taken by the restart checkpoint straight after the
6104    /// dispatch `match` (`Detcore::wait_end_checkpoint`), which turns the
6105    /// wait's restart code into the guest's result. `None` on every other
6106    /// call, and always with the gate off.
6107    #[serde(skip)]
6108    pub(crate) wait_signal_end: Option<crate::wait_signal_end::WaitSignalEnd>,
6109
6110    /// With `delivery_interruption` `AfterFrame`, the handler address the
6111    /// kernel's frame jumps to: a further delivery at the same exit, before
6112    /// any guest instruction, stops with the instruction pointer there.
6113    #[serde(skip)]
6114    pub(crate) delivery_frame_handler: u64,
6115
6116    /// With `delivery_interruption` `AfterFrame`, the signal that frame
6117    /// delivers, which Linux puts in the handler's first argument register
6118    /// (`signal_shadow::at_frame_entry`).
6119    #[serde(skip)]
6120    pub(crate) delivery_frame_signal: i32,
6121
6122    /// With `delivery_interruption` `AfterFrame`, `orig_ax` at that frame's
6123    /// delivery stop, which Linux keeps until the thread next enters the
6124    /// kernel (`signal_shadow::at_frame_entry`).
6125    #[serde(skip)]
6126    pub(crate) delivery_frame_orig_ax: i64,
6127
6128    /// This thread's counts of checked signal deliveries (A5 step 3.2),
6129    /// logged when it exits.
6130    #[serde(skip)]
6131    pub(crate) delivery_counters: crate::syscalls::signal_shadow::DeliveryCounters,
6132
6133    /// The process's record of its dispositions publications (A5 step 2b,
6134    /// `signal_shadow::publish_dispositions`): what the scheduler holds, the
6135    /// latest generation used, and the latest one the scheduler took. Shared
6136    /// by the threads of a process, which publish one entry.
6137    #[serde(default)]
6138    pub(crate) published_dispositions:
6139        Arc<Mutex<crate::syscalls::signal_shadow::PublishedDispositions>>,
6140
6141    /// What the scheduler holds as this thread's mask from Detcore's copy
6142    /// (A5 step 2b, `signal_shadow::publish_mask`): `Some(None)` nothing,
6143    /// `Some(Some(mask))` that mask; `None` when that is unknown (after an
6144    /// exec), so the next request publishes or withdraws.
6145    #[serde(default)]
6146    pub(crate) published_mask: Option<Option<u64>>,
6147
6148    /// The SIGALRM blocked bit this thread last published to the scheduler's
6149    /// ledger (signal phase 1). A handler's return changes the virtual mask
6150    /// without a syscall; the next syscall's entry compares the runtime's
6151    /// virtual bit with this one and publishes a change.
6152    pub(crate) sigalrm_blocked_published: bool,
6153
6154    /// The real-time signals this thread's process has already recorded as
6155    /// unblocked by a handler's restored mask
6156    /// (`Scheduler::handler_restore_unblocked`), so a delivery adds nothing
6157    /// new without asking the kernel or the scheduler.
6158    pub(crate) handler_restore_rt_reported: u64,
6159
6160    /// While Detcore runs its signal pipeline for a SIGALRM it delivers at a
6161    /// syscall's completion: the registers as that completion leaves them
6162    /// (`completed_syscall_projection`), which the pipeline reads instead of
6163    /// the backend's, whose context still shows the syscall's entry. Never
6164    /// written back.
6165    #[serde(skip)]
6166    pub(crate) signal_register_projection: Option<libc::user_regs_struct>,
6167
6168    /// Per-thread checkpoints used to add only new work to the process totals.
6169    pub(crate) last_accounted_user_time: LogicalTime,
6170    pub(crate) last_accounted_system_time: LogicalTime,
6171
6172    /// Absolute logical-clock values at this thread's creation. Child clocks
6173    /// inherit the parent's absolute position for scheduler ordering, but Linux
6174    /// `RUSAGE_THREAD` starts accounting at the calling thread's own creation.
6175    pub(crate) thread_cpu_start_user_time: LogicalTime,
6176    pub(crate) thread_cpu_start_system_time: LogicalTime,
6177
6178    /// pseudo random number state
6179    pub prng: Pcg64Mcg,
6180
6181    /// One initial-image auxv write completed by an authenticated backend
6182    /// before libc initialization. Consumed by the first post-exec callback;
6183    /// normal construction and child derivation never manufacture this fact.
6184    #[serde(default)]
6185    /// The initial image whose AT_RANDOM a backend wrote before post-exec, the
6186    /// bytes it wrote and its getrandom fills, until post-exec emits their
6187    /// records.
6188    pub(crate) initialized_random_auxv: Option<(
6189        crate::random::InitialImage,
6190        [u8; 16],
6191        Vec<crate::random::EarlyRequest>,
6192    )>,
6193
6194    /// RNG to drive chaos scheduling decisions, separate from other (guest) RNG.
6195    pub chaos_prng: Pcg64Mcg,
6196
6197    /// logical time, measuring progress of this thread and only this thread.
6198    pub thread_logical_time: DetTime,
6199
6200    /// the last RCB clock value committed to `thread_logical_time`
6201    pub committed_clock_value: u64,
6202
6203    /// Syscalls this thread made in the current backend-runtime bootstrap
6204    /// window without charging their cost to `thread_logical_time`. Zero
6205    /// outside a window; see [`ThreadState::charge_syscall_time`].
6206    #[serde(default)]
6207    pub(crate) uncharged_bootstrap_syscalls: u32,
6208
6209    /// True from the moment [`ThreadState::charge_syscall_time`] leaves the
6210    /// syscall this thread is handling uncharged until that syscall's handler
6211    /// finishes. Resource requests made meanwhile through
6212    /// `tool_global::resource_request` are marked
6213    /// [`Resources::backend_runtime_bootstrap`], so the scheduler withholds
6214    /// their turn time as well, except for an IO-polling retry, which still
6215    /// advances it. Requests that bypass `resource_request` (direct futex and
6216    /// thread-lifecycle requests) are not marked and advance it as usual.
6217    /// Cleared at the start of every handler and before the syscall handler's
6218    /// post-hook.
6219    #[serde(default)]
6220    pub(crate) in_uncharged_bootstrap_syscall: bool,
6221
6222    /// Set when this thread may have passed descriptors over `SCM_RIGHTS` on
6223    /// a socket that is not guest-held. Requests and the consuming exit
6224    /// carry it to the scheduler. It stays set for this owner's lifetime:
6225    /// cancellation can drop a request before its publication is received.
6226    #[serde(default)]
6227    pub(crate) passed_descriptors_out_unreported: bool,
6228
6229    /// Set once a resource request of this thread carried
6230    /// `Resources::passes_descriptors_out`, after which the scheduler trusts
6231    /// no guest-held mark for the rest of the run, so this thread need not
6232    /// report a pass-out again (`Detcore::passes_descriptors_out_now`).
6233    #[serde(default)]
6234    pub(crate) passed_descriptors_out_reported: bool,
6235
6236    /// Set by the nonblocking retry loop when the call it completed returned
6237    /// a result Detcore rewrote for the guest
6238    /// (`NonblockableSyscall::normalize_nonblocking_result`): a retried
6239    /// `connect` whose `EISCONN` reads as success. Such an attempt saw only
6240    /// that the socket was connected, by an earlier attempt or by another
6241    /// thread's connect of the same open file, not where to; `handle_connect`
6242    /// clears it before the call and reads it after.
6243    #[serde(default)]
6244    pub(crate) nonblocking_result_normalized: bool,
6245
6246    /// Set by the syscall handler when a `FUTEX_WAKE` woke a waiter and
6247    /// [`Config::yields_after_futex_wake`] holds, so that the
6248    /// syscall's post-hook takes a yield turn. A logically killed waker gets
6249    /// `EINTR` (4) as its wake count and may set this although the scheduler
6250    /// did not requeue it; that thread is already being torn down. The other
6251    /// way round, the scheduler requeues the waker without a yield following
6252    /// when the wake is the robust-list owner-death wake of an exiting thread
6253    /// (which never sets this and skips the post-hook), and when the syscall
6254    /// handler returns an error after the wake and skips the post-hook. In
6255    /// both the thread is retiring or the run is failing, and the requeue
6256    /// alone only reorders the run queue deterministically. Cleared at the
6257    /// start of every handler and by that post-hook.
6258    #[serde(default)]
6259    pub(crate) yield_after_futex_wake: bool,
6260    /// Under `--pipe-wake-latency` (`crate::pipe_wake`): the thread time at
6261    /// which this thread hands the turn to the pollers its actions readied,
6262    /// set at the readying action that the scheduler says starts a window
6263    /// (`crate::pipe_wake::ReadyOutcome`), checked at syscall boundaries, and
6264    /// cleared by the hand-off or by a scheduler request of its own. Always
6265    /// `None` with the option unset.
6266    #[serde(default)]
6267    pub(crate) pipe_handoff_due: Option<LogicalTime>,
6268    /// A signal this thread sent to its own multi-threaded process, which it
6269    /// must dequeue before another thread runs: until its own delivery stop
6270    /// for that signal no timeslice of it ends, at the target, at the PMU
6271    /// maximum or in a timer callback (<https://github.com/rrnewton/hermit/issues/4034>).
6272    #[serde(default)]
6273    pub(crate) own_signal_turn_hold: Option<libc::c_int>,
6274    /// Set when one of this thread's signal deliveries that runs a handler is
6275    /// granted (`delivery_runs_handler`), cleared at the thread's next
6276    /// grant by the scheduler, whatever request it came through
6277    /// (`note_scheduler_handoff`). A signal stop that finds it set
6278    /// comes before this thread made any scheduling point since that
6279    /// delivery: in particular the
6280    /// delivery Linux makes, on the return from that handler's
6281    /// `rt_sigreturn`, of a signal the restored mask unblocks, or a second
6282    /// delivery on the same return to user space. Its turn continues this
6283    /// thread's (`Resources::continues_sigreturn`,
6284    /// <https://github.com/rrnewton/hermit/issues/4505>). It needs no report
6285    /// of the `rt_sigreturn` itself, so a recording and every replay of it
6286    /// (one a gdbserver serves too, where the backend reports no sigreturn)
6287    /// mark the same deliveries. Where the reports are reliable, a run also
6288    /// continues after a reported `rt_sigreturn` (`returned_from_sigreturn`).
6289    ///
6290    /// Where this differs from Linux. On Linux a signal the restored mask
6291    /// unblocks is delivered on the return from `rt_sigreturn`
6292    /// (`SYSCALL_DEFINE0(rt_sigreturn)`, v6.12 arch/x86/kernel/signal_64.c:246)
6293    /// by `arch_do_signal_or_restart` (arch/x86/kernel/signal.c:333, called
6294    /// from kernel/entry/common.c:111) whatever the handler did before it
6295    /// returned. Under `hermit record` and every replay, where no report is
6296    /// read, a handler that took a scheduler turn (a syscall that files a
6297    /// scheduler request, such as `sched_yield` or a write to a device;
6298    /// `getpid` files none) makes its next delivery an ordinary one, which
6299    /// waits behind the other runnable threads. The guest can observe which
6300    /// thread runs next: a sibling may run, and read or write shared memory,
6301    /// between the handler's return and that delivery, and a thread whose
6302    /// handler yields while a sibling re-signals it at every turn progresses
6303    /// only as fast as it did before continuations existed. It cannot observe
6304    /// a different signal, mask, handler, `siginfo` or restart decision: the
6305    /// delivery is still made before the thread runs any code of its own, as
6306    /// if the thread were preempted at the rescheduling point that precedes
6307    /// signal work on the same exit (common.c:101-102), which Linux permits.
6308    /// It is deterministic, and the same in a recording and its replays.
6309    ///
6310    /// A delivery that runs no handler (`SIG_IGN`, or a default action that
6311    /// ignores) does not set it. Linux dequeues the next pending signal on
6312    /// the same exit after discarding one (`get_signal`, v6.12
6313    /// kernel/signal.c:2815-2816 and 2831-2832), so here too the stop that
6314    /// follows is ordered as if preempted at that exit, with the same
6315    /// consequence: only which thread runs next changes. Continuing it
6316    /// would let a thread that dequeues an ignored `ITIMER_REAL` copy run
6317    /// on to the end of its slice instead of waiting at its next dequeue,
6318    /// so the following expiry's copy stays queued while a sibling runs a
6319    /// whole slice, and the timer, which re-arms at the dequeue, reads 0
6320    /// for that slice (`itimer_mixed_masks_keep_the_timer_running`).
6321    #[serde(default)]
6322    pub(crate) delivered_since_request: bool,
6323    /// Set when the backend reports this thread's `rt_sigreturn`
6324    /// (`Tool::handle_sigreturn`), cleared by its next event of any other
6325    /// kind (`pre_handler_hook`). A signal stop that finds it set is the
6326    /// delivery Linux makes on that return to user space, and it continues
6327    /// the thread's turn even when the handler took a scheduler turn and so
6328    /// cleared `delivered_since_request`: without it, a reader whose handler
6329    /// calls `sched_yield` while a sibling re-signals it at every turn did
6330    /// not get past its handler within 900 s, where the report trigger alone
6331    /// took about 32,000 sends
6332    /// (<https://github.com/rrnewton/hermit/issues/4505>). It is read only
6333    /// where `signal_shadow::observes_sigreturns` holds, which it never does
6334    /// under `hermit record` or any replay (their signal-state copy is off,
6335    /// and a gdbserver turns the reports off), so a recording and every
6336    /// replay of it decide continuations by `delivered_since_request` alone.
6337    #[serde(default)]
6338    pub(crate) returned_from_sigreturn: bool,
6339    /// Deliveries announced to `Tool::announce_signal_event` and neither
6340    /// committed nor withdrawn yet, with what each one's turn left for its
6341    /// commit. A backend settles every announcement before it returns to
6342    /// guest code, so this is empty at any other point.
6343    #[serde(default)]
6344    pub(crate) signal_announcements: Vec<SignalAnnouncementState>,
6345    /// The token the next announcement gets.
6346    #[serde(default)]
6347    pub(crate) next_signal_announcement: u64,
6348    /// How many of this thread's latest scheduler requests in a row were
6349    /// deliveries marked `Resources::continues_sigreturn`; any other request
6350    /// resets it. A delivery is marked only while it is below
6351    /// [`MAX_SIGRETURN_CONTINUATIONS`]. That cap is what stops a handler that
6352    /// raises its own signal from holding the CPU for ever, in every mode: a
6353    /// timeslice end did not interrupt such a run in practice (7,773
6354    /// deliveries in 15 s without one), and under `--max-timeslice=disabled`
6355    /// there is none.
6356    #[serde(default)]
6357    pub(crate) consecutive_continuations: u8,
6358    /// How many times the scheduler has granted this thread a turn
6359    /// (`note_scheduler_handoff`). A signal stop compares it before and after
6360    /// its own awaits, so a grant among them ends the delivery window too.
6361    #[serde(default)]
6362    pub(crate) scheduler_grants: u64,
6363
6364    /// Thread state associated with record/replay.
6365    pub record_or_replay: T,
6366
6367    /// How much longer does this thread get to run before it must check-in with the
6368    /// scheduler?  Note that this notion of time slice can extend across a region of time
6369    /// that includes syscalls (and thus handlers).
6370    ///
6371    /// If set to `None`, the thread can run indefinitely without preemption.
6372    ///
6373    /// This is in units of virtual Nanoseconds.  And it is an exact time in the future,
6374    /// not a relative duration.
6375    pub end_of_timeslice: Option<LogicalTime>,
6376
6377    // AUTONOMOUS-BOT-IMPLEMENTED
6378    // TODO-HUMAN-REVIEW(PR-1151)
6379    /// Exact per-thread PMU RCB target for the active preemption-replay slice.
6380    /// `None` selects the legacy logical-time deadline path.
6381    #[serde(default)]
6382    pub replay_rcb_end: Option<u64>,
6383
6384    /// In a preemption replay, the logical time of the last priority change
6385    /// point this thread filed (`ThreadState::file_replay_change_point`).
6386    // Native-exec admission rejects live replay state; supported snapshots
6387    // retain their existing positional layout.
6388    #[serde(skip)]
6389    pub last_replay_change_point: Option<LogicalTime>,
6390
6391    // AUTONOMOUS-BOT-IMPLEMENTED
6392    // TODO-HUMAN-REVIEW(PR-1151)
6393    /// Deterministic chaos epoch this thread was in at its last `next_timeslice`.
6394    /// Used only to detect epoch transitions for `CHAOSEPOCH` logging; the epoch
6395    /// itself is recomputed each slice from `thread_logical_time`. Sentinel
6396    /// `u64::MAX` guarantees the first slice always logs its initial epoch.
6397    #[serde(default = "chaos_epoch_sentinel")]
6398    pub chaos_epoch: u64,
6399
6400    // AUTONOMOUS-BOT-IMPLEMENTED
6401    // TODO-HUMAN-REVIEW(PR-1151)
6402    /// Exact multiplier currently used to convert this thread's RCBs to virtual time.
6403    #[serde(default)]
6404    pub chaos_slowdown_factor: RcbTimeMultiplier,
6405
6406    // AUTONOMOUS-BOT-IMPLEMENTED
6407    // TODO-HUMAN-REVIEW(PR-1151)
6408    /// True when live chaos configuration or a replay artifact supplies the factor.
6409    #[serde(default)]
6410    pub chaos_slowdown_active: bool,
6411
6412    /// This thread claimed a `SIGCHLD` for a sleep (`SleepSignal::Claimed`),
6413    /// and the claimed copy's delivery stop has not come yet. That stop places
6414    /// the delivery (`ChildExitSigchldControl::ClaimedDelivery`) just before
6415    /// it asks for its turn, so the mark the scheduler keeps is bound to that
6416    /// copy. A system call, or a copy the ledger drops, clears this once
6417    /// SIGCHLD is no longer pending for the thread (the claimed copy was never
6418    /// delivered to it; `syscalls::signal::claim_still_pending`).
6419    #[serde(default)]
6420    pub(crate) claimed_sigchld: bool,
6421
6422    // AUTONOMOUS-BOT-IMPLEMENTED
6423    // TODO-HUMAN-REVIEW(PR-1151)
6424    /// Transitions waiting to be committed into the preemption artifact.
6425    #[serde(default)]
6426    pub pending_chaos_epochs: Vec<ChaosEpochTransition>,
6427
6428    /// Absolute deadline enforced by the PMU-backed `--max-timeslice` timer. This is separate from
6429    /// `end_of_timeslice` so syscall-heavy workloads can use a shorter, cheap target deadline.
6430    pub max_timeslice_end: Option<LogicalTime>,
6431
6432    /// The deadline hint carried by the last scheduler reply this thread
6433    /// received (`ThreadState::cap_timeslice_at_deadline`). Applied when the
6434    /// reply arrives, and again by the two paths that then install a fresh
6435    /// slice end: thread start and the exec continuation.
6436    pub last_grant_deadline: Option<Duration>,
6437
6438    /// Track what our last timer was set for, just to double check that RCB timers are behaving
6439    /// as expected and see if we went over.  (For exmaple, this behaves badly if threads are not
6440    /// pinned and our we migrate between cores.)
6441    pub last_rcb_timer: Option<u64>,
6442
6443    /// Whether `last_rcb_timer` represents the maximum deadline rather than a manual interrupt.
6444    #[serde(default)]
6445    pub last_rcb_timer_is_max: bool,
6446
6447    /// Whether the scheduler's last reply to this thread was a grant that
6448    /// found it the only thread able to run before it next enters Detcore
6449    /// (`GlobalResponse::WithLoneGrant`). Every reply without that mark
6450    /// clears it.
6451    #[serde(default)]
6452    pub lone_grant: bool,
6453
6454    /// While the thread runs alone (`Config::replays_lone_preemptions`): the
6455    /// branch count, in `committed_clock_value` units, at which its current
6456    /// timeslice ends. No timer stops the guest there; the thread's next
6457    /// entry into Detcore processes every such end at or before the count it
6458    /// reached, in order (`Detcore::catch_up_lone_slice_ends`).
6459    #[serde(default)]
6460    pub lone_slice_end: Option<u64>,
6461
6462    /// While such an end is processed: its branch count, which the clock
6463    /// update reads in place of the hardware counter.
6464    #[serde(default)]
6465    pub replaying_lone_slice_end: Option<u64>,
6466
6467    /// Set when a grant while such an end was processed did not find the
6468    /// thread alone, which the scheduler's predicate rules out; taken by
6469    /// `Detcore::catch_up_lone_slice_ends`, which reports it.
6470    #[serde(default)]
6471    pub lone_replay_unsound: bool,
6472
6473    /// Are we past the global moment when the guest's first execve of its root binary completes
6474    /// (with a successful exit code).
6475    pub(crate) past_global_first_execve: bool,
6476
6477    /// Guest address of this thread's `struct robust_list_head`, as last
6478    /// registered by a successful `set_robust_list(2)`.
6479    ///
6480    /// Linux clears `task->robust_list` in `copy_process()` and in `execve`, so
6481    /// this is never inherited: every thread re-registers its own head (glibc
6482    /// does so in `start_thread`). Detcore replays the kernel's
6483    /// `exit_robust_list()` walk from this address when the thread exits; see
6484    /// `crate::syscalls::robust_list`.
6485    #[serde(default)]
6486    pub(crate) robust_list_head: Option<usize>,
6487
6488    /// Per-thread registrations are mirrored here while their shared address
6489    /// space is readable, so a group exit can walk every dying sibling's list.
6490    #[serde(default)]
6491    pub(crate) robust_list_process: Arc<Mutex<RobustListProcessState>>,
6492
6493    /// Staged boundary protocol state. Runtime creation/exec does not enable
6494    /// it yet; legacy state keeps `None`. Native transfer cannot discard it.
6495    #[serde(default)]
6496    pub(crate) slice_boundary: Option<crate::slice_boundary::Observations>,
6497    #[serde(default)]
6498    pub(crate) callback_history: crate::preemptions::callback_history::State,
6499
6500    /// Under `--replay-schedule-from`, whether the replayed schedule is over
6501    /// for this thread: it ran out, or the replay released it
6502    /// (`Scheduler::schedule_replay_over`). The thread learns it from the
6503    /// reply to its next traced event, and a new thread inherits it from the
6504    /// thread that created it. From then on its slices end as in a run
6505    /// without a schedule (`follows_replayed_schedule`).
6506    #[serde(default)]
6507    pub(crate) schedule_replay_over: bool,
6508}
6509
6510/// We cannot assume that the record_or_replay "subtool" is Debug, so it is handy to be able to
6511/// print the Detcore threadstate alone.
6512impl<T> std::fmt::Debug for ThreadState<T> {
6513    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
6514        f.debug_struct("ThreadState")
6515            .field("dettid", &self.dettid)
6516            .field("detpid", &self.detpid)
6517            .field("physical_tid", &self.physical_tid)
6518            .field("mm_id", &self.mm_id)
6519            .field("memory_metadata", &self.memory_metadata)
6520            .field("stats", &self.stats)
6521            .field("clone_flags", &self.clone_flags)
6522            .field("file_metadata", &self.file_metadata)
6523            .field("process_files", &self.process_files)
6524            .field("posix_timers", &self.posix_timers)
6525            .field("resource_limits", &self.resource_limits)
6526            .field("process_cpu_time", &self.process_cpu_time)
6527            .field("prng", &self.prng)
6528            .field("chaos_prng", &self.chaos_prng)
6529            .field("thread_logical_time", &self.thread_logical_time)
6530            .field("committed_clock_value", &self.committed_clock_value)
6531            .field("end_of_timeslice", &self.end_of_timeslice)
6532            .field("replay_rcb_end", &self.replay_rcb_end)
6533            .field("chaos_epoch", &self.chaos_epoch)
6534            .field("chaos_slowdown_factor", &self.chaos_slowdown_factor)
6535            .field("chaos_slowdown_active", &self.chaos_slowdown_active)
6536            .field("max_timeslice_end", &self.max_timeslice_end)
6537            .field("last_rcb_timer", &self.last_rcb_timer)
6538            .field("last_rcb_timer_is_max", &self.last_rcb_timer_is_max)
6539            .finish()
6540    }
6541}
6542
6543impl<T> Default for ThreadState<T> {
6544    fn default() -> Self {
6545        unreachable!()
6546    }
6547}
6548
6549// AUTONOMOUS-BOT-IMPLEMENTED
6550// TODO-HUMAN-REVIEW(PR-1151)
6551/// Sentinel for `ThreadState::chaos_epoch` before the first `next_timeslice`.
6552/// `u64::MAX` can never equal a real epoch (`current_ns / N`), so the first
6553/// chaos slice always emits its `CHAOSEPOCH` transition.
6554pub(crate) fn chaos_epoch_sentinel() -> u64 {
6555    u64::MAX
6556}
6557
6558impl<T> AsRef<T> for ThreadState<T> {
6559    fn as_ref(&self) -> &T {
6560        &self.record_or_replay
6561    }
6562}
6563
6564impl<T> AsMut<T> for ThreadState<T> {
6565    fn as_mut(&mut self) -> &mut T {
6566        &mut self.record_or_replay
6567    }
6568}
6569
6570// AUTONOMOUS-BOT-IMPLEMENTED
6571// TODO-HUMAN-REVIEW(PR-1149)
6572/// RR-style stable per-thread slowdown factor for chaos scheduling.
6573///
6574/// Returns the multiplier applied to a thread's *mean* chaos timeslice length.
6575/// A factor `> 1.0` means the thread is preempted less often (runs "faster"
6576/// between preemptions — a slower relative wall-clock for its peers), `< 1.0`
6577/// means it is preempted more often. The factor is drawn log-uniformly from
6578/// `[1/max_factor, max_factor]`, so slow and fast are symmetric in log-space
6579/// and `1.0` is the geometric center.
6580///
6581/// The value is a **pure, deterministic function** of `(sched_seed, dettid)`:
6582/// it depends on no run-order, no wall-clock, and no shared PRNG state, so it
6583/// is stable for a thread across the whole run and reproducible under a fixed
6584/// seed (unlike the per-timeslice `chaos_prng` draw, which is redrawn every
6585/// slice and averages out over a long run). Threads are perturbed by a fixed
6586/// constant so the factor stream differs from other seed-derived streams (e.g.
6587/// `post_fork_prng`) that also start from `sched_seed`.
6588///
6589/// `max_factor <= 1.0` disables the spread and returns `1.0` (nominal) for
6590/// every thread; callers validate `max_factor >= 1.0`.
6591// AUTONOMOUS-BOT-IMPLEMENTED
6592// TODO-HUMAN-REVIEW(PR-1151)
6593/// `epoch` selects a deterministic chaos EPOCH: the factor is redrawn per
6594/// (thread, epoch) so a thread's bias changes in deterministic phases across a
6595/// long run instead of staying fixed. `epoch == 0` reproduces the epoch-less
6596/// per-thread-slowdown value EXACTLY (the epoch term is `0` and cancels out of
6597/// the mix), so enabling epochs never perturbs the first epoch's behavior.
6598pub(crate) fn chaos_per_thread_slowdown_factor(
6599    sched_seed: u64,
6600    dettid: DetTid,
6601    epoch: u64,
6602    max_factor: f64,
6603) -> RcbTimeMultiplier {
6604    // `<=` (rather than `!(max_factor > 1.0)`) keeps clippy's partial-ord lint
6605    // happy; validate_invariants already rejects non-finite factors upstream.
6606    if max_factor <= 1.0 {
6607        return RcbTimeMultiplier::ONE;
6608    }
6609    // Mix the seed with the (stable) deterministic thread id to give each
6610    // thread its own point in the factor distribution. The salt keeps this
6611    // stream distinct from other sched_seed-derived streams.
6612    const SLOWDOWN_SALT: u64 = 0x736c_6f77_646f_776e; // "slowdown"
6613    // AUTONOMOUS-BOT-IMPLEMENTED
6614    // TODO-HUMAN-REVIEW(PR-1151)
6615    // Fold the epoch into the mix with its own golden-ratio multiplier. At
6616    // `epoch == 0` this term is 0, leaving `mixed` identical to the epoch-less
6617    // factor; each successive epoch decorrelates the draw deterministically.
6618    const EPOCH_GOLDEN: u64 = 0xbf58_476d_1ce4_e5b9;
6619    let mixed = sched_seed
6620        ^ SLOWDOWN_SALT
6621        ^ ((dettid.as_raw() as u32 as u64).wrapping_mul(0x9e37_79b9_7f4a_7c15))
6622        ^ epoch.wrapping_mul(EPOCH_GOLDEN);
6623    let mut prng = Pcg64Mcg::seed_from_u64(mixed);
6624    // u in [0,1); map to exponent in [-1, 1) then factor = max_factor^exp,
6625    // i.e. a log-uniform draw over [1/max_factor, max_factor).
6626    let u: f64 = prng.random::<f64>();
6627    let exponent = 2.0 * u - 1.0;
6628    RcbTimeMultiplier::from_f64(max_factor.powf(exponent))
6629}
6630
6631impl<T> ThreadState<T> {
6632    /// Account one hand-off of this thread to the scheduler for a grant, in
6633    /// which other threads may run: it ends the window in which a signal stop
6634    /// continues the thread's last delivery (`delivered_since_request`), and
6635    /// a request that is itself such a continuation (`continues_sigreturn`)
6636    /// counts toward `consecutive_continuations`, which any other resets.
6637    /// Called by `tool_global::send_and_update_time` when the reply says the
6638    /// thread was granted a turn while its request was pending
6639    /// (`GlobalResponse::Granted`). The scheduler marks every point where it
6640    /// resumes a waiting thread (`Scheduler::granted`: a grant, or a parked
6641    /// wait resumed to observe a signal before its grant, whose nested signal
6642    /// hook then runs), so every hand-off is noted
6643    /// whatever the request (an ordinary or parked request, a futex wait, a
6644    /// parent's continuation after a fork, a schedule-replay trace, an exec's
6645    /// continuation), and an RPC answered without a grant is none.
6646    pub(crate) fn note_scheduler_handoff(&mut self, continues_sigreturn: bool) {
6647        self.scheduler_grants = self.scheduler_grants.wrapping_add(1);
6648        self.delivered_since_request = false;
6649        self.consecutive_continuations = if continues_sigreturn {
6650            self.consecutive_continuations.saturating_add(1)
6651        } else {
6652            0
6653        };
6654    }
6655
6656    /// What `observe_guest_clock(raw)` would return, without raising the
6657    /// clock's floor: the guest clock as of a raw observation.
6658    pub(crate) fn peek_guest_clock(&self, raw: LogicalTime) -> LogicalTime {
6659        let clock = self.guest_clock.lock().expect("guest clock mutex poisoned");
6660        clock.now.max(raw)
6661    }
6662
6663    /// Inspect the live per-thread cut. Physical task count, childlessness,
6664    /// coordinator leases and signal dispositions need their own admission;
6665    /// neither serde nor these lock probes establish those facts.
6666    pub(crate) fn validate_native_exec_cut(
6667        &self,
6668        binding: &crate::native_exec::NativeExecCutBinding,
6669        config: &Config,
6670    ) -> Result<(), crate::native_exec::NativeExecCutError> {
6671        use crate::native_exec::NativeExecCutError;
6672        use crate::native_exec::try_native_exec_lock;
6673
6674        if self.dettid != binding.dettid
6675            || self.detpid != Some(binding.detpid)
6676            || self.physical_tid != Some(binding.physical_tid)
6677        {
6678            return Err(NativeExecCutError::Identity("ThreadState IDs"));
6679        }
6680        if !self.thread_start_entered || !self.past_global_first_execve {
6681            return Err(NativeExecCutError::PreparedState("initial lifecycle"));
6682        }
6683        if self.tool_shares_guest_descriptor_table
6684            != config.backend.tool_shares_guest_descriptor_table
6685        {
6686            return Err(NativeExecCutError::Identity("descriptor-table capability"));
6687        }
6688        if self.nested_pid_namespace {
6689            return Err(NativeExecCutError::LiveCapability("nested PID namespace"));
6690        }
6691        if self.signal_task_identity.is_some() {
6692            return Err(NativeExecCutError::LiveCapability("signal task identity"));
6693        }
6694        if self.parent_process_cpu_time.is_some() || self.parent_cpu_publication.is_some() {
6695            return Err(NativeExecCutError::LiveCapability("parent CPU ownership"));
6696        }
6697        if self.restart_block.is_some() || self.signal_register_projection.is_some() {
6698            return Err(NativeExecCutError::LiveCapability("restart/projection"));
6699        }
6700        if self.claimed_sigchld {
6701            return Err(NativeExecCutError::LiveCapability("claimed SIGCHLD"));
6702        }
6703        if !self.signal_announcements.is_empty() {
6704            return Err(NativeExecCutError::LiveCapability(
6705                "unsettled signal announcement",
6706            ));
6707        }
6708        if self.clone_flags.is_some() || self.pending_vfork.is_some() {
6709            return Err(NativeExecCutError::LiveCapability("clone/vfork"));
6710        }
6711        if self.own_signal_turn_hold.is_some()
6712            || self.sigalrm_handled
6713            || self.handler_restore_rt_reported != 0
6714        {
6715            return Err(NativeExecCutError::LiveCapability("guest signal handler"));
6716        }
6717        if self.initialized_random_auxv.is_some() {
6718            return Err(NativeExecCutError::LiveCapability("initial random handoff"));
6719        }
6720        if self.uncharged_bootstrap_syscalls != 0
6721            || self.in_uncharged_bootstrap_syscall
6722            || self.yield_after_futex_wake
6723        {
6724            return Err(NativeExecCutError::LiveCapability(
6725                "bootstrap/yield transient",
6726            ));
6727        }
6728        if self.preemption_points.is_some()
6729            || self.slice_boundary.is_some()
6730            || !self.interrupt_at.is_empty()
6731            || self.replay_rcb_end.is_some()
6732            || self.last_replay_change_point.is_some()
6733            || self.last_rcb_timer.is_some()
6734            || self.last_rcb_timer_is_max
6735            || self.max_timeslice_end.is_some()
6736            || self.chaos_slowdown_active
6737            || !self.pending_chaos_epochs.is_empty()
6738        {
6739            return Err(NativeExecCutError::LiveCapability("RCB/replay/chaos state"));
6740        }
6741        let user = self.thread_logical_time.user_cpu_time();
6742        let system = self.thread_logical_time.system_cpu_time();
6743        if self.thread_cpu_start_user_time > self.last_accounted_user_time
6744            || self.last_accounted_user_time > user
6745            || self.thread_cpu_start_system_time > self.last_accounted_system_time
6746            || self.last_accounted_system_time > system
6747        {
6748            return Err(NativeExecCutError::PreparedState(
6749                "CPU accounting checkpoints",
6750            ));
6751        }
6752        try_native_exec_lock(&self.process_cpu_time, "process_cpu_time")?
6753            .validate_native_exec_cut()?;
6754        // Do not call observe/accounting routines: pending deltas and the
6755        // process-tree guest floor are continuing state, not a new epoch.
6756        drop(try_native_exec_lock(&self.guest_clock, "guest_clock")?);
6757        drop(try_native_exec_lock(
6758            &self.resource_limits,
6759            "resource_limits",
6760        )?);
6761        drop(try_native_exec_lock(&self.posix_timers, "posix_timers")?);
6762        Ok(())
6763    }
6764
6765    pub(crate) fn validate_old_native_exec_robust_state(
6766        &self,
6767    ) -> Result<(), crate::native_exec::NativeExecCutError> {
6768        use crate::native_exec::NativeExecCutError;
6769        let process = crate::native_exec::try_native_exec_lock(
6770            &self.robust_list_process,
6771            "robust_list_process",
6772        )?;
6773        if process.heads.len() != usize::from(self.robust_list_head.is_some())
6774            || process.heads.get(&self.dettid).copied() != self.robust_list_head
6775            || !process.pending.is_empty()
6776            || !process.matched_physical_exits.is_empty()
6777            || !process.exec_pending.is_empty()
6778            || !process.tool_walked.is_empty()
6779            || process.exit_group_filed
6780            || !process.exit_group_filers.is_empty()
6781            || process.exit_group_walker.is_some()
6782            || process.exit_group_walked
6783        {
6784            return Err(NativeExecCutError::LiveCapability(
6785                "old robust process state",
6786            ));
6787        }
6788        Ok(())
6789    }
6790
6791    pub(crate) fn validate_prepared_native_exec_cut(
6792        &self,
6793        binding: &crate::native_exec::NativeExecCutBinding,
6794    ) -> Result<(), crate::native_exec::NativeExecCutError> {
6795        use crate::native_exec::NativeExecCutError;
6796        use crate::native_exec::try_native_exec_lock;
6797
6798        if self.mm_id != binding.prepared_mm() {
6799            return Err(NativeExecCutError::Identity("prepared mm"));
6800        }
6801        try_native_exec_lock(&self.file_metadata, "prepared file_metadata")?
6802            .validate_native_exec_cut(binding.prepared_files(), true)?;
6803        if !try_native_exec_lock(&self.memory_metadata, "memory_metadata")?
6804            .is_fresh_for_native_exec()
6805        {
6806            return Err(NativeExecCutError::PreparedState("memory_metadata"));
6807        }
6808        let process = try_native_exec_lock(&self.robust_list_process, "robust_list_process")?;
6809        if self.robust_list_head.is_some()
6810            || !process.heads.is_empty()
6811            || !process.pending.is_empty()
6812            || !process.matched_physical_exits.is_empty()
6813            || !process.tool_walked.is_empty()
6814            || process.exit_group_filed
6815            || !process.exit_group_filers.is_empty()
6816            || process.exit_group_walker.is_some()
6817            || process.exit_group_walked
6818        {
6819            return Err(NativeExecCutError::PreparedState("robust process state"));
6820        }
6821        // These wakes belong to the old image. Preserve their order and keys
6822        // for the actual successful post-exec hook; do not rebase or consume.
6823        for (owner, wake) in &process.exec_pending {
6824            if *owner != binding.dettid
6825                || matches!(wake.futex, FutexID::Private { mm, .. } if mm != binding.old_mm)
6826            {
6827                return Err(NativeExecCutError::PreparedState("staged robust wake"));
6828            }
6829        }
6830        Ok(())
6831    }
6832
6833    pub(crate) fn observe_guest_clock(&self, raw: LogicalTime) -> LogicalTime {
6834        self.guest_clock
6835            .lock()
6836            .expect("guest clock mutex poisoned")
6837            .observe(raw)
6838    }
6839
6840    // AUTONOMOUS-BOT-IMPLEMENTED
6841    // TODO-HUMAN-REVIEW(PR-1060): Review backend-stable child RNG reseeding.
6842    /// Replaces the host-TID-derived child streams with a backend-provided,
6843    /// deterministic identity before the thread enters its start hook.
6844    pub fn reseed_child_rngs(&mut self, parent: &Self, entropy: u128) {
6845        self.prng = thread_rng_from_parent_entropy("USER RAND", &parent.prng, entropy);
6846        self.chaos_prng = thread_rng_from_parent_entropy("CHAOSRAND", &parent.chaos_prng, entropy);
6847    }
6848
6849    // AUTONOMOUS-BOT-IMPLEMENTED
6850    // TODO-HUMAN-REVIEW(PR-845): Review SaBRe orphan-thread memory identity recovery.
6851    /// Repair a thread state that a remote backend had to initialize without
6852    /// access to its parent's state.
6853    pub(crate) fn recover_process_mm_id(&mut self, detpid: DetPid) -> bool {
6854        if self.dettid == detpid || self.mm_id != MmId::initial(self.dettid) {
6855            return false;
6856        }
6857
6858        self.mm_id = MmId::initial(detpid);
6859        true
6860    }
6861
6862    pub(crate) fn account_process_cpu_time(&mut self) {
6863        let user = self.thread_logical_time.user_cpu_time();
6864        let system = self.thread_logical_time.system_cpu_time();
6865        let user_delta = user - self.last_accounted_user_time;
6866        let system_delta = system - self.last_accounted_system_time;
6867        let own = self.thread_cpu_time();
6868        let mut process = self
6869            .process_cpu_time
6870            .lock()
6871            .expect("process CPU time mutex poisoned");
6872        process.add_thread_delta(user_delta, system_delta);
6873        process.record_thread(self.dettid, own);
6874        drop(process);
6875        self.last_accounted_user_time = user;
6876        self.last_accounted_system_time = system;
6877    }
6878
6879    /// Drop this exited thread's entry from its process's per-thread table,
6880    /// once its final time is accounted: a thread other than the leader is
6881    /// gone for good, and its id names no thread's clock any more.
6882    pub(crate) fn forget_thread_cpu_time(&self) {
6883        self.process_cpu_time
6884            .lock()
6885            .expect("process CPU time mutex poisoned")
6886            .forget_thread(self.dettid);
6887    }
6888
6889    /// Another thread of this process's own (user, system) time, as of that
6890    /// thread's last accounting, which happens at each of its syscalls and
6891    /// each RCB commit: with threads sequentialized, where it last stopped.
6892    pub(crate) fn sibling_cpu_time(&self, thread: DetTid) -> (LogicalTime, LogicalTime) {
6893        self.process_cpu_time
6894            .lock()
6895            .expect("process CPU time mutex poisoned")
6896            .thread_time(thread)
6897    }
6898
6899    /// The own (user, system) time of `thread`, this thread or another of
6900    /// its process: this thread's now ([`Self::thread_cpu_time`]), another's
6901    /// as of its last accounting ([`Self::sibling_cpu_time`]).
6902    pub(crate) fn task_cpu_time(&self, thread: DetTid) -> (LogicalTime, LogicalTime) {
6903        if thread == self.dettid {
6904            self.thread_cpu_time()
6905        } else {
6906            self.sibling_cpu_time(thread)
6907        }
6908    }
6909
6910    /// Decide whether the syscall this thread is making now charges its cost
6911    /// to `thread_logical_time`. `in_backend_runtime_bootstrap` is the
6912    /// backend's `Guest::is_backend_runtime_bootstrap()` answer for this
6913    /// thread at this syscall.
6914    ///
6915    /// Outside a bootstrap window every syscall is charged, and the
6916    /// per-window count of uncharged syscalls goes back to zero. Inside a
6917    /// window a syscall that observes virtual time is always charged; any
6918    /// other syscall is left uncharged until
6919    /// [`syscall_time::MAX_UNCHARGED_BOOTSTRAP_SYSCALLS`] of them have been,
6920    /// and every syscall after that in the same window is charged. Reaching
6921    /// the cap logs one info line for the window.
6922    ///
6923    /// [`syscall_time::MAX_UNCHARGED_BOOTSTRAP_SYSCALLS`]: crate::syscall_time::MAX_UNCHARGED_BOOTSTRAP_SYSCALLS
6924    pub(crate) fn charge_syscall_time(
6925        &mut self,
6926        in_backend_runtime_bootstrap: bool,
6927        sysno: Sysno,
6928    ) -> bool {
6929        if !in_backend_runtime_bootstrap {
6930            if self.uncharged_bootstrap_syscalls != 0 {
6931                debug!(
6932                    "[dtid {}] backend runtime bootstrap window left {} syscalls uncharged",
6933                    self.dettid, self.uncharged_bootstrap_syscalls
6934                );
6935                self.uncharged_bootstrap_syscalls = 0;
6936            }
6937            return true;
6938        }
6939        if crate::syscall_time::observes_virtual_time(sysno) {
6940            return true;
6941        }
6942        if self.uncharged_bootstrap_syscalls < crate::syscall_time::MAX_UNCHARGED_BOOTSTRAP_SYSCALLS
6943        {
6944            self.uncharged_bootstrap_syscalls += 1;
6945            if self.uncharged_bootstrap_syscalls
6946                == crate::syscall_time::MAX_UNCHARGED_BOOTSTRAP_SYSCALLS
6947            {
6948                // Once per window: the count stays at the cap from here on.
6949                info!(
6950                    "[dtid {}] backend runtime bootstrap window reached its cap of {} uncharged syscalls; every further syscall in this window is charged",
6951                    self.dettid,
6952                    crate::syscall_time::MAX_UNCHARGED_BOOTSTRAP_SYSCALLS
6953                );
6954            }
6955            return false;
6956        }
6957        true
6958    }
6959
6960    pub(crate) fn process_cpu_time(&mut self) -> ProcessCpuSnapshot {
6961        self.account_process_cpu_time();
6962        self.process_cpu_time
6963            .lock()
6964            .expect("process CPU time mutex poisoned")
6965            .snapshot
6966    }
6967
6968    /// This thread's own logical (user, system) CPU time.
6969    ///
6970    /// `getrusage(RUSAGE_THREAD)` needs the per-thread counters, not the process aggregate
6971    /// that [`Self::process_cpu_time`] returns; for a multithreaded guest the two differ and
6972    /// reporting the aggregate would over-report every thread. Reads the same
6973    /// `thread_logical_time` counters that `account_process_cpu_time` folds into the process
6974    /// total, minus the absolute clock inherited when this thread was created.
6975    pub(crate) fn thread_cpu_time(&self) -> (LogicalTime, LogicalTime) {
6976        (
6977            self.thread_logical_time.user_cpu_time() - self.thread_cpu_start_user_time,
6978            self.thread_logical_time.system_cpu_time() - self.thread_cpu_start_system_time,
6979        )
6980    }
6981
6982    pub(crate) fn record_exited_child_process_cpu_time(&mut self, pid: DetPid) -> bool {
6983        self.account_process_cpu_time();
6984        let Some(parent) = &self.parent_process_cpu_time else {
6985            return true;
6986        };
6987        let child = self
6988            .process_cpu_time
6989            .lock()
6990            .expect("process CPU time mutex poisoned")
6991            .snapshot;
6992        {
6993            let mut parent = parent
6994                .lock()
6995                .expect("parent process CPU time mutex poisoned");
6996            if let Some(publication) = &self.parent_cpu_publication
6997                && !parent
6998                    .child_cpu_publications
6999                    .get(&pid)
7000                    .is_some_and(|current| Arc::ptr_eq(current, publication))
7001            {
7002                return false;
7003            }
7004            parent.record_exited_child(pid, child);
7005        }
7006        if let Some(publication) = &self.parent_cpu_publication {
7007            publication.finish(true);
7008        }
7009        true
7010    }
7011
7012    /// Called once at the process child's ThreadState birth. The later external
7013    /// registration's legacy prepare_child call must not replace this token.
7014    pub(crate) fn prepare_child_cpu_publication(&self, pid: DetPid) -> Arc<ChildCpuPublication> {
7015        let publication = ChildCpuPublication::pending();
7016        let previous = {
7017            let mut parent = self
7018                .process_cpu_time
7019                .lock()
7020                .expect("process CPU time mutex poisoned");
7021            parent.prepare_child(pid);
7022            parent
7023                .child_cpu_publications
7024                .insert(pid, Arc::clone(&publication))
7025        };
7026        if let Some(previous) = previous {
7027            previous.finish(false);
7028        }
7029        publication
7030    }
7031
7032    pub(crate) async fn wait_for_child_cpu_publication(&self, pid: DetPid) -> bool {
7033        let publication = {
7034            let parent = self
7035                .process_cpu_time
7036                .lock()
7037                .expect("process CPU time mutex poisoned");
7038            let Some(publication) = parent.child_cpu_publications.get(&pid) else {
7039                return false;
7040            };
7041            Arc::clone(publication)
7042        };
7043        // Subscription and publication share the token mutex. Registration
7044        // followed by a state recheck cannot lose a publication wake. Neither
7045        // this mutex nor the accounting mutex survives a Pending return.
7046        if !publication.wait().await {
7047            return false;
7048        }
7049        let parent = self
7050            .process_cpu_time
7051            .lock()
7052            .expect("process CPU time mutex poisoned");
7053        parent
7054            .child_cpu_publications
7055            .get(&pid)
7056            .is_some_and(|current| Arc::ptr_eq(current, &publication))
7057            && parent.exited_children.contains_key(&pid)
7058    }
7059
7060    pub(crate) fn has_exited_child_process_cpu_time(&self, pid: DetPid) -> bool {
7061        self.process_cpu_time
7062            .lock()
7063            .expect("process CPU time mutex poisoned")
7064            .exited_children
7065            .contains_key(&pid)
7066    }
7067
7068    pub(crate) fn reap_child_process_cpu_time(&mut self, pid: DetPid) {
7069        self.account_process_cpu_time();
7070        let publication = {
7071            let mut parent = self
7072                .process_cpu_time
7073                .lock()
7074                .expect("process CPU time mutex poisoned");
7075            // Preserve legacy missing-snapshot behavior. The KVM waitid path
7076            // requires the snapshot before its irreversible completion.
7077            let had_snapshot = parent.exited_children.contains_key(&pid);
7078            parent.reap_child(pid);
7079            if had_snapshot {
7080                parent.child_cpu_publications.remove(&pid)
7081            } else {
7082                None
7083            }
7084        };
7085        if let Some(publication) = publication {
7086            publication.finish(false);
7087        }
7088    }
7089
7090    pub(crate) fn prepare_child_process_cpu_time(&self, pid: DetPid) {
7091        let mut parent = self
7092            .process_cpu_time
7093            .lock()
7094            .expect("process CPU time mutex poisoned");
7095        // External registration may repeat preparation after ThreadState birth.
7096        // Only that birth replaces a KVM token; registration must preserve both
7097        // a pending token and any already-published snapshot for this birth.
7098        if !parent.child_cpu_publications.contains_key(&pid) {
7099            parent.prepare_child(pid);
7100        }
7101    }
7102
7103    /// Create a fresh new thread state from nothing.  In practice this is only used for the thread
7104    /// state of the root thread of the container.
7105    pub fn new(pid: DetPid, cfg: &Config, record_or_replay: T) -> Self {
7106        crate::detlog::root_seed_records(pid.as_raw(), cfg.rng_seed(), cfg.sched_seed());
7107        let thread_logical_time = DetTime::new(cfg);
7108        let last_accounted_user_time = thread_logical_time.user_cpu_time();
7109        let last_accounted_system_time = thread_logical_time.system_cpu_time();
7110        let file_metadata = if cfg.backend.tool_shares_guest_descriptor_table {
7111            let mut metadata = FileMetadata::new(pid);
7112            metadata.note_proc_level(cfg.proc_level);
7113            for fd in 0..=2 {
7114                match metadata.discover_fd_from_current_process(pid, fd) {
7115                    Ok(()) | Err(Errno::EBADF) => {}
7116                    Err(error) => panic!("unable to discover guest stdio {fd}: {error}"),
7117                }
7118            }
7119            metadata
7120        } else {
7121            let mut metadata =
7122                FileMetadata::new(pid).setup_stdio(pid.into(), pid, cfg.standard_stream_aliases);
7123            metadata.note_proc_level(cfg.proc_level);
7124            metadata
7125        };
7126        let process_files = Some(file_metadata.files_id);
7127        ThreadState {
7128            dettid: pid,
7129            detpid: None, // Initialized later.
7130            thread_start_entered: false,
7131            vfork_origin: None, // The root thread.
7132            physical_tid: None,
7133            signal_task_identity: None,
7134            open_file_creator: None,
7135            receive_geometry: None,
7136            entry_import: None,
7137            send_inspection: None,
7138            inotify_queue: None,
7139            call_parts: None,
7140            traced_call: Default::default(),
7141            mm_id: MmId::initial(pid),
7142            memory_metadata: Arc::new(Mutex::new(MemoryMetadata::new())),
7143            pedigree: Pedigree::new(), // Root thread.
7144            stats: ThreadStats::new(),
7145            file_metadata: Arc::new(Mutex::new(file_metadata)),
7146            tool_shares_guest_descriptor_table: cfg.backend.tool_shares_guest_descriptor_table,
7147            timer_slack_ns: DEFAULT_TIMER_SLACK_NS,
7148            default_timer_slack_ns: DEFAULT_TIMER_SLACK_NS,
7149            nested_pid_namespace: false,
7150            changed_root: Default::default(),
7151            fs_apart_from_leader: false,
7152            exe: None,
7153            process_files,
7154            posix_timers: Arc::new(Mutex::new(PosixTimers::default())),
7155            resource_limits: Arc::new(Mutex::new(ResourceLimits::default())),
7156            process_cpu_time: Arc::new(Mutex::new(ProcessCpuTime::default())),
7157            guest_clock: Arc::new(Mutex::new(GuestClock::default())),
7158            unix_connections: Default::default(),
7159            parent_process_cpu_time: None,
7160            parent_cpu_publication: None,
7161            restart_block: None,
7162            syscall_cwd: None,
7163            sigalrm_handled: false,
7164            signal_shadow: None,
7165            signal_shadow_lost: false,
7166            delivery_interruption: None,
7167            wait_signal_end: None,
7168            delivery_frame_handler: 0,
7169            delivery_frame_signal: 0,
7170            delivery_frame_orig_ax: 0,
7171            delivery_counters: Default::default(),
7172            published_mask: Some(None),
7173            published_dispositions: Arc::default(),
7174            sigalrm_blocked_published: false,
7175            handler_restore_rt_reported: 0,
7176            signal_register_projection: None,
7177            last_accounted_user_time,
7178            last_accounted_system_time,
7179            thread_cpu_start_user_time: last_accounted_user_time,
7180            thread_cpu_start_system_time: last_accounted_system_time,
7181            clone_flags: None,
7182            pending_vfork: None,
7183            restarted_clone_id: None,
7184            procfs_sequence_taken: None,
7185            // For the root thread, we initialize from the seed in the config:
7186            prng: crate::random::root_prng(cfg.rng_seed()),
7187            initialized_random_auxv: None,
7188            chaos_prng: Pcg64Mcg::seed_from_u64(cfg.sched_seed()),
7189            thread_logical_time,
7190            committed_clock_value: 0,
7191            uncharged_bootstrap_syscalls: 0,
7192            in_uncharged_bootstrap_syscall: false,
7193            passed_descriptors_out_unreported: false,
7194            passed_descriptors_out_reported: false,
7195            nonblocking_result_normalized: false,
7196            yield_after_futex_wake: false,
7197            pipe_handoff_due: None,
7198            own_signal_turn_hold: None,
7199            delivered_since_request: false,
7200            returned_from_sigreturn: false,
7201            signal_announcements: Vec::new(),
7202            next_signal_announcement: 0,
7203            consecutive_continuations: 0,
7204            scheduler_grants: 0,
7205            end_of_timeslice: None, // Temporary/bogus.
7206            replay_rcb_end: None,
7207            last_replay_change_point: None,
7208            // AUTONOMOUS-BOT-IMPLEMENTED
7209            // TODO-HUMAN-REVIEW(PR-1151)
7210            chaos_epoch: chaos_epoch_sentinel(),
7211            // AUTONOMOUS-BOT-IMPLEMENTED
7212            // TODO-HUMAN-REVIEW(PR-1151)
7213            chaos_slowdown_factor: RcbTimeMultiplier::ONE,
7214            chaos_slowdown_active: false,
7215            claimed_sigchld: false,
7216            pending_chaos_epochs: Vec::new(),
7217            max_timeslice_end: None,
7218            last_grant_deadline: None,
7219            published_runtime_private_memory: None,
7220            last_rcb_timer: None,
7221            last_rcb_timer_is_max: false,
7222            lone_grant: false,
7223            lone_slice_end: None,
7224            replaying_lone_slice_end: None,
7225            lone_replay_unsound: false,
7226            record_or_replay,
7227            preemption_points: None,
7228            past_global_first_execve: false,
7229            interrupt_at: cfg.interrupts_for_thread(pid),
7230            robust_list_head: None,
7231            robust_list_process: Arc::new(Mutex::new(RobustListProcessState::default())),
7232            slice_boundary: None,
7233            callback_history: Default::default(),
7234            schedule_replay_over: false,
7235        }
7236    }
7237
7238    /// Select the current image before its first registration, only from the
7239    /// authenticated initial-image Tool hook. No registered identity is rekeyed.
7240    pub(crate) fn bind_initial_current_image(&mut self, tid: DetTid) -> Result<(), Errno> {
7241        if tid != self.dettid
7242            || self.detpid.is_some()
7243            || self.thread_start_entered
7244            || self.past_global_first_execve
7245            || self.mm_id != MmId::initial(tid)
7246            || self.clone_flags.is_some()
7247            || self.pending_vfork.is_some()
7248            || self.pedigree.raw() != Pedigree::new().raw()
7249            || self.published_runtime_private_memory.is_some()
7250        {
7251            return Err(Errno::EPROTO);
7252        }
7253        self.mm_id = self.mm_id.for_exec(tid);
7254        Ok(())
7255    }
7256
7257    /// Apply a required, authenticated initial-image random handoff to the
7258    /// normal root. Errors are tool initialization failures, never guest errno.
7259    pub fn apply_initial_random_state(
7260        &mut self,
7261        bytes: &[u8],
7262        config: &Config,
7263        image: crate::random::InitialImage,
7264    ) -> Result<(), Errno> {
7265        if self.initialized_random_auxv.is_some()
7266            || self.past_global_first_execve
7267            || self.dettid.as_raw() != image.pid
7268            || self.clone_flags.is_some()
7269            || self.pedigree.raw() != Pedigree::new().raw()
7270        {
7271            return Err(Errno::EPROTO);
7272        }
7273        let (prng, at_random_value, early_requests) =
7274            crate::random::decode_initial_state(bytes, config, image)?;
7275        self.prng = prng;
7276        self.initialized_random_auxv = Some((image, at_random_value, early_requests));
7277        Ok(())
7278    }
7279
7280    /// Consume the early auxv write's fact, returning the bytes it wrote and
7281    /// the requests served before post-exec, in order, or `None` when no
7282    /// backend wrote AT_RANDOM before post-exec.
7283    pub(crate) fn complete_initial_random_auxv(
7284        &mut self,
7285        pointer: Option<usize>,
7286    ) -> Result<Option<crate::random::EarlyRandomRecords>, Errno> {
7287        let Some((image, _, _)) = self.initialized_random_auxv.as_ref() else {
7288            return Ok(None);
7289        };
7290        if pointer != Some(image.at_random) || self.dettid.as_raw() != image.pid {
7291            return Err(Errno::EPROTO);
7292        }
7293        let (_, at_random_value, early_requests) =
7294            self.initialized_random_auxv.take().expect("checked above");
7295        Ok(Some((at_random_value, early_requests)))
7296    }
7297
7298    pub(crate) fn record_robust_list_head(&mut self, head: Option<usize>) {
7299        self.robust_list_head = head;
7300        let mut process = self
7301            .robust_list_process
7302            .lock()
7303            .expect("robust-list process state mutex poisoned");
7304        if let Some(head) = head {
7305            process.heads.insert(self.dettid, head);
7306        } else {
7307            process.heads.remove(&self.dettid);
7308        }
7309    }
7310
7311    /// Record that Detcore has walked this thread's own robust list at its
7312    /// exit (`OwnerDeathPath::Barrier`), so a later exit_group of its group
7313    /// does not walk the list again ([`exit_group_robust_list_owners`]).
7314    pub(crate) fn mark_robust_list_walked_by_tool(&self) {
7315        self.robust_list_process
7316            .lock()
7317            .expect("robust-list process state mutex poisoned")
7318            .tool_walked
7319            .insert(self.dettid);
7320    }
7321
7322    /// Note that an exit_group on DBT (`OwnerDeathPath::Barrier`) has been
7323    /// filed. From now on a thread the exit_group's grant kills waits for its
7324    /// walk before its native exit ([`Self::wait_for_exit_group_robust_walk`]),
7325    /// and a thread of the group that exits keeps its head registered for the
7326    /// walk, so the walk sees the registrations current at the grant even if
7327    /// an exit hook runs first. A registration, replacement or unregistration
7328    /// a thread makes still takes effect.
7329    ///
7330    /// Nothing clears the filing: the thread group ends with the exit_group.
7331    /// The one exception is an exec granted first, which kills the filer.
7332    /// Where exec keeps this shared state for the new image, killed threads'
7333    /// old heads would then stay listed for a later exit_group. Only DBT files,
7334    /// and there the exec replaces the in-guest runtime's memory, this state
7335    /// included; modeling exec on DBT
7336    /// (<https://github.com/rrnewton/hermit/issues/4268>) must keep it so.
7337    pub(crate) fn file_exit_group_for_robust_lists(&self) {
7338        let mut process = self
7339            .robust_list_process
7340            .lock()
7341            .expect("robust-list process state mutex poisoned");
7342        process.exit_group_filed = true;
7343        process.exit_group_filers.insert(self.dettid);
7344    }
7345
7346    /// Note that this thread's exit_group was granted and that it now walks
7347    /// the group's lists. Only this thread does not wait for the walk: any
7348    /// other thread, including one that filed an exit_group of its own and
7349    /// was killed by this one's grant, waits.
7350    pub(crate) fn begin_exit_group_robust_walk(&self) {
7351        self.robust_list_process
7352            .lock()
7353            .expect("robust-list process state mutex poisoned")
7354            .exit_group_walker = Some(self.dettid);
7355    }
7356
7357    /// Note that this thread was killed, so any exit_group it filed can never
7358    /// be granted. Once every filing is cancelled this way with none granted
7359    /// (an exec ended the group first), no walk will come, and the threads
7360    /// waiting for one are released ([`Self::wait_for_exit_group_robust_walk`]).
7361    pub(crate) fn cancel_exit_group_filing(&self) {
7362        self.robust_list_process
7363            .lock()
7364            .expect("robust-list process state mutex poisoned")
7365            .exit_group_filers
7366            .remove(&self.dettid);
7367    }
7368
7369    /// Note that the filed exit_group's walk has finished, releasing the
7370    /// threads it killed ([`Self::wait_for_exit_group_robust_walk`]).
7371    pub(crate) fn finish_exit_group_robust_walk(&self) {
7372        self.robust_list_process
7373            .lock()
7374            .expect("robust-list process state mutex poisoned")
7375            .exit_group_walked = true;
7376    }
7377
7378    /// Where an exit_group of this thread's group is filed and this thread is
7379    /// not walking it, wait until that exit_group's walk has finished, or until
7380    /// every filing has been cancelled with none granted, at most `bound`.
7381    /// Returns false if the bound expired first, which the caller treats as a
7382    /// failure of the run, never as permission to go on.
7383    ///
7384    /// A thread the exit_group kills retires with a native exit. On DBT that
7385    /// exit first rewrites the thread's robust-list owner words to its host
7386    /// TID and then lets Linux mark them, which would race the walk: the walk
7387    /// could find a word that no longer names its owner, and could not tell
7388    /// whose it was. Waiting makes the walk see every word as the guest left
7389    /// it. That includes a thread that filed an exit_group of its own and was
7390    /// killed by another's grant; only the thread whose exit_group was granted
7391    /// ([`Self::begin_exit_group_robust_walk`]) does not wait. The wait is on
7392    /// host time, but nothing guest-visible depends on how long it takes: the
7393    /// thread is already logically dead. A walk that never comes, because an
7394    /// exec killed every filer first, is recognised when the last filing is
7395    /// cancelled ([`Self::cancel_exit_group_filing`]). Only DBT files an
7396    /// exit_group, and there each guest thread polls its own callbacks, so the
7397    /// blocking wait holds up only this thread.
7398    pub(crate) fn wait_for_exit_group_robust_walk(&self, bound: std::time::Duration) -> bool {
7399        let started = std::time::Instant::now();
7400        loop {
7401            {
7402                let process = self
7403                    .robust_list_process
7404                    .lock()
7405                    .expect("robust-list process state mutex poisoned");
7406                if !process.exit_group_filed
7407                    || process.exit_group_walked
7408                    || process.exit_group_walker == Some(self.dettid)
7409                    || (process.exit_group_walker.is_none() && process.exit_group_filers.is_empty())
7410                {
7411                    return true;
7412                }
7413            }
7414            if started.elapsed() >= bound {
7415                return false;
7416            }
7417            std::thread::sleep(std::time::Duration::from_micros(200));
7418        }
7419    }
7420
7421    /// The exit hook's counterpart of `record_robust_list_head(None)`: the
7422    /// thread is gone, so its own head is cleared, but while an exit_group is
7423    /// filed ([`Self::file_exit_group_for_robust_lists`]) its registration
7424    /// stays in the group's index.
7425    pub(crate) fn drop_robust_list_head_at_exit(&mut self) {
7426        self.robust_list_head = None;
7427        let mut process = self
7428            .robust_list_process
7429            .lock()
7430            .expect("robust-list process state mutex poisoned");
7431        if !process.exit_group_filed {
7432            process.heads.remove(&self.dettid);
7433        }
7434    }
7435
7436    /// The registrations an exit_group walks on DBT
7437    /// (`OwnerDeathPath::Barrier`), read at its grant: every registration current then, including
7438    /// those of threads the grant killed, less any thread whose own exit
7439    /// Detcore already replayed, so no list is walked twice.
7440    pub(crate) fn robust_list_owners_for_exit_group(&self) -> Vec<RobustListOwner> {
7441        let walked = self
7442            .robust_list_process
7443            .lock()
7444            .expect("robust-list process state mutex poisoned")
7445            .tool_walked
7446            .clone();
7447        self.robust_list_owners()
7448            .into_iter()
7449            .filter(|owner| !walked.contains(&owner.dettid))
7450            .collect()
7451    }
7452
7453    /// [`Self::robust_list_heads`] as [`RobustListOwner`]s.
7454    pub(crate) fn robust_list_owners(&self) -> Vec<RobustListOwner> {
7455        let process = self
7456            .robust_list_process
7457            .lock()
7458            .expect("robust-list process state mutex poisoned");
7459        process
7460            .heads
7461            .iter()
7462            .map(|(&dettid, &head)| RobustListOwner { dettid, head })
7463            .collect()
7464    }
7465
7466    #[cfg(test)]
7467    pub(crate) fn robust_list_heads(&self) -> Vec<(DetTid, usize)> {
7468        self.robust_list_process
7469            .lock()
7470            .expect("robust-list process state mutex poisoned")
7471            .heads
7472            .iter()
7473            .map(|(&tid, &head)| (tid, head))
7474            .collect()
7475    }
7476
7477    pub(crate) fn stage_robust_list_wakes(
7478        &self,
7479        reason: RobustListExit,
7480        wakes: Vec<(DetTid, Vec<RobustListWake>)>,
7481    ) {
7482        let mut process = self
7483            .robust_list_process
7484            .lock()
7485            .expect("robust-list process state mutex poisoned");
7486        process.pending = wakes
7487            .into_iter()
7488            .map(|(owner, wakes)| (owner, PendingRobustListWakes { reason, wakes }))
7489            .collect();
7490        process.matched_physical_exits.clear();
7491    }
7492
7493    pub(crate) fn has_matching_robust_list_exit(&self, exit_signal: Option<i32>) -> bool {
7494        self.robust_list_process
7495            .lock()
7496            .expect("robust-list process state mutex poisoned")
7497            .pending
7498            .get(&self.dettid)
7499            .is_some_and(|pending| pending.matches_exit(exit_signal))
7500    }
7501
7502    pub(crate) fn take_robust_list_wakes_after_exit(
7503        &self,
7504        exit_signal: Option<i32>,
7505        exit_time: DetTime,
7506    ) -> Option<(DetTime, Vec<(DetTid, RobustListWake)>)> {
7507        let mut process = self
7508            .robust_list_process
7509            .lock()
7510            .expect("robust-list process state mutex poisoned");
7511        if !process.exit_group_filed {
7512            process.heads.remove(&self.dettid);
7513        }
7514        let pending = process.pending.get(&self.dettid)?;
7515        if !pending.matches_exit(exit_signal) {
7516            return None;
7517        }
7518
7519        process
7520            .matched_physical_exits
7521            .insert(self.dettid, exit_time);
7522        if process
7523            .pending
7524            .keys()
7525            .any(|owner| !process.matched_physical_exits.contains_key(owner))
7526        {
7527            return None;
7528        }
7529
7530        let request_time = process
7531            .matched_physical_exits
7532            .values()
7533            .max()
7534            .expect("a completed robust-list exit group has a physical-exit time")
7535            .clone();
7536        process.matched_physical_exits.clear();
7537        let pending = std::mem::take(&mut process.pending);
7538        let mut ready = Vec::new();
7539        for (owner, mut pending) in pending {
7540            pending
7541                .wakes
7542                .sort_by_key(|wake| format!("{:?}", wake.futex));
7543            ready.extend(pending.wakes.into_iter().map(|wake| (owner, wake)));
7544        }
7545        Some((request_time, ready))
7546    }
7547
7548    /// Hold the owner-death wakes a successful `execve` makes Linux deliver.
7549    ///
7550    /// Past exec's point of no return, `de_thread` kills every other thread,
7551    /// and each one's exit runs `futex_exit_release` with its own TID; then
7552    /// `exec_mm_release` runs `futex_exec_release` for the caller itself
7553    /// (kernel/fork.c `exit_mm_release`/`exec_mm_release`, kernel/futex/core.c
7554    /// `futex_cleanup`, Linux 7.1.3). Both walk the robust list with
7555    /// `exit_robust_list` before the old address space goes away. A caller that
7556    /// is not the group leader already holds the leader's TID by then
7557    /// (`exchange_tids` in `de_thread`), so its own list is walked with the
7558    /// thread group ID. The lists can only be read before the exec, and a
7559    /// failed exec never reaches that point, so the wakes wait here for
7560    /// `take_robust_list_wakes_for_exec`.
7561    pub(crate) fn stage_robust_list_wakes_for_exec(
7562        &self,
7563        wakes: Vec<(DetTid, Vec<RobustListWake>)>,
7564    ) {
7565        let mut process = self
7566            .robust_list_process
7567            .lock()
7568            .expect("robust-list process state mutex poisoned");
7569        process.exec_pending = flatten_exec_robust_list_wakes(wakes);
7570    }
7571
7572    /// Take the wakes staged at the exec's entry: to deliver them once it has
7573    /// succeeded, or to drop them when it failed and left the old image running.
7574    pub(crate) fn take_robust_list_wakes_for_exec(&self) -> Vec<(DetTid, RobustListWake)> {
7575        std::mem::take(
7576            &mut self
7577                .robust_list_process
7578                .lock()
7579                .expect("robust-list process state mutex poisoned")
7580                .exec_pending,
7581        )
7582    }
7583
7584    /// Clear the robust-list head for a candidate `execve` image, returning the
7585    /// previous value so a *failed* exec can put it back.
7586    ///
7587    /// Linux clears `task->robust_list` when `execve` succeeds, and doing the
7588    /// same is load-bearing rather than tidy: a head recorded against the old
7589    /// address space is a wild pointer in the new one, and consulting it at
7590    /// thread exit would walk unrelated guest memory and write
7591    /// `FUTEX_OWNER_DIED` into whatever happened to look like a futex word.
7592    pub(crate) fn take_robust_list_for_exec(&mut self) -> Option<usize> {
7593        let previous = self.robust_list_head.take();
7594        self.robust_list_process
7595            .lock()
7596            .expect("robust-list process state mutex poisoned")
7597            .heads
7598            .remove(&self.dettid);
7599        previous
7600    }
7601
7602    /// Restore the head after an `execve` that failed and left the old address
7603    /// space intact.
7604    pub(crate) fn restore_robust_list_after_failed_exec(&mut self, previous: Option<usize>) {
7605        self.record_robust_list_head(previous);
7606    }
7607
7608    /// Resolve a futex key from its opcode mode and virtual address.
7609    pub(crate) fn futex_id(&self, address: usize, is_private: bool) -> FutexID {
7610        if is_private {
7611            FutexID::private(self.mm_id, address)
7612        } else {
7613            self.memory_metadata
7614                .lock()
7615                .expect("memory metadata mutex poisoned")
7616                .futex_id(self.mm_id, address)
7617        }
7618    }
7619
7620    /// Record that the address space allocated or applied a protection key
7621    /// (`MemoryMetadata::protection_keys`).
7622    pub(crate) fn note_protection_keys(&self) {
7623        self.memory_metadata
7624            .lock()
7625            .expect("memory metadata mutex poisoned")
7626            .protection_keys = true;
7627    }
7628
7629    /// Whether the address space has allocated or applied a protection key.
7630    pub(crate) fn may_use_protection_keys(&self) -> bool {
7631        self.memory_metadata
7632            .lock()
7633            .expect("memory metadata mutex poisoned")
7634            .protection_keys
7635    }
7636
7637    /// Record an anonymous shared mapping.
7638    pub(crate) fn map_shared_anonymous(&self, start: usize, len: usize) {
7639        self.memory_metadata
7640            .lock()
7641            .expect("memory metadata mutex poisoned")
7642            .map_anonymous(self.mm_id, start, len);
7643    }
7644
7645    /// Record a file-backed shared mapping.
7646    pub(crate) fn map_shared_object(
7647        &self,
7648        start: usize,
7649        len: usize,
7650        object: SharedMemoryObjectId,
7651        object_offset: u64,
7652    ) {
7653        self.memory_metadata
7654            .lock()
7655            .expect("memory metadata mutex poisoned")
7656            .map_object(start, len, object, object_offset);
7657    }
7658
7659    /// Remove a range from the shared mapping model.
7660    pub(crate) fn unmap_memory(&self, start: usize, len: usize) {
7661        self.memory_metadata
7662            .lock()
7663            .expect("memory metadata mutex poisoned")
7664            .unmap(start, len);
7665    }
7666
7667    /// Move or resize a range in the shared mapping model.
7668    pub(crate) fn remap_memory(
7669        &self,
7670        old_start: usize,
7671        old_len: usize,
7672        new_start: usize,
7673        new_len: usize,
7674        keep_source: bool,
7675    ) {
7676        self.memory_metadata
7677            .lock()
7678            .expect("memory metadata mutex poisoned")
7679            .remap(old_start, old_len, new_start, new_len, keep_source);
7680    }
7681
7682    /// Build a singleton resource request from the current thread.
7683    pub fn mk_request(&self, rid: ResourceID, perm: Permission) -> Resources {
7684        let mut resources = HashMap::new();
7685        resources.insert(rid, perm);
7686        Resources {
7687            tid: self.dettid,
7688            resources,
7689            poll_attempt: 0,
7690            fyi: String::new(),
7691            signal_interrupt_errno: None,
7692            backend_runtime_bootstrap: false,
7693            blocked_signal_mask: None,
7694            blocked_signal_mask_unknown: false,
7695            replay_served_from_log: false,
7696            reads_signalfd: false,
7697            read_fd: None,
7698            polls_guest_state_only: false,
7699            polls_a_named_fifo: false,
7700            relies_on_guest_held_marks: false,
7701            passes_descriptors_out: false,
7702            waits_until_nothing_else_can_run: false,
7703            previous_attempt_made_progress: false,
7704            upgraded_from_attempt: 0,
7705            polls_channel: None,
7706            poll_deadline: None,
7707            in_guest_sleep_sigalrm: None,
7708            signal_state_task: None,
7709            decides_signal_interruption_itself: false,
7710            continues_sigreturn: false,
7711            private_sigchld_decides: false,
7712            slice_boundary: None,
7713            guest_signal_mask: None,
7714            callback_checkpoint: None,
7715            priority_row: None,
7716            slice_expired: false,
7717            zero_length_sleep: false,
7718        }
7719    }
7720
7721    /// Generate the next random number using the thread-local chaos_seed.
7722    pub fn chaos_prng_next_u64(&mut self, msg: &str) -> u64 {
7723        let r = self.chaos_prng.next_u64();
7724        detlog!("[dtid {}] CHAOSRAND({}): u64 => {}", self.dettid, msg, r);
7725        r
7726    }
7727
7728    /// get file metadata
7729    fn metadata(&self) -> MutexGuard<'_, FileMetadata> {
7730        self.file_metadata.lock().unwrap()
7731    }
7732
7733    /// The descriptors this thread's table holds, ascending, modelled or not
7734    /// (`FileMetadata::descriptor_numbers`), its capacity
7735    /// (`FileMetadata::descriptor_capacity`), and whether a receive may have
7736    /// installed descriptors Detcore does not know
7737    /// (`FileMetadata::unaccounted_receipts`).
7738    pub(crate) fn descriptor_table(&self) -> (Vec<RawFd>, u64, bool) {
7739        let metadata = self.metadata();
7740        (
7741            metadata.descriptor_numbers(),
7742            metadata.descriptor_capacity,
7743            metadata.unaccounted_receipts,
7744        )
7745    }
7746
7747    /// Record that descriptor `fd`, close-on-exec or not, is open in this
7748    /// thread's table though Detcore keeps no description of it
7749    /// (`FileMetadata::present_unmodelled`).
7750    pub(crate) fn note_unmodelled_descriptor(&self, fd: RawFd, cloexec: bool) {
7751        let mut metadata = self.metadata();
7752        if !metadata.file_handles.contains_key(&fd) {
7753            metadata.note_installed(fd);
7754            metadata.present_unmodelled.insert(fd, cloexec);
7755        }
7756    }
7757
7758    /// Grow this thread's table to hold descriptor `fd`, as a `dup2` or
7759    /// `dup3` to it of a descriptor that is not open does on Linux
7760    /// (`FileMetadata::grow_to_hold`): `newfd` below `RLIMIT_NOFILE`, as
7761    /// Detcore keeps it for the process.
7762    pub(crate) fn note_failed_dup_target(&self, fd: RawFd) {
7763        let limit = self
7764            .resource_limits
7765            .lock()
7766            .unwrap()
7767            .get(reverie::syscalls::linux_abi::RLIMIT_NOFILE)
7768            .map_or(0, |limit| limit.current);
7769        if u64::try_from(fd).is_ok_and(|fd| fd < limit) {
7770            self.metadata().grow_to_hold(fd);
7771        }
7772    }
7773
7774    /// Set the close-on-exec flag of descriptor `fd` if Detcore lists it
7775    /// without modelling it (`FileMetadata::present_unmodelled`), after the
7776    /// kernel changed it (`F_SETFD`, `FIOCLEX`, `FIONCLEX`); whether it is
7777    /// one.
7778    pub(crate) fn set_unmodelled_cloexec(&self, fd: RawFd, cloexec: bool) -> bool {
7779        self.metadata().set_unmodelled_cloexec(fd, cloexec)
7780    }
7781
7782    /// Record that a receive may have installed descriptors in this thread's
7783    /// table whose numbers Detcore does not know
7784    /// (`FileMetadata::unaccounted_receipts`).
7785    pub(crate) fn note_unaccounted_receipt(&self) {
7786        self.metadata().unaccounted_receipts = true;
7787    }
7788
7789    /// Identity of this thread's descriptor table.
7790    pub(crate) fn files_id(&self) -> detcore_model::fd::FilesId {
7791        self.metadata().files_id
7792    }
7793
7794    /// Whether this thread holds the descriptor table its process holds
7795    /// ([`ThreadState::process_files`]), the one procfs reads for the
7796    /// process's descriptor links. `false` where that is not known.
7797    pub(crate) fn holds_process_files(&self) -> bool {
7798        self.process_files == Some(self.files_id())
7799    }
7800
7801    /// Make the descriptor table this thread holds its process's, for a new
7802    /// group: after a successful exec the exec'ing thread is its process's
7803    /// only thread, and leads it, whether or not it led it before.
7804    pub(crate) fn adopt_process_files(&mut self) {
7805        self.process_files = Some(self.files_id());
7806    }
7807
7808    /// Add a new fd, with optional stat data, have side effects on other
7809    /// threads.
7810    ///
7811    /// If stat data is not available, then perform an extra stat ourselves to populate it.
7812    ///
7813    /// # Arguments
7814    ///
7815    /// * `fd` - file descriptor to add
7816    ///
7817    /// * `flags` - flags when creating `fd`
7818    ///
7819    /// * `ty` - fd type (regular file, socket, pipe, etc..)
7820    ///
7821    /// * `stat` - stat returned from fstat
7822    pub fn add_fd(
7823        &self,
7824        fd: RawFd,
7825        flags: OFlag,
7826        ty: FdType,
7827        stat: Option<DetStat>,
7828    ) -> Result<(), Errno> {
7829        let mut metadata = self.metadata();
7830        metadata.add_fd(
7831            self.open_file_creator.unwrap_or(self.dettid),
7832            fd,
7833            flags,
7834            ty,
7835            stat,
7836        )?;
7837        if self.traced_call.is_active() {
7838            self.traced_call
7839                .note(metadata.slot_note(fd, FdNoteOp::Open));
7840        }
7841        Ok(())
7842    }
7843
7844    /// Record descriptor `fd`, which Linux installed by a route Detcore did not
7845    /// model as it ran (`SCM_RIGHTS`), from the kernel's facts about it, as an
7846    /// in-guest backend records a descriptor it discovers
7847    /// (`FileMetadata::add_discovered_fd`).
7848    pub(crate) fn add_discovered_fd(
7849        &self,
7850        fd: RawFd,
7851        fd_flags: libc::c_int,
7852        status_flags: libc::c_int,
7853        raw_stat: &libc::stat,
7854        link: Option<&[u8]>,
7855    ) -> Result<(), Errno> {
7856        let mut metadata = self.metadata();
7857        metadata.add_discovered_fd(
7858            self.open_file_creator.unwrap_or(self.dettid),
7859            fd,
7860            fd_flags,
7861            status_flags,
7862            raw_stat,
7863            link,
7864        )?;
7865        if self.traced_call.is_active() {
7866            self.traced_call
7867                .note(metadata.slot_note(fd, FdNoteOp::Open));
7868        }
7869        Ok(())
7870    }
7871
7872    /// Make `fd`, which this thread just opened, a random device
7873    /// (`FileMetadata::classify_random_device`).
7874    pub(crate) fn classify_random_device(&self, fd: RawFd) -> Result<(), Errno> {
7875        self.metadata()
7876            .classify_random_device(self.open_file_creator.unwrap_or(self.dettid), fd)
7877    }
7878
7879    /// This table's count of random-device opens, for the exec exchange
7880    /// (`ExecFdState::random_device_sequence`).
7881    pub(crate) fn random_device_sequence_for_exec(&self) -> u64 {
7882        self.metadata().next_random_device_sequence
7883    }
7884
7885    // AUTONOMOUS-BOT-IMPLEMENTED
7886    // TODO-HUMAN-REVIEW(PR-1063): Review backend-supplied open-file creator identity.
7887    /// Overrides the task identity embedded in subsequently allocated open-file descriptions.
7888    pub fn set_open_file_creator(&mut self, creator: DetTid) {
7889        self.open_file_creator = Some(creator);
7890    }
7891
7892    /// This process's timerfds that Detcore keeps on virtual time
7893    /// (`crate::fd::VirtualTimerfd`), one descriptor per open file
7894    /// description, lowest descriptor number first, so callers visit them in
7895    /// a deterministic order.
7896    pub(crate) fn virtual_timerfds(&self) -> Vec<(RawFd, DetFd)> {
7897        let mut timerfds: Vec<(RawFd, DetFd)> = self
7898            .metadata()
7899            .file_handles
7900            .iter()
7901            .filter(|(_, detfd)| detfd.ty() == FdType::Timerfd && detfd.virtual_timerfd().is_some())
7902            .map(|(&fd, detfd)| (fd, detfd.clone()))
7903            .collect();
7904        timerfds.sort_unstable_by_key(|(fd, _)| *fd);
7905        let mut descriptions = Vec::with_capacity(timerfds.len());
7906        timerfds.retain(|(_, detfd)| {
7907            let id = detfd.open_file_id();
7908            let first = !descriptions.contains(&id);
7909            descriptions.push(id);
7910            first
7911        });
7912        timerfds
7913    }
7914
7915    /// Whether this thread's descriptor table holds a signalfd, as Detcore
7916    /// records its descriptors.
7917    pub(crate) fn holds_a_signalfd(&self) -> bool {
7918        self.metadata()
7919            .file_handles
7920            .values()
7921            .any(|detfd| matches!(detfd.ty(), FdType::Signalfd))
7922    }
7923
7924    /// Whether this thread's descriptor table holds a procfs file Detcore
7925    /// renders (`syscalls::files::rendered_procfs`).
7926    pub(crate) fn holds_a_rendered_procfs_file(&self) -> bool {
7927        self.metadata()
7928            .file_handles
7929            .values()
7930            .any(|detfd| detfd.procfs_rendered_path().is_some())
7931    }
7932
7933    /// Get a mutable reference of `DetFd` from a raw file descriptor, and
7934    /// run mutable function `f` on it (`&mut DetFd`).
7935    pub fn with_detfd<F, U>(&self, fd: RawFd, f: F) -> Result<U, Errno>
7936    where
7937        F: FnMut(&mut DetFd) -> U,
7938    {
7939        let mut metadata = self.metadata();
7940        if self.tool_shares_guest_descriptor_table {
7941            metadata.discover_fd_from_current_process(
7942                self.open_file_creator.unwrap_or(self.dettid),
7943                fd,
7944            )?;
7945        }
7946        metadata.with_detfd(fd, f)
7947    }
7948
7949    /// Preserve logical blocking for scheduler-managed descriptors when exec
7950    /// replaces an in-process tool and its descriptor metadata.
7951    pub fn exec_blocking_overrides(&self) -> ExecFdBlockingOverrides {
7952        self.metadata().exec_blocking_overrides()
7953    }
7954
7955    /// Promote the OFDs whose model must stay one copy -- random devices
7956    /// (their cursor) and timerfds (their virtual-time expirations,
7957    /// `crate::fd::VirtualTimerfd`) -- before a backend physically copies or
7958    /// replaces its in-process Tool. The shared model owns their full state;
7959    /// this does not promote sockets or other OFDs. The record sequences of
7960    /// per-process files move to Detcore's global state on their own
7961    /// (`share_procfs_sequences`, first). Call only while this task
7962    /// owns its serial grant, before the native lifecycle operation can expose
7963    /// a child/new image. Ptrace's host-side Arc needs no promotion. See
7964    /// <https://github.com/rrnewton/hermit/issues/4381>.
7965    pub fn promote_shared_open_files(&self) -> Result<(), crate::fd::SharedOpenFileError> {
7966        self.share_procfs_sequences();
7967        let mut metadata = self.metadata();
7968        let mut fds: Vec<_> = metadata.file_handles.keys().copied().collect();
7969        fds.sort_unstable();
7970        for fd in fds {
7971            let handle = &metadata.file_handles[&fd];
7972            if matches!(handle.ty(), FdType::Rng | FdType::Timerfd) {
7973                handle.promote()?;
7974                // Keep history even if every promoted descriptor later closes.
7975                metadata.has_promoted_shared_files = true;
7976            }
7977        }
7978        Ok(())
7979    }
7980
7981    /// Before a backend whose tool runs in each guest process physically
7982    /// copies this process (a fork) or replaces its image: every description
7983    /// of a per-process file that follows its record's sequence keeps that
7984    /// sequence in Detcore's global state from then on, so the copies a fork
7985    /// makes share it as Linux's processes share the one `struct file` and
7986    /// `seq_file` (`ProcfsFile::share_sequence`, `Detcore::take_procfs_sequence`).
7987    /// Local and RPC-free: the first operation after the fork, in either
7988    /// process, finds no sequence there yet and keeps its copy, which is the
7989    /// one the fork made. Runs in the caller's turn, before the copy exists
7990    /// (`promote_shared_open_files`, and `Detcore::handle_clone_family` for
7991    /// SaBRe, whose runtime promotes nothing).
7992    pub fn share_procfs_sequences(&self) {
7993        let mut metadata = self.metadata();
7994        let mut shared = false;
7995        for handle in metadata.file_handles.values() {
7996            shared |= handle.share_procfs_sequence();
7997        }
7998        metadata.has_shared_procfs_sequences |= shared;
7999    }
8000
8001    /// Before a send passes descriptor `fd` over `SCM_RIGHTS`, where the tool
8002    /// runs in each guest process: a rendered per-process file that follows
8003    /// its record's sequence keeps that sequence in Detcore's global state
8004    /// from then on, as a fork's copies do (`share_procfs_sequences`). The
8005    /// identity it is kept under, which the caller then takes and stores
8006    /// (`Detcore::share_sent_record_sequences`). `None` for any other
8007    /// descriptor, and for one whose sequence Detcore cannot know here
8008    /// (`DetFd::record_sequence_unknown`). Local and RPC-free.
8009    pub(crate) fn share_sent_procfs_sequence(&self, fd: RawFd) -> Option<OpenFileId> {
8010        let mut metadata = self.metadata();
8011        let handle = metadata.file_handles.get(&fd)?;
8012        if handle.is_shared()
8013            || handle.record_sequence_unknown()
8014            || handle.procfs_rendered_path().is_none()
8015            || !handle.share_procfs_sequence()
8016        {
8017            return None;
8018        }
8019        let id = handle.procfs_shared_sequence();
8020        metadata.has_shared_procfs_sequences |= id.is_some();
8021        id
8022    }
8023
8024    /// The descriptors of this thread's table open on a rendered procfs file
8025    /// (`DetFd::procfs_rendered_path`), lowest first. A shared model (a
8026    /// random device, a timerfd) is never one, and is passed over without a
8027    /// lease.
8028    pub(crate) fn rendered_descriptors(&self) -> Vec<RawFd> {
8029        let metadata = self.metadata();
8030        let mut fds: Vec<RawFd> = metadata
8031            .file_handles
8032            .iter()
8033            .filter(|(_, detfd)| !detfd.is_shared() && detfd.procfs_rendered_path().is_some())
8034            .map(|(&fd, _)| fd)
8035            .collect();
8036        fds.sort_unstable();
8037        fds
8038    }
8039
8040    /// Rebuild only process-local locks after a physical process copy. This
8041    /// is RPC-free and precedes the child's first scheduler admission. Ordinary
8042    /// same-process table clones retain their original shared locks.
8043    pub fn reset_copied_open_file_locks(&self) {
8044        let mut metadata = self.metadata();
8045        let mut handles = metadata
8046            .file_handles
8047            .iter()
8048            .map(|(&fd, handle)| (fd, handle.fork_shared_handle()))
8049            .collect();
8050        crate::fd::intern_open_files(&mut handles);
8051        metadata.file_handles = handles;
8052    }
8053
8054    /// Under `--pipe-wake-latency`: the channel end each descriptor that
8055    /// survives exec is.
8056    pub(crate) fn pipe_channels_for_exec(&self) -> BTreeMap<RawFd, crate::pipe_wake::ChannelEnd> {
8057        self.metadata()
8058            .file_handles
8059            .iter()
8060            .filter(|(_, detfd)| !detfd.is_cloexec())
8061            .filter_map(|(&fd, detfd)| detfd.channel().map(|end| (fd, end)))
8062            .collect()
8063    }
8064
8065    /// The open-file creator and next sequence of this thread's table.
8066    pub(crate) fn open_file_namespace_for_exec(&self) -> (DetTid, u64) {
8067        (
8068            self.open_file_creator.unwrap_or(self.dettid),
8069            self.metadata().next_open_file_sequence,
8070        )
8071    }
8072
8073    pub(crate) fn shared_open_files_for_exec(&self) -> Result<Option<ExecOpenFiles>, Errno> {
8074        let metadata = self.metadata();
8075        if !metadata.has_promoted_shared_files {
8076            return Ok(None);
8077        }
8078        let mut handles = BTreeMap::new();
8079        for (&fd, handle) in &metadata.file_handles {
8080            if !handle.is_shared() || handle.is_cloexec() {
8081                continue;
8082            }
8083            assert!(
8084                matches!(handle.ty(), FdType::Rng | FdType::Timerfd),
8085                "only random and timerfd OFDs are promoted here"
8086            );
8087            handles.insert(fd, ExecOpenFile::capture(fd, handle.open_file_id())?);
8088        }
8089        Ok(Some(ExecOpenFiles {
8090            creator: self.open_file_creator.unwrap_or(self.dettid),
8091            next_sequence: metadata.next_open_file_sequence,
8092            next_socket_sequence: metadata.next_socket_open_file_sequence,
8093            handles,
8094        }))
8095    }
8096
8097    /// Before an exec on a backend that rebuilds its descriptor table after
8098    /// exec: each surviving descriptor of a per-process file that follows its
8099    /// record's sequence, with its open file's identity, model and sequence
8100    /// (`DetFd::procfs_record_for_exec`), so the new image's table names the
8101    /// same open file as every other process holding it, and a read there
8102    /// continues where the open file's sequence is (before, the new image
8103    /// rebuilt it from its placeholder with no sequence: an exec'd child's
8104    /// read moved the shared position while the other holder kept its own
8105    /// sequence, and on DBT and SaBRe that holder's next read ran show where
8106    /// Linux continues the record, Claude's narrow check of the kvm lane's
8107    /// slice N3, round 5); and this table's open file creator and next
8108    /// identity, so no description of the new image takes an identity whose
8109    /// sequence another process may still hold in Detcore's global state
8110    /// (before, the new image numbered its descriptions from 0 again, Codex's
8111    /// narrow check, round 5). `None` where the table has no such descriptor
8112    /// and never shared a sequence (`FileMetadata::has_shared_procfs_sequences`).
8113    pub(crate) fn procfs_records_for_exec(&self) -> Result<Option<ExecProcfsRecords>, Errno> {
8114        let metadata = self.metadata();
8115        let mut handles = BTreeMap::new();
8116        for (&fd, handle) in &metadata.file_handles {
8117            if handle.is_cloexec() || handle.is_shared() {
8118                continue;
8119            }
8120            let Some(procfs) = handle.procfs_record_for_exec() else {
8121                continue;
8122            };
8123            let sequence = procfs.sequence();
8124            handles.insert(
8125                fd,
8126                ExecProcfsRecord {
8127                    identity: ExecOpenFile::capture(fd, handle.open_file_id())?,
8128                    procfs,
8129                    sequence,
8130                    flags: handle.status_flags(),
8131                    path: handle.path(),
8132                    sequence_unknown: handle.record_sequence_unknown(),
8133                },
8134            );
8135        }
8136        if handles.is_empty() && !metadata.has_shared_procfs_sequences {
8137            return Ok(None);
8138        }
8139        Ok(Some(ExecProcfsRecords {
8140            creator: self.open_file_creator.unwrap_or(self.dettid),
8141            next_sequence: metadata.next_open_file_sequence,
8142            handles,
8143        }))
8144    }
8145
8146    pub(crate) fn maps_for_exec(
8147        &self,
8148        private_memory: Option<&reverie::RuntimePrivateMemory>,
8149    ) -> Result<Option<ExecMapsFiles>, Errno> {
8150        // An unavailable advertised provider still owns saved maps bindings.
8151        // Backends without a provider retain their existing exec behavior.
8152        if private_memory.is_none() {
8153            return Ok(None);
8154        }
8155        let metadata = self.metadata();
8156        let mut handles = BTreeMap::new();
8157        for (&fd, handle) in &metadata.file_handles {
8158            if handle.is_cloexec() {
8159                continue;
8160            }
8161            if let Some(procfs) = handle.maps_for_exec() {
8162                handles.insert(
8163                    fd,
8164                    ExecMapsFile {
8165                        identity: ExecOpenFile::capture(fd, handle.open_file_id())?,
8166                        procfs,
8167                        flags: handle.status_flags(),
8168                        path: handle.path(),
8169                    },
8170                );
8171            }
8172        }
8173        Ok((!handles.is_empty()).then_some(ExecMapsFiles {
8174            creator: self.open_file_creator.unwrap_or(self.dettid),
8175            next_sequence: metadata.next_open_file_sequence,
8176            handles,
8177        }))
8178    }
8179
8180    pub(crate) fn inherited_reads_for_exec(
8181        &self,
8182    ) -> BTreeMap<RawFd, crate::fd::InheritedExternalRead> {
8183        self.metadata().inherited_reads_for_exec()
8184    }
8185
8186    /// Descriptors in this table that Detcore manages as internal pipes: typed
8187    /// `Pipe`, physically nonblocking and logically blocking, which is how
8188    /// `handle_pipe2` leaves a guest-created pipe (and how exec restores it).
8189    /// On ptrace, a pipe inherited from the host is not in this set: stdio is
8190    /// typed `Regular`, and another inherited FIFO is physically nonblocking
8191    /// only when it is logically nonblocking too. On a backend whose tool
8192    /// shares the guest's descriptor table, a readable pipe inherited on fds
8193    /// 0-2 is an inherited external read (`discover_inherited_pipes`), so it is
8194    /// not here either, and a guest `F_SETFL` leaves its kernel status flags
8195    /// as the guest set them (`keeps_physically_nonblocking`).
8196    /// Sorted, so callers probe them in a deterministic order.
8197    pub(crate) fn scheduler_managed_pipe_fds(&self) -> Vec<RawFd> {
8198        let mut fds: Vec<RawFd> = self
8199            .metadata()
8200            .file_handles
8201            .iter()
8202            .filter(|(_, detfd)| {
8203                detfd.ty() == FdType::Pipe
8204                    && detfd.inherited_external_read().is_none()
8205                    && detfd.physically_nonblocking()
8206                    && !detfd.is_nonblocking()
8207            })
8208            .map(|(&fd, _)| fd)
8209            .collect();
8210        fds.sort_unstable();
8211        fds
8212    }
8213
8214    /// Whether a descriptor of this table other than `fd` names, through a
8215    /// guest-held description (`DetFd::is_guest_held`), the object at host
8216    /// device `dev` and inode `inode`: the same socket end, or either end of
8217    /// the same pipe. Only descriptions whose host `fstat` Detcore keeps can
8218    /// match.
8219    pub(crate) fn names_guest_held_object(&self, fd: RawFd, dev: u64, inode: u64) -> bool {
8220        self.metadata().file_handles.iter().any(|(&other, detfd)| {
8221            other != fd
8222                && detfd.is_guest_held()
8223                && detfd
8224                    .stat()
8225                    .is_some_and(|stat| stat.dev == dev && stat.inode == inode)
8226        })
8227    }
8228
8229    /// Mark every listener in this table eligible for accept in turn as passed
8230    /// over `SCM_RIGHTS` (`DetFd::set_exported`), for a message whose
8231    /// descriptors Detcore could not read.
8232    // TODO-HUMAN-REVIEW(PR-3908)
8233    pub(crate) fn mark_accept_in_turn_listeners_exported(&self) {
8234        for detfd in self.metadata().file_handles.values() {
8235            if detfd.is_accept_in_turn_eligible() {
8236                detfd.set_exported();
8237            }
8238        }
8239    }
8240
8241    /// Every socket of this table may have been passed to a table that holds
8242    /// it as an open file of its own: no pair's queues hold every message
8243    /// any more (`DetFd::give_up_pair_completeness`).
8244    pub(crate) fn give_up_every_pair_completeness(&self) {
8245        for detfd in self.metadata().file_handles.values() {
8246            detfd.give_up_pair_completeness();
8247        }
8248    }
8249
8250    /// Descriptors in this table that name a network record/replay channel,
8251    /// sorted.
8252    pub(crate) fn network_channel_fds(&self) -> Vec<RawFd> {
8253        let mut fds: Vec<RawFd> = self
8254            .metadata()
8255            .file_handles
8256            .iter()
8257            .filter(|(_, detfd)| detfd.is_network_channel())
8258            .map(|(&fd, _)| fd)
8259            .collect();
8260        fds.sort_unstable();
8261        fds
8262    }
8263
8264    pub(crate) fn count_open_files_at_paths(&self, paths: &[&Path]) -> usize {
8265        self.metadata().count_open_files_at_paths(paths)
8266    }
8267
8268    /// Whether this task owns a socket that attempted a loopback connection.
8269    pub(crate) fn has_loopback_peer(&self) -> bool {
8270        self.metadata().has_loopback_peer()
8271    }
8272
8273    /// Whether some open file description holds a flock mode Detcore knows,
8274    /// or was invalidated by a process copy after a flock was granted through
8275    /// it (`FileMetadata::has_unsafe_vfork_flock_state`).
8276    pub fn has_unsafe_vfork_flock_state(&self) -> bool {
8277        self.metadata().has_unsafe_vfork_flock_state()
8278    }
8279
8280    /// Whether a description in this table may hold an OFD lock. A copied
8281    /// `vfork` child (DBT) runs its lock calls natively, so the
8282    /// `procfs::locks::LockLedger` entry of such a description could go stale
8283    /// and admit a host's identical `/proc/locks` row.
8284    pub fn has_unsafe_vfork_ofd_state(&self) -> bool {
8285        self.metadata().has_unsafe_vfork_ofd_state()
8286    }
8287
8288    /// A new process can change every inherited open file description through
8289    /// a separately hosted tool state, so neither side may keep treating its
8290    /// cached flock mode as authoritative after fork.
8291    pub fn forget_flock_modes(&self) {
8292        self.metadata().forget_flock_modes();
8293    }
8294
8295    /// remove a rawfd
8296    pub fn remove_fd(&self, fd: RawFd) -> Option<OpenFileId> {
8297        let mut metadata = self.metadata();
8298        if self.traced_call.is_active()
8299            && let Some(note) = metadata.close_note(fd)
8300        {
8301            self.traced_call.note(note);
8302        }
8303        metadata.remove_fd(fd)
8304    }
8305
8306    /// This thread is exiting, with its whole process where `whole_process`
8307    /// (an `exit_group`), and the scheduler has granted the exit: it no longer
8308    /// holds its descriptor table (`FileMetadata::leave`). Where no thread
8309    /// holds the table any more, as guest calls alone count its holders
8310    /// (`FileMetadata::holders`), the table goes with this exit, as Linux
8311    /// releases it with its last holder's exit (`exit_files`): every
8312    /// descriptor of it is let go now, in this thread's turn, before the
8313    /// native call that ends it (`Detcore::release_table_at_exit`), so that
8314    /// whether another table's later close is a
8315    /// description's last reference depends on this call and not on when the
8316    /// backend tears thread states down. A socket whose last reference goes
8317    /// takes its queue, for the exit to release
8318    /// ([`Self::take_released_rights`]), as Linux purges a released socket's
8319    /// receive queue. The descriptions themselves keep their ports, as an
8320    /// exiting process's sockets always have. Returns whether the table was
8321    /// let go; a table that another thread still holds is kept.
8322    pub(crate) fn release_table_at_exit(&self, whole_process: bool) -> bool {
8323        let Some(process) = self.detpid else {
8324            return false;
8325        };
8326        let mut metadata = self.metadata();
8327        if !metadata.leave(process, whole_process) {
8328            return false;
8329        }
8330        metadata.release_every_descriptor();
8331        true
8332    }
8333
8334    /// The new image's first callback, after an exec that replaced a table
8335    /// (`FileMetadata::keep_replaced_table`): the exec'ing process leaves it,
8336    /// all its threads at once (`FileMetadata::leave`), as Linux's exec kills
8337    /// the process's other threads (`de_thread`) and gives the process a table
8338    /// of its own where another process still holds the old one
8339    /// (`unshare_files`). Where that leaves the table to no holder, every
8340    /// descriptor of it goes now, in order. The close-on-exec ones go as Linux
8341    /// closes them at the exec (`do_close_on_exec`); the others, which this
8342    /// thread's new table holds too, release nothing by going. So the replaced
8343    /// table names no description any more, however long the backend keeps
8344    /// it, and whether a later close is a description's last reference
8345    /// depends on the exec, not on when the backend drops that table. Where
8346    /// another process still holds it (`CLONE_FILES` without `CLONE_THREAD`),
8347    /// that process keeps every descriptor, and the table goes with that
8348    /// process's own exit or exec, which the exec'ing process no longer
8349    /// counts against. A socket whose last reference goes takes its queue,
8350    /// for the exec to release ([`Self::take_released_rights`]), as Linux
8351    /// purges a released socket's receive queue (`unix_release_sock`). No
8352    /// port is released here: `Detcore::handle_execveat` releases those of the
8353    /// descriptions the exec closes before the exec runs, as before. Returns
8354    /// whether that left anything to release.
8355    pub(crate) fn release_exec_dropped_descriptors(&self) -> bool {
8356        let Some((replaced, process)) = self.metadata().exec_replaced.0.take() else {
8357            return false;
8358        };
8359        let released = {
8360            let mut replaced = replaced.lock().expect("file metadata mutex poisoned");
8361            if !replaced.leave(process, true) {
8362                return false;
8363            }
8364            replaced.release_every_descriptor();
8365            replaced.take_released_rights()
8366        };
8367        if released.is_empty() {
8368            return false;
8369        }
8370        self.defer_released_rights(released);
8371        true
8372    }
8373
8374    /// What the current call made Linux release from receiving sockets'
8375    /// queues, for the call to release when it completes
8376    /// (`Detcore::settle_released_rights`): what closes in this thread's
8377    /// table took (`FileMetadata::release_detfd`), and what receives and
8378    /// purges handed over ([`Self::defer_released_rights`]).
8379    pub(crate) fn take_released_rights(&self) -> Vec<SentDescriptor> {
8380        self.metadata().take_released_rights()
8381    }
8382
8383    /// Whether the current call holds descriptors to release
8384    /// ([`Self::take_released_rights`]).
8385    pub(crate) fn holds_released_rights(&self) -> bool {
8386        !self.metadata().released_rights.0.is_empty()
8387    }
8388
8389    /// Hold `released`, descriptors queued to a socket that Linux has just
8390    /// released with their message, for the current call to release when it
8391    /// completes ([`Self::take_released_rights`]).
8392    pub(crate) fn defer_released_rights(&self, released: impl IntoIterator<Item = SentDescriptor>) {
8393        self.metadata().released_rights.0.extend(released);
8394    }
8395
8396    /// Remove every modeled descriptor in an inclusive close_range interval.
8397    pub(crate) fn remove_fd_range(&self, first: u32, last: u32) -> Vec<OpenFileId> {
8398        let mut metadata = self.metadata();
8399        if self.traced_call.is_active() {
8400            let mut released: Vec<RawFd> = metadata
8401                .file_handles
8402                .keys()
8403                .copied()
8404                .filter(|fd| *fd >= 0 && first <= *fd as u32 && *fd as u32 <= last)
8405                .collect();
8406            released.sort_unstable();
8407            for note in released
8408                .into_iter()
8409                .filter_map(|fd| metadata.close_note(fd))
8410            {
8411                self.traced_call.note(note);
8412            }
8413        }
8414        metadata.remove_fd_range(first, last)
8415    }
8416
8417    /// How many slots of this thread's table name the open file description
8418    /// `open_file` (`procfs::locks::LockLedger`).
8419    pub(crate) fn slots_naming(&self, open_file: OpenFileId) -> u32 {
8420        let count = self
8421            .metadata()
8422            .file_handles
8423            .values()
8424            .filter(|detfd| detfd.open_file_id() == open_file)
8425            .count();
8426        u32::try_from(count).unwrap_or(u32::MAX)
8427    }
8428
8429    /// Whether the exec that made this thread's table dropped a close-on-exec
8430    /// description whose locks Detcore follows (`FileMetadata::exec_dropped_lock_tracked`).
8431    pub(crate) fn exec_dropped_lock_tracked(&self) -> bool {
8432        self.metadata().exec_dropped_lock_tracked
8433    }
8434
8435    /// Whether this thread's table holds a descriptor Detcore serves as a
8436    /// `/proc` directory on a placeholder (`DetFd::placeholder`).
8437    pub(crate) fn holds_placeholder(&self) -> bool {
8438        self.metadata()
8439            .file_handles
8440            .values()
8441            .any(|detfd| detfd.placeholder().is_some())
8442    }
8443
8444    /// The lowest numbered descriptor of this thread's table that names the
8445    /// open file description `open_file`, if any.
8446    pub(crate) fn lowest_slot_naming(&self, open_file: OpenFileId) -> Option<RawFd> {
8447        self.metadata()
8448            .file_handles
8449            .iter()
8450            .filter(|(_, detfd)| detfd.open_file_id() == open_file)
8451            .map(|(&fd, _)| fd)
8452            .min()
8453    }
8454
8455    /// The descriptions in this thread's table whose locks
8456    /// `procfs::locks::LockLedger` follows, each with how many slots name it,
8457    /// in description order.
8458    pub(crate) fn lock_tracked_slots(&self) -> Vec<(OpenFileId, u32)> {
8459        let mut slots: BTreeMap<OpenFileId, u32> = BTreeMap::new();
8460        for detfd in self.metadata().file_handles.values() {
8461            if detfd.is_lock_tracked() {
8462                *slots.entry(detfd.open_file_id()).or_default() += 1;
8463            }
8464        }
8465        slots.into_iter().collect()
8466    }
8467
8468    /// The descriptors Detcore's table holds for this thread, in order.
8469    pub(crate) fn open_fds(&self) -> Vec<RawFd> {
8470        let mut fds: Vec<RawFd> = self.metadata().file_handles.keys().copied().collect();
8471        fds.sort_unstable();
8472        fds
8473    }
8474
8475    /// dup raw fds.
8476    pub fn dup_fd(
8477        &mut self,
8478        oldfd: RawFd,
8479        newfd: RawFd,
8480        flags: OFlag,
8481    ) -> Result<Option<OpenFileId>, Errno> {
8482        let mut metadata = self.metadata();
8483        if self.tool_shares_guest_descriptor_table {
8484            metadata.discover_fd_from_current_process(
8485                self.open_file_creator.unwrap_or(self.dettid),
8486                oldfd,
8487            )?;
8488        }
8489        let replaced = (self.traced_call.is_active() && oldfd != newfd)
8490            .then(|| metadata.close_note(newfd))
8491            .flatten();
8492        let released = metadata.dup_fd(oldfd, newfd, flags)?;
8493        if self.traced_call.is_active() && oldfd != newfd {
8494            if let Some(note) = replaced {
8495                self.traced_call.note(note);
8496            }
8497            self.traced_call
8498                .note(metadata.slot_note(newfd, FdNoteOp::Dup(oldfd)));
8499        }
8500        Ok(released)
8501    }
8502
8503    /// Atomically validate a pidfd target and capture its source descriptor.
8504    ///
8505    /// Both lookups occur under the one descriptor-table mutex. The caller must
8506    /// hold the normal serialized Tool turn until the nonblocking kernel call
8507    /// returns and the captured alias is installed.
8508    pub(crate) fn capture_pidfd_getfd_source(
8509        &self,
8510        pidfd: RawFd,
8511        targetfd: RawFd,
8512        current_tgid: DetPid,
8513        current_tid: DetTid,
8514    ) -> Result<CapturedDetFd, Errno> {
8515        let mut metadata = self.metadata();
8516        if self.tool_shares_guest_descriptor_table {
8517            metadata.discover_fd_from_current_process(
8518                self.open_file_creator.unwrap_or(self.dettid),
8519                pidfd,
8520            )?;
8521        }
8522        let (is_pidfd, target) = metadata.with_detfd(pidfd, |detfd| {
8523            (matches!(detfd.ty(), FdType::Pidfd), detfd.pidfd_target())
8524        })?;
8525        if !is_pidfd {
8526            return Err(Errno::EBADF);
8527        }
8528        if !pidfd_getfd_targets_calling_task(target, current_tgid, current_tid) {
8529            return Err(Errno::EOPNOTSUPP);
8530        }
8531        if self.tool_shares_guest_descriptor_table {
8532            metadata.discover_fd_from_current_process(
8533                self.open_file_creator.unwrap_or(self.dettid),
8534                targetfd,
8535            )?;
8536        }
8537        metadata.capture_fd(targetfd)
8538    }
8539
8540    /// Install an alias from a pre-syscall capture without resolving the source
8541    /// fd slot a second time.
8542    pub(crate) fn install_captured_fd(
8543        &mut self,
8544        captured: CapturedDetFd,
8545        newfd: RawFd,
8546        flags: OFlag,
8547    ) -> Result<Option<OpenFileId>, CapturedDetFdInstallError> {
8548        let mut metadata = self.metadata();
8549        let replaced = self
8550            .traced_call
8551            .is_active()
8552            .then(|| metadata.close_note(newfd))
8553            .flatten();
8554        let released = metadata.install_captured_fd(captured, newfd, flags)?;
8555        if self.traced_call.is_active() {
8556            if let Some(note) = replaced {
8557                self.traced_call.note(note);
8558            }
8559            self.traced_call
8560                .note(metadata.slot_note(newfd, FdNoteOp::Open));
8561        }
8562        Ok(released)
8563    }
8564
8565    /// Install `detfd`, which refers to an open file description Detcore
8566    /// models, as descriptor `detfd.fd` of this table: a rendered procfs
8567    /// descriptor that reached the description over `SCM_RIGHTS`, adopted
8568    /// when it is received (`Detcore::record_received_descriptors`) or when
8569    /// the guest first names it (`Detcore::adopt_received_placeholders`).
8570    pub(crate) fn install_alias(&self, detfd: DetFd) {
8571        let fd = detfd.fd;
8572        let mut metadata = self.metadata();
8573        metadata.add_detfd(detfd);
8574        if self.traced_call.is_active() {
8575            self.traced_call
8576                .note(metadata.slot_note(fd, FdNoteOp::Open));
8577        }
8578    }
8579
8580    /// The descriptors among `fds` this thread's table names, as the access
8581    /// trace needs them (`crate::access_trace::FdInfo`).
8582    pub(crate) fn access_trace_fds(
8583        &self,
8584        fds: &[RawFd],
8585    ) -> BTreeMap<RawFd, crate::access_trace::FdInfo> {
8586        let metadata = self.metadata();
8587        fds.iter()
8588            .filter_map(|fd| {
8589                metadata
8590                    .file_handles
8591                    .get(fd)
8592                    .map(|detfd| (*fd, crate::access_trace::FdInfo::of(detfd)))
8593            })
8594            .collect()
8595    }
8596
8597    /// Abandon an uninstalled capture and identify a deferred final-OFD release.
8598    pub(crate) fn abandon_captured_fd(&self, captured: CapturedDetFd) -> Option<OpenFileId> {
8599        self.metadata().abandon_captured_fd(captured)
8600    }
8601
8602    /// get thread prng, note this rng is deterministic and should not be used
8603    /// for crypto.
8604    pub fn thread_prng(&mut self) -> &mut Pcg64Mcg {
8605        &mut self.prng
8606    }
8607
8608    /// Whether this thread's slices still follow a replayed schedule
8609    /// (`--replay-schedule-from`): they end where the schedule says, and no
8610    /// pending deadline or PMU maximum ends them. False without a schedule,
8611    /// and once the schedule is over for this thread
8612    /// (`schedule_replay_over`): its slices then end as in a run without a
8613    /// schedule, which `hermit analyze` assumes of a replayed prefix's
8614    /// continuation.
8615    pub(crate) fn follows_replayed_schedule(&self, cfg: &Config) -> bool {
8616        cfg.replay_schedule_from.is_some() && !self.schedule_replay_over
8617    }
8618
8619    /// Whether a trapped instruction (`rdtsc`, `cpuid`) should end the current
8620    /// timeslice.
8621    ///
8622    /// By default it ends an expired target, as a syscall does. A trapped
8623    /// instruction can sit inside a user-space spinlock critical section,
8624    /// though, and a thread that yields there leaves every thread that wants
8625    /// the lock spinning until the PMU maximum preempts it. QEMU's
8626    /// `cpu_get_ticks` reads the host TSC under its `vm_clock_lock` spinlock,
8627    /// so a multi-threaded TCG vCPU that yielded at every expired target while
8628    /// reading the TSC held that lock at every yield, and the other vCPU never
8629    /// acquired it. With `--target-timeslice-syscalls-only`, a trapped
8630    /// instruction ends the slice only at the PMU maximum, as a branch would.
8631    /// With no PMU maximum it is the only way out of a busy-wait on the TSC,
8632    /// and it still ends the expired target. Replay of recorded preemptions or
8633    /// schedules keeps its recorded boundaries.
8634    #[cfg(target_arch = "x86_64")]
8635    pub(crate) fn instruction_trap_ends_timeslice(&self, cfg: &Config) -> bool {
8636        let replaying = self.preemption_points.is_some() || self.follows_replayed_schedule(cfg);
8637        match self.max_timeslice_end {
8638            Some(max_end) if cfg.target_timeslice_syscalls_only && !replaying => {
8639                self.thread_logical_time.as_nanos() >= max_end
8640            }
8641            _ => self.timeslice_expired(),
8642        }
8643    }
8644
8645    /// End the current slice no later than `remaining` from now: the virtual
8646    /// time that was left before the earliest pending deadline when this
8647    /// thread was granted its turn.
8648    ///
8649    /// Global time advances exactly as this thread's clock does while it runs
8650    /// alone, so its slice then ends, and it takes a turn, once global time has
8651    /// passed that deadline.
8652    ///
8653    /// The slice keeps at least `min_slice`, one scheduler turn charge, and
8654    /// at least one RCB's worth of time. Every cap-induced slice end is a turn
8655    /// charged that much, so a thread runs at least as long as the turn it
8656    /// pays for: the virtual time the cap adds is at most the thread's own
8657    /// work. With a shorter floor, a sibling sleeping just over one turn
8658    /// charge made each slice about 1 us of work per 1-1.5 ms of virtual time.
8659    ///
8660    /// Only the software slice end moves, which takes effect at the thread's
8661    /// next check-in: a syscall, signal or timer event. The PMU maximum keeps
8662    /// its value, so a thread that makes no syscalls still runs to it: a
8663    /// precise PMU stop at every deadline costs about 70 ms of host time each,
8664    /// and a fast timer would make them constant
8665    /// (<https://github.com/rrnewton/hermit/issues/4121>). A schedule replay follows
8666    /// its recorded turns, and a preemption replay its recorded slice ends,
8667    /// neither capped. Past the last recorded point a replay's slice end is
8668    /// `LogicalTime::MAX` (its PMU maximum stays bounded), and the cap may
8669    /// lower it; that need not reproduce the record's own slice ends.
8670    pub(crate) fn cap_timeslice_at_deadline(
8671        &mut self,
8672        remaining: Duration,
8673        clock_multiplier: f64,
8674        min_slice: Duration,
8675        schedule_replay: bool,
8676    ) {
8677        let recorded_slice =
8678            self.preemption_points.is_some() && self.stats.last_recorded_slice.is_none();
8679        if recorded_slice || schedule_replay {
8680            return;
8681        }
8682        let Some(end) = self.end_of_timeslice else {
8683            return;
8684        };
8685        let one_rcb =
8686            (NANOS_PER_RCB * clock_multiplier * self.rcb_time_multiplier().as_f64()).ceil() as u64;
8687        let current = self.thread_logical_time.as_nanos();
8688        let cap = current
8689            + remaining
8690                .max(min_slice)
8691                .max(Duration::from_nanos(one_rcb + 1));
8692        if cap < end {
8693            debug!(
8694                "[dtid {}] end of slice moved from {} to {}, the earliest pending deadline (current {})",
8695                self.dettid, end, cap, current
8696            );
8697            self.end_of_timeslice = Some(cap);
8698        }
8699    }
8700
8701    /// The recorded priority-history row whose end the current slice waits
8702    /// for, under `--replay-preemptions-from`; `None` without a history.
8703    /// Derived from the history's own cursor: rows are installed as slice
8704    /// ends in order, and the final slice follows the last.
8705    pub(crate) fn active_priority_row(
8706        &self,
8707    ) -> Option<crate::preemptions::callback_history::ActiveRow> {
8708        use crate::preemptions::callback_history::ActiveRow;
8709        let history = self.preemption_points.as_ref()?;
8710        Some(if self.stats.last_recorded_slice.is_some() {
8711            ActiveRow::Exhausted
8712        } else {
8713            match history.installed_rows() {
8714                0 => ActiveRow::BeforeFirst,
8715                installed => ActiveRow::Row(installed - 1),
8716            }
8717        })
8718    }
8719
8720    /// The active row's callback checkpoint, when the row has one and it has
8721    /// not been consumed (`Config::annotated_priority_replay`).
8722    pub(crate) fn pending_checkpoint(
8723        &self,
8724    ) -> Option<&crate::preemptions::callback_history::Checkpoint> {
8725        use crate::preemptions::callback_history::ActiveRow;
8726        let ActiveRow::Row(row) = self.active_priority_row()? else {
8727            return None;
8728        };
8729        if !self.callback_history.annotates(row) {
8730            return None;
8731        }
8732        self.callback_history.remaining().next()
8733    }
8734
8735    /// The post-hook's margin of one retired conditional branch, in logical
8736    /// time at this thread's current clock and chaos slowdown: a PMU
8737    /// deadline closer than this ends the slice at once.
8738    pub(crate) fn pmu_epsilon(&self, cfg: &Config) -> Duration {
8739        Duration::from_nanos((NANOS_PER_RCB * self.clock_multiplier(cfg)).ceil() as u64)
8740    }
8741
8742    /// Logical nanoseconds per nanosecond of retired-branch time for this
8743    /// thread now: the configured clock multiplier times its chaos slowdown.
8744    pub(crate) fn clock_multiplier(&self, cfg: &Config) -> f64 {
8745        cfg.clock_multiplier.unwrap_or(1.0) * self.rcb_time_multiplier().as_f64()
8746    }
8747
8748    /// Files a priority change point at `time` in a preemption replay, or
8749    /// returns the thread's last one when `time` is not after it.
8750    ///
8751    /// A replay that reproduces its record files each thread's change points
8752    /// at the record's times, which are strictly increasing (the loaded
8753    /// record is validated), so it never gets the error. A change point at or
8754    /// before the last one is a thread that left the record: a slice closed
8755    /// with no time in it, as when the slice before ran onto the next
8756    /// recorded end's branch count while that end's time was still ahead and
8757    /// the thread's time did not move (the adjacent zero-RCB slices of
8758    /// `end_timeslice_with_checkpoint`). Filed, such a change point was never
8759    /// granted, and the replay stalled until `--timeout`. Outside a
8760    /// preemption replay nothing is checked or kept, and nor is it for an
8761    /// edited record (`--replay-edited-preemptions`), which keeps a slice
8762    /// whose RCB target is the thread's count and ends it at once
8763    /// (`next_edited_slice`): its change points need not advance.
8764    pub(crate) fn file_replay_change_point(
8765        &mut self,
8766        cfg: &Config,
8767        time: LogicalTime,
8768    ) -> Result<(), LogicalTime> {
8769        if cfg.replay_preemptions_from.is_none() || cfg.replay_edited_preemptions {
8770            return Ok(());
8771        }
8772        if let Some(last) = self.last_replay_change_point.filter(|last| time <= *last) {
8773            return Err(last);
8774        }
8775        self.last_replay_change_point = Some(time);
8776        Ok(())
8777    }
8778
8779    /// Runs the rest of a preemption replay's thread as its final slice: no
8780    /// recorded end governs it any more, and only the run's own PMU maximum
8781    /// (if any) bounds its slices, as when the record runs out. For a thread
8782    /// that left its record ([`Self::file_replay_change_point`]), whose
8783    /// remaining recorded ends no longer describe it.
8784    pub(crate) fn abandon_replayed_slices(&mut self, cfg: &Config) {
8785        self.stats.last_recorded_slice = Some(self.stats.timeslice_count);
8786        self.end_of_timeslice = Some(LogicalTime::MAX);
8787        self.replay_rcb_end = None;
8788        let now = self.thread_logical_time.as_nanos();
8789        self.max_timeslice_end = cfg
8790            .max_timeslice
8791            .map(|max_timeslice| now + Duration::from_nanos(u64::from(max_timeslice)));
8792        self.last_rcb_timer = None;
8793        self.last_rcb_timer_is_max = false;
8794    }
8795
8796    /// Whether this thread has consumed its current logical timeslice.
8797    pub(crate) fn timeslice_expired(&self) -> bool {
8798        if let Some(replay_rcb_end) = self.replay_rcb_end {
8799            return self.committed_clock_value >= replay_rcb_end;
8800        }
8801        let current_time = self.thread_logical_time.as_nanos();
8802        self.end_of_timeslice
8803            .is_some_and(|end_of_timeslice| current_time >= end_of_timeslice)
8804    }
8805
8806    // AUTONOMOUS-BOT-IMPLEMENTED
8807    // TODO-HUMAN-REVIEW(PR-1151)
8808    /// Current RCB virtual-time multiplier, including recorded replay state.
8809    pub(crate) fn rcb_time_multiplier(&self) -> RcbTimeMultiplier {
8810        if self.chaos_slowdown_active {
8811            self.chaos_slowdown_factor
8812        } else {
8813            RcbTimeMultiplier::ONE
8814        }
8815    }
8816
8817    // AUTONOMOUS-BOT-IMPLEMENTED
8818    // TODO-HUMAN-REVIEW(PR-1151)
8819    pub(crate) fn take_pending_chaos_epochs(&mut self) -> Vec<ChaosEpochTransition> {
8820        std::mem::take(&mut self.pending_chaos_epochs)
8821    }
8822
8823    // AUTONOMOUS-BOT-IMPLEMENTED
8824    // TODO-HUMAN-REVIEW(PR-1151)
8825    fn install_chaos_epoch(&mut self, transition: ChaosEpochTransition, record: bool) {
8826        self.chaos_epoch = transition.epoch;
8827        self.chaos_slowdown_factor = transition.factor;
8828        self.chaos_slowdown_active = true;
8829        if record {
8830            self.pending_chaos_epochs.push(transition);
8831        }
8832        detlog!(
8833            "[dtid {}] CHAOSEPOCH => epoch = {}, factor = {}, logical_time = {}",
8834            self.dettid,
8835            transition.epoch,
8836            transition.factor.as_f64(),
8837            transition.logical_time
8838        );
8839    }
8840
8841    // AUTONOMOUS-BOT-IMPLEMENTED
8842    /// Choose the logical target and PMU maximum deadlines for the next timeslice.
8843    ///
8844    /// Effects:
8845    /// - Sets `end_of_timeslice` for the new timeslice.
8846    /// - Sets `max_timeslice_end` when PMU-backed preemption is enabled.
8847    /// - Resets the statistics for the timeslice.
8848    ///
8849    /// Returns: an optional new priority.
8850    pub fn next_timeslice(&mut self, cfg: &Config) -> Option<Priority> {
8851        let logical_timeslice = cfg.target_timeslice.or(cfg.max_timeslice);
8852        if let Some(timeout_ns) = logical_timeslice {
8853            let current_ns = self.thread_logical_time.as_nanos();
8854
8855            // AUTONOMOUS-BOT-IMPLEMENTED
8856            // TODO-HUMAN-REVIEW(PR-1151)
8857            // Redraw only at scheduler commit boundaries, keyed to elapsed
8858            // deterministic logical time. Replay artifacts take precedence and
8859            // restore the exact recorded Q32 factor even without ambient flags.
8860            let replay_has_epochs = self
8861                .preemption_points
8862                .as_ref()
8863                .is_some_and(ThreadHistoryIterator::has_chaos_epochs);
8864            if replay_has_epochs {
8865                // Precise PMU replay deliberately checks in up to one RCB
8866                // before the recorded logical boundary. The new slice still
8867                // begins at that recorded boundary, so install its factor
8868                // against the prior slice's exact target rather than the
8869                // slightly-early observed clock. Otherwise a short epoch can
8870                // be skipped and the following RCBs receive the wrong weight.
8871                let transition_time = self.end_of_timeslice.unwrap_or(current_ns);
8872                let transition = self
8873                    .preemption_points
8874                    .as_mut()
8875                    .and_then(|history| history.advance_chaos_epoch(transition_time));
8876                if let Some(transition) = transition {
8877                    self.install_chaos_epoch(transition, false);
8878                }
8879            } else if cfg.chaos && cfg.chaos_per_thread_slowdown {
8880                let elapsed_ns = self.thread_logical_time.without_starting().as_nanos();
8881                let epoch = elapsed_ns
8882                    .checked_div(cfg.chaos_epoch_length_ns)
8883                    .unwrap_or(0);
8884                let factor = chaos_per_thread_slowdown_factor(
8885                    cfg.sched_seed(),
8886                    self.dettid,
8887                    epoch,
8888                    cfg.chaos_slowdown_max_factor,
8889                );
8890                if epoch != self.chaos_epoch {
8891                    self.install_chaos_epoch(
8892                        ChaosEpochTransition {
8893                            logical_time: current_ns,
8894                            epoch,
8895                            factor,
8896                        },
8897                        true,
8898                    );
8899                } else {
8900                    self.chaos_slowdown_factor = factor;
8901                    self.chaos_slowdown_active = true;
8902                }
8903            } else {
8904                self.chaos_slowdown_factor = RcbTimeMultiplier::ONE;
8905                self.chaos_slowdown_active = false;
8906                self.pending_chaos_epochs.clear();
8907            }
8908
8909            let mut result = None;
8910            self.replay_rcb_end = None;
8911            let replay_controls_deadline =
8912                self.preemption_points.is_some() || self.follows_replayed_schedule(cfg);
8913
8914            // How far ahead an edited record's slice without an RCB target
8915            // must end to be replayed (`next_edited_slice`): the post-hook's
8916            // one-RCB margin at this slice's clock, which ends a closer slice
8917            // at once. Without a PMU maximum there is no such margin, so a
8918            // slice is kept whenever it ends ahead, as in a faithful replay.
8919            let margin = if cfg.max_timeslice.is_some() {
8920                self.pmu_epsilon(cfg)
8921            } else {
8922                Duration::from_nanos(1)
8923            };
8924            // Preemption-point replay from recorded --chaos configuration.
8925            if let Some(thi) = &mut self.preemption_points {
8926                if self.stats.last_recorded_slice.is_none() {
8927                    // We have not tapped out the recording yet.
8928                    let current_rcbs = self.committed_clock_value;
8929                    // For an edited record, skip the recorded slices that
8930                    // already ended (`next_edited_slice`). The chaos epoch
8931                    // advanced only to the previous slice's end (above), so
8932                    // a slice after skipped ones keeps that epoch's
8933                    // slowdown: deterministic, if stale. At a thread's start
8934                    // the scheduler has already applied the record's first
8935                    // priority, which then holds until the kept slice ends
8936                    // (the startup reset files no scheduler request).
8937                    let next = if cfg.replay_edited_preemptions {
8938                        let (next, skipped) =
8939                            next_edited_slice(thi, current_ns, current_rcbs, margin);
8940                        if skipped > 0 {
8941                            debug!(
8942                                "[dtid {}] edited record: skipped {} recorded slices that ended before {} (RCB {})",
8943                                self.dettid, skipped, current_ns, current_rcbs
8944                            );
8945                        }
8946                        next
8947                    } else {
8948                        thi.next_with_rcbs()
8949                    };
8950                    if let Some((end_time, prio, replay_rcb_end)) = next {
8951                        debug!(
8952                            "[dtid {}] next timeslice (T{}), set by recording to {:?} (current {}), priority {}",
8953                            self.dettid,
8954                            self.stats.timeslice_count + 1,
8955                            end_time,
8956                            current_ns,
8957                            prio
8958                        );
8959                        if let Some(target_rcbs) = replay_rcb_end
8960                            && target_rcbs < current_rcbs
8961                        {
8962                            panic!(
8963                                "Cannot set RCB end of timeslice to {} for thread {}, when current RCB count is already {}. \
8964                                 An edited record, not the recording of this run's schedule, needs \
8965                                 --replay-edited-preemptions.",
8966                                target_rcbs, self.dettid, current_rcbs
8967                            )
8968                        }
8969                        let exact_deadline_is_now =
8970                            replay_rcb_end.is_some_and(|target| target == current_rcbs);
8971                        // An edited record keeps a slice whose end has passed
8972                        // while its RCB target is ahead: the target governs.
8973                        let rcb_target_governs =
8974                            cfg.replay_edited_preemptions && replay_rcb_end.is_some();
8975                        if end_time <= current_ns && !exact_deadline_is_now && !rcb_target_governs {
8976                            // Records that store their epoch are reconciled with
8977                            // this run's epoch before any guest starts (see
8978                            // `adopt_replayed_schedule_epoch` in hermit-cli), so
8979                            // reaching this with such a record means the replay
8980                            // really diverged. A record without an epoch cannot
8981                            // be checked up front; say what usually causes it.
8982                            panic!(
8983                                "Cannot set end of timeslice to {} for thread {}, when current thread logical time is already {}. \
8984                                 The replayed preemption record's timeslice ends are absolute virtual times; if it was \
8985                                 recorded under a different virtual-time epoch, replay it with that --epoch \
8986                                 (https://github.com/rrnewton/hermit/issues/3413). An edited record, not the \
8987                                 recording of this run's schedule, needs --replay-edited-preemptions.",
8988                                end_time, self.dettid, current_ns
8989                            )
8990                        }
8991                        self.end_of_timeslice = Some(end_time);
8992                        self.replay_rcb_end = replay_rcb_end;
8993                        result = Some(prio);
8994                    } else {
8995                        let max = LogicalTime::MAX;
8996                        let prio = thi.final_priority();
8997                        debug!(
8998                            "[dtid {}] next timeslice (T{}) final slice after recorded preemption points... setting end_of_timeslice to max {}, final priority {}",
8999                            self.dettid,
9000                            self.stats.timeslice_count + 1,
9001                            max,
9002                            prio
9003                        );
9004                        self.stats.last_recorded_slice = Some(self.stats.timeslice_count);
9005                        self.end_of_timeslice = Some(max);
9006                        result = Some(prio)
9007                    }
9008                } else {
9009                    tracing::warn!(
9010                        "[dtid {}] next timeslice: timer expired beyond the last recorded preemption.  Not handled yet.",
9011                        self.dettid
9012                    );
9013                    self.end_of_timeslice = Some(LogicalTime::MAX);
9014                    result = Some(thi.final_priority())
9015                }
9016            } else if !cfg.chaos {
9017                if self.follows_replayed_schedule(cfg) {
9018                    if cfg.no_rcb_time {
9019                        let max_timeslice = cfg
9020                            .max_timeslice
9021                            .expect("schedule replay with PMU requires a maximum");
9022                        self.end_of_timeslice =
9023                            Some(current_ns + Duration::from_nanos(u64::from(max_timeslice)));
9024                    } else {
9025                        // Branch-event replay will overwrite this deadline when needed.
9026                        debug!(
9027                            "[dtid {}] next timeslice (T{}), in replay mode setting timeslice to max (current time {})",
9028                            self.dettid,
9029                            self.stats.timeslice_count + 1,
9030                            current_ns
9031                        );
9032                        self.end_of_timeslice = Some(LogicalTime::MAX);
9033                    }
9034                } else {
9035                    // In non-chaos mode, we only care about preemption for breaking busy-waits,
9036                    // and we can safely reset the clock every time we get control back from the
9037                    // guest.  This is our preemption-of-last-resort:
9038                    self.end_of_timeslice =
9039                        Some(current_ns + Duration::from_nanos(u64::from(timeout_ns)));
9040                    debug!(
9041                        "[dtid {}] next timeslice (T{}), end of slice set to {} (current {})",
9042                        self.dettid,
9043                        self.stats.timeslice_count + 1,
9044                        self.end_of_timeslice.unwrap(),
9045                        current_ns,
9046                    );
9047                }
9048            } else {
9049                // AUTONOMOUS-BOT-IMPLEMENTED
9050                // TODO-HUMAN-REVIEW(PR-1151)
9051                // The slowdown changes RCB-to-virtual-time progression. Converting
9052                // the sampled virtual duration back to RCBs with the SAME factor
9053                // keeps the deadline and guest-visible clock internally consistent.
9054                let slowdown = self.rcb_time_multiplier().as_f64();
9055                let nanos_per_rcb = NANOS_PER_RCB * cfg.clock_multiplier.unwrap_or(1.0) * slowdown;
9056                let target_timeout_rcbs = u64::from(timeout_ns) as f64 / nanos_per_rcb;
9057                if self.chaos_slowdown_active {
9058                    detlog!(
9059                        "[dtid {}] CHAOSSLOWDOWN => factor = {}, virtual ns/rcb = {}",
9060                        self.dettid,
9061                        slowdown,
9062                        nanos_per_rcb
9063                    );
9064                }
9065                let next_rcbs: u64 = if cfg.chaos {
9066                    // `--chaos-timeslice-range`: this slice's mean is drawn
9067                    // log-uniformly from the range, from the same seeded chaos
9068                    // stream, so slices of every scale in it mix in one run. A
9069                    // schedule replay draws none: its slices end where the
9070                    // record says, and the drawn ends would only add PMU
9071                    // check-ins. A per-run timeslice (`--chaos-timeslice-draw=run`,
9072                    // or one a replayed record states) replaces the draw too.
9073                    let target_timeout_rcbs = match cfg.chaos_timeslice_range {
9074                        Some(range)
9075                            if !replay_controls_deadline && cfg.chaos_timeslice.is_none() =>
9076                        {
9077                            range.log_uniform(self.chaos_prng.random::<f64>()) / nanos_per_rcb
9078                        }
9079                        _ => target_timeout_rcbs,
9080                    };
9081                    // Average frequency of preemptions per nanosecond:
9082                    let lambda = 1.0 / target_timeout_rcbs;
9083                    let exp = Exp::new(lambda).unwrap();
9084                    // Add one to prevent generating a zero time slice:
9085                    let rcbs = 1 + exp.sample(&mut self.chaos_prng) as u64;
9086                    detlog!("[dtid {}] CHAOSRAND => next_rcbs = {}", self.dettid, rcbs);
9087                    rcbs
9088                } else {
9089                    target_timeout_rcbs as u64
9090                };
9091                assert!(next_rcbs > 0);
9092                self.last_rcb_timer = None;
9093                self.end_of_timeslice = Some(
9094                    current_ns
9095                        + Duration::from_nanos((next_rcbs as f64 * nanos_per_rcb).ceil() as u64),
9096                );
9097                debug!(
9098                    "[dtid {}] next timeslice (T{}) chosen as {} rcbs, end of slice = {} (current {})",
9099                    self.dettid,
9100                    self.stats.timeslice_count + 1,
9101                    next_rcbs,
9102                    self.end_of_timeslice.unwrap(),
9103                    current_ns
9104                );
9105            }
9106
9107            // Past an edited record's last recorded slice, a restored chaos
9108            // slowdown can make one RCB longer than the configured maximum
9109            // (the configuration checks that maximum at the ambient
9110            // slowdown): keep at least the post-hook's one-RCB margin, or it
9111            // would end this slice at once and find itself past its maximum
9112            // again.
9113            let past_an_edited_record = cfg.replay_edited_preemptions
9114                && self.preemption_points.is_some()
9115                && self.end_of_timeslice == Some(LogicalTime::MAX);
9116            let configured_max_end = cfg.max_timeslice.map(|max_timeslice| {
9117                let length = Duration::from_nanos(u64::from(max_timeslice));
9118                current_ns
9119                    + if past_an_edited_record {
9120                        length.max(margin)
9121                    } else {
9122                        length
9123                    }
9124            });
9125            self.max_timeslice_end = if replay_controls_deadline {
9126                if cfg.max_timeslice.is_some() {
9127                    // A replay history uses `LogicalTime::MAX` after its last
9128                    // recorded preemption. Keep periodic PMU check-ins bounded
9129                    // instead of trying to program that sentinel as an RCB
9130                    // timer, which the kernel rejects with EINVAL.
9131                    self.end_of_timeslice
9132                        .filter(|end| *end != LogicalTime::MAX)
9133                        .or(configured_max_end)
9134                } else {
9135                    None
9136                }
9137            } else if cfg.target_timeslice.is_none() {
9138                match (self.end_of_timeslice, configured_max_end) {
9139                    (Some(logical_end), Some(configured_end)) => {
9140                        Some(logical_end.min(configured_end))
9141                    }
9142                    (_, configured_end) => configured_end,
9143                }
9144            } else {
9145                configured_max_end
9146            };
9147
9148            if let (Some(target_end), Some(max_end)) =
9149                (self.end_of_timeslice, self.max_timeslice_end)
9150                && target_end > max_end
9151            {
9152                self.end_of_timeslice = Some(max_end);
9153            }
9154
9155            self.last_rcb_timer = None;
9156            self.last_rcb_timer_is_max = false;
9157            self.reset_timeslice_stats(current_ns);
9158            result
9159        } else {
9160            self.end_of_timeslice = None;
9161            self.replay_rcb_end = None;
9162            self.max_timeslice_end = None;
9163            self.last_rcb_timer = None;
9164            self.last_rcb_timer_is_max = false;
9165            None
9166        }
9167    }
9168
9169    /// Close the current logical-timeslice statistics without selecting a new
9170    /// preemption point. Used when preemption replay reaches a deterministic
9171    /// guest sched_yield boundary.
9172    pub fn reset_timeslice_for_explicit_yield(&mut self) {
9173        let current_ns = self.thread_logical_time.as_nanos();
9174        self.reset_timeslice_stats(current_ns);
9175    }
9176
9177    fn reset_timeslice_stats(&mut self, current_ns: LogicalTime) {
9178        if let Some(start) = self.stats.timeslice_start_ns
9179            && current_ns >= start
9180        {
9181            self.stats
9182                .timeslice_stats
9183                .record((current_ns - start).as_nanos());
9184        }
9185        self.stats.timeslice_start_ns = Some(current_ns);
9186        self.stats.reset_timeslice();
9187    }
9188
9189    /// Are we within the execution of the (first) guest binary or any child processes called by it?
9190    /// Returns false if we are in the very beginning of execution, when the hermit container has
9191    /// forked our process, but we have not yet executed the guest binary.  There are few guarantees
9192    /// during this early initialization period, and Detcore should make no assumptions, nor
9193    /// guarantee determinism!
9194    pub fn guest_past_first_execve(&self) -> bool {
9195        self.past_global_first_execve
9196    }
9197}
9198
9199/// The first recorded slice of an edited record (`--replay-edited-preemptions`)
9200/// still ahead of a thread at logical time `now` and committed PMU count
9201/// `rcbs`, and how many recorded slices before it were skipped.
9202///
9203/// An edited record (`hermit analyze` knocks preemptions out of a recording)
9204/// changes when threads run, so a thread can reach recorded slices that have
9205/// already ended. Those no longer apply. A slice is ahead when its RCB target
9206/// is, since that target governs where there is one (its end may have
9207/// passed), and otherwise when it ends at least `margin` ahead
9208/// ([`ThreadState::next_timeslice`] passes the post-hook's one-RCB margin), so
9209/// a recorded one-RCB slice that starts where the previous one ended is kept.
9210/// Only the baseline search's knock-outs keep RCB targets; minimization's
9211/// records skip by time alone. The returned target is the kept slice's own,
9212/// or none when its end governs.
9213fn next_edited_slice(
9214    history: &mut ThreadHistoryIterator,
9215    now: LogicalTime,
9216    rcbs: u64,
9217    margin: Duration,
9218) -> (Option<(LogicalTime, Priority, Option<u64>)>, usize) {
9219    let mut skipped = 0;
9220    loop {
9221        match history.next_with_rcbs() {
9222            None => return (None, skipped),
9223            Some((end, prio, Some(target))) if target >= rcbs => {
9224                return (Some((end, prio, Some(target))), skipped);
9225            }
9226            Some((end, prio, _)) if end >= now + margin => {
9227                return (Some((end, prio, None)), skipped);
9228            }
9229            Some(_) => skipped += 1,
9230        }
9231    }
9232}
9233
9234#[cfg(test)]
9235mod traced_call_tests {
9236    use detcore_model::access_trace::FdOp;
9237
9238    use super::*;
9239
9240    /// While a traced call is in progress, the descriptor table notes each
9241    /// slot it opens, dups onto and closes, in order: a dup2 onto an open
9242    /// number closes it first, and a close_range lists every slot it
9243    /// releases with the open file it held. Without a traced call nothing is
9244    /// noted.
9245    #[test]
9246    fn a_traced_call_notes_the_slots_it_changes() {
9247        let mut state = ThreadState::<()>::new(DetTid::from_raw(3), &Config::default(), ());
9248        for fd in [10, 11] {
9249            state.add_fd(fd, OFlag::O_RDWR, FdType::Pipe, None).unwrap();
9250        }
9251        let id = |state: &ThreadState<()>, fd| state.with_detfd(fd, |d| d.open_file_id()).unwrap();
9252        let (ten, eleven) = (id(&state, 10), id(&state, 11));
9253        state.remove_fd(12);
9254        assert!(
9255            state.traced_call.take().is_none(),
9256            "no traced call, no notes"
9257        );
9258
9259        state.traced_call.begin(BTreeMap::new());
9260        state.dup_fd(10, 11, OFlag::empty()).unwrap();
9261        state.remove_fd_range(10, u32::MAX);
9262        let notes = state.traced_call.take().expect("a traced call").notes;
9263        let slot = |fd| detcore_model::fd::FdSlot {
9264            files: state.files_id(),
9265            fd,
9266        };
9267        let summary: Vec<_> = notes
9268            .iter()
9269            .map(|note| {
9270                (
9271                    note.slot,
9272                    note.open_file,
9273                    note.op.clone(),
9274                    note.released.as_ref().map(|r| (r.open_file, r.ty)),
9275                )
9276            })
9277            .collect();
9278        assert_eq!(
9279            summary,
9280            vec![
9281                (
9282                    slot(11),
9283                    Some(eleven),
9284                    FdOp::Close,
9285                    Some((eleven, FdType::Pipe))
9286                ),
9287                (slot(11), Some(ten), FdOp::Dup { source: 10 }, None),
9288                (slot(10), Some(ten), FdOp::Close, Some((ten, FdType::Pipe))),
9289                (slot(11), Some(ten), FdOp::Close, Some((ten, FdType::Pipe))),
9290            ]
9291        );
9292    }
9293}
9294
9295#[cfg(test)]
9296mod timeslice_tests {
9297    use std::num::NonZeroU64;
9298
9299    use super::*;
9300    use crate::DEFAULT_PRIORITY;
9301    use crate::preemptions::ThreadHistory;
9302
9303    /// Once an exit_group is filed on DBT (`OwnerDeathPath::Barrier`), an
9304    /// exiting thread's head stays registered for the exit_group's walk, but
9305    /// an unregistration still counts, a registration made after the filing
9306    /// is seen, and a thread whose own exit Detcore replayed is left out
9307    /// (https://github.com/rrnewton/hermit/issues/4238).
9308    #[test]
9309    fn an_exit_group_walks_the_registrations_current_at_its_grant() {
9310        let owner = |tid: i32, head: usize| RobustListOwner {
9311            dettid: DetTid::from_raw(tid),
9312            head,
9313        };
9314        let mut caller = ThreadState::<()>::new(DetTid::from_raw(3), &Config::default(), ());
9315        let sibling = |tid: i32| {
9316            let mut state = caller.clone();
9317            state.dettid = DetTid::from_raw(tid);
9318            state
9319        };
9320        let (mut killed, mut exited, mut late, mut replaced, mut unregistered) =
9321            (sibling(4), sibling(5), sibling(6), sibling(7), sibling(8));
9322        caller.record_robust_list_head(Some(0x3000));
9323        killed.record_robust_list_head(Some(0x4000));
9324        exited.record_robust_list_head(Some(0x5000));
9325        replaced.record_robust_list_head(Some(0x7000));
9326        unregistered.record_robust_list_head(Some(0x8000));
9327
9328        caller.file_exit_group_for_robust_lists();
9329        // Before the grant: thread 5 makes its own exit, which Detcore
9330        // replays; thread 6 registers; thread 7 replaces its head; thread 8
9331        // unregisters.
9332        exited.mark_robust_list_walked_by_tool();
9333        exited.drop_robust_list_head_at_exit();
9334        late.record_robust_list_head(Some(0x6000));
9335        replaced.record_robust_list_head(Some(0x7700));
9336        unregistered.record_robust_list_head(None);
9337        // The grant kills thread 4, whose exit hook runs before the walk.
9338        killed.drop_robust_list_head_at_exit();
9339        assert_eq!(
9340            killed.take_robust_list_wakes_after_exit(None, DetTime::default()),
9341            None
9342        );
9343
9344        assert_eq!(
9345            caller.robust_list_owners_for_exit_group(),
9346            vec![
9347                owner(3, 0x3000),
9348                owner(4, 0x4000),
9349                owner(6, 0x6000),
9350                owner(7, 0x7700)
9351            ]
9352        );
9353    }
9354
9355    /// A thread an exit_group killed waits for that exit_group's walk before
9356    /// its native exit, including one that filed an exit_group of its own,
9357    /// whichever of two competing filings is granted; the walker does not
9358    /// wait, nor does a group with no filing.
9359    #[test]
9360    fn a_killed_thread_waits_for_the_exit_group_walk() {
9361        use std::sync::atomic::AtomicBool;
9362        use std::sync::atomic::Ordering;
9363        const BOUND: std::time::Duration = EXIT_GROUP_ROBUST_WALK_WAIT;
9364
9365        for granted_first_filer in [true, false] {
9366            let first = ThreadState::<()>::new(DetTid::from_raw(3), &Config::default(), ());
9367            let mut second = first.clone();
9368            second.dettid = DetTid::from_raw(4);
9369            // Nothing filed: no wait.
9370            assert!(second.wait_for_exit_group_robust_walk(BOUND));
9371
9372            first.file_exit_group_for_robust_lists();
9373            second.file_exit_group_for_robust_lists();
9374            let (walker, killed) = if granted_first_filer {
9375                (first, second)
9376            } else {
9377                (second, first)
9378            };
9379            walker.begin_exit_group_robust_walk();
9380            assert!(walker.wait_for_exit_group_robust_walk(BOUND));
9381
9382            let released = Arc::new(AtomicBool::new(false));
9383            let entered = Arc::new(AtomicBool::new(false));
9384            let waiter = {
9385                let released = Arc::clone(&released);
9386                let entered = Arc::clone(&entered);
9387                std::thread::spawn(move || {
9388                    killed.cancel_exit_group_filing();
9389                    entered.store(true, Ordering::SeqCst);
9390                    let finished = killed.wait_for_exit_group_robust_walk(BOUND);
9391                    (finished, released.load(Ordering::SeqCst))
9392                })
9393            };
9394            // The killed thread is in the wait before the walk finishes, so the
9395            // check below is not passed by a waiter that started late (Claude
9396            // review of https://github.com/rrnewton/hermit/pull/4277, N6).
9397            while !entered.load(Ordering::SeqCst) {
9398                std::thread::yield_now();
9399            }
9400            std::thread::sleep(std::time::Duration::from_millis(50));
9401            assert!(
9402                !waiter.is_finished(),
9403                "the killed filer did not wait for the walk"
9404            );
9405            released.store(true, Ordering::SeqCst);
9406            walker.finish_exit_group_robust_walk();
9407            assert_eq!(
9408                waiter.join().unwrap(),
9409                (true, true),
9410                "the killed filer went on before the walk finished (first filer granted: {granted_first_filer})"
9411            );
9412        }
9413    }
9414
9415    /// A granted walk that has not finished keeps its killed threads waiting
9416    /// to the bound, which is reported, not taken as completion.
9417    #[test]
9418    fn an_unfinished_walk_is_never_released_by_the_bound() {
9419        let walker = ThreadState::<()>::new(DetTid::from_raw(3), &Config::default(), ());
9420        let mut killed = walker.clone();
9421        killed.dettid = DetTid::from_raw(4);
9422        walker.file_exit_group_for_robust_lists();
9423        walker.begin_exit_group_robust_walk();
9424        killed.cancel_exit_group_filing();
9425        assert!(!killed.wait_for_exit_group_robust_walk(std::time::Duration::from_millis(20)));
9426    }
9427
9428    /// When every filing is cancelled by its filer's death and none was
9429    /// granted (an exec ended the group first), no walk will come, and the
9430    /// killed threads are released at once.
9431    #[test]
9432    fn a_wait_ends_when_every_filing_is_cancelled() {
9433        let filer = ThreadState::<()>::new(DetTid::from_raw(3), &Config::default(), ());
9434        let mut other = filer.clone();
9435        other.dettid = DetTid::from_raw(4);
9436        filer.file_exit_group_for_robust_lists();
9437        other.cancel_exit_group_filing();
9438        assert!(!other.wait_for_exit_group_robust_walk(std::time::Duration::from_millis(20)));
9439        filer.cancel_exit_group_filing();
9440        assert!(other.wait_for_exit_group_robust_walk(std::time::Duration::from_millis(20)));
9441    }
9442
9443    /// Without a filed exit_group an exiting thread's head leaves the index.
9444    #[test]
9445    fn an_exit_drops_its_head_when_no_exit_group_is_filed() {
9446        let mut state = ThreadState::<()>::new(DetTid::from_raw(4), &Config::default(), ());
9447        state.record_robust_list_head(Some(0x4000));
9448        state.drop_robust_list_head_at_exit();
9449        assert!(state.robust_list_heads().is_empty());
9450        assert_eq!(state.robust_list_head, None);
9451    }
9452
9453    /// A stale robust-list head must never survive into a new address space.
9454    /// `run_robust_list_owner_death` walks from whatever this field holds, so if
9455    /// `execve` left the old image's head here, thread exit would follow a wild
9456    /// pointer through the *new* image and write `FUTEX_OWNER_DIED` into
9457    /// whatever happened to sit at the computed word address.
9458    #[test]
9459    fn a_successful_exec_clears_the_robust_list_head() {
9460        let mut state = ThreadState::<()>::new(DetTid::from_raw(3), &Config::default(), ());
9461        assert_eq!(state.robust_list_head, None, "a fresh thread has no list");
9462
9463        state.record_robust_list_head(Some(0x7ffff7bff920));
9464        assert_eq!(
9465            state.take_robust_list_for_exec(),
9466            Some(0x7ffff7bff920),
9467            "the previous head is handed back for the failure path"
9468        );
9469        assert_eq!(
9470            state.robust_list_head, None,
9471            "the candidate exec image starts with no robust list, as after copy_process"
9472        );
9473        assert!(
9474            state.robust_list_heads().is_empty(),
9475            "the old address-space registration must also leave the shared index"
9476        );
9477    }
9478
9479    /// `execve` returns only on failure, and then the old address space -- and
9480    /// so the old head -- is still valid.
9481    #[test]
9482    fn a_failed_exec_restores_the_robust_list_head() {
9483        let mut state = ThreadState::<()>::new(DetTid::from_raw(3), &Config::default(), ());
9484        state.record_robust_list_head(Some(0x404100));
9485
9486        let saved = state.take_robust_list_for_exec();
9487        state.restore_robust_list_after_failed_exec(saved);
9488
9489        assert_eq!(state.robust_list_head, Some(0x404100));
9490        assert_eq!(
9491            state.robust_list_heads(),
9492            vec![(DetTid::from_raw(3), 0x404100)],
9493            "a failed exec keeps the old address-space registration indexed"
9494        );
9495    }
9496
9497    fn two_owner_robust_list_state() -> (
9498        ThreadState<()>,
9499        ThreadState<()>,
9500        RobustListWake,
9501        RobustListWake,
9502        RobustListWake,
9503    ) {
9504        let first_owner = DetTid::from_raw(3);
9505        let second_owner = DetTid::from_raw(4);
9506        let mm = MmId::initial(first_owner);
9507        let lower = RobustListWake {
9508            futex: FutexID::private(mm, 0x4040),
9509        };
9510        let middle = RobustListWake {
9511            futex: FutexID::private(mm, 0x5050),
9512        };
9513        let higher = RobustListWake {
9514            futex: FutexID::private(mm, 0x6060),
9515        };
9516        let mut first = ThreadState::<()>::new(first_owner, &Config::default(), ());
9517        let mut second = first.clone();
9518        second.dettid = second_owner;
9519        first.record_robust_list_head(Some(0x404100));
9520        second.record_robust_list_head(Some(0x404200));
9521        first.stage_robust_list_wakes(
9522            RobustListExit::Signal(libc::SIGTERM),
9523            vec![
9524                (second_owner, vec![higher]),
9525                (first_owner, vec![middle, lower]),
9526            ],
9527        );
9528        (first, second, lower, middle, higher)
9529    }
9530
9531    #[test]
9532    fn group_robust_list_wakes_wait_for_every_owner_and_sort_the_request() {
9533        let (first, second, lower, middle, higher) = two_owner_robust_list_state();
9534        let mut first_time = DetTime::default();
9535        first_time.add_syscall();
9536        let mut second_time = first_time.clone();
9537        second_time.add_syscall();
9538
9539        assert_eq!(
9540            second.take_robust_list_wakes_after_exit(Some(libc::SIGTERM), second_time.clone()),
9541            None,
9542            "the first physical exit must not release part of the group"
9543        );
9544        assert_eq!(
9545            first.take_robust_list_wakes_after_exit(Some(libc::SIGTERM), first_time),
9546            Some((
9547                second_time,
9548                vec![
9549                    (DetTid::from_raw(3), lower),
9550                    (DetTid::from_raw(3), middle),
9551                    (DetTid::from_raw(4), higher),
9552                ],
9553            )),
9554            "the final matching exit releases one owner/futex-sorted request"
9555        );
9556    }
9557
9558    #[test]
9559    fn group_robust_list_wake_request_is_independent_of_exit_arrival_order() {
9560        let release = |reverse: bool| {
9561            let (first, second, _, _, _) = two_owner_robust_list_state();
9562            let first_time = DetTime::default();
9563            let mut second_time = first_time.clone();
9564            second_time.add_syscall();
9565            if reverse {
9566                assert_eq!(
9567                    second.take_robust_list_wakes_after_exit(
9568                        Some(libc::SIGTERM),
9569                        second_time.clone(),
9570                    ),
9571                    None
9572                );
9573                first.take_robust_list_wakes_after_exit(Some(libc::SIGTERM), first_time)
9574            } else {
9575                assert_eq!(
9576                    first.take_robust_list_wakes_after_exit(Some(libc::SIGTERM), first_time),
9577                    None
9578                );
9579                second.take_robust_list_wakes_after_exit(Some(libc::SIGTERM), second_time)
9580            }
9581        };
9582
9583        assert_eq!(release(false), release(true));
9584    }
9585
9586    #[test]
9587    fn mismatched_or_nonfatal_exit_does_not_release_group_robust_list_wakes() {
9588        let (first, second, _, _, _) = two_owner_robust_list_state();
9589        assert_eq!(
9590            first.take_robust_list_wakes_after_exit(Some(libc::SIGKILL), DetTime::default()),
9591            None
9592        );
9593        assert_eq!(
9594            second.take_robust_list_wakes_after_exit(Some(libc::SIGTERM), DetTime::default()),
9595            None,
9596            "one matching owner cannot release a group whose peer exited for another reason"
9597        );
9598
9599        let (first, second, _, _, _) = two_owner_robust_list_state();
9600        assert_eq!(
9601            first.take_robust_list_wakes_after_exit(None, DetTime::default()),
9602            None
9603        );
9604        assert_eq!(
9605            second.take_robust_list_wakes_after_exit(Some(libc::SIGTERM), DetTime::default()),
9606            None,
9607            "a normal exit must not satisfy a staged fatal-signal exit"
9608        );
9609    }
9610
9611    #[test]
9612    fn regular_file_opens_do_not_shift_socket_cookie_identity() {
9613        let owner = DetTid::from_raw(3);
9614        let mut files = FileMetadata::new(owner);
9615        let first = files.allocate_open_file_id(owner, FdType::Socket);
9616        for _ in 0..4 {
9617            files.allocate_open_file_id(owner, FdType::Regular);
9618        }
9619        let second = files.allocate_open_file_id(owner, FdType::Socket);
9620
9621        assert_eq!(first.deterministic_socket_cookie(), 3_u64 << 32);
9622        assert_eq!(second.deterministic_socket_cookie(), (3_u64 << 32) | 1);
9623    }
9624
9625    #[test]
9626    fn guest_clock_tracks_raw_logical_time_without_lag() {
9627        let epoch = LogicalTime::from_secs(1_000);
9628        let mut clock = GuestClock::default();
9629
9630        assert_eq!(
9631            clock.observe(epoch + Duration::from_nanos(41_000_000)),
9632            epoch + Duration::from_nanos(41_000_000)
9633        );
9634        assert_eq!(
9635            clock.observe(epoch + Duration::from_nanos(41_025_000)),
9636            epoch + Duration::from_nanos(41_025_000)
9637        );
9638        // A stale backend-local sample cannot move the process-tree clock back.
9639        assert_eq!(
9640            clock.observe(epoch + Duration::from_nanos(41_010_000)),
9641            epoch + Duration::from_nanos(41_025_000)
9642        );
9643    }
9644
9645    #[test]
9646    fn guest_clock_absolute_deadline_stays_ahead_of_committed_time() {
9647        let committed_time = LogicalTime::from_secs(1_000) + Duration::from_millis(250);
9648        let mut clock = GuestClock::default();
9649        let guest_now = clock.observe(committed_time);
9650        let deadline = guest_now + Duration::from_millis(100);
9651
9652        assert_eq!(guest_now, committed_time);
9653        assert!(deadline > committed_time);
9654    }
9655
9656    #[test]
9657    fn guest_clock_process_tree_shares_one_monotonic_domain() {
9658        let epoch = LogicalTime::from_secs(1_000);
9659        let root = Arc::new(Mutex::new(GuestClock::default()));
9660        let forked_child = Arc::clone(&root);
9661
9662        assert!(Arc::ptr_eq(&root, &forked_child));
9663        assert_eq!(
9664            root.lock().unwrap().observe(epoch + Duration::from_secs(1)),
9665            epoch + Duration::from_secs(1)
9666        );
9667        assert_eq!(
9668            forked_child
9669                .lock()
9670                .unwrap()
9671                .observe(epoch + Duration::from_secs(2)),
9672            epoch + Duration::from_secs(2)
9673        );
9674
9675        // Exec retains the same clock object and does not rebase elapsed time.
9676        let execed_child = Arc::clone(&forked_child);
9677        assert!(Arc::ptr_eq(&root, &execed_child));
9678        assert_eq!(
9679            execed_child
9680                .lock()
9681                .unwrap()
9682                .observe(epoch + Duration::from_secs(9)),
9683            epoch + Duration::from_secs(9)
9684        );
9685    }
9686
9687    #[test]
9688    fn initial_current_image_is_selected_once_before_registration() {
9689        let tid = DetTid::from_raw(3);
9690        let mut state = ThreadState::new(tid, &Config::default(), ());
9691        assert_eq!(state.mm_id, MmId::initial(tid)); // No fact: old default.
9692        state.physical_tid = Some(tid.as_raw());
9693        let physical_tid = state.physical_tid;
9694        state.bind_initial_current_image(tid).unwrap();
9695        let current = MmId::initial(tid).for_exec(tid);
9696        assert_eq!(state.mm_id, current);
9697        assert_eq!(state.physical_tid, physical_tid);
9698        assert!(!state.thread_start_entered);
9699        assert!(state.detpid.is_none());
9700        assert_eq!(state.bind_initial_current_image(tid), Err(Errno::EPROTO));
9701        assert_eq!(state.mm_id, current);
9702    }
9703
9704    #[test]
9705    fn initial_current_image_rejects_nonfresh_or_other_thread_state() {
9706        let tid = DetTid::from_raw(3);
9707        for case in 0..7 {
9708            let mut state = ThreadState::new(tid, &Config::default(), ());
9709            let requested = if case == 0 { DetTid::from_raw(4) } else { tid };
9710            match case {
9711                1 => state.detpid = Some(tid),
9712                2 => state.thread_start_entered = true,
9713                3 => state.past_global_first_execve = true,
9714                4 => state.mm_id = state.mm_id.for_exec(tid),
9715                5 => state.clone_flags = Some(CloneFlags::empty()),
9716                6 => state.mm_id = MmId::initial(DetTid::from_raw(4)),
9717                _ => {}
9718            }
9719            let before = state.mm_id;
9720            assert_eq!(
9721                state.bind_initial_current_image(requested),
9722                Err(Errno::EPROTO)
9723            );
9724            assert_eq!(state.mm_id, before);
9725        }
9726    }
9727
9728    #[test]
9729    fn unparented_thread_recovers_process_memory_identity() {
9730        let detpid = DetPid::from_raw(4);
9731        let dettid = DetTid::from_raw(7);
9732        let mut state = ThreadState::new(dettid, &Config::default(), ());
9733
9734        assert!(state.recover_process_mm_id(detpid));
9735        assert_eq!(state.mm_id, MmId::initial(detpid));
9736        assert!(!state.recover_process_mm_id(detpid));
9737    }
9738
9739    #[test]
9740    fn inherited_thread_keeps_existing_memory_identity() {
9741        let detpid = DetPid::from_raw(4);
9742        let dettid = DetTid::from_raw(7);
9743        let inherited_mm = MmId::initial(detpid).for_exec(detpid);
9744        let mut state = ThreadState::new(dettid, &Config::default(), ());
9745        state.mm_id = inherited_mm;
9746
9747        assert!(!state.recover_process_mm_id(detpid));
9748        assert_eq!(state.mm_id, inherited_mm);
9749    }
9750
9751    #[test]
9752    fn backend_can_override_open_file_creator_identity() {
9753        let host_tid = DetTid::from_raw(10_003);
9754        let virtual_tid = DetTid::from_raw(3);
9755        let mut state = ThreadState::new(host_tid, &Config::default(), ());
9756
9757        assert_eq!(state.open_file_creator, None);
9758        state.set_open_file_creator(virtual_tid);
9759        assert_eq!(state.open_file_creator, Some(virtual_tid));
9760    }
9761
9762    // AUTONOMOUS-BOT-IMPLEMENTED
9763    // TODO-HUMAN-REVIEW(PR-1149)
9764    #[test]
9765    fn chaos_per_thread_slowdown_factor_is_stable_and_deterministic() {
9766        let seed = 0xdead_beef_u64;
9767        let max_factor: f64 = 10.0;
9768        // Deterministic: same (seed, dettid) -> identical factor, every call.
9769        for raw in 1..=64 {
9770            let tid = DetTid::from_raw(raw);
9771            let a = chaos_per_thread_slowdown_factor(seed, tid, 0, max_factor);
9772            let b = chaos_per_thread_slowdown_factor(seed, tid, 0, max_factor);
9773            assert_eq!(
9774                a, b,
9775                "factor must be a pure function of (seed, dettid, epoch)"
9776            );
9777            // Log-uniform in [1/R, R].
9778            let a = a.as_f64();
9779            assert!(
9780                a >= 1.0 / max_factor - 1e-9 && a <= max_factor + 1e-9,
9781                "factor {} out of [1/{max_factor}, {max_factor}] for tid {raw}",
9782                a
9783            );
9784        }
9785    }
9786
9787    // AUTONOMOUS-BOT-IMPLEMENTED
9788    // TODO-HUMAN-REVIEW(PR-1149)
9789    #[test]
9790    fn chaos_per_thread_slowdown_factor_varies_across_threads_and_seeds() {
9791        let max_factor = 10.0;
9792        // Different threads (same seed) get a spread of factors, not all equal.
9793        let factors: Vec<f64> = (1..=32)
9794            .map(|raw| {
9795                chaos_per_thread_slowdown_factor(1234, DetTid::from_raw(raw), 0, max_factor)
9796                    .as_f64()
9797            })
9798            .collect();
9799        let first = factors[0];
9800        assert!(
9801            factors.iter().any(|&f| (f - first).abs() > 1e-6),
9802            "per-thread factors should differ across threads"
9803        );
9804        // Different seeds give a different factor for the same thread.
9805        let tid = DetTid::from_raw(7);
9806        let f_a = chaos_per_thread_slowdown_factor(1, tid, 0, max_factor).as_f64();
9807        let f_b = chaos_per_thread_slowdown_factor(2, tid, 0, max_factor).as_f64();
9808        assert!(
9809            (f_a - f_b).abs() > 1e-12,
9810            "different seeds should yield different factors for the same thread"
9811        );
9812    }
9813
9814    // AUTONOMOUS-BOT-IMPLEMENTED
9815    // TODO-HUMAN-REVIEW(PR-1149)
9816    #[test]
9817    fn chaos_per_thread_slowdown_factor_disabled_when_max_factor_at_most_one() {
9818        // max_factor <= 1.0 disables the spread: every thread is nominal (1.0).
9819        for raw in 1..=16 {
9820            let tid = DetTid::from_raw(raw);
9821            assert_eq!(
9822                chaos_per_thread_slowdown_factor(99, tid, 0, 1.0),
9823                RcbTimeMultiplier::ONE
9824            );
9825            assert_eq!(
9826                chaos_per_thread_slowdown_factor(99, tid, 0, 0.5),
9827                RcbTimeMultiplier::ONE
9828            );
9829        }
9830    }
9831
9832    // AUTONOMOUS-BOT-IMPLEMENTED
9833    // TODO-HUMAN-REVIEW(PR-1151)
9834    #[test]
9835    fn chaos_epoch_zero_reproduces_epochless_factor() {
9836        // Enabling epochs must never perturb the FIRST epoch: epoch 0 has to
9837        // yield exactly the value the epoch-less #1149 code produced (the epoch
9838        // term folds to 0 in the mix).
9839        use rand::RngExt as _;
9840        use rand::SeedableRng as _;
9841        let max_factor: f64 = 10.0;
9842        for raw in 1..=64 {
9843            let tid = DetTid::from_raw(raw);
9844            for &seed in &[0u64, 1, 7, 0xdead_beef, u64::MAX] {
9845                let epochless = {
9846                    // Reconstruct the exact epoch-less mix inline to pin the
9847                    // invariant independent of the production function body.
9848                    const SLOWDOWN_SALT: u64 = 0x736c_6f77_646f_776e;
9849                    let mixed = seed
9850                        ^ SLOWDOWN_SALT
9851                        ^ ((tid.as_raw() as u32 as u64).wrapping_mul(0x9e37_79b9_7f4a_7c15));
9852                    let mut prng = Pcg64Mcg::seed_from_u64(mixed);
9853                    let u: f64 = prng.random::<f64>();
9854                    max_factor.powf(2.0 * u - 1.0)
9855                };
9856                assert_eq!(
9857                    chaos_per_thread_slowdown_factor(seed, tid, 0, max_factor),
9858                    RcbTimeMultiplier::from_f64(epochless),
9859                    "epoch 0 must reproduce the epoch-less factor for seed {seed}, tid {raw}"
9860                );
9861            }
9862        }
9863    }
9864
9865    // AUTONOMOUS-BOT-IMPLEMENTED
9866    // TODO-HUMAN-REVIEW(PR-1151)
9867    #[test]
9868    fn chaos_epoch_factor_varies_deterministically_across_epochs() {
9869        let max_factor = 10.0;
9870        let seed = 0x1234_5678_u64;
9871        let tid = DetTid::from_raw(3);
9872        // Each epoch draws a fresh factor for the same thread; the sequence is
9873        // a pure function of (seed, dettid, epoch), hence replayable.
9874        let factors: Vec<f64> = (0..16)
9875            .map(|epoch| chaos_per_thread_slowdown_factor(seed, tid, epoch, max_factor).as_f64())
9876            .collect();
9877        // Purity: recomputing any epoch yields the identical value.
9878        for (epoch, &f) in factors.iter().enumerate() {
9879            assert_eq!(
9880                chaos_per_thread_slowdown_factor(seed, tid, epoch as u64, max_factor).as_f64(),
9881                f,
9882                "factor must be pure in epoch"
9883            );
9884            // Stays in the log-uniform range.
9885            assert!(f >= 1.0 / max_factor - 1e-9 && f <= max_factor + 1e-9);
9886        }
9887        // The factor actually changes across epochs (not a constant stream).
9888        let first = factors[0];
9889        assert!(
9890            factors.iter().any(|&f| (f - first).abs() > 1e-6),
9891            "per-epoch factors should differ across epochs"
9892        );
9893    }
9894
9895    fn cpu_snapshot(
9896        user: u64,
9897        system: u64,
9898        children_user: u64,
9899        children_system: u64,
9900    ) -> ProcessCpuSnapshot {
9901        ProcessCpuSnapshot {
9902            user: LogicalTime::from_nanos(user),
9903            system: LogicalTime::from_nanos(system),
9904            children_user: LogicalTime::from_nanos(children_user),
9905            children_system: LogicalTime::from_nanos(children_system),
9906        }
9907    }
9908
9909    #[test]
9910    fn child_cpu_time_is_hidden_until_reap() {
9911        let pid = DetPid::from_raw(2);
9912        let mut parent = ProcessCpuTime::default();
9913        parent.record_exited_child(pid, cpu_snapshot(10, 20, 3, 4));
9914
9915        assert_eq!(parent.snapshot.children_user, LogicalTime::ZERO);
9916        assert_eq!(parent.snapshot.children_system, LogicalTime::ZERO);
9917
9918        parent.reap_child(pid);
9919        assert_eq!(parent.snapshot.children_user, LogicalTime::from_nanos(13));
9920        assert_eq!(parent.snapshot.children_system, LogicalTime::from_nanos(24));
9921    }
9922
9923    #[test]
9924    fn reaping_nonexited_child_does_not_change_accounting() {
9925        let pid = DetPid::from_raw(2);
9926        let mut parent = ProcessCpuTime::default();
9927
9928        parent.reap_child(pid);
9929        assert_eq!(parent.snapshot.children_user, LogicalTime::ZERO);
9930        assert_eq!(parent.snapshot.children_system, LogicalTime::ZERO);
9931    }
9932
9933    #[test]
9934    fn child_cpu_time_uses_final_thread_snapshot_and_drops_reaped_state() {
9935        let pid = DetPid::from_raw(2);
9936        let mut parent = ProcessCpuTime::default();
9937
9938        parent.record_exited_child(pid, cpu_snapshot(10, 20, 3, 4));
9939        parent.record_exited_child(pid, cpu_snapshot(12, 25, 4, 5));
9940
9941        parent.reap_child(pid);
9942        assert_eq!(parent.snapshot.children_user, LogicalTime::from_nanos(16));
9943        assert_eq!(parent.snapshot.children_system, LogicalTime::from_nanos(30));
9944        assert!(parent.exited_children.is_empty());
9945
9946        parent.reap_child(pid);
9947        assert_eq!(parent.snapshot.children_user, LogicalTime::from_nanos(16));
9948        assert_eq!(parent.snapshot.children_system, LogicalTime::from_nanos(30));
9949    }
9950
9951    #[tokio::test]
9952    async fn child_cpu_publication_replacement_rejects_stale_owner_and_snapshot() {
9953        use std::future::Future;
9954        use std::task::Context;
9955
9956        let cfg = Config::default();
9957        let child = DetPid::from_raw(7);
9958        let mut parent = ThreadState::new(DetPid::from_raw(3), &cfg, ());
9959        assert!(!parent.wait_for_child_cpu_publication(child).await);
9960        let before = serde_json::to_value(&*parent.process_cpu_time.lock().unwrap()).unwrap();
9961        let first = parent.prepare_child_cpu_publication(child);
9962        let mut old_child = ThreadState::new(child, &cfg, ());
9963        old_child.parent_process_cpu_time = Some(Arc::clone(&parent.process_cpu_time));
9964        old_child.parent_cpu_publication = Some(Arc::clone(&first));
9965        let mut old_wait = Box::pin(parent.wait_for_child_cpu_publication(child));
9966        let mut cx = Context::from_waker(futures::task::noop_waker_ref());
9967        assert!(old_wait.as_mut().poll(&mut cx).is_pending());
9968        let second = parent.prepare_child_cpu_publication(child);
9969        assert!(!Arc::ptr_eq(&first, &second));
9970        assert!(!old_wait.await);
9971        assert!(!old_child.record_exited_child_process_cpu_time(child));
9972        assert!(!parent.has_exited_child_process_cpu_time(child));
9973        // Neither runtime notification state nor host wakers enter recordings.
9974        {
9975            let cpu = parent.process_cpu_time.lock().unwrap();
9976            assert_eq!(serde_json::to_value(&*cpu).unwrap(), before);
9977            let clone = cpu.clone();
9978            assert!(Arc::ptr_eq(
9979                clone.child_cpu_publications.get(&child).unwrap(),
9980                &second
9981            ));
9982            let restored: ProcessCpuTime = serde_json::from_value(before).unwrap();
9983            assert!(restored.child_cpu_publications.is_empty());
9984        }
9985
9986        let mut current_child = ThreadState::new(child, &cfg, ());
9987        current_child.parent_process_cpu_time = Some(Arc::clone(&parent.process_cpu_time));
9988        current_child.parent_cpu_publication = Some(Arc::clone(&second));
9989        assert!(current_child.record_exited_child_process_cpu_time(child));
9990        parent.prepare_child_process_cpu_time(child);
9991        assert!(parent.has_exited_child_process_cpu_time(child));
9992        assert!(parent.wait_for_child_cpu_publication(child).await);
9993        // ThreadState clones retain the same process identity.
9994        assert!(Arc::ptr_eq(
9995            current_child
9996                .clone()
9997                .parent_cpu_publication
9998                .as_ref()
9999                .unwrap(),
10000            &second
10001        ));
10002        parent.reap_child_process_cpu_time(child);
10003        assert!(!parent.wait_for_child_cpu_publication(child).await);
10004        assert!(!current_child.record_exited_child_process_cpu_time(child));
10005        assert!(!parent.has_exited_child_process_cpu_time(child));
10006    }
10007
10008    fn nz(value: u64) -> Option<NonZeroU64> {
10009        NonZeroU64::new(value)
10010    }
10011
10012    /// The cap of hermit#4121 only lowers the software slice end, keeps one
10013    /// RCB of room (10 ns at the default clock) plus one nanosecond, and
10014    /// leaves the PMU maximum alone.
10015    #[test]
10016    fn a_deadline_cap_lowers_the_slice_end_and_keeps_one_rcb() {
10017        let config = Config {
10018            max_timeslice: nz(200_000_000),
10019            ..Default::default()
10020        };
10021        let mut state = ThreadState::new(DetPid::from_raw(3), &config, ());
10022        state.next_timeslice(&config);
10023        let now = state.thread_logical_time.as_nanos();
10024        let end = state.end_of_timeslice.unwrap();
10025        assert_eq!(end, now + Duration::from_nanos(200_000_000));
10026        assert_eq!(state.max_timeslice_end, Some(end));
10027
10028        state.cap_timeslice_at_deadline(
10029            Duration::from_nanos(5_000_000),
10030            1.0,
10031            Duration::ZERO,
10032            false,
10033        );
10034        let capped = now + Duration::from_nanos(5_000_000);
10035        assert_eq!(state.end_of_timeslice, Some(capped));
10036        assert_eq!(state.max_timeslice_end, Some(end));
10037
10038        // A later deadline never raises the end.
10039        state.cap_timeslice_at_deadline(
10040            Duration::from_nanos(9_000_000),
10041            1.0,
10042            Duration::ZERO,
10043            false,
10044        );
10045        assert_eq!(state.end_of_timeslice, Some(capped));
10046
10047        // A deadline already due leaves one RCB and a nanosecond.
10048        state.cap_timeslice_at_deadline(Duration::ZERO, 1.0, Duration::ZERO, false);
10049        assert_eq!(state.end_of_timeslice, Some(now + Duration::from_nanos(11)));
10050        assert_eq!(state.max_timeslice_end, Some(end));
10051    }
10052
10053    /// The cap never ends a slice sooner than one scheduler turn charge
10054    /// (`min_slice`) after it is applied, so the turn it causes costs no more
10055    /// virtual time than the work the thread did.
10056    #[test]
10057    fn a_deadline_cap_keeps_at_least_one_turn_charge() {
10058        let config = Config {
10059            max_timeslice: nz(200_000_000),
10060            ..Default::default()
10061        };
10062        let mut state = ThreadState::new(DetPid::from_raw(3), &config, ());
10063        state.next_timeslice(&config);
10064        let now = state.thread_logical_time.as_nanos();
10065        let turn = Duration::from_nanos(500_000);
10066        state.cap_timeslice_at_deadline(Duration::from_nanos(1_000), 1.0, turn, false);
10067        assert_eq!(state.end_of_timeslice, Some(now + turn));
10068        state.cap_timeslice_at_deadline(Duration::ZERO, 1.0, turn, false);
10069        assert_eq!(state.end_of_timeslice, Some(now + turn));
10070        state.cap_timeslice_at_deadline(Duration::from_nanos(700_000), 1.0, turn, false);
10071        assert_eq!(state.end_of_timeslice, Some(now + turn));
10072    }
10073
10074    /// Schedule replay follows its recorded turns: the cap leaves its slices
10075    /// alone.
10076    #[test]
10077    fn schedule_replay_takes_no_deadline_cap() {
10078        let config = Config {
10079            max_timeslice: nz(200_000_000),
10080            ..Default::default()
10081        };
10082        let mut state = ThreadState::new(DetPid::from_raw(3), &config, ());
10083        state.next_timeslice(&config);
10084        let end = state.end_of_timeslice;
10085        state.cap_timeslice_at_deadline(Duration::from_nanos(5_000), 1.0, Duration::ZERO, true);
10086        assert_eq!(state.end_of_timeslice, end);
10087    }
10088
10089    // AUTONOMOUS-BOT-IMPLEMENTED
10090    // TODO-HUMAN-REVIEW(PR-1151)
10091    #[test]
10092    fn constant_slowdown_is_the_single_epoch_case() {
10093        let config = Config {
10094            chaos: true,
10095            chaos_per_thread_slowdown: true,
10096            chaos_epoch_length_ns: 0,
10097            target_timeslice: nz(10_000),
10098            max_timeslice: nz(100_000),
10099            ..Default::default()
10100        };
10101        let mut state = ThreadState::new(DetPid::from_raw(3), &config, ());
10102        state.next_timeslice(&config);
10103        let first = state.take_pending_chaos_epochs().pop().unwrap();
10104        assert_eq!(first.epoch, 0);
10105        assert_eq!(state.chaos_epoch, 0);
10106
10107        state
10108            .thread_logical_time
10109            .add_rcbs_with_multiplier(10_000, first.factor);
10110        state.next_timeslice(&config);
10111        assert_eq!(state.chaos_epoch, 0);
10112        assert_eq!(state.chaos_slowdown_factor, first.factor);
10113        assert!(state.take_pending_chaos_epochs().is_empty());
10114    }
10115
10116    // AUTONOMOUS-BOT-IMPLEMENTED
10117    // TODO-HUMAN-REVIEW(PR-1151)
10118    #[test]
10119    fn epoch_redraw_uses_elapsed_logical_time_at_commit_boundaries() {
10120        let config = Config {
10121            chaos: true,
10122            chaos_per_thread_slowdown: true,
10123            chaos_epoch_length_ns: 100,
10124            target_timeslice: nz(10_000),
10125            max_timeslice: nz(100_000),
10126            ..Default::default()
10127        };
10128        let tid = DetPid::from_raw(5);
10129        let mut state = ThreadState::new(tid, &config, ());
10130        state.next_timeslice(&config);
10131        let first = state.pending_chaos_epochs[0];
10132
10133        state
10134            .thread_logical_time
10135            .add_rcbs_with_multiplier(1_000, first.factor);
10136        let expected_epoch =
10137            state.thread_logical_time.without_starting().as_nanos() / config.chaos_epoch_length_ns;
10138        assert!(expected_epoch > 0);
10139
10140        state.next_timeslice(&config);
10141        let transitions = state.take_pending_chaos_epochs();
10142        assert_eq!(transitions.len(), 2);
10143        assert_eq!(transitions[0], first);
10144        let redraw = transitions[1];
10145        assert_eq!(redraw.epoch, expected_epoch);
10146        assert_eq!(
10147            redraw.factor,
10148            chaos_per_thread_slowdown_factor(
10149                config.sched_seed(),
10150                tid,
10151                expected_epoch,
10152                config.chaos_slowdown_max_factor,
10153            )
10154        );
10155        assert!(redraw.logical_time > first.logical_time);
10156    }
10157
10158    // AUTONOMOUS-BOT-IMPLEMENTED
10159    // TODO-HUMAN-REVIEW(PR-1151)
10160    #[test]
10161    fn replay_installs_recorded_epoch_without_ambient_chaos_flags() {
10162        let config = Config {
10163            target_timeslice: nz(10_000),
10164            max_timeslice: nz(100_000),
10165            ..Default::default()
10166        };
10167        let transition = ChaosEpochTransition {
10168            logical_time: LogicalTime::ZERO,
10169            epoch: 7,
10170            factor: RcbTimeMultiplier::from_f64(3.25),
10171        };
10172        let mut state = ThreadState::new(DetPid::from_raw(5), &config, ());
10173        state.preemption_points = Some(
10174            ThreadHistory::new()
10175                .with_chaos_epochs(vec![transition])
10176                .into_iter(),
10177        );
10178
10179        state.next_timeslice(&config);
10180
10181        assert!(state.chaos_slowdown_active);
10182        assert_eq!(state.chaos_epoch, transition.epoch);
10183        assert_eq!(state.chaos_slowdown_factor, transition.factor);
10184        assert!(state.take_pending_chaos_epochs().is_empty());
10185    }
10186
10187    // AUTONOMOUS-BOT-IMPLEMENTED
10188    // TODO-HUMAN-REVIEW(PR-1151)
10189    #[test]
10190    fn replay_installs_boundary_epoch_when_pmu_checks_in_early() {
10191        let config = Config {
10192            target_timeslice: nz(10_000),
10193            max_timeslice: nz(100_000),
10194            ..Default::default()
10195        };
10196        let mut state = ThreadState::new(DetPid::from_raw(5), &config, ());
10197        let now = state.thread_logical_time.as_nanos();
10198        let first = ChaosEpochTransition {
10199            logical_time: now,
10200            epoch: 0,
10201            factor: RcbTimeMultiplier::from_f64(2.0),
10202        };
10203        let second = ChaosEpochTransition {
10204            logical_time: now + Duration::from_nanos(100),
10205            epoch: 1,
10206            factor: RcbTimeMultiplier::from_f64(3.0),
10207        };
10208        let history = ThreadHistory::new()
10209            .with_prio_changes(vec![
10210                (now + Duration::from_nanos(100), DEFAULT_PRIORITY),
10211                (now + Duration::from_nanos(200), DEFAULT_PRIORITY),
10212            ])
10213            .with_preemption_rcbs(vec![4, 8])
10214            .with_chaos_epochs(vec![first, second]);
10215        state.preemption_points = Some(history.into_iter());
10216
10217        state.next_timeslice(&config);
10218        assert_eq!(state.chaos_slowdown_factor, first.factor);
10219        assert_eq!(
10220            state.end_of_timeslice,
10221            Some(now + Duration::from_nanos(100))
10222        );
10223
10224        // Four 2x RCBs advance to 80ns: within one RCB of the exact 100ns
10225        // boundary, matching the early check-in path in post_handler_hook.
10226        state
10227            .thread_logical_time
10228            .add_rcbs_with_multiplier(4, first.factor);
10229        state.committed_clock_value = 4;
10230        assert!(state.timeslice_expired());
10231        state.next_timeslice(&config);
10232
10233        assert_eq!(
10234            state.thread_logical_time.as_nanos(),
10235            now + Duration::from_nanos(80)
10236        );
10237        assert_eq!(state.chaos_epoch, second.epoch);
10238        assert_eq!(state.chaos_slowdown_factor, second.factor);
10239        assert_eq!(
10240            state.end_of_timeslice,
10241            Some(now + Duration::from_nanos(200))
10242        );
10243    }
10244
10245    /// A thread at 500 ns of logical time (50 RCBs at multiplier 1) and PMU
10246    /// count 10, replaying `history` from its start with `config`: returns
10247    /// the thread, the priority its next slice gets, and the time it chose
10248    /// that slice at.
10249    fn replay_from_500ns(
10250        config: &Config,
10251        history: ThreadHistory,
10252    ) -> (ThreadState<()>, Option<Priority>, LogicalTime) {
10253        let mut state = ThreadState::new(DetPid::from_raw(5), config, ());
10254        state.preemption_points = Some(history.into_iter());
10255        state
10256            .thread_logical_time
10257            .add_rcbs_with_multiplier(50, RcbTimeMultiplier::ONE);
10258        state.committed_clock_value = 10;
10259        let now = state.thread_logical_time.as_nanos();
10260        let prio = state.next_timeslice(config);
10261        (state, prio, now)
10262    }
10263
10264    fn edited_replay_config() -> Config {
10265        Config {
10266            target_timeslice: nz(10_000),
10267            max_timeslice: nz(100_000),
10268            replay_edited_preemptions: true,
10269            ..Default::default()
10270        }
10271    }
10272
10273    /// With --replay-edited-preemptions, recorded slices that ended before
10274    /// the thread got to them are skipped: the first slice still ahead
10275    /// applies, with its own end, priority and RCB target, and the slice
10276    /// after it follows as recorded. Slices whose RCB target has passed, or
10277    /// that end within the post-hook's one-RCB margin, count as ended.
10278    #[test]
10279    fn an_edited_record_skips_the_slices_that_already_ended() {
10280        let config = edited_replay_config();
10281        let start = ThreadState::new(DetPid::from_raw(5), &config, ())
10282            .thread_logical_time
10283            .as_nanos();
10284        let at = |ns: u64| start + Duration::from_nanos(ns);
10285        let history = ThreadHistory::new()
10286            .with_prio_changes(vec![
10287                (at(100), DEFAULT_PRIORITY + 1),
10288                // RCB target behind, end within one RCB (10 ns) of 500 ns.
10289                (at(505), DEFAULT_PRIORITY + 2),
10290                (at(900), DEFAULT_PRIORITY + 3),
10291                (at(1300), DEFAULT_PRIORITY + 4),
10292            ])
10293            .with_preemption_rcbs(vec![4, 9, 90, 130]);
10294        let (mut state, prio, _) = replay_from_500ns(&config, history);
10295        assert_eq!(prio, Some(DEFAULT_PRIORITY + 3));
10296        assert_eq!(state.end_of_timeslice, Some(at(900)));
10297        assert_eq!(state.replay_rcb_end, Some(90));
10298
10299        state
10300            .thread_logical_time
10301            .add_rcbs_with_multiplier(40, RcbTimeMultiplier::ONE);
10302        state.committed_clock_value = 90;
10303        assert!(state.timeslice_expired());
10304        assert_eq!(state.next_timeslice(&config), Some(DEFAULT_PRIORITY + 4));
10305        assert_eq!(state.end_of_timeslice, Some(at(1300)));
10306        assert_eq!(state.replay_rcb_end, Some(130));
10307    }
10308
10309    /// Only one clock may have passed: an RCB target still ahead governs a
10310    /// slice whose end has passed, and an end still ahead (with no RCB
10311    /// target, or a passed one) governs on its own.
10312    #[test]
10313    fn an_edited_record_keeps_a_slice_one_of_whose_deadlines_is_ahead() {
10314        let config = edited_replay_config();
10315        let start = ThreadState::new(DetPid::from_raw(5), &config, ())
10316            .thread_logical_time
10317            .as_nanos();
10318        let at = |ns: u64| start + Duration::from_nanos(ns);
10319        let one_slice = |end: u64, rcbs: Option<u64>| {
10320            let history = ThreadHistory::new().with_prio_changes(vec![
10321                (at(end), DEFAULT_PRIORITY + 7),
10322                (at(5000), DEFAULT_PRIORITY),
10323            ]);
10324            match rcbs {
10325                Some(rcbs) => history.with_preemption_rcbs(vec![rcbs, 500]),
10326                None => history,
10327            }
10328        };
10329        // End passed, RCB target ahead: the target governs.
10330        let (state, prio, _) = replay_from_500ns(&config, one_slice(100, Some(20)));
10331        assert_eq!(prio, Some(DEFAULT_PRIORITY + 7));
10332        assert_eq!(state.replay_rcb_end, Some(20));
10333        assert!(!state.timeslice_expired());
10334        // RCB target passed, end ahead: the end governs, without the target.
10335        let (state, prio, _) = replay_from_500ns(&config, one_slice(800, Some(4)));
10336        assert_eq!(prio, Some(DEFAULT_PRIORITY + 7));
10337        assert_eq!(
10338            (state.end_of_timeslice, state.replay_rcb_end),
10339            (Some(at(800)), None)
10340        );
10341        // An RCB target exactly at the current count ends the slice at once,
10342        // as in any replay.
10343        let (state, prio, _) = replay_from_500ns(&config, one_slice(100, Some(10)));
10344        assert_eq!(prio, Some(DEFAULT_PRIORITY + 7));
10345        assert_eq!(state.replay_rcb_end, Some(10));
10346        assert!(state.timeslice_expired());
10347    }
10348
10349    /// When every recorded slice has ended, the thread goes on at the
10350    /// record's final priority, as after the last recorded slice.
10351    #[test]
10352    fn an_edited_record_whose_slices_all_ended_goes_on_at_its_final_priority() {
10353        let config = edited_replay_config();
10354        let start = ThreadState::new(DetPid::from_raw(5), &config, ())
10355            .thread_logical_time
10356            .as_nanos();
10357        let history = ThreadHistory::new().with_prio_changes(vec![
10358            (start + Duration::from_nanos(100), DEFAULT_PRIORITY + 1),
10359            (start + Duration::from_nanos(200), DEFAULT_PRIORITY + 2),
10360        ]);
10361        let final_priority = history.clone().into_iter().final_priority();
10362        let (state, prio, now) = replay_from_500ns(&config, history);
10363        assert_eq!(prio, Some(final_priority));
10364        // As after the last recorded slice of any replay: no recorded end,
10365        // so the configured maximum timeslice bounds it.
10366        assert_eq!(
10367            state.end_of_timeslice,
10368            Some(now + Duration::from_nanos(100_000))
10369        );
10370        assert!(state.stats.last_recorded_slice.is_some());
10371    }
10372
10373    /// The one-RCB margin follows the clock multiplier and the thread's chaos
10374    /// slowdown: at multiplier 0.3 one RCB is 3 ns, so an end 2 ns ahead is
10375    /// within it (the post-hook would end that slice at once) and one 3 ns
10376    /// ahead is not. Without a PMU maximum there is no margin.
10377    #[test]
10378    fn the_skipping_margin_is_one_rcb_at_the_thread_clock() {
10379        let config = Config {
10380            clock_multiplier: Some(0.3),
10381            ..edited_replay_config()
10382        };
10383        let state = ThreadState::new(DetPid::from_raw(5), &config, ());
10384        assert_eq!(state.pmu_epsilon(&config), Duration::from_nanos(3));
10385        let mut slowed = ThreadState::new(DetPid::from_raw(5), &config, ());
10386        slowed.chaos_slowdown_factor = RcbTimeMultiplier::from_f64(2.0);
10387        slowed.chaos_slowdown_active = true;
10388        assert_eq!(slowed.pmu_epsilon(&config), Duration::from_nanos(6));
10389
10390        let start = state.thread_logical_time.as_nanos();
10391        for (ahead, skipped) in [(2, true), (3, false)] {
10392            // 50 RCBs at multiplier 1 are 500 ns; at 0.3 the thread's time
10393            // advances by 150 ns.
10394            let mut state = ThreadState::new(DetPid::from_raw(5), &config, ());
10395            state
10396                .thread_logical_time
10397                .add_rcbs_with_multiplier(50, RcbTimeMultiplier::ONE);
10398            let now = state.thread_logical_time.as_nanos();
10399            state.preemption_points = Some(
10400                ThreadHistory::new()
10401                    .with_prio_changes(vec![
10402                        (now + Duration::from_nanos(ahead), DEFAULT_PRIORITY + 1),
10403                        (start + Duration::from_nanos(9000), DEFAULT_PRIORITY + 2),
10404                    ])
10405                    .into_iter(),
10406            );
10407            let prio = state.next_timeslice(&config);
10408            let expected = if skipped {
10409                DEFAULT_PRIORITY + 2
10410            } else {
10411                DEFAULT_PRIORITY + 1
10412            };
10413            assert_eq!(prio, Some(expected), "{ahead} ns ahead");
10414        }
10415
10416        // At a 2 times slowdown (a recorded epoch) one RCB is 6 ns: an end
10417        // 5 ns ahead is within it, one 6 ns ahead is not.
10418        for (ahead, skipped) in [(5, true), (6, false)] {
10419            let mut state = ThreadState::new(DetPid::from_raw(5), &config, ());
10420            let begin = state.thread_logical_time.as_nanos();
10421            state.preemption_points = Some(
10422                ThreadHistory::new()
10423                    .with_prio_changes(vec![
10424                        (begin + Duration::from_nanos(ahead), DEFAULT_PRIORITY + 1),
10425                        (begin + Duration::from_nanos(9000), DEFAULT_PRIORITY + 2),
10426                    ])
10427                    .with_chaos_epochs(vec![ChaosEpochTransition {
10428                        logical_time: begin,
10429                        epoch: 0,
10430                        factor: RcbTimeMultiplier::from_f64(2.0),
10431                    }])
10432                    .into_iter(),
10433            );
10434            let prio = state.next_timeslice(&config);
10435            assert_eq!(state.pmu_epsilon(&config), Duration::from_nanos(6));
10436            let expected = if skipped {
10437                DEFAULT_PRIORITY + 2
10438            } else {
10439                DEFAULT_PRIORITY + 1
10440            };
10441            assert_eq!(prio, Some(expected), "{ahead} ns ahead at 2 times");
10442        }
10443
10444        // Without a PMU maximum the post-hook has no margin: a slice is kept
10445        // whenever it ends ahead, as in a faithful replay.
10446        let unbounded = Config {
10447            max_timeslice: None,
10448            ..edited_replay_config()
10449        };
10450        for (end, expected) in [(501, DEFAULT_PRIORITY + 1), (500, DEFAULT_PRIORITY + 2)] {
10451            let history = ThreadHistory::new().with_prio_changes(vec![
10452                (start + Duration::from_nanos(end), DEFAULT_PRIORITY + 1),
10453                (start + Duration::from_nanos(9000), DEFAULT_PRIORITY + 2),
10454            ]);
10455            let (_, prio, _) = replay_from_500ns(&unbounded, history);
10456            assert_eq!(
10457                prio,
10458                Some(expected),
10459                "an end at {end} ns, without a maximum"
10460            );
10461        }
10462    }
10463
10464    /// A recorded one-RCB slice that starts exactly where the previous one
10465    /// ended is one the post-hook runs, not one that ended: on a record
10466    /// without RCB targets (as minimization writes), an edited replay keeps
10467    /// every such slice, with the same ends and priorities as a faithful one.
10468    #[test]
10469    fn an_edited_replay_keeps_one_rcb_slices_as_a_faithful_one_does() {
10470        let edited = edited_replay_config();
10471        let faithful = Config {
10472            replay_edited_preemptions: false,
10473            ..edited_replay_config()
10474        };
10475        let start = ThreadState::new(DetPid::from_raw(5), &edited, ())
10476            .thread_logical_time
10477            .as_nanos();
10478        let at = |ns: u64| start + Duration::from_nanos(ns);
10479        // At 500 ns, one RCB (10 ns) per slice, then a longer one.
10480        let history = ThreadHistory::new().with_prio_changes(vec![
10481            (at(510), DEFAULT_PRIORITY + 1),
10482            (at(520), DEFAULT_PRIORITY + 2),
10483            (at(530), DEFAULT_PRIORITY + 3),
10484            (at(900), DEFAULT_PRIORITY + 4),
10485        ]);
10486        let slices = |config: &Config| {
10487            let (mut state, prio, _) = replay_from_500ns(config, history.clone());
10488            let mut slices = vec![(prio, state.end_of_timeslice)];
10489            for _ in 0..3 {
10490                state.thread_logical_time.add_rcbs(1);
10491                let prio = state.next_timeslice(config);
10492                slices.push((prio, state.end_of_timeslice));
10493            }
10494            slices
10495        };
10496        assert_eq!(slices(&edited), slices(&faithful));
10497        assert_eq!(
10498            slices(&edited)[..3],
10499            [
10500                (Some(DEFAULT_PRIORITY + 1), Some(at(510))),
10501                (Some(DEFAULT_PRIORITY + 2), Some(at(520))),
10502                (Some(DEFAULT_PRIORITY + 3), Some(at(530))),
10503            ]
10504        );
10505    }
10506
10507    /// Past an edited record's last recorded slice, a restored chaos slowdown
10508    /// can make one RCB longer than the configured maximum timeslice: the
10509    /// slice's PMU maximum is kept at least one RCB ahead, so the post-hook
10510    /// neither ends it at once nor finds itself past it again. A faithful
10511    /// replay keeps the configured maximum.
10512    #[test]
10513    fn past_an_edited_record_the_maximum_keeps_one_rcb_at_a_restored_slowdown() {
10514        let config = |edited: bool| Config {
10515            target_timeslice: None,
10516            max_timeslice: nz(10),
10517            replay_edited_preemptions: edited,
10518            ..Default::default()
10519        };
10520        for (edited, expected) in [(true, 20), (false, 10)] {
10521            let config = config(edited);
10522            let mut state = ThreadState::new(DetPid::from_raw(5), &config, ());
10523            let now = state.thread_logical_time.as_nanos();
10524            let doubled = ChaosEpochTransition {
10525                logical_time: now,
10526                epoch: 1,
10527                factor: RcbTimeMultiplier::from_f64(2.0),
10528            };
10529            state.preemption_points = Some(
10530                ThreadHistory::new()
10531                    .with_chaos_epochs(vec![doubled])
10532                    .into_iter(),
10533            );
10534            state.next_timeslice(&config);
10535            assert_eq!(state.chaos_slowdown_factor, doubled.factor);
10536            assert_eq!(state.pmu_epsilon(&config), Duration::from_nanos(20));
10537            assert!(state.stats.last_recorded_slice.is_some());
10538            assert_eq!(
10539                state.max_timeslice_end,
10540                Some(now + Duration::from_nanos(expected)),
10541                "edited: {edited}"
10542            );
10543        }
10544    }
10545
10546    /// Without --replay-edited-preemptions, a replay that reaches a recorded
10547    /// slice end already passed stops: a faithful replay never does, so the
10548    /// replay diverged.
10549    #[test]
10550    #[should_panic(expected = "needs --replay-edited-preemptions")]
10551    fn a_faithful_replay_past_a_recorded_end_stops() {
10552        let config = Config {
10553            replay_edited_preemptions: false,
10554            ..edited_replay_config()
10555        };
10556        let start = ThreadState::new(DetPid::from_raw(5), &config, ())
10557            .thread_logical_time
10558            .as_nanos();
10559        let history = ThreadHistory::new().with_prio_changes(vec![
10560            (start + Duration::from_nanos(100), DEFAULT_PRIORITY + 1),
10561            (start + Duration::from_nanos(900), DEFAULT_PRIORITY),
10562        ]);
10563        replay_from_500ns(&config, history);
10564    }
10565
10566    /// The same for a recorded RCB deadline the PMU count has passed.
10567    #[test]
10568    #[should_panic(expected = "count is already 10. An edited record")]
10569    fn a_faithful_replay_past_a_recorded_rcb_end_stops() {
10570        let config = Config {
10571            replay_edited_preemptions: false,
10572            ..edited_replay_config()
10573        };
10574        let start = ThreadState::new(DetPid::from_raw(5), &config, ())
10575            .thread_logical_time
10576            .as_nanos();
10577        let history = ThreadHistory::new()
10578            .with_prio_changes(vec![
10579                (start + Duration::from_nanos(100), DEFAULT_PRIORITY + 1),
10580                (start + Duration::from_nanos(900), DEFAULT_PRIORITY),
10581            ])
10582            .with_preemption_rcbs(vec![4, 90]);
10583        replay_from_500ns(&config, history);
10584    }
10585
10586    // Exercise the same production selector with larger counts and both
10587    // settings of the optional skid diagnostic. A faithful replay must stop
10588    // at its first already-passed RCB end, even when later ends remain ahead.
10589    fn assert_faithful_rcb_ends_stop(panic_on_rcb_overshoot: bool) {
10590        for counts in [
10591            vec![40, 80],
10592            vec![40, 45, 80],
10593            vec![40, 51, 80],
10594            vec![40, 45],
10595        ] {
10596            let config = Config {
10597                target_timeslice: nz(10_000),
10598                max_timeslice: nz(100_000),
10599                panic_on_rcb_overshoot,
10600                ..Default::default()
10601            };
10602            let mut state = ThreadState::new(DetPid::from_raw(5), &config, ());
10603            let now = state.thread_logical_time.as_nanos();
10604            let history = ThreadHistory::new()
10605                .with_prio_changes(
10606                    (0..counts.len())
10607                        .map(|i| {
10608                            (
10609                                now + Duration::from_nanos(100 * (i as u64 + 1)),
10610                                DEFAULT_PRIORITY,
10611                            )
10612                        })
10613                        .collect(),
10614                )
10615                .with_preemption_rcbs(counts.clone());
10616            state.preemption_points = Some(history.into_iter());
10617            state.committed_clock_value = 51;
10618            let panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
10619                state.next_timeslice(&config)
10620            }))
10621            .expect_err("faithful replay must not pass a recorded RCB end");
10622            let message = panic.downcast_ref::<String>().unwrap();
10623            assert!(
10624                message.contains("Cannot set RCB end of timeslice to 40"),
10625                "{counts:?}: {message}"
10626            );
10627            assert!(
10628                message.contains("count is already 51. An edited record"),
10629                "{counts:?}: {message}"
10630            );
10631            assert!(
10632                message.contains("needs --replay-edited-preemptions"),
10633                "{message}"
10634            );
10635        }
10636    }
10637
10638    #[test]
10639    fn a_faithful_replay_past_larger_rcb_ends_stops_by_default() {
10640        assert_faithful_rcb_ends_stop(false);
10641    }
10642
10643    #[test]
10644    fn a_faithful_replay_past_larger_rcb_ends_stops_with_skid_panic_enabled() {
10645        assert_faithful_rcb_ends_stop(true);
10646    }
10647
10648    /// Edited histories retain the semantics of
10649    /// https://github.com/rrnewton/hermit/pull/4672: an equal-count slice
10650    /// ends at once, a future time-only slice is kept, and filed change
10651    /// points need not advance. The optional diagnostic must not panic.
10652    #[test]
10653    fn an_edited_record_reports_no_departure() {
10654        let config = Config {
10655            panic_on_rcb_overshoot: true,
10656            replay_preemptions_from: Some(std::path::PathBuf::from("history.json")),
10657            ..edited_replay_config()
10658        };
10659        let start = ThreadState::new(DetPid::from_raw(5), &config, ())
10660            .thread_logical_time
10661            .as_nanos();
10662        let at = |ns: u64| start + Duration::from_nanos(ns);
10663        let history = |end: u64, rcbs: u64| {
10664            ThreadHistory::new()
10665                .with_prio_changes(vec![
10666                    (at(end), DEFAULT_PRIORITY + 7),
10667                    (at(5000), DEFAULT_PRIORITY),
10668                ])
10669                .with_preemption_rcbs(vec![rcbs, 500])
10670        };
10671        let (mut state, prio, now) = replay_from_500ns(&config, history(100, 10));
10672        assert_eq!(prio, Some(DEFAULT_PRIORITY + 7));
10673        assert_eq!(
10674            (state.end_of_timeslice, state.replay_rcb_end),
10675            (Some(at(100)), Some(10))
10676        );
10677        assert!(state.timeslice_expired());
10678        assert_eq!(state.file_replay_change_point(&config, now), Ok(()));
10679        assert_eq!(state.file_replay_change_point(&config, now), Ok(()));
10680        assert_eq!(state.file_replay_change_point(&config, at(100)), Ok(()));
10681        let (state, prio, _) = replay_from_500ns(&config, history(800, 4));
10682        assert_eq!(prio, Some(DEFAULT_PRIORITY + 7));
10683        assert_eq!(
10684            (state.end_of_timeslice, state.replay_rcb_end),
10685            (Some(at(800)), None)
10686        );
10687        // Main also keeps this equal-count slice in faithful mode. Unlike
10688        // edited exploration, filing the same change point twice is refused.
10689        let faithful = Config {
10690            replay_edited_preemptions: false,
10691            ..config
10692        };
10693        let (mut state, _, now) = replay_from_500ns(&faithful, history(100, 10));
10694        assert_eq!(state.replay_rcb_end, Some(10));
10695        assert_eq!(state.file_replay_change_point(&faithful, now), Ok(()));
10696        assert_eq!(state.file_replay_change_point(&faithful, now), Err(now));
10697    }
10698
10699    /// Main admits an equal-RCB end even when its timestamp is behind.
10700    /// Preserve that instant slice in this Android-shaped history, but refuse
10701    /// filing it twice at the preceding slice's unchanged logical instant.
10702    #[test]
10703    fn a_replay_keeps_an_equal_rcb_end_and_refuses_repeated_filing() {
10704        let config = Config {
10705            target_timeslice: nz(10_000),
10706            max_timeslice: nz(100_000),
10707            replay_preemptions_from: Some("history.json".into()),
10708            ..Default::default()
10709        };
10710        let mut state = ThreadState::new(DetPid::from_raw(31754), &config, ());
10711        state.thread_logical_time.add_rcbs(863_958);
10712        state.committed_clock_value = 863_958;
10713        let now = state.thread_logical_time.as_nanos();
10714        let history = ThreadHistory::new()
10715            .with_prio_changes(vec![
10716                (
10717                    LogicalTime::from_nanos(now.as_nanos() - 1_802_680),
10718                    DEFAULT_PRIORITY + 1,
10719                ),
10720                (
10721                    LogicalTime::from_nanos(now.as_nanos() - 1_802_670),
10722                    DEFAULT_PRIORITY + 2,
10723                ),
10724                (now + Duration::from_nanos(10_780), DEFAULT_PRIORITY + 3),
10725            ])
10726            .with_preemption_rcbs(vec![863_958, 863_959, 864_037]);
10727        state.preemption_points = Some(history.into_iter());
10728
10729        assert_eq!(state.file_replay_change_point(&config, now), Ok(()));
10730        let prio = state.next_timeslice(&config);
10731        assert_eq!(prio, Some(DEFAULT_PRIORITY + 1));
10732        assert_eq!(state.replay_rcb_end, Some(863_958));
10733        assert_eq!(
10734            state.end_of_timeslice,
10735            Some(LogicalTime::from_nanos(now.as_nanos() - 1_802_680))
10736        );
10737        assert!(state.timeslice_expired());
10738        assert_eq!(state.file_replay_change_point(&config, now), Err(now));
10739        state.abandon_replayed_slices(&config);
10740        assert!(!state.timeslice_expired());
10741        assert_eq!(state.end_of_timeslice, Some(LogicalTime::MAX));
10742    }
10743
10744    /// An end exactly at the thread's count whose time is not behind it is
10745    /// the slice's end, now, as before (the record's adjacent zero-RCB
10746    /// slices, `replay_preserves_adjacent_zero_rcb_slices`).
10747    #[test]
10748    fn a_replay_follows_an_end_at_its_count_whose_time_is_ahead() {
10749        let config = Config {
10750            target_timeslice: nz(10_000),
10751            max_timeslice: nz(100_000),
10752            ..Default::default()
10753        };
10754        let mut state = ThreadState::new(DetPid::from_raw(5), &config, ());
10755        let now = state.thread_logical_time.as_nanos();
10756        let history = ThreadHistory::new()
10757            .with_prio_changes(vec![(
10758                now + Duration::from_nanos(25_000),
10759                DEFAULT_PRIORITY + 1,
10760            )])
10761            .with_preemption_rcbs(vec![0]);
10762        state.preemption_points = Some(history.into_iter());
10763
10764        assert_eq!(state.next_timeslice(&config), Some(DEFAULT_PRIORITY + 1));
10765        assert_eq!(state.replay_rcb_end, Some(0));
10766        assert_eq!(
10767            state.end_of_timeslice,
10768            Some(now + Duration::from_nanos(25_000))
10769        );
10770
10771        // At the thread's time exactly, too.
10772        let mut state = ThreadState::new(DetPid::from_raw(6), &config, ());
10773        let now = state.thread_logical_time.as_nanos();
10774        let history = ThreadHistory::new()
10775            .with_prio_changes(vec![(now, DEFAULT_PRIORITY + 1)])
10776            .with_preemption_rcbs(vec![0]);
10777        state.preemption_points = Some(history.into_iter());
10778        assert_eq!(state.next_timeslice(&config), Some(DEFAULT_PRIORITY + 1));
10779        assert_eq!(state.replay_rcb_end, Some(0));
10780        assert_eq!(state.end_of_timeslice, Some(now));
10781    }
10782
10783    /// The shape of a stalled Android emulator replay (`--chaos
10784    /// --preemption-timeout=1000`): a slice ended at the branch count of the
10785    /// next recorded end, whose time was still ahead, so that end was
10786    /// followed and its slice was over at once (adjacent zero-RCB slices);
10787    /// but the thread's time had not moved, so the second change point would
10788    /// have been filed at the instant of the first. It is caught before it
10789    /// is filed, and the thread runs on as its final slice, not already over.
10790    #[test]
10791    fn a_replay_change_point_at_the_last_ones_instant_is_caught_before_filing() {
10792        let config = Config {
10793            target_timeslice: nz(10_000),
10794            max_timeslice: nz(100_000),
10795            replay_preemptions_from: Some(std::path::PathBuf::from("history.json")),
10796            ..Default::default()
10797        };
10798        let mut state = ThreadState::new(DetPid::from_raw(31744), &config, ());
10799        let start = state.thread_logical_time.as_nanos();
10800        let history = ThreadHistory::new()
10801            .with_prio_changes(vec![
10802                (start + Duration::from_nanos(70), DEFAULT_PRIORITY + 1),
10803                (start + Duration::from_nanos(80), DEFAULT_PRIORITY + 2),
10804                (start + Duration::from_nanos(500), DEFAULT_PRIORITY + 3),
10805            ])
10806            .with_preemption_rcbs(vec![4, 4, 40]);
10807        state.preemption_points = Some(history.into_iter());
10808
10809        state.next_timeslice(&config);
10810        state.thread_logical_time.add_rcbs(4);
10811        state.committed_clock_value = 4;
10812        assert!(state.timeslice_expired());
10813        let now = state.thread_logical_time.as_nanos();
10814        assert_eq!(state.file_replay_change_point(&config, now), Ok(()));
10815
10816        // The end at the same count, 10 ns later, is followed (its time is
10817        // ahead), and its slice is over at once; the thread's time is still
10818        // `now`.
10819        state.next_timeslice(&config);
10820        assert_eq!(state.replay_rcb_end, Some(4));
10821        assert!(state.timeslice_expired());
10822        assert_eq!(state.file_replay_change_point(&config, now), Err(now));
10823
10824        state.abandon_replayed_slices(&config);
10825        assert!(!state.timeslice_expired());
10826        assert_eq!(state.replay_rcb_end, None);
10827        assert_eq!(state.end_of_timeslice, Some(LogicalTime::MAX));
10828        assert_eq!(
10829            state.max_timeslice_end,
10830            Some(now + Duration::from_nanos(100_000))
10831        );
10832
10833        // A later change point files as usual; outside a preemption replay
10834        // nothing is checked.
10835        let later = now + Duration::from_nanos(1);
10836        assert_eq!(state.file_replay_change_point(&config, later), Ok(()));
10837        let recording = Config {
10838            replay_preemptions_from: None,
10839            ..config
10840        };
10841        assert_eq!(state.file_replay_change_point(&recording, later), Ok(()));
10842    }
10843
10844    // AUTONOMOUS-BOT-IMPLEMENTED
10845    // TODO-HUMAN-REVIEW(PR-1151)
10846    #[test]
10847    fn replay_preserves_adjacent_zero_rcb_slices() {
10848        let config = Config {
10849            target_timeslice: nz(10_000),
10850            max_timeslice: nz(100_000),
10851            ..Default::default()
10852        };
10853        let mut state = ThreadState::new(DetPid::from_raw(5), &config, ());
10854        let now = state.thread_logical_time.as_nanos();
10855        let history = ThreadHistory::new()
10856            .with_prio_changes(vec![
10857                (now + Duration::from_nanos(70), DEFAULT_PRIORITY),
10858                (now + Duration::from_nanos(80), DEFAULT_PRIORITY),
10859            ])
10860            .with_preemption_rcbs(vec![4, 4]);
10861        state.preemption_points = Some(history.into_iter());
10862
10863        state.next_timeslice(&config);
10864        state.thread_logical_time.add_rcbs(4);
10865        state.committed_clock_value = 4;
10866        assert!(state.timeslice_expired());
10867
10868        state.next_timeslice(&config);
10869        assert_eq!(state.replay_rcb_end, Some(4));
10870        assert!(state.timeslice_expired());
10871    }
10872
10873    #[test]
10874    fn target_and_pmu_deadlines_are_independent() {
10875        let config = Config {
10876            target_timeslice: nz(20_000),
10877            max_timeslice: nz(100_000),
10878            ..Default::default()
10879        };
10880        let mut state = ThreadState::new(DetPid::from_raw(1), &config, ());
10881        let now = state.thread_logical_time.as_nanos();
10882
10883        state.next_timeslice(&config);
10884
10885        assert_eq!(
10886            state.end_of_timeslice,
10887            Some(now + Duration::from_nanos(20_000))
10888        );
10889        assert_eq!(
10890            state.max_timeslice_end,
10891            Some(now + Duration::from_nanos(100_000))
10892        );
10893    }
10894
10895    #[test]
10896    fn target_only_mode_does_not_create_a_pmu_deadline() {
10897        let config = Config {
10898            target_timeslice: nz(20_000),
10899            max_timeslice: None,
10900            ..Default::default()
10901        };
10902        let mut state = ThreadState::new(DetPid::from_raw(1), &config, ());
10903        let now = state.thread_logical_time.as_nanos();
10904
10905        state.next_timeslice(&config);
10906
10907        assert_eq!(
10908            state.end_of_timeslice,
10909            Some(now + Duration::from_nanos(20_000))
10910        );
10911        assert_eq!(state.max_timeslice_end, None);
10912    }
10913
10914    #[test]
10915    fn max_timeslice_caps_a_larger_target() {
10916        let config = Config {
10917            target_timeslice: nz(100_000),
10918            max_timeslice: nz(20_000),
10919            ..Default::default()
10920        };
10921        let mut state = ThreadState::new(DetPid::from_raw(1), &config, ());
10922        let now = state.thread_logical_time.as_nanos();
10923
10924        state.next_timeslice(&config);
10925
10926        let max_end = now + Duration::from_nanos(20_000);
10927        assert_eq!(state.end_of_timeslice, Some(max_end));
10928        assert_eq!(state.max_timeslice_end, Some(max_end));
10929    }
10930
10931    #[test]
10932    fn chaos_without_target_caps_randomized_deadline_at_maximum() {
10933        let config = Config {
10934            chaos: true,
10935            target_timeslice: None,
10936            max_timeslice: nz(100_000),
10937            clock_multiplier: Some(1.05),
10938            ..Default::default()
10939        };
10940        let mut state = ThreadState::new(DetPid::from_raw(1), &config, ());
10941        let now = state.thread_logical_time.as_nanos();
10942        let configured_max = now + Duration::from_nanos(100_000);
10943        let minimum_progress = now + Duration::from_nanos(11);
10944
10945        state.next_timeslice(&config);
10946
10947        assert_eq!(state.max_timeslice_end, state.end_of_timeslice);
10948        assert!(state.max_timeslice_end.unwrap() <= configured_max);
10949        assert!(state.max_timeslice_end.unwrap() >= minimum_progress);
10950    }
10951
10952    /// Under `--chaos-timeslice-range` every slice ends within one RCB and
10953    /// MAX of its start, and the slices span the range's scales.
10954    #[test]
10955    fn a_chaos_timeslice_range_draws_slices_of_every_scale_up_to_its_max() {
10956        let config = Config {
10957            chaos: true,
10958            max_timeslice: nz(100_000_000),
10959            chaos_timeslice_range: Some("1000..100000000".parse().unwrap()),
10960            ..Default::default()
10961        };
10962        let mut state = ThreadState::new(DetPid::from_raw(1), &config, ());
10963        let now = state.thread_logical_time.as_nanos();
10964        let lengths: Vec<u64> = (0..3000)
10965            .map(|_| {
10966                state.next_timeslice(&config);
10967                let end = state.end_of_timeslice.unwrap();
10968                assert_eq!(state.max_timeslice_end, Some(end));
10969                (end - now).as_nanos()
10970            })
10971            .collect();
10972        assert!(
10973            lengths
10974                .iter()
10975                .all(|length| (10..=100_000_000).contains(length)),
10976            "{:?}",
10977            lengths.iter().max()
10978        );
10979        assert!(lengths.iter().filter(|length| **length < 10_000).count() > 100);
10980        assert!(
10981            lengths
10982                .iter()
10983                .filter(|length| **length > 10_000_000)
10984                .count()
10985                > 100
10986        );
10987    }
10988
10989    /// A schedule replay draws no slice mean from the range: its deadlines
10990    /// and generator state are those of the same replay without one.
10991    #[test]
10992    fn a_schedule_replay_draws_nothing_from_the_chaos_timeslice_range() {
10993        let plain = Config {
10994            chaos: true,
10995            max_timeslice: nz(100_000),
10996            replay_schedule_from: Some(std::path::PathBuf::from("schedule.json")),
10997            ..Default::default()
10998        };
10999        let ranged = Config {
11000            chaos_timeslice_range: Some("1000..100000".parse().unwrap()),
11001            ..plain.clone()
11002        };
11003        let mut without = ThreadState::new(DetPid::from_raw(1), &plain, ());
11004        let mut with = ThreadState::new(DetPid::from_raw(1), &ranged, ());
11005        for _ in 0..16 {
11006            without.next_timeslice(&plain);
11007            with.next_timeslice(&ranged);
11008            assert_eq!(with.end_of_timeslice, without.end_of_timeslice);
11009            assert_eq!(with.max_timeslice_end, without.max_timeslice_end);
11010        }
11011        assert!(with.chaos_prng == without.chaos_prng);
11012    }
11013
11014    #[test]
11015    fn schedule_replay_without_rcb_time_arms_pmu_maximum() {
11016        let config = Config {
11017            no_rcb_time: true,
11018            max_timeslice: nz(100_000),
11019            replay_schedule_from: Some(std::path::PathBuf::from("schedule.json")),
11020            ..Default::default()
11021        };
11022        let mut state = ThreadState::new(DetPid::from_raw(1), &config, ());
11023        let now = state.thread_logical_time.as_nanos();
11024
11025        state.next_timeslice(&config);
11026
11027        let expected = now + Duration::from_nanos(100_000);
11028        assert_eq!(state.end_of_timeslice, Some(expected));
11029        assert_eq!(state.max_timeslice_end, Some(expected));
11030    }
11031
11032    #[test]
11033    fn exhausted_preemption_replay_uses_bounded_pmu_maximum() {
11034        let config = Config {
11035            max_timeslice: nz(100_000),
11036            ..Default::default()
11037        };
11038        let mut state = ThreadState::new(DetPid::from_raw(3), &config, ());
11039        state.preemption_points = Some(ThreadHistory::new().into_iter());
11040        let now = state.thread_logical_time.as_nanos();
11041
11042        state.next_timeslice(&config);
11043
11044        let bounded_end = Some(now + Duration::from_nanos(100_000));
11045        assert_eq!(state.end_of_timeslice, bounded_end);
11046        assert_eq!(state.max_timeslice_end, bounded_end);
11047    }
11048
11049    /// A preemption replay keeps a recorded slice end uncapped. Once the
11050    /// recording runs out its slice end is `LogicalTime::MAX`, which the cap
11051    /// may lower.
11052    #[test]
11053    fn preemption_replay_caps_only_past_its_recording() {
11054        let config = Config {
11055            max_timeslice: nz(100_000),
11056            ..Default::default()
11057        };
11058        let mut state = ThreadState::new(DetPid::from_raw(3), &config, ());
11059        let now = state.thread_logical_time.as_nanos();
11060        let recorded_end = now + Duration::from_nanos(50_000);
11061        let history = ThreadHistory::new()
11062            .with_prio_changes(vec![(recorded_end, DEFAULT_PRIORITY)])
11063            .with_preemption_rcbs(vec![5_000]);
11064        state.preemption_points = Some(history.into_iter());
11065
11066        state.next_timeslice(&config);
11067        assert_eq!(state.end_of_timeslice, Some(recorded_end));
11068        state.cap_timeslice_at_deadline(Duration::from_nanos(1_000), 1.0, Duration::ZERO, false);
11069        assert_eq!(state.end_of_timeslice, Some(recorded_end));
11070
11071        let mut exhausted = ThreadState::new(DetPid::from_raw(3), &config, ());
11072        exhausted.preemption_points = Some(ThreadHistory::new().into_iter());
11073        exhausted.next_timeslice(&config);
11074        exhausted.cap_timeslice_at_deadline(
11075            Duration::from_nanos(1_000),
11076            1.0,
11077            Duration::ZERO,
11078            false,
11079        );
11080        let capped = Some(now + Duration::from_nanos(1_000));
11081        assert_eq!(exhausted.end_of_timeslice, capped);
11082        assert_eq!(
11083            exhausted.max_timeslice_end,
11084            Some(now + Duration::from_nanos(100_000))
11085        );
11086    }
11087
11088    #[test]
11089    fn timeslice_expiry_is_inclusive() {
11090        let config = Config::default();
11091        let mut state = ThreadState::new(DetPid::from_raw(1), &config, ());
11092        let now = state.thread_logical_time.as_nanos();
11093
11094        state.end_of_timeslice = Some(now + Duration::from_nanos(1));
11095        assert!(!state.timeslice_expired());
11096        state.end_of_timeslice = Some(now);
11097        assert!(state.timeslice_expired());
11098    }
11099
11100    #[test]
11101    fn instruction_trap_ends_an_expired_target_only_at_the_pmu_maximum() {
11102        let config = Config {
11103            target_timeslice: nz(1_000),
11104            target_timeslice_syscalls_only: true,
11105            max_timeslice: nz(20_000),
11106            ..Default::default()
11107        };
11108        let mut state = ThreadState::new(DetPid::from_raw(1), &config, ());
11109        state.next_timeslice(&config);
11110
11111        // Past the target: a syscall would end the slice, an rdtsc must not.
11112        state.thread_logical_time.add_rcbs(500);
11113        assert!(state.timeslice_expired());
11114        assert!(!state.instruction_trap_ends_timeslice(&config));
11115
11116        // At the PMU maximum the trap ends it, as a branch would.
11117        state.thread_logical_time.add_rcbs(1_500);
11118        assert!(state.instruction_trap_ends_timeslice(&config));
11119    }
11120
11121    #[test]
11122    fn instruction_trap_ends_an_expired_target_by_default() {
11123        let config = Config {
11124            target_timeslice: nz(1_000),
11125            max_timeslice: nz(20_000),
11126            ..Default::default()
11127        };
11128        assert!(!config.target_timeslice_syscalls_only);
11129        let mut state = ThreadState::new(DetPid::from_raw(1), &config, ());
11130        state.next_timeslice(&config);
11131        state.thread_logical_time.add_rcbs(99);
11132        assert!(!state.instruction_trap_ends_timeslice(&config));
11133        state.thread_logical_time.add_rcbs(1);
11134        assert!(state.instruction_trap_ends_timeslice(&config));
11135    }
11136
11137    #[test]
11138    fn instruction_trap_ends_an_expired_target_without_a_pmu_maximum() {
11139        // With PMU preemption disabled nothing else can break a busy-wait on
11140        // the TSC, so the trap keeps ending the expired target.
11141        let config = Config {
11142            target_timeslice: nz(1_000),
11143            target_timeslice_syscalls_only: true,
11144            max_timeslice: None,
11145            ..Default::default()
11146        };
11147        let mut state = ThreadState::new(DetPid::from_raw(1), &config, ());
11148        state.next_timeslice(&config);
11149        assert_eq!(state.max_timeslice_end, None);
11150
11151        // 1,000 ns is 100 RCBs at the default 10 ns per RCB.
11152        state.thread_logical_time.add_rcbs(99);
11153        assert!(!state.instruction_trap_ends_timeslice(&config));
11154        state.thread_logical_time.add_rcbs(1);
11155        assert!(state.instruction_trap_ends_timeslice(&config));
11156    }
11157
11158    #[test]
11159    fn instruction_trap_keeps_replayed_and_untargeted_boundaries() {
11160        // Without a target the slice end is the PMU maximum itself, and a
11161        // replayed preemption record keeps its recorded boundaries: in both
11162        // cases a trap ends exactly the slices it ended before.
11163        let untargeted = Config {
11164            max_timeslice: nz(20_000),
11165            ..Default::default()
11166        };
11167        let mut state = ThreadState::new(DetPid::from_raw(1), &untargeted, ());
11168        state.next_timeslice(&untargeted);
11169        state.thread_logical_time.add_rcbs(1_999);
11170        assert_eq!(
11171            state.instruction_trap_ends_timeslice(&untargeted),
11172            state.timeslice_expired()
11173        );
11174        state.thread_logical_time.add_rcbs(1);
11175        assert!(state.instruction_trap_ends_timeslice(&untargeted));
11176
11177        let targeted = Config {
11178            target_timeslice: nz(1_000),
11179            target_timeslice_syscalls_only: true,
11180            max_timeslice: nz(20_000),
11181            ..Default::default()
11182        };
11183        let mut state = ThreadState::new(DetPid::from_raw(1), &targeted, ());
11184        state.next_timeslice(&targeted);
11185        state.preemption_points = Some(ThreadHistory::new().into_iter());
11186        state.thread_logical_time.add_rcbs(500);
11187        assert!(state.timeslice_expired());
11188        assert!(state.instruction_trap_ends_timeslice(&targeted));
11189    }
11190
11191    #[test]
11192    fn child_rng_distinguishes_adjacent_thread_ids() {
11193        let parent = Pcg64Mcg::seed_from_u64(0);
11194        let mut even = thread_rng_from_parent("test", &parent, DetTid::from_raw(8));
11195        let mut odd = thread_rng_from_parent("test", &parent, DetTid::from_raw(9));
11196
11197        let even_values: [u64; 4] = std::array::from_fn(|_| even.next_u64());
11198        let odd_values: [u64; 4] = std::array::from_fn(|_| odd.next_u64());
11199        assert_ne!(even_values, odd_values);
11200    }
11201
11202    #[test]
11203    fn child_rng_uses_high_entropy_bits() {
11204        let parent = Pcg64Mcg::seed_from_u64(0);
11205        let mut low = thread_rng_from_parent_entropy("test", &parent, 1);
11206        let mut high = thread_rng_from_parent_entropy("test", &parent, (1_u128 << 64) | 1);
11207
11208        let low_values: [u64; 4] = std::array::from_fn(|_| low.next_u64());
11209        let high_values: [u64; 4] = std::array::from_fn(|_| high.next_u64());
11210        assert_ne!(low_values, high_values);
11211    }
11212
11213    #[test]
11214    fn child_rng_pedigree_uses_the_full_unbounded_path() {
11215        let parent_rng = Pcg64Mcg::seed_from_u64(0);
11216        let mut long_path = Pedigree::new();
11217        for index in 0..2_048 {
11218            let (parent, child) = long_path.fork();
11219            long_path = if index % 2 == 0 { child } else { parent };
11220        }
11221        let (left, right) = long_path.fork();
11222        let mut left_rng =
11223            thread_rng_from_parent_pedigree("test", &parent_rng, &left, ChildRngStream::User);
11224        let mut right_rng =
11225            thread_rng_from_parent_pedigree("test", &parent_rng, &right, ChildRngStream::User);
11226
11227        let left_values: [u64; 4] = std::array::from_fn(|_| left_rng.next_u64());
11228        let right_values: [u64; 4] = std::array::from_fn(|_| right_rng.next_u64());
11229        assert_ne!(left_values, right_values);
11230    }
11231
11232    #[test]
11233    fn child_rng_pedigree_separates_user_and_chaos_streams() {
11234        let parent_rng = Pcg64Mcg::seed_from_u64(0);
11235        let (_, child) = Pedigree::new().fork();
11236        let mut user_rng = thread_rng_from_parent_pedigree(
11237            "same logging label",
11238            &parent_rng,
11239            &child,
11240            ChildRngStream::User,
11241        );
11242        let mut chaos_rng = thread_rng_from_parent_pedigree(
11243            "same logging label",
11244            &parent_rng,
11245            &child,
11246            ChildRngStream::Chaos,
11247        );
11248
11249        let user_values: [u64; 4] = std::array::from_fn(|_| user_rng.next_u64());
11250        let chaos_values: [u64; 4] = std::array::from_fn(|_| chaos_rng.next_u64());
11251        assert_ne!(user_values, chaos_values);
11252    }
11253}
11254
11255/// Generate a new thread-local PRNG from the parent's PRNG state, mixing in the
11256/// new DetTid for some deterministic entropy. This ensures sequentially-spawned
11257/// threads get distinct PRNG states.
11258// TODO-HUMAN-REVIEW(PR-1052): Review collision-free child-thread PRNG seeding.
11259pub fn thread_rng_from_parent(msg: &str, parent: &Pcg64Mcg, child: DetTid) -> Pcg64Mcg {
11260    thread_rng_from_parent_entropy_labeled(msg, parent, child.as_raw() as u32 as u128, "tid")
11261}
11262
11263#[derive(Clone, Copy, Debug)]
11264pub(crate) enum ChildRngStream {
11265    User,
11266    Chaos,
11267}
11268
11269impl ChildRngStream {
11270    fn domain(self) -> &'static [u8] {
11271        match self {
11272            Self::User => b"user",
11273            Self::Chaos => b"chaos",
11274        }
11275    }
11276}
11277
11278// TODO-HUMAN-REVIEW(PR-1052): Review pedigree-derived child-thread PRNG seeding.
11279pub(crate) fn thread_rng_from_parent_pedigree(
11280    msg: &str,
11281    parent: &Pcg64Mcg,
11282    child: &Pedigree,
11283    stream: ChildRngStream,
11284) -> Pcg64Mcg {
11285    let bits = child.raw();
11286    let mut packed = Vec::with_capacity(bits.len().div_ceil(8));
11287    let mut byte = 0_u8;
11288    for (index, bit) in bits.iter().enumerate() {
11289        if *bit {
11290            byte |= 1 << (index % 8);
11291        }
11292        if index % 8 == 7 {
11293            packed.push(byte);
11294            byte = 0;
11295        }
11296    }
11297    if !bits.len().is_multiple_of(8) {
11298        packed.push(byte);
11299    }
11300
11301    // Pedigrees are unbounded, while Pcg64Mcg has a 128-bit state. Hash the
11302    // complete, length-delimited path instead of using the fallible compressed
11303    // virtual-PID encoding, so child RNG identity has no artificial tree-depth
11304    // boundary. The domain string prevents this digest from being confused
11305    // with another future use of the same pedigree serialization. Pcg64Mcg
11306    // forces its low state bit, so this retains 127 effective digest bits.
11307    let mut hasher = Sha256::new();
11308    hasher.update(b"hermit-child-rng-pedigree-v1\0");
11309    hasher.update(stream.domain());
11310    hasher.update([0]);
11311    hasher.update((bits.len() as u64).to_le_bytes());
11312    hasher.update(&packed);
11313    let digest = hasher.finalize();
11314
11315    let mut seed = <Pcg64Mcg as SeedableRng>::Seed::default();
11316    parent.clone().fill_bytes(seed.as_mut());
11317    for (seed_byte, pedigree_byte) in seed.iter_mut().zip(digest) {
11318        *seed_byte ^= pedigree_byte;
11319    }
11320    detlog!(
11321        "RNG {} seeding child {:?} pedigree {}: {:?} from parent {:?}",
11322        msg,
11323        stream,
11324        child,
11325        seed,
11326        parent
11327    );
11328    let mut rng = Pcg64Mcg::from_seed(seed);
11329    rng.next_u64();
11330    rng.next_u64();
11331    rng.next_u64();
11332    rng.next_u64();
11333    rng
11334}
11335
11336#[cfg(test)]
11337mod native_exec_cut_tests {
11338    use super::*;
11339    use crate::native_exec::NativeExecCutError;
11340    use crate::native_exec::capture_prepared_native_exec;
11341    use crate::native_exec::tests::fixture;
11342    use crate::native_exec::tests::prepare;
11343    use crate::native_exec::tests::state_bytes;
11344    use crate::native_exec::validate_old_native_exec_cut;
11345
11346    #[test]
11347    fn native_exec_child_publications_refuse_every_runtime_token_state() {
11348        for completion in [None, Some(true), Some(false)] {
11349            let (tool, thread, binding) = fixture();
11350            let before = state_bytes(&thread);
11351            let child = DetPid::from_raw(43);
11352            let token = thread.prepare_child_cpu_publication(child);
11353            if let Some(published) = completion {
11354                token.finish(published);
11355            }
11356            // The map is genuinely invisible to serde in all three states.
11357            assert_eq!(state_bytes(&thread), before);
11358            assert!(matches!(
11359                validate_old_native_exec_cut(&tool, &thread, &binding),
11360                Err(NativeExecCutError::LiveCapability("child_cpu_publications"))
11361            ));
11362            assert!(Arc::ptr_eq(
11363                thread
11364                    .process_cpu_time
11365                    .lock()
11366                    .unwrap()
11367                    .child_cpu_publications
11368                    .get(&child)
11369                    .unwrap(),
11370                &token
11371            ));
11372        }
11373    }
11374
11375    #[test]
11376    fn native_exec_historical_cpu_accounting_survives_without_live_publications() {
11377        let (tool, mut thread, binding) = fixture();
11378        {
11379            let mut cpu = thread.process_cpu_time.lock().unwrap();
11380            cpu.add_thread_delta(LogicalTime::from_nanos(17), LogicalTime::from_nanos(31));
11381            cpu.record_exited_child(
11382                DetPid::from_raw(43),
11383                ProcessCpuSnapshot {
11384                    user: LogicalTime::from_nanos(19),
11385                    system: LogicalTime::from_nanos(23),
11386                    children_user: LogicalTime::from_nanos(29),
11387                    children_system: LogicalTime::from_nanos(37),
11388                },
11389            );
11390            cpu.reap_child(DetPid::from_raw(43));
11391            cpu.record_exited_child(
11392                DetPid::from_raw(44),
11393                ProcessCpuSnapshot {
11394                    user: LogicalTime::from_nanos(41),
11395                    system: LogicalTime::from_nanos(47),
11396                    ..ProcessCpuSnapshot::default()
11397                },
11398            );
11399        }
11400        let before = serde_json::to_value(&*thread.process_cpu_time.lock().unwrap()).unwrap();
11401        let receipt = validate_old_native_exec_cut(&tool, &thread, &binding).unwrap();
11402        prepare(&mut thread, &binding);
11403        let bytes = bincode::serde::encode_to_vec(
11404            capture_prepared_native_exec(&tool, &thread, &binding, &receipt).unwrap(),
11405            bincode::config::legacy(),
11406        )
11407        .unwrap();
11408        let (payload, used): (crate::native_exec::DetcoreExecSnapshotV2, _) =
11409            bincode::serde::decode_from_slice(&bytes, bincode::config::legacy()).unwrap();
11410        assert_eq!(used, bytes.len());
11411        let (_, restored) = payload.into_validated_parts(&binding, &tool.cfg).unwrap();
11412        assert_eq!(
11413            serde_json::to_value(&*restored.process_cpu_time.lock().unwrap()).unwrap(),
11414            before
11415        );
11416        let cpu = restored.process_cpu_time.lock().unwrap();
11417        assert_eq!(cpu.snapshot.children_user, LogicalTime::from_nanos(48));
11418        assert_eq!(cpu.snapshot.children_system, LogicalTime::from_nanos(60));
11419        assert_eq!(cpu.exited_children.len(), 1);
11420        assert!(cpu.child_cpu_publications.is_empty());
11421    }
11422
11423    #[test]
11424    fn native_exec_old_accept_waits_and_invalid_table_slots_refuse() {
11425        let (tool, thread, binding) = fixture();
11426        let operation = {
11427            let mut table = thread.file_metadata.lock().unwrap();
11428            table
11429                .add_fd(binding.dettid, 33, OFlag::O_CLOEXEC, FdType::Socket, None)
11430                .unwrap();
11431            table.add_accept_wait(33)
11432        };
11433        assert!(matches!(
11434            validate_old_native_exec_cut(&tool, &thread, &binding),
11435            Err(NativeExecCutError::LiveCapability("accept_waits"))
11436        ));
11437        let mut table = thread.file_metadata.lock().unwrap();
11438        assert!(table.for_exec(binding.dettid).accept_waits.is_empty());
11439        assert_eq!(table.accept_waits, [(operation, 33)]);
11440        table.remove_accept_wait(operation);
11441        // An old operation counter is historical, but the existing prepared
11442        // table has a fresh counter. Neither validation path resets it.
11443        assert_eq!(table.next_accept_wait, operation + 1);
11444        table
11445            .validate_native_exec_cut(binding.old_files, false)
11446            .unwrap();
11447        let mut prepared = table.for_exec(binding.dettid);
11448        prepared
11449            .validate_native_exec_cut(binding.prepared_files(), true)
11450            .unwrap();
11451        prepared.next_accept_wait = 1;
11452        assert_eq!(
11453            prepared.validate_native_exec_cut(binding.prepared_files(), true),
11454            Err(NativeExecCutError::PreparedState("next_accept_wait"))
11455        );
11456        let only = table.file_handles.remove(&33).unwrap();
11457        table.file_handles.insert(34, only);
11458        assert_eq!(
11459            table.validate_native_exec_cut(binding.old_files, false),
11460            Err(NativeExecCutError::Descriptor("slot number"))
11461        );
11462        table.file_handles.clear();
11463        table.file_handles.insert(
11464            -1,
11465            DetFd::new(
11466                -1,
11467                OFlag::empty(),
11468                FdType::Rng,
11469                OpenFileId::new(binding.dettid, 1),
11470            ),
11471        );
11472        assert_eq!(
11473            table.validate_native_exec_cut(binding.old_files, false),
11474            Err(NativeExecCutError::Descriptor("slot number"))
11475        );
11476    }
11477
11478    #[test]
11479    fn native_exec_each_skipped_or_live_thread_transient_refuses() {
11480        for field in 0..26 {
11481            let (tool, mut thread, binding) = fixture();
11482            match field {
11483                0 => {
11484                    thread.parent_process_cpu_time =
11485                        Some(Arc::new(Mutex::new(ProcessCpuTime::default())))
11486                }
11487                1 => thread.parent_cpu_publication = Some(ChildCpuPublication::pending()),
11488                2 => {
11489                    thread.restart_block = Some(crate::syscalls::helpers::RestartBlock {
11490                        rip: 0x1234,
11491                        deadline: None,
11492                        call: crate::syscalls::helpers::RestartCall::Poll(
11493                            reverie::syscalls::Poll::new(),
11494                        ),
11495                    })
11496                }
11497                3 => thread.signal_register_projection = Some(unsafe { std::mem::zeroed() }),
11498                4 => thread.clone_flags = Some(CloneFlags::empty()),
11499                5 => {
11500                    thread.pending_vfork = Some(PendingVfork {
11501                        parent_dettid: binding.dettid,
11502                        op: ExternalOpId::new(binding.dettid, 0),
11503                        parent_detpid: binding.detpid,
11504                        child_tid_addr: 0,
11505                        flags: CloneFlags::CLONE_VFORK,
11506                        exit_signal: libc::SIGCHLD,
11507                        child_priority_entropy: None,
11508                        user_namespace_owner_may_differ: false,
11509                    })
11510                }
11511                6 => thread.own_signal_turn_hold = Some(libc::SIGALRM),
11512                7 => thread.sigalrm_handled = true,
11513                8 => thread.handler_restore_rt_reported = 1,
11514                9 => {
11515                    thread.initialized_random_auxv = Some((
11516                        crate::random::InitialImage {
11517                            pid: binding.physical_tid,
11518                            start_time_ticks: 1,
11519                            at_random: 0x234000,
11520                        },
11521                        [7; 16],
11522                        Vec::new(),
11523                    ))
11524                }
11525                10 => thread.uncharged_bootstrap_syscalls = 1,
11526                11 => thread.in_uncharged_bootstrap_syscall = true,
11527                12 => thread.yield_after_futex_wake = true,
11528                13 => {
11529                    thread.interrupt_at.insert(1);
11530                }
11531                14 => thread.replay_rcb_end = Some(1),
11532                15 => thread.last_rcb_timer = Some(1),
11533                16 => thread.last_rcb_timer_is_max = true,
11534                17 => thread.max_timeslice_end = Some(LogicalTime::from_nanos(1)),
11535                18 => thread.chaos_slowdown_active = true,
11536                19 => thread.nested_pid_namespace = true,
11537                20 => thread.thread_start_entered = false,
11538                21 => thread.past_global_first_execve = false,
11539                22 => {
11540                    thread.last_accounted_system_time =
11541                        thread.thread_logical_time.system_cpu_time() + Duration::from_nanos(1)
11542                }
11543                23 => {
11544                    thread.preemption_points =
11545                        Some(crate::preemptions::ThreadHistory::new().into_iter())
11546                }
11547                24 => thread.pending_chaos_epochs.push(ChaosEpochTransition {
11548                    logical_time: LogicalTime::from_nanos(1),
11549                    epoch: 1,
11550                    factor: RcbTimeMultiplier::from_f64(1.5),
11551                }),
11552                25 => {
11553                    thread.signal_task_identity = Some(reverie::SignalTaskIdentity {
11554                        process: reverie::SignalProcessId {
11555                            tgid: reverie::Pid::from_raw(binding.physical_tid),
11556                            generation: 0,
11557                        },
11558                        tid: reverie::Pid::from_raw(binding.physical_tid),
11559                        task_generation: 0,
11560                    })
11561                }
11562                _ => unreachable!(),
11563            }
11564            assert!(
11565                validate_old_native_exec_cut(&tool, &thread, &binding).is_err(),
11566                "field {field}"
11567            );
11568        }
11569    }
11570
11571    #[test]
11572    fn native_exec_robust_and_prepared_memory_must_match_the_actual_cut() {
11573        for field in 0..4 {
11574            let (tool, mut thread, binding) = fixture();
11575            thread.record_robust_list_head(Some(0x1000));
11576            {
11577                let mut robust = thread.robust_list_process.lock().unwrap();
11578                match field {
11579                    0 => {
11580                        robust.heads.insert(DetTid::from_raw(43), 0x2000);
11581                    }
11582                    1 => {
11583                        robust.pending.insert(
11584                            binding.dettid,
11585                            PendingRobustListWakes {
11586                                reason: RobustListExit::ExitGroup,
11587                                wakes: Vec::new(),
11588                            },
11589                        );
11590                    }
11591                    2 => {
11592                        robust
11593                            .matched_physical_exits
11594                            .insert(binding.dettid, thread.thread_logical_time.clone());
11595                    }
11596                    3 => robust.exec_pending.push((
11597                        binding.dettid,
11598                        RobustListWake {
11599                            futex: FutexID::private(binding.old_mm, 0x3000),
11600                        },
11601                    )),
11602                    _ => unreachable!(),
11603                }
11604            }
11605            assert!(
11606                validate_old_native_exec_cut(&tool, &thread, &binding).is_err(),
11607                "old field {field}"
11608            );
11609        }
11610        for field in 0..8 {
11611            let (tool, mut thread, binding) = fixture();
11612            let receipt = validate_old_native_exec_cut(&tool, &thread, &binding).unwrap();
11613            prepare(&mut thread, &binding);
11614            match field {
11615                0 => thread.memory_metadata.lock().unwrap().observe_brk(0x4000),
11616                1 => thread.memory_metadata.lock().unwrap().map_anonymous(
11617                    binding.prepared_mm(),
11618                    0x5000,
11619                    0x1000,
11620                ),
11621                2 => thread.record_robust_list_head(Some(0x1000)),
11622                3 => thread.stage_robust_list_wakes_for_exec(vec![(
11623                    binding.dettid,
11624                    vec![RobustListWake {
11625                        futex: FutexID::private(binding.prepared_mm(), 0x3000),
11626                    }],
11627                )]),
11628                4 => thread.stage_robust_list_wakes_for_exec(vec![(
11629                    DetTid::from_raw(43),
11630                    vec![RobustListWake {
11631                        futex: FutexID::private(binding.old_mm, 0x3000),
11632                    }],
11633                )]),
11634                5 => thread.file_metadata.lock().unwrap().files_id = binding.old_files,
11635                6 => {
11636                    thread.file_metadata.lock().unwrap().file_handles.insert(
11637                        3,
11638                        DetFd::new(
11639                            3,
11640                            OFlag::O_CLOEXEC,
11641                            FdType::Rng,
11642                            OpenFileId::new(binding.dettid, 1),
11643                        ),
11644                    );
11645                }
11646                7 => thread.mm_id = binding.old_mm,
11647                _ => unreachable!(),
11648            }
11649            assert!(
11650                capture_prepared_native_exec(&tool, &thread, &binding, &receipt).is_err(),
11651                "prepared field {field}"
11652            );
11653        }
11654    }
11655
11656    #[test]
11657    fn native_exec_model_lock_contention_is_a_refusal_without_mutation() {
11658        let (tool, thread, binding) = fixture();
11659        let before = state_bytes(&thread);
11660        macro_rules! locked {
11661            ($field:ident) => {{
11662                let guard = thread.$field.lock().unwrap();
11663                assert!(matches!(
11664                    validate_old_native_exec_cut(&tool, &thread, &binding),
11665                    Err(NativeExecCutError::LockUnavailable(_))
11666                ));
11667                drop(guard);
11668                assert_eq!(state_bytes(&thread), before);
11669            }};
11670        }
11671        locked!(file_metadata);
11672        locked!(process_cpu_time);
11673        locked!(guest_clock);
11674        locked!(resource_limits);
11675        locked!(posix_timers);
11676        locked!(robust_list_process);
11677        validate_old_native_exec_cut(&tool, &thread, &binding).unwrap();
11678    }
11679}
11680
11681fn thread_rng_from_parent_entropy(msg: &str, parent: &Pcg64Mcg, entropy: u128) -> Pcg64Mcg {
11682    thread_rng_from_parent_entropy_labeled(msg, parent, entropy, "entropy")
11683}
11684
11685fn thread_rng_from_parent_entropy_labeled(
11686    msg: &str,
11687    parent: &Pcg64Mcg,
11688    entropy: u128,
11689    identity_kind: &str,
11690) -> Pcg64Mcg {
11691    // Perform the default SeedableRng::from_seed procedure
11692    let mut seed = <Pcg64Mcg as SeedableRng>::Seed::default();
11693    // Generate a seed from the parent:
11694    parent.clone().fill_bytes(seed.as_mut());
11695    detlog!("RNG {} Generated new seed {:?}", msg, seed);
11696    // Pcg64Mcg forces its internal state odd, so seed bit zero carries no
11697    // entropy. DBT uses 96 bits for a stable process/thread sequence; mix those
11698    // bytes after the forced bit while retaining the existing DetTid layout.
11699    let entropy_bytes = entropy.to_le_bytes();
11700    for (seed_byte, entropy_byte) in seed[4..].iter_mut().zip(entropy_bytes) {
11701        *seed_byte ^= entropy_byte;
11702    }
11703    detlog!(
11704        "RNG {} seeding child {} {}: {:?} from parent {:?}",
11705        msg,
11706        identity_kind,
11707        entropy,
11708        seed,
11709        parent
11710    );
11711    let mut rng = Pcg64Mcg::from_seed(seed);
11712    // Pcg64Mcg integrates flipped bits across the state quickly. Some PRNGs don't.
11713    // Defensively ensure flipped bits "propagate":
11714    rng.next_u64();
11715    rng.next_u64();
11716    rng.next_u64();
11717    rng.next_u64();
11718    rng
11719}
11720
11721/// The procfs path that descriptor `fd` of the calling process is a
11722/// placeholder for, which this image of the process has not seen: a memfd
11723/// named for it and sealed as Detcore seals its placeholders, which a guest
11724/// could imitate (a known limitation, see
11725/// <https://github.com/rrnewton/hermit/issues/4263>). Read with raw
11726/// system calls (under in-guest backends the tool shares the guest's C
11727/// library).
11728/// What this thread's descriptor `fd` links to (`/proc/thread-self/fd/<fd>`),
11729/// read with a raw `readlinkat` as the in-guest runtime must.
11730fn own_fd_link(fd: RawFd) -> Option<Vec<u8>> {
11731    // The calling thread's own table: `/proc/self/fd` is the leader's.
11732    let link = std::ffi::CString::new(format!("/proc/thread-self/fd/{fd}")).ok()?;
11733    let mut target = [0_u8; 256];
11734    // SAFETY: the path is NUL-terminated and `target` is writable.
11735    let length = unsafe {
11736        crate::util::raw_syscall(
11737            libc::SYS_readlinkat,
11738            [
11739                libc::AT_FDCWD as u64,
11740                link.as_ptr() as u64,
11741                target.as_mut_ptr() as u64,
11742                target.len() as u64,
11743                0,
11744                0,
11745            ],
11746        )
11747    };
11748    let length = usize::try_from(length)
11749        .ok()
11750        .filter(|length| *length < target.len())?;
11751    Some(target[..length].to_vec())
11752}
11753
11754/// The path a descriptor's link `link` names where it is a per-process file
11755/// that follows its record's sequence as the kernel opens one, and the
11756/// descriptor's file (`raw_stat`) is an empty regular file with a link, as
11757/// every procfs file is (a placeholder has none): a task's `stat`, `statm`,
11758/// `io`, `arch_status` or `cgroup` (`ProcfsFile::follows_record_sequence`),
11759/// not its `comm`, which Detcore leaves to the kernel's own file whoever
11760/// opened it, so the kernel's one open file serves every holder. ASSUMED,
11761/// as for `fixed_procfs_behind`: a link under `/proc` naming such a file is
11762/// the procfs file, not an ordinary file a changed root put at that path.
11763pub(crate) fn kernel_record_file_link(
11764    raw_stat: &libc::stat,
11765    link: &[u8],
11766) -> Option<std::path::PathBuf> {
11767    if raw_stat.st_mode & libc::S_IFMT != libc::S_IFREG
11768        || raw_stat.st_size != 0
11769        || raw_stat.st_nlink == 0
11770    {
11771        return None;
11772    }
11773    let path =
11774        std::path::Path::new(<std::ffi::OsStr as std::os::unix::ffi::OsStrExt>::from_bytes(link));
11775    crate::procfs::ProcfsFile::from_path(path)
11776        .filter(|file| file.follows_record_sequence() && !file.is_task_comm())
11777        .map(|_| path.to_path_buf())
11778}
11779
11780/// The fixed procfs file the kernel opened at `fd`, if its descriptor link
11781/// names one (`ProcfsFile::snapshot_is_fixed`).
11782fn fixed_procfs_behind(fd: RawFd) -> Option<crate::procfs::ProcfsFile> {
11783    let target = own_fd_link(fd)?;
11784    let path = std::path::Path::new(
11785        <std::ffi::OsStr as std::os::unix::ffi::OsStrExt>::from_bytes(&target),
11786    );
11787    crate::procfs::ProcfsFile::from_path(path).filter(crate::procfs::ProcfsFile::snapshot_is_fixed)
11788}
11789
11790fn placeholder_behind(
11791    fd: RawFd,
11792    files: crate::procfs::RenderedFiles,
11793) -> Option<(&'static std::path::Path, Vec<u8>)> {
11794    // SAFETY: F_GET_SEALS only reads the descriptor's seals.
11795    let seals = unsafe {
11796        crate::util::raw_syscall(
11797            libc::SYS_fcntl,
11798            [fd as u64, libc::F_GET_SEALS as u64, 0, 0, 0, 0],
11799        )
11800    };
11801    // An `O_PATH` descriptor cannot show its seals (EBADF); its name decides.
11802    let wanted = i64::from(crate::syscalls::rendered_procfs::PLACEHOLDER_SEALS);
11803    if seals != -i64::from(libc::EBADF) && (seals < 0 || seals & wanted != wanted) {
11804        return None;
11805    }
11806    // The calling thread's own table: `/proc/self/fd` is the leader's.
11807    let link = std::ffi::CString::new(format!("/proc/thread-self/fd/{fd}")).ok()?;
11808    let mut target = [0_u8; crate::procfs::PLACEHOLDER_LINK_CAPACITY];
11809    // SAFETY: the path is NUL-terminated and `target` is writable.
11810    let length = unsafe {
11811        crate::util::raw_syscall(
11812            libc::SYS_readlinkat,
11813            [
11814                libc::AT_FDCWD as u64,
11815                link.as_ptr() as u64,
11816                target.as_mut_ptr() as u64,
11817                target.len() as u64,
11818                0,
11819                0,
11820            ],
11821        )
11822    };
11823    let length = usize::try_from(length).ok()?.min(target.len());
11824    crate::procfs::placeholder_link(&target[..length], files)
11825}
11826
11827#[cfg(test)]
11828mod native_main_state_tests {
11829    use super::*;
11830    use crate::native_exec::capture_prepared_native_exec;
11831    use crate::native_exec::tests::fixture;
11832    use crate::native_exec::tests::prepare;
11833    use crate::native_exec::tests::state_bytes;
11834    use crate::native_exec::validate_old_native_exec_cut;
11835    fn snapshot(
11836        tool: &crate::Detcore<crate::native_exec::NoopTool>,
11837        thread: &ThreadState<()>,
11838        binding: &crate::native_exec::NativeExecCutBinding,
11839        receipt: &crate::native_exec::NativeExecPreparationReceipt,
11840    ) -> crate::native_exec::DetcoreExecSnapshotV2 {
11841        let bytes = bincode::serde::encode_to_vec(
11842            capture_prepared_native_exec(tool, thread, binding, receipt).unwrap(),
11843            bincode::config::legacy(),
11844        )
11845        .unwrap();
11846        crate::native_exec::tests::decoded(&bytes)
11847    }
11848
11849    #[test]
11850    fn native_main_claimed_sigchld_refuses_live_and_decoded_without_reset() {
11851        let (tool, mut thread, binding) = fixture();
11852        thread.claimed_sigchld = true;
11853        let before = state_bytes(&thread);
11854        assert!(matches!(
11855            validate_old_native_exec_cut(&tool, &thread, &binding),
11856            Err(crate::native_exec::NativeExecCutError::LiveCapability(
11857                "claimed SIGCHLD"
11858            ))
11859        ));
11860        assert_eq!(state_bytes(&thread), before);
11861        thread.claimed_sigchld = false;
11862        let receipt = validate_old_native_exec_cut(&tool, &thread, &binding).unwrap();
11863        prepare(&mut thread, &binding);
11864        let mut payload = snapshot(&tool, &thread, &binding, &receipt);
11865        crate::native_exec::tests::thread_for_test(&mut payload).claimed_sigchld = true;
11866        assert!(matches!(
11867            payload.validate(&binding, &tool.cfg),
11868            Err(crate::native_exec::NativeExecCutError::LiveCapability(
11869                "claimed SIGCHLD"
11870            ))
11871        ));
11872        assert!(crate::native_exec::tests::thread_for_test(&mut payload).claimed_sigchld);
11873    }
11874
11875    fn mark_barrier(process: &mut RobustListProcessState, field: usize, owner: DetTid) {
11876        match field {
11877            0 => {
11878                process.tool_walked.insert(owner);
11879            }
11880            1 => process.exit_group_filed = true,
11881            2 => {
11882                process.exit_group_filers.insert(owner);
11883            }
11884            3 => process.exit_group_walker = Some(owner),
11885            4 => process.exit_group_walked = true,
11886            _ => unreachable!(),
11887        }
11888    }
11889
11890    #[test]
11891    fn native_main_robust_barrier_state_refuses_old_prepared_and_decoded() {
11892        for field in 0..5 {
11893            let (tool, thread, binding) = fixture();
11894            mark_barrier(
11895                &mut thread.robust_list_process.lock().unwrap(),
11896                field,
11897                binding.dettid,
11898            );
11899            let before = state_bytes(&thread);
11900            assert!(
11901                validate_old_native_exec_cut(&tool, &thread, &binding).is_err(),
11902                "old {field}"
11903            );
11904            assert_eq!(state_bytes(&thread), before);
11905
11906            let (tool, mut thread, binding) = fixture();
11907            let receipt = validate_old_native_exec_cut(&tool, &thread, &binding).unwrap();
11908            prepare(&mut thread, &binding);
11909            let mut payload = snapshot(&tool, &thread, &binding, &receipt);
11910            mark_barrier(
11911                &mut thread.robust_list_process.lock().unwrap(),
11912                field,
11913                binding.dettid,
11914            );
11915            let before = state_bytes(&thread);
11916            assert!(
11917                capture_prepared_native_exec(&tool, &thread, &binding, &receipt).is_err(),
11918                "prepared {field}"
11919            );
11920            assert_eq!(state_bytes(&thread), before);
11921            let decoded = crate::native_exec::tests::thread_for_test(&mut payload);
11922            mark_barrier(
11923                &mut decoded.robust_list_process.lock().unwrap(),
11924                field,
11925                binding.dettid,
11926            );
11927            let before = state_bytes(decoded);
11928            assert!(
11929                payload.validate(&binding, &tool.cfg).is_err(),
11930                "decoded {field}"
11931            );
11932            assert_eq!(
11933                state_bytes(crate::native_exec::tests::thread_for_test(&mut payload)),
11934                before
11935            );
11936        }
11937    }
11938
11939    #[test]
11940    fn native_main_nanosleep_restart_refuses_even_when_serde_skips_it() {
11941        for deadline in [None, Some(crate::types::LogicalTime::from_nanos(4567))] {
11942            let (tool, mut thread, binding) = fixture();
11943            let before = state_bytes(&thread);
11944            thread.restart_block = Some(crate::syscalls::helpers::RestartBlock {
11945                rip: 0x1234,
11946                deadline,
11947                call: crate::syscalls::helpers::RestartCall::Nanosleep(
11948                    reverie::syscalls::Nanosleep::new().into(),
11949                ),
11950            });
11951            assert_eq!(state_bytes(&thread), before);
11952            assert!(matches!(
11953                validate_old_native_exec_cut(&tool, &thread, &binding),
11954                Err(crate::native_exec::NativeExecCutError::LiveCapability(
11955                    "restart/projection"
11956                ))
11957            ));
11958            assert!(thread.restart_block.is_some());
11959        }
11960    }
11961
11962    #[test]
11963    fn native_main_rng_promotion_history_survives_snapshot_and_some_empty() {
11964        use std::os::fd::AsRawFd;
11965        crate::fd::install_test_shared_open_file_channel();
11966        {
11967            let (tool, mut thread, binding) = fixture();
11968            let local = std::fs::File::open("/dev/urandom").unwrap();
11969            thread
11970                .add_fd(local.as_raw_fd(), OFlag::O_RDONLY, FdType::Rng, None)
11971                .unwrap();
11972            assert!(thread.shared_open_files_for_exec().unwrap().is_none());
11973            let receipt = validate_old_native_exec_cut(&tool, &thread, &binding).unwrap();
11974            prepare(&mut thread, &binding);
11975            let (_, restored) = snapshot(&tool, &thread, &binding, &receipt)
11976                .into_validated_parts(&binding, &tool.cfg)
11977                .unwrap();
11978            assert!(restored.shared_open_files_for_exec().unwrap().is_none());
11979        }
11980        let (tool, mut thread, binding) = fixture();
11981        let creator = DetTid::from_raw(930001);
11982        thread.set_open_file_creator(creator);
11983        let file = std::fs::File::open("/dev/urandom").unwrap();
11984        let alias = nix::unistd::dup(&file).unwrap();
11985        let cloexec = nix::unistd::dup(&file).unwrap();
11986        let distinct = std::fs::File::open("/dev/urandom").unwrap();
11987        let (fd, alias_fd, cloexec_fd, distinct_fd) = (
11988            file.as_raw_fd(),
11989            alias.as_raw_fd(),
11990            cloexec.as_raw_fd(),
11991            distinct.as_raw_fd(),
11992        );
11993        thread
11994            .add_fd(fd, OFlag::O_RDONLY, FdType::Rng, None)
11995            .unwrap();
11996        thread.dup_fd(fd, alias_fd, OFlag::empty()).unwrap();
11997        thread.dup_fd(fd, cloexec_fd, OFlag::O_CLOEXEC).unwrap();
11998        thread
11999            .add_fd(distinct_fd, OFlag::O_RDONLY, FdType::Rng, None)
12000            .unwrap();
12001        assert!(
12002            thread.shared_open_files_for_exec().unwrap().is_none(),
12003            "local RNG is not promotion"
12004        );
12005        let high = thread
12006            .metadata()
12007            .allocate_open_file_id(creator, FdType::Rng);
12008        let high_socket = thread
12009            .metadata()
12010            .allocate_open_file_id(creator, FdType::Socket);
12011        thread.promote_shared_open_files().unwrap();
12012        let expected = thread.shared_open_files_for_exec().unwrap().unwrap();
12013        assert_eq!(expected.creator, creator);
12014        assert_eq!(expected.handles[&fd].id, expected.handles[&alias_fd].id);
12015        assert_ne!(expected.handles[&fd].id, expected.handles[&distinct_fd].id);
12016        assert!(!expected.handles.contains_key(&cloexec_fd));
12017        let old_table = Arc::clone(&thread.file_metadata);
12018        let receipt = validate_old_native_exec_cut(&tool, &thread, &binding).unwrap();
12019        prepare(&mut thread, &binding);
12020        assert!(
12021            old_table
12022                .lock()
12023                .unwrap()
12024                .file_handles
12025                .contains_key(&cloexec_fd)
12026        );
12027        let (_, restored) = snapshot(&tool, &thread, &binding, &receipt)
12028            .into_validated_parts(&binding, &tool.cfg)
12029            .unwrap();
12030        assert_eq!(
12031            restored.shared_open_files_for_exec().unwrap(),
12032            Some(expected)
12033        );
12034        assert_eq!(restored.open_file_creator, Some(creator));
12035        assert_eq!(
12036            restored.metadata().next_open_file_sequence,
12037            thread.metadata().next_open_file_sequence
12038        );
12039        assert_eq!(
12040            restored.metadata().next_socket_open_file_sequence,
12041            thread.metadata().next_socket_open_file_sequence
12042        );
12043        restored.metadata().file_handles[&alias_fd]
12044            .with_random_device_stream::<Errno>(|offset| {
12045                assert_eq!(offset, 0);
12046                Ok(11)
12047            })
12048            .unwrap();
12049        thread.metadata().file_handles[&fd]
12050            .with_random_device_stream::<Errno>(|offset| {
12051                assert_eq!(offset, 11);
12052                Ok(0)
12053            })
12054            .unwrap();
12055        // Both tables retain the same canonical shared ID, not a copied cursor.
12056        for slot in [fd, alias_fd, distinct_fd] {
12057            thread.remove_fd(slot);
12058        }
12059        let empty = thread.shared_open_files_for_exec().unwrap().unwrap();
12060        assert!(empty.handles.is_empty());
12061        assert_eq!(empty.creator, creator);
12062        assert_ne!(OpenFileId::new(creator, empty.next_sequence), high);
12063        assert_ne!(
12064            OpenFileId::new_socket(creator, empty.next_socket_sequence),
12065            high_socket
12066        );
12067        // Drop the test-owned metadata guard before the validator probes it.
12068        let old_files = thread.metadata().files_id;
12069        let binding2 = crate::native_exec::NativeExecCutBinding {
12070            old_mm: thread.mm_id,
12071            old_files,
12072            ..binding
12073        };
12074        let receipt2 = validate_old_native_exec_cut(&tool, &thread, &binding2).unwrap();
12075        prepare(&mut thread, &binding2);
12076        let (_, again) = snapshot(&tool, &thread, &binding2, &receipt2)
12077            .into_validated_parts(&binding2, &tool.cfg)
12078            .unwrap();
12079        assert_eq!(again.shared_open_files_for_exec().unwrap(), Some(empty));
12080    }
12081}
12082/// The hint every report of a preemption replay that left its record ends
12083/// with: a faithful replay never leaves it, but a record edited after it was
12084/// recorded (preemptions knocked out, as `hermit analyze` does) does by
12085/// design, and is replayed with `--replay-edited-preemptions`, which skips the
12086/// slices that already ended instead (`next_edited_slice`) and reports none
12087/// of these.
12088pub(crate) const EDITED_RECORD_HINT: &str = "An edited record, not the recording of this run's \
12089     schedule, needs --replay-edited-preemptions.";
12090
12091/// Reports a preemption replay's thread about to file a change point at
12092/// `time`, not after its last one at `last`
12093/// ([`ThreadState::file_replay_change_point`]). This is not evidence of PMU
12094/// skid. The caller publishes it to the coordinator before abandonment, unless
12095/// `--panic-on-rcb-overshoot` preserves its synchronous stop here.
12096#[cold]
12097pub(crate) fn report_stale_replay_filing(
12098    panic_on_rcb_overshoot: bool,
12099    dettid: DetTid,
12100    slice: u64,
12101    time: LogicalTime,
12102    last: LogicalTime,
12103) {
12104    let message = format!(
12105        "{} preemption replay: thread {} would end slice T{} with a change point at {}, not \
12106         after its last change point at {}: the slice closed with no time in it, which the \
12107         replayed record never does. The thread files no change point here and runs on as \
12108         its final slice; the rest of this run does not replay the record exactly. {}",
12109        "HERMIT_REPLAY_DEPARTURE:", dettid, slice, time, last, EDITED_RECORD_HINT,
12110    );
12111    if panic_on_rcb_overshoot {
12112        panic!("{message}");
12113    }
12114    tracing::error!("{message}");
12115}