Skip to main content

detcore_model/
config.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9//! Detcore configuration and widely used types.
10
11use std::collections::BTreeSet;
12use std::ffi::OsString;
13use std::fmt;
14use std::num::NonZeroU32;
15use std::num::NonZeroU64;
16use std::path::PathBuf;
17use std::str::FromStr;
18use std::time::SystemTime;
19
20use chrono::DateTime;
21use chrono::Utc;
22use clap::Parser;
23use reverie::BackendCapabilities;
24use serde::Deserialize;
25use serde::Serialize;
26
27use crate::happens_before::HappensBeforeProgram;
28use crate::network_trace::NetworkTraceConfig;
29use crate::pid::DetTid;
30use crate::schedule::SigWrapper;
31use crate::time::NANOS_PER_RCB;
32use crate::time::NANOS_PER_SCHED;
33use crate::time::RcbTimeMultiplier;
34
35const fn default_backend_capabilities() -> BackendCapabilities {
36    DEFAULT_BACKEND_CAPABILITIES
37}
38
39/// The capabilities a [`Config`] has before the host prepares it for the
40/// backend that runs it: ptrace's, the backend Detcore's own tests use, except
41/// that eight facts stay off (`supports_blocked_wait_signal_interruption` and
42/// the four split out of it, which [`set_kernel_signal_facts`] clears
43/// together, `delivers_preemption_timer_events`,
44/// `runs_guest_code_between_injections`, `reports_sigreturns`). Until
45/// `supports_blocked_wait_signal_interruption`,
46/// `delivers_preemption_timer_events` and
47/// `runs_guest_code_between_injections` became backend capabilities they were
48/// `Config` fields that only the host set, for the backend it had selected,
49/// and were false otherwise; a configuration nobody prepared keeps that
50/// behaviour, and the four readings of the first with it. A configuration
51/// nobody prepared cannot know that sigreturns are reported, so that is off
52/// too (the DBT runtime's standalone
53/// fallback starts from these). `reports_kernel_child_exit_signals` keeps
54/// ptrace's value, as `reports_child_exit_publication` does: it took over the
55/// child-exit ledger's gates from that field, so such a configuration behaves
56/// as before. The host copies the running backend's own answer over all of
57/// these before a run.
58pub const DEFAULT_BACKEND_CAPABILITIES: BackendCapabilities = {
59    let mut backend = BackendCapabilities::PTRACE;
60    set_kernel_signal_facts(&mut backend, false);
61    backend.delivers_preemption_timer_events = false;
62    backend.runs_guest_code_between_injections = false;
63    backend.reports_sigreturns = false;
64    backend
65};
66
67/// Sets `supports_blocked_wait_signal_interruption` and the four capabilities
68/// Detcore read from it until A5 step 3c.0 split them out
69/// (`kernel_signal_state_is_guests`, `kernel_signal_targets_are_predictable`,
70/// `host_task_reads_reach_guest_threads`, `host_timed_signals_are_physical`)
71/// to `on`, together. Every backend has the five equal, so a configuration
72/// that set or cleared the one field before the split sets or clears all five
73/// with this, and behaves as it did.
74pub const fn set_kernel_signal_facts(backend: &mut BackendCapabilities, on: bool) {
75    backend.supports_blocked_wait_signal_interruption = on;
76    backend.kernel_signal_state_is_guests = on;
77    backend.kernel_signal_targets_are_predictable = on;
78    backend.host_task_reads_reach_guest_threads = on;
79    backend.host_timed_signals_are_physical = on;
80}
81
82/// One mount row whose kernel-private root must be replaced before it becomes
83/// guest-visible.
84///
85/// The CLI populates this only after proving, with held file descriptors in the
86/// completed mount namespace, that the mount is one Hermit created.  The raw
87/// mount ID is namespace-local, so these entries are valid only for the one
88/// container run whose configuration carries them.
89#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
90pub struct MountInfoRootRewrite {
91    /// Mount ID read from the held target descriptor's `/proc/self/fdinfo`.
92    pub raw_mount_id: u64,
93    /// Stable guest-visible replacement for the row's root field.
94    pub deterministic_root: Vec<u8>,
95    /// Exact encoded kernel root prefix used for descendant mount rows.
96    ///
97    /// This is present only for a proven private `/tmp`. Mounts installed below
98    /// that directory before it is bound over guest `/tmp` otherwise expose the
99    /// randomly named backing directory in mountinfo field 5.
100    #[serde(default)]
101    pub raw_root_prefix: Option<Vec<u8>>,
102    /// Guest-visible prefix replacing `raw_root_prefix`.
103    #[serde(default)]
104    pub deterministic_root_prefix: Option<Vec<u8>>,
105    /// Exact encoded host path prefix used for descendant mountpoints.
106    #[serde(default)]
107    pub raw_mountpoint_prefix: Option<Vec<u8>>,
108    /// Guest-visible prefix replacing `raw_mountpoint_prefix`.
109    #[serde(default)]
110    pub deterministic_mountpoint_prefix: Option<Vec<u8>>,
111}
112
113/// The seccomp state of the thread that starts the guest, which the guest
114/// inherits: every filter that thread has applies to the guest's system calls,
115/// and to the calls Detcore injects into it. Detcore refuses the guest's own
116/// `seccomp(2)` and `prctl(PR_SET_SECCOMP)`, so these and the backend's own
117/// filter are the only filters a guest has.
118#[derive(
119    Clone,
120    Copy,
121    Debug,
122    Default,
123    PartialEq,
124    Eq,
125    Hash,
126    Serialize,
127    Deserialize
128)]
129pub enum InheritedSeccomp {
130    /// Not measured, or not readable, so a filter may apply.
131    #[default]
132    Unknown,
133    /// No seccomp (`Seccomp: 0`).
134    Disabled,
135    /// Strict mode (`Seccomp: 1`).
136    Strict,
137    /// Filter mode (`Seccomp: 2`), with the number of filters when
138    /// `Seccomp_filters` gives it.
139    Filter(Option<u32>),
140}
141
142impl InheritedSeccomp {
143    /// The state of the calling thread, which a process it forks inherits,
144    /// from `/proc/thread-self/status`.
145    pub fn measure() -> Self {
146        std::fs::read_to_string("/proc/thread-self/status")
147            .map_or(Self::Unknown, |status| Self::parse(&status))
148    }
149
150    /// The state the `Seccomp` and `Seccomp_filters` lines of the text of a
151    /// `/proc/<pid>/status` file give. A missing or unexpected `Seccomp` line
152    /// is unknown.
153    pub fn parse(status: &str) -> Self {
154        let field = |name: &str| {
155            status
156                .lines()
157                .find_map(|line| line.strip_prefix(name)?.strip_prefix(':'))
158                .map(str::trim)
159        };
160        match field("Seccomp") {
161            Some("0") => Self::Disabled,
162            Some("1") => Self::Strict,
163            Some("2") => Self::Filter(field("Seccomp_filters").and_then(|n| n.parse().ok())),
164            _ => Self::Unknown,
165        }
166    }
167}
168
169/// What a launch tells Detcore about the guest's first image
170/// ([`Config::launch_image`]).
171#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
172pub struct LaunchImage {
173    /// The first image as its `/proc/self/exe` link names it: the program's
174    /// canonical path, or for a script the interpreter it runs
175    /// (`detcore::exec_record::ExecRecord::resolve`). `None` where the launch
176    /// could not resolve it, which leaves the first image without a record.
177    pub exe: Option<PathBuf>,
178    /// The image the kernel exec'd in the guest's place, where a launcher
179    /// loads the guest itself (SaBRe's loader, DynamoRIO's library), so that
180    /// the kernel's `/proc/<pid>/exe` names the launcher. Detcore keeps a recorded image
181    /// only where the kernel's link names that image or this one
182    /// (`detcore::exec_record`). `None` where the kernel execs the guest.
183    pub launcher: Option<PathBuf>,
184}
185
186/// One mount namespace, as `DetConfig::mountinfo_mount_namespace` names it.
187///
188/// `id` is the kernel's 64-bit namespace ID (`ioctl(NS_GET_MNTNS_ID)` on a
189/// `/proc/<pid>/ns/mnt` descriptor, on kernels that have it). Linux draws it from
190/// one counter that only increases (`__ns_tree_gen_id` in kernel/nstree.c), so
191/// it names one namespace for the life of the boot. `device` and `inode` are
192/// the descriptor's nsfs device and inode numbers, which Linux recycles once a
193/// namespace is gone; they alone identify the namespace only on a kernel
194/// without the ioctl, where `id` is `None`.
195#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
196pub struct MountNamespaceIdentity {
197    /// The kernel's namespace ID, where the kernel reports one.
198    pub id: Option<u64>,
199    /// The nsfs device number of the namespace's descriptor.
200    pub device: u64,
201    /// The nsfs inode number of the namespace's descriptor.
202    pub inode: u64,
203}
204
205/// What `--detlog-anon` records for each anonymous mapping.
206#[derive(
207    Debug,
208    Clone,
209    Copy,
210    PartialEq,
211    Eq,
212    Serialize,
213    Deserialize,
214    clap::ValueEnum
215)]
216pub enum DetlogAnon {
217    /// One digest per mapping.
218    Mappings,
219    /// One digest per mapping, and a short one for each of its 4 KiB pages that is not all zero.
220    Pages,
221}
222
223/// Configuration options for detcore.
224#[derive(Debug, Serialize, Deserialize, Clone, Parser)]
225pub struct Config {
226    /// Disable virtual/logical time. Note that virtual time is required for virtual metadata.
227    #[clap(long = "no-virtualize-time", action = clap::ArgAction::SetFalse)]
228    pub virtualize_time: bool,
229
230    /// Disable virtual cpuid
231    #[clap(long = "no-virtualize-cpuid", action = clap::ArgAction::SetFalse)]
232    pub virtualize_cpuid: bool,
233
234    /// The CPU the guest sees: virtual x86-64-v2/v3 or intercepted host
235    /// features without hardware randomness. The long help states each
236    /// profile's host requirements and determinism limits. Like
237    /// HERMIT_EPOCH, HERMIT_CPU_PROFILE supplies the value when the flag is
238    /// absent; an explicit flag wins, unset means x86-64-v2, and a value that
239    /// names no profile is refused. Admission is the same either way.
240    #[clap(long, value_enum, default_value_t, env = "HERMIT_CPU_PROFILE", long_help = crate::cpu_profile::CPU_PROFILE_LONG_HELP)]
241    #[serde(default)]
242    pub cpu_profile: crate::cpu_profile::CpuProfile,
243
244    /// The host facts a v3 guest's leaf 0xD and xstate answers come from, set
245    /// by the CLI after the host qualifies; `None` under v2. Neither profile
246    /// field enters the legacy form the DBT runtime reads (DBT refuses v3).
247    #[clap(skip)]
248    #[serde(default)]
249    pub cpu_profile_host: Option<crate::cpu_profile::HostXstate>,
250
251    /// Native CPUID source for host-no-rdrand, selected during admission from
252    /// the caller's original allowed CPU set, before container placement.
253    /// The ptrace launch pins its actual tracer to this CPU. Never restored
254    /// from recording metadata: host-profile recording/replay is refused.
255    #[clap(skip)]
256    #[serde(default)]
257    pub cpu_profile_source: Option<usize>,
258
259    // AUTONOMOUS-BOT-IMPLEMENTED
260    // TODO-HUMAN-REVIEW(PR-845): Review in-process backend descriptor discovery.
261    // AUTONOMOUS-BOT-IMPLEMENTED
262    // TODO-HUMAN-REVIEW(PR-845): Review host-clock futex deadline detection.
263    // AUTONOMOUS-BOT-IMPLEMENTED
264    // TODO-HUMAN-REVIEW(PR-845): Review backend-owned syscall-clobber determinism.
265    // AUTONOMOUS-BOT-IMPLEMENTED
266    // TODO-HUMAN-REVIEW(PR-845): Review backend-local exit-group RPC cancellation.
267    // AUTONOMOUS-BOT-IMPLEMENTED
268    // TODO-HUMAN-REVIEW(PR-1058): Review process-signal identity translation.
269    // AUTONOMOUS-BOT-IMPLEMENTED
270    // TODO-HUMAN-REVIEW(PR-1125): Review backend-owned capability-control prctls.
271    // AUTONOMOUS-BOT-IMPLEMENTED
272    // TODO-HUMAN-REVIEW(PR-1152): Review deferred vfork child registration.
273    /// How the execution backend runs the guest, as the backend reports it
274    /// through Reverie (`reverie::Backend::capabilities`).
275    ///
276    /// Detcore reads facts about the backend only from here, never from the
277    /// backend's name, so a new backend is modelled correctly by reporting its
278    /// capabilities. The host copies the running backend's answer into this
279    /// field before the run starts. The default,
280    /// [`DEFAULT_BACKEND_CAPABILITIES`], describes a ptrace-hosted guest, the
281    /// backend Detcore's own tests use, with the three host-set facts and
282    /// `reports_sigreturns` off.
283    ///
284    /// JSON that the guest can see is written by [`to_legacy_backend_json`],
285    /// which carries this field as the separate keys used before it existed;
286    /// [`from_legacy_backend_json`] reads them back.
287    #[serde(default = "default_backend_capabilities")]
288    #[clap(skip = DEFAULT_BACKEND_CAPABILITIES)]
289    pub backend: BackendCapabilities,
290
291    /// The guest may start with a terminal: one of the launcher's standard
292    /// descriptors is a terminal, the launcher has a controlling terminal that
293    /// the guest inherits with its session, or the launcher could not tell.
294    /// Such a terminal makes Linux send signals at moments the host sets:
295    /// SIGHUP and SIGCONT when it hangs up, SIGINT, SIGQUIT and SIGTSTP for its
296    /// interrupt, quit and suspend characters, SIGWINCH when it is resized, and
297    /// SIGTTIN and SIGTTOU for a background process group's reads and writes.
298    /// The guest makes no traced call that arms them, so where Detcore treats
299    /// such signals as reaching the guest from the host
300    /// (`BackendCapabilities::host_timed_signals_are_physical`)
301    /// the scheduler records all eight as host-timed for every process before the
302    /// guest's first instruction, and no gated wait ends for them
303    /// (<https://github.com/rrnewton/hermit/issues/3146>).
304    ///
305    /// The host sets it while it prepares the backend configuration. It has no
306    /// legacy key: [`to_legacy_backend_json`] leaves it out and
307    /// [`from_legacy_backend_json`] reads it back as false.
308    #[serde(default)]
309    #[clap(skip)]
310    pub guest_may_inherit_a_terminal: bool,
311
312    /// The forwarding policy (`detcore::detlog::ForwardPolicy::encode`) every
313    /// in-guest Tool must forward its DETLOG records with, on the socket Hermit
314    /// passed it (`hermit run --verify` under SaBRe or in-guest LiteInst);
315    /// `None` when no socket is passed.
316    ///
317    /// The host sets it, not a flag, and every Tool image receives it in the
318    /// configuration handshake on its coordinator connection, which guest code
319    /// cannot change. A Tool image whose forwarder is missing, or forwards by
320    /// another policy (guest code that ran first changed its private
321    /// forwarding variables, so some records would be dropped before they are
322    /// counted), then reports every forwarded-record count as UNCOUNTED
323    /// (`detcore::detlog::require_forwarding`), so verification refuses the run
324    /// instead of accepting a log without those records. Like
325    /// `guest_may_inherit_a_terminal`, it has no legacy key.
326    #[serde(default)]
327    #[clap(skip)]
328    pub in_guest_detlog_forward_policy: Option<String>,
329
330    /// Whether every in-guest Tool image must run with syscall site patching
331    /// off and collect its exit statistics (`hermit --backend=in-guest-trap`).
332    ///
333    /// The host sets it, not a flag, and every Tool image receives it in the
334    /// configuration handshake on its coordinator connection, which guest code
335    /// cannot change. The in-guest LiteInst runtime takes both settings from
336    /// the guest environment, which guest code running before or during its
337    /// installation could change, so once installation returns a Tool image
338    /// compares the settings the runtime actually captured with this one and
339    /// refuses to run the program when they differ
340    /// (`detcore::in_guest_site_patching`). Like
341    /// `in_guest_detlog_forward_policy`, it has no legacy key.
342    #[serde(default)]
343    #[clap(skip)]
344    pub in_guest_site_patching_off: bool,
345
346    /// The signals an in-guest runtime keeps for itself, which a guest may
347    /// not send to another process or arm as a notification signal: bit
348    /// `n - 1` for signal `n`, as Detcore's other signal sets are kept.
349    ///
350    /// Such a signal sent from one process to another would run the target's
351    /// runtime handler on a thread that does not hold the scheduler's turn
352    /// (A5 step 3c, design section 16.30,
353    /// <https://github.com/rrnewton/hermit/issues/4485>). So Detcore answers a
354    /// send of one of these signals to a process other than the caller's own
355    /// (`kill`, `tkill`, `tgkill`, `rt_sigqueueinfo`, `rt_tgsigqueueinfo`,
356    /// `pidfd_send_signal`), and any arming of one (`fcntl(F_SETSIG)`,
357    /// `timer_create`, `prctl(PR_SET_PDEATHSIG)`), with `EPERM` and a WARN
358    /// ([`Config::reserves_in_guest_signal`]). A send to the caller's own
359    /// process is unchanged. Empty (0), the default, changes nothing.
360    ///
361    /// The host sets it, not a flag, when it prepares an in-guest run on the
362    /// default route: SIGSYS, SIGSEGV while the runtime subscribes
363    /// instructions, and SIGTRAP while LiteInst's guard router may be
364    /// installed: on `--backend=liteinst`, never with
365    /// `in_guest_site_patching_off` (a liteinst image whose environment turns
366    /// patching off has no router but keeps SIGTRAP reserved, a known
367    /// over-refusal).
368    /// Every other backend, and the in-guest loader route, leave it empty.
369    /// Every Tool image receives it in the configuration handshake, which
370    /// guest code cannot change. Like `in_guest_site_patching_off`, it has no
371    /// legacy key.
372    #[serde(default)]
373    #[clap(skip)]
374    pub in_guest_reserved_signals: u64,
375
376    /// Whether Detcore refuses the guest calls that would let the kernel send
377    /// a guest process a signal at a moment host timing decides (A5 step 3c,
378    /// slice 0b; design sections 16.30, D28 and D28a,
379    /// <https://github.com/rrnewton/hermit/issues/4485>).
380    ///
381    /// While the in-guest runtime runs its own code, ordinary signals are
382    /// blocked, and Linux keeps a blocked signal pending even under
383    /// `SIG_IGN`, so such a signal could become pending at a host-timed
384    /// moment whatever the guest's action. So where this is set, Detcore
385    /// answers with `EPERM` and a WARN the operation that completes such a
386    /// producer: an owner for a socket or for a descriptor with `O_ASYNC`
387    /// (`F_SETOWN`, `F_SETOWN_EX`, `FIOSETOWN`, `SIOCSPGRP`), `O_ASYNC` for a
388    /// descriptor with an owner (`F_SETFL`, `FIOASYNC`), a lease
389    /// (`F_SETLEASE`) and a directory notification (`F_NOTIFY`). It holds a
390    /// parent-death signal it can send itself, as it does on ptrace, and
391    /// refuses the run by name (`pdeathsig-would-fire`) if one would fire; it
392    /// refuses with `EPERM` one it cannot hold. A SIGKILL parent-death signal
393    /// keeps the kernel's path. False, the default, changes nothing.
394    ///
395    /// The host sets it, not a flag, when it prepares an in-guest run on the
396    /// default route, where it also clears the physical `RLIMIT_CPU` soft
397    /// limit and refuses an inherited descriptor that is already such a
398    /// producer (D25). Every other backend, and the in-guest loader route,
399    /// leave it false. Every Tool image receives it in the configuration
400    /// handshake, which guest code cannot change. Like
401    /// `in_guest_site_patching_off`, it has no legacy key.
402    #[serde(default)]
403    #[clap(skip)]
404    pub in_guest_refuses_host_timed_producers: bool,
405
406    /// The guest's first image, as its `/proc/self/exe` link names it, and
407    /// the image the kernel exec'd in its place where a launcher loads the
408    /// guest ([`LaunchImage`]). Detcore answers that link from it until the
409    /// guest's next exec, which Detcore resolves itself. `None` leaves the
410    /// first image's link to the kernel.
411    ///
412    /// The host sets it, not a flag, from the program it launches. Like
413    /// `in_guest_detlog_forward_policy` it has no legacy key, so DBT's
414    /// legacy form never carries it.
415    #[serde(default)]
416    #[clap(skip)]
417    pub launch_image: Option<LaunchImage>,
418
419    /// The seccomp state the guest inherits from the thread that starts it
420    /// ([`InheritedSeccomp`]). The caller that starts the guest states it, as
421    /// only the caller knows the filters its launch installs, such as those of
422    /// a `pre_exec` callback; `hermit run` measures it. Not stated, it is
423    /// unknown. Nothing reads it. Whether a guest may start under an inherited
424    /// filter is decided by the `hermit` command, before it starts the guest,
425    /// from its own reading of `/proc/self/status` (`host_seccomp::admit`).
426    /// It has no legacy key:
427    /// [`to_legacy_backend_json`] leaves it out and
428    /// [`from_legacy_backend_json`] reads it back as unknown.
429    #[serde(default)]
430    #[clap(skip)]
431    pub inherited_seccomp: InheritedSeccomp,
432
433    /// Epoch of the logical time.
434    ///
435    /// This is the datetime from which all time and date modtimes begin and
436    /// monotonically increase. It is in RFC3339 format such as `2026-01-01T00:00:00Z`.
437    /// The stable default here is for library callers and wire-format fixtures;
438    /// the `hermit run` and `hermit oci run` commands replace an omitted CLI
439    /// default with one host wall-clock sample taken before backend dispatch.
440    ///
441    /// A guest that checks TLS certificates over the host network (`--network=host` or
442    /// `--record-networking`) checks them against this clock, so an epoch outside a server
443    /// certificate's validity fails the HTTPS connections to that server: curl exits 60 with "SSL
444    /// certificate problem: certificate is not yet valid" (or "certificate has expired"), and a
445    /// libcurl caller sees "SSL peer certificate or SSH remote key was not OK". For such a run omit
446    /// `--epoch`: `hermit run` then starts the clock at the current time and saves it in the run's
447    /// manifest, and `hermit run --from last-run` repeats the run with that epoch.
448    #[clap(
449        long,
450        env = "HERMIT_EPOCH",
451        value_name = "YYYY-MM-DDThh:mm:ssZ",
452        default_value = DEFAULT_EPOCH_STR,
453        hide_default_value = true
454    )]
455    pub epoch: DateTime<Utc>,
456
457    /// Use this number to seed the PRNG randomness for both RNG and scheduler.
458    /// This acts as a global fallback in case either `sched_seed` or `rng-seed`
459    /// are not explicitly specified
460    #[clap(
461        long = "seed",
462        env = "HERMIT_PRNG",
463        default_value = "0",
464        value_name = "uint64"
465    )]
466    pub seed: u64,
467
468    /// Use this number to seed the PRNG that supplies randomness to the guest.
469    /// This supplies guest system calls that expose randomness, as well as
470    /// the `/dev/[u]random` files. It does not affect the `rdrand` instruction,
471    /// which is disabled in the guest.
472    #[clap(long, value_name = "uint64")]
473    pub rng_seed: Option<u64>,
474
475    /// Seeds the PRNG which drives syscall response fuzzing (i.e. chaotically exercising syscall
476    /// nondeterminism).  Like other seeds, this is initialized from the `--seed` if not
477    /// specifically provided.
478    #[clap(long, value_name = "uint64")]
479    pub fuzz_seed: Option<u64>,
480
481    /// Logical clock multiplier. Values above one make time appear to go faster within the sandbox.
482    #[clap(long, value_name = "float")]
483    pub clock_multiplier: Option<f64>,
484
485    /// Disable substitution of virtual (deterministic) file metadata in lieu
486    /// of the real metadata returned by `stat`/`statx`. This also preserves raw
487    /// mountinfo device numbers so those interfaces continue to agree. Raw
488    /// device values are host/filesystem observations and are not promised to
489    /// reproduce across machines. Virtual metadata implies `virtualize_time`.
490    #[clap(long = "no-virtualize-metadata", action = clap::ArgAction::SetFalse)]
491    pub virtualize_metadata: bool,
492
493    /// Proven Hermit-owned mount roots to hide from `/proc/*/mountinfo`.
494    ///
495    /// This is runtime provenance, not a user option.  `serde(default)` keeps
496    /// older serialized configurations compatible and makes backends which do
497    /// not use the common container setup explicitly receive no rewrite claim.
498    #[serde(default)]
499    #[clap(skip)]
500    pub mountinfo_root_rewrites: Vec<MountInfoRootRewrite>,
501
502    /// Backend-proven pairs of (`mountinfo` raw device, `stat`/`statx` raw device).
503    ///
504    /// A backend may synthesize mountinfo independently from its pathname
505    /// metadata implementation.  These pairs state that the two raw numbers
506    /// describe the same filesystem, so Detcore can feed both surfaces through
507    /// one device identity.  The pairs are runtime provenance, not a user
508    /// option; an absent pair must never be inferred from numeric coincidence.
509    #[serde(default)]
510    #[clap(skip)]
511    pub mountinfo_device_rewrites: Vec<(u64, u64)>,
512
513    /// Recording/container namespace mount IDs in canonical row order.
514    ///
515    /// Detcore validates every `/proc/*/mountinfo` view against this order.
516    /// It does not number mount IDs by position in it; see
517    /// `mount_id_assignment_order`. It is runtime provenance rather than a
518    /// user option; replay retains recording-time raw IDs because its read
519    /// events contain recording-time kernel bytes.
520    #[serde(default)]
521    #[clap(skip)]
522    pub mountinfo_mount_ids: Vec<u64>,
523
524    /// Whether `mountinfo_mount_ids` is an exact producer-owned snapshot.
525    ///
526    /// The distinction matters for an empty mountinfo file: an absent snapshot
527    /// asks Detcore to observe the completed guest namespace, while a captured
528    /// empty snapshot must remain empty during replay.
529    #[serde(default)]
530    #[clap(skip)]
531    pub mountinfo_mount_ids_captured: bool,
532
533    /// The mount namespace `mountinfo_mount_ids` was captured in
534    /// ([`MountNamespaceIdentity`]). A backend that checks its provenance
535    /// (KVM) requires its own namespace to be this one, instead of
536    /// recapturing the namespace's rows: a host can detach a copied host mount
537    /// from a private namespace at any time by removing that mount's
538    /// directory, so two captures of one namespace need not agree. `None`
539    /// when nothing was captured, and in recordings. Runtime provenance, not a
540    /// user option.
541    #[serde(default)]
542    #[clap(skip)]
543    pub mountinfo_mount_namespace: Option<MountNamespaceIdentity>,
544
545    /// Raw mount IDs in the order Detcore numbered them for the guest.
546    ///
547    /// Detcore numbers a raw mount ID when the guest first observes it through
548    /// `/proc/*/mountinfo` or `/proc/*/fdinfo/*`; `mountinfo_mount_ids` only
549    /// validates mountinfo views. A live run starts empty. Recording persists
550    /// the completed order so replay gives every raw ID its recorded number
551    /// rather than deriving one from its fresh namespace or launch descriptor
552    /// shape.
553    #[serde(default)]
554    #[clap(skip)]
555    pub mount_id_assignment_order: Vec<u64>,
556
557    /// The guest's container mount table, which Detcore renders as every
558    /// spelling of `/proc/<pid>/mountinfo` and `/proc/<pid>/mounts` on every
559    /// backend (`crate::container_mounts`,
560    /// <https://github.com/rrnewton/hermit/issues/4213>). hermit-cli builds it
561    /// once, in the completed container namespace before the guest starts,
562    /// or, for `--no-namespace` and a library caller on KVM that built no
563    /// container, in the namespace the guest runs in (its launch mounts and
564    /// base mountpoints); a recording carries it, so its replay renders the
565    /// recorded table. `None` (a library caller on a backend other than KVM,
566    /// whose command may chroot or mount where Hermit cannot see, and every
567    /// older configuration) leaves both files to the kernel, sanitized as
568    /// before. Runtime provenance, not a user option.
569    #[serde(default)]
570    #[clap(skip)]
571    pub container_mount_table: Option<crate::container_mounts::ContainerMountTable>,
572
573    /// The guest path of the empty, read-only directory hermit-cli mounted in
574    /// the guest's container for Detcore's placeholder directories where the
575    /// guest's procfs is not the kernel's (KVM: the backend provides process
576    /// signal control, `procfs_inode::tool_can_name_guest_descriptors`): a
577    /// tmpfs at `/proc/fs/nfsd`, procfs's own permanently empty mount point,
578    /// which Detcore's `--proc` level table hides from the guest at every
579    /// level, and which the guest's mount table leaves out. Each descriptor
580    /// Detcore serves there for `/proc` itself or the caller's own
581    /// `/proc/<pid>/fd` is open on this directory, and Detcore answers every
582    /// call through it from its model; an open, stat or statx of the guest's
583    /// that reaches the directory otherwise fails as `/proc/fs` does. `None`
584    /// where the guest's procfs is the kernel's (ptrace, the in-guest
585    /// backends: nothing is mounted, see [`Self::container_procfs`]), and
586    /// where hermit-cli mounted none or a later mount covers it:
587    /// `--no-namespace`, `--namespace-only`, record and replay, a user mount
588    /// over the placeholder, every library caller, and every older
589    /// configuration. On KVM, Detcore then serves no placeholder directory,
590    /// and an open of `/proc` or a descriptor directory is refused (`EACCES`).
591    /// Runtime provenance, not a user option.
592    #[serde(default)]
593    #[clap(skip)]
594    pub directory_placeholder: Option<PathBuf>,
595
596    /// Whether the guest runs in hermit-cli's container, whose procfs mount
597    /// at `/proc` is its own (every container hermit-cli runs a guest in, not
598    /// `--no-namespace`, not a library caller, not an older configuration).
599    /// Where the guest's procfs is the kernel's (ptrace), Detcore serves
600    /// `/proc` itself and the caller's own `/proc/<pid>/fd` then, each on a
601    /// descriptor on that procfs's root, and serves neither without it.
602    /// Runtime provenance, not a user option.
603    #[serde(default)]
604    #[clap(skip)]
605    pub container_procfs: bool,
606
607    /// Sequentialize thread execution deterministically.
608    #[clap(long)]
609    pub sequentialize_threads: bool,
610
611    /// Choose which side of an ordinary fork/clone runs first after the child is registered.
612    /// Random choices are deterministic under `--sched-seed`.
613    #[serde(default)]
614    #[clap(long, default_value = "child", value_name = "child|parent|random")]
615    pub runs_post_fork: RunsPostFork,
616
617    /// Use the optimized partial syscall subscription set instead of intercepting every syscall.
618    /// This permits unlisted syscalls to bypass Detcore and therefore weakens deterministic
619    /// accounting; leave it disabled for fail-closed execution.
620    #[serde(default)]
621    #[clap(long)]
622    pub passthru_opt: bool,
623
624    /// In chaos mode, uses much cheaper approximate preemption timers.  Only makes sense
625    /// when recording preemptions for later (precise) replay.
626    #[clap(long)]
627    pub imprecise_timers: bool,
628
629    /// Stop the guest at every timeslice end, including one where no other
630    /// guest thread could run. By default Hermit does not stop a thread that
631    /// is the only one able to run before it next enters Detcore (every other
632    /// thread waits on a futex without a timeout, on a child or in
633    /// `rt_sigsuspend`; no timer, sleeper, poll deadline or host call is
634    /// pending): it processes those timeslice ends when the thread next
635    /// enters Detcore, at the same branch counts and in the same order, so the
636    /// schedule, the virtual clock and the chaos draws are the same either
637    /// way and only the stops are saved. This option turns that off, for
638    /// comparison.
639    #[serde(default)]
640    #[clap(long)]
641    pub eager_lone_preemptions: bool,
642
643    /// Turn off Detcore's own copy of each guest thread's signal dispositions
644    /// and masks (A5, docs/SIGNAL_STATE_CHECK.md). The copy is on by default
645    /// where it can run (ptrace with serialized threads): it is checked
646    /// against the kernel, and decisions A5 converted read it in place of the
647    /// kernel's `/proc` view. With it off, those decisions read `/proc`, and
648    /// no handler return is reported. A recorded schedule states the setting
649    /// it ran under, and replaying it under the other setting is refused.
650    #[serde(default = "signal_state_copy_default")]
651    #[clap(long = "no-signal-state-copy", action = clap::ArgAction::SetFalse)]
652    pub signal_state_copy: bool,
653
654    /// Whether a signal delivery that the kernel contradicts, where Detcore's
655    /// copy and the kernel are both certain (A5 step 3.2), ends the run as an
656    /// internal failure (`HERMIT_INTERNAL_FAILURE class=signal-delivery-check`)
657    /// rather than being logged as a divergence that drops the copy. It is a
658    /// Detcore defect either way. `hermit run --strict` sets it; nothing else
659    /// does, and it changes nothing while the copy matches the kernel.
660    ///
661    /// The host sets it. It has no legacy key: [`to_legacy_backend_json`]
662    /// leaves it out and [`from_legacy_backend_json`] reads it back as false.
663    #[serde(default)]
664    #[clap(skip)]
665    pub signal_delivery_check_fatal: bool,
666
667    /// Schedule threads chaotically.
668    ///
669    /// The behavior of this flag is subject to change. Current behavior is to randomize thread
670    /// priorities at every logical timeslice. Other randomization strategies are possible with
671    /// `--sched-heuristic`.
672    ///
673    /// Thread scheduling remains deterministic, determined by the random seed.
674    #[clap(long, long_help = chaos_long_help())]
675    pub chaos: bool,
676
677    /// Draw the mean of each chaos slice from MIN..MAX virtual nanoseconds (both included),
678    /// log-uniformly, instead of using `--max-timeslice` as every slice's mean, and cap every
679    /// slice at MAX. Slices stay exponentially distributed around the drawn mean, so some fall
680    /// below MIN. Races differ in the slice scale they need: some
681    /// need a preemption in a short window, others a long uninterrupted run of one thread, and a
682    /// seed sweep under one timeslice explores only one scale. Under a range every run mixes all
683    /// the scales in it. The draws come from the scheduler seed's chaos stream, so a seed
684    /// reproduces them, and a preemption record pins every slice end as usual. Sets the run's
685    /// `--max-timeslice` to MAX: requires `--chaos` and PMU-backed preemption, and conflicts
686    /// with an explicit `--max-timeslice`, with `--target-timeslice` and with `hermit run
687    /// --chaos-seeded-timeslice`. A small MIN makes runs slower. Example: `1000..200000000`.
688    #[serde(default)]
689    #[clap(
690        long,
691        value_name = "MIN..MAX",
692        requires = "chaos",
693        conflicts_with = "target_timeslice"
694    )]
695    pub chaos_timeslice_range: Option<TimesliceRange>,
696
697    /// How `--chaos-timeslice-range` draws. `slice` (the default) draws each slice's mean from
698    /// the range. `run` instead draws the run's `--max-timeslice` once, log-uniformly from the
699    /// range as a pure function of the scheduler seed, and the run is then an ordinary chaos run
700    /// under it: a sweep's seeds spread over the scales of the range, each seed at one scale.
701    /// That reaches a race needing one scale throughout in more seeds, at a higher cost per run
702    /// for the fine draws. The drawn value is printed in `--summary-json`
703    /// (`preemption.max_timeslice_ns`) and stored in a `--record-preemptions-to` record, from
704    /// which a replay takes it. Without an explicit range, `run` uses
705    /// [`RECOMMENDED_CHAOS_TIMESLICE_RANGE`]; `slice` still requires a range.
706    #[serde(default)]
707    #[clap(
708        long,
709        value_enum,
710        default_value_t,
711        requires = "chaos",
712        requires_if("slice", "chaos_timeslice_range"),
713        conflicts_with = "target_timeslice",
714        long_help = chaos_timeslice_draw_long_help()
715    )]
716    pub chaos_timeslice_draw: ChaosTimesliceDraw,
717
718    /// The `--max-timeslice` this run drew under `--chaos-timeslice-draw=run`, or took from the
719    /// preemption record it replays. `hermit run` sets it, and `max_timeslice` to the same
720    /// value, before the run starts; the run's own preemption record stores it. When set, no
721    /// slice draws its mean from `--chaos-timeslice-range`.
722    #[serde(default)]
723    #[clap(skip)]
724    pub chaos_timeslice: Option<NonZeroU64>,
725
726    /// Uses the `--fuzz-seed` to generate randomness and fuzz nondeterminism in the futex semantics.
727    #[clap(long)]
728    pub fuzz_futexes: bool,
729
730    /// Targeted chaos: bias scheduling toward known concurrency race patterns
731    /// instead of exploring interleavings uniformly. At the scheduler's existing
732    /// nondeterminism points it uses `--fuzz-seed` to (a) deliver a
733    /// process-directed signal to a randomly chosen thread in the group (signal
734    /// timing races) and (b) randomize the requeue position of a force-unblocked
735    /// thread (lock-ordering / wakeup races). Only takes effect with `--chaos`;
736    /// like the rest of chaos mode it remains reproducible under a fixed seed.
737    #[clap(long)]
738    pub chaos_target_races: bool,
739
740    // AUTONOMOUS-BOT-IMPLEMENTED
741    // TODO-HUMAN-REVIEW(PR-1149)
742    // TODO-HUMAN-REVIEW(PR-1151)
743    /// Reproducible per-thread slowdown factors for chaos mode. A factor greater
744    /// than one makes each RCB consume proportionally more virtual time, while a
745    /// factor below one makes it consume less. Thus scheduling deadlines and the
746    /// guest-visible virtual clock describe the same slowed execution rather than
747    /// applying an out-of-band scheduling bias. The factor is a pure function of
748    /// scheduler seed, stable deterministic thread id, and chaos epoch. A fixed
749    /// seed therefore reproduces both timing and interleavings.
750    #[clap(long)]
751    pub chaos_per_thread_slowdown: bool,
752
753    // AUTONOMOUS-BOT-IMPLEMENTED
754    // TODO-HUMAN-REVIEW(PR-1149)
755    // TODO-HUMAN-REVIEW(PR-1151)
756    /// Maximum ratio between the slowest and fastest per-thread slowdown factor
757    /// for `--chaos-per-thread-slowdown`. Each thread's factor is drawn
758    /// log-uniformly from `[1/R, R]` where `R` is this value. Must fit the Q32
759    /// virtual-time representation and be `>= 1.0`; `1.0` disables the spread.
760    #[clap(long, default_value = "10.0", value_name = "double")]
761    pub chaos_slowdown_max_factor: f64,
762
763    // AUTONOMOUS-BOT-IMPLEMENTED
764    // TODO-HUMAN-REVIEW(PR-1151)
765    /// Length of a deterministic slowdown epoch in elapsed per-thread logical
766    /// nanoseconds. At the first scheduler commit at or after each boundary the
767    /// factor is redrawn as `factor(seed, stable_dettid, epoch)`. This is never
768    /// wall time. `0` means one epoch for the entire run, making constant slowdown
769    /// the single-epoch special case. Recorded preemption artifacts carry exact
770    /// epoch transitions and factors for replay. Inert without chaos slowdown.
771    #[clap(long, default_value = "0", value_name = "nanos")]
772    pub chaos_epoch_length_ns: u64,
773
774    /// Record the timing of preemption events for future replay or experimentation.
775    /// This is only useful in chaos modes.
776    #[clap(long)]
777    pub record_preemptions: bool,
778
779    /// Record and replay the complete slice-boundary protocol used by strict
780    /// trace verification. This mode refuses mismatches, extra observations,
781    /// incomplete replay and legacy schedule files; it does not resynchronize.
782    /// Ordinary recording/replay and schedule editing retain their legacy mode.
783    #[serde(default)]
784    #[clap(long)]
785    pub strict_schedule_boundaries: bool,
786
787    /// File to write the record of preemptions (in JSON).  Implies `--record-preemptions`.
788    #[clap(long, value_name = "filepath")]
789    pub record_preemptions_to: Option<PathBuf>,
790
791    /// JSON file to read recorded preemptions from.  When `--chaos` mode is activated, these
792    /// recorded preemption points take the place of randomized scheduling decisions.
793    #[clap(long, value_name = "filepath", conflicts_with = "replay_schedule_from")]
794    pub replay_preemptions_from: Option<PathBuf>,
795
796    /// File to read recorded schedule trace from. This execution will replay the schedule verbatim
797    /// from the file.
798    #[clap(
799        long,
800        value_name = "filepath",
801        conflicts_with = "replay_preemptions_from"
802    )]
803    pub replay_schedule_from: Option<PathBuf>,
804
805    /// If we run out of events while replaying a schedule, treat that as a fatal event and panic,
806    /// rather than continuing execution.
807    #[clap(long)]
808    pub replay_exhausted_panic: bool,
809
810    /// When playing a schedule trace from disk, bail out on the first time we desynchronize from
811    /// the event sequence specified in the trace.
812    #[clap(long)]
813    pub die_on_desync: bool,
814
815    /// The preemption record `--replay-preemptions-from` reads is not the recording of this
816    /// run's schedule but an edited one, for example one `hermit analyze` knocked preemptions
817    /// out of. A thread then can reach recorded slices that have already ended, which a faithful
818    /// replay never does: the replay skips them and goes on with the first recorded slice still
819    /// ahead, at its priority, instead of stopping the run. (At a thread's start the record's
820    /// first priority, which the scheduler has already applied, holds until that slice ends.)
821    #[serde(default)]
822    #[clap(long, requires = "replay_preemptions_from")]
823    pub replay_edited_preemptions: bool,
824    /// Once the schedule replayed from `--replay-schedule-from` runs out,
825    /// order the threads queued in turn at each priority by the virtual time
826    /// each has run since then, least first, and equal run times by least
827    /// recent selection, instead of in queue order. A thread's run time is
828    /// what its own clock has advanced since that point; a thread seen for
829    /// the first time, or runnable again after it was not, starts at no less
830    /// than the least run time of the runnable threads. That is all it
831    /// changes: a selected thread still runs until its slice ends, there is no
832    /// preemption on wakeup, and threads pushed to the front of the queue (a
833    /// timed wake) and pollers keep their places. Without it threads of equal
834    /// priority take turns, so a thread that only computes or reads the clock
835    /// keeps the CPU for a whole timeslice at each committing call of another.
836    /// It reads nothing from the recording and draws nothing at random, so
837    /// the rest of the run is as neutral as without it: what happens there
838    /// depends only on the replayed prefix.
839    #[clap(
840        long,
841        requires = "replay_schedule_from",
842        conflicts_with_all = ["chaos", "replay_exhausted_panic"]
843    )]
844    #[serde(default)]
845    pub replay_continue_fairly: bool,
846
847    /// Given schedule events traced on recording or replaying, print the stack trace at the moment
848    /// after the Nth event in the trace. Optionally, provide an output file into which the stack
849    /// trace will be printed, otherwise it goes to stderr. With a recorded or replayed schedule, N
850    /// counts every traced event, polling retries and events the schedule does not hold included,
851    /// as the desync and missed-stack-trace lines do; the event number a schedule release reports
852    /// leaves polling retries out, so it is not such an index. Under
853    /// `--strict-schedule-boundaries`, N is instead a position in the complete boundary stream,
854    /// `TimesliceEnd` records included: a replay requires it to lie inside the stored stream,
855    /// while a recording counts the stream as it writes it.
856    #[clap(long,
857           short = 's',
858           value_name = "index[,path]",
859           value_parser = parse_index_with_path)]
860    pub stacktrace_event: Vec<(u64, Option<PathBuf>)>,
861
862    /// Internal feature used to signal the guest with SIGINT at every `--stacktrace-event`, this is
863    /// in-lieu of using hermit's internal stacktrace printing facility, to instead have an external
864    /// debugger handle it.  Accepts either signal names or numbers.
865    #[clap(long, value_name = "signame")]
866    pub stacktrace_signal: Option<SigWrapper>,
867
868    /// Write an access trace to this path as the run records its schedule; its end line is written
869    /// when the run ends, and a trace without it was cut short. The trace is JSON Lines with one
870    /// record per syscall (its prehook and posthook positions in the recorded schedule, the thread,
871    /// the thread's syscall count, the arguments, the result, and the resources it used: descriptor
872    /// slots and the open files behind them, file contents, pipes, sockets, futex words, memory
873    /// ranges, timers, threads and processes), one per signal delivery (its code, fault address and
874    /// sender), and the first 256 bytes of each write to fd 1 or 2. It records resource accesses
875    /// beside the schedule for inspection and comparison. An observer: it adds no scheduler request and
876    /// moves no virtual time, so the schedule and the guest's results are those of a run without
877    /// it, except that Hermit's own command line, which a guest can read as PID 1's, shows the
878    /// option, and that the trace file itself is inside the container: it exists from before the
879    /// guest's first instruction and grows during the run, so a guest that lists or watches its
880    /// directory sees it. Needs a recorded schedule (`--record-preemptions-to` or `--record-preemptions`),
881    /// whose positions the records name, sequentialized threads and the ptrace backend. The format
882    /// is `detcore_model::access_trace`.
883    #[serde(default)]
884    #[clap(long = "record-access-trace-to", value_name = "path")]
885    pub access_trace: Option<PathBuf>,
886
887    /// With `--record-access-trace-to`, leave out the records whose last schedule event comes
888    /// before this position. 0, the default, keeps every record. Resource state from before
889    /// the position, such as which open file a descriptor number names, is then only partly
890    /// visible in the trace.
891    #[serde(default)]
892    #[clap(long, value_name = "event", default_value = "0")]
893    pub access_trace_from: u64,
894
895    /// With `--record-access-trace-to`, write at most this many records from
896    /// `--access-trace-from` on. Past it the trace stops, without its end line, so a reader
897    /// sees it cut short; a run whose records all fit still ends with it. Unlimited by
898    /// default.
899    #[serde(default)]
900    #[clap(long, value_name = "records")]
901    pub access_trace_max_records: Option<u64>,
902
903    /// With `--record-access-trace-to`, write at most this many bytes of header and records.
904    /// The record that would pass it is not written: the trace stops there, without its end
905    /// line, so a reader sees it cut short, and Hermit says so once on stderr. Defaults to
906    /// 1073741824 (1 GiB, `detcore_model::access_trace::ACCESS_TRACE_DEFAULT_MAX_BYTES`), about
907    /// three million records at their typical 350 bytes, so a long run cannot fill the disk by
908    /// default; pass a larger count for a longer trace.
909    #[serde(default)]
910    #[clap(long, value_name = "bytes")]
911    pub access_trace_max_bytes: Option<u64>,
912
913    /// **Deprecated:** Print a stacktrace each time the program is preempted.  Only makes sense in `--chaos` mode
914    /// and typically goes with preemption recording/replaying.
915    #[clap(long)]
916    pub preemption_stacktrace: bool,
917
918    /// File to write preemption stacktraces to. Implies `--preemption-stacktrace`. If a
919    /// log file is not specified, preemption stacktraces are printed to stderr by default.
920    #[clap(long, value_name = "filepath")]
921    pub preemption_stacktrace_log_file: Option<PathBuf>,
922
923    /// Enable deterministic IO by reassuring we always read/write the maximum possible bytes
924    /// from IO syscalls. There might be cases that read/write syscalls return less bytes than
925    /// requests. Detcore, makes an effort to request additional bytes until we reach the ones
926    /// requested or EOF.
927    #[clap(long)]
928    pub deterministic_io: bool,
929
930    /// Fail immediately on unsupported syscalls instead of forwarding them.
931    /// Ordinary `hermit run` enables this policy; compatibility requires the
932    /// explicit `--allow-unsupported-syscalls` opt-out.
933    #[clap(long)]
934    pub panic_on_unsupported_syscalls: bool,
935
936    // AUTONOMOUS-BOT-IMPLEMENTED
937    // TODO-HUMAN-REVIEW(PR-644): Review backend-safe fail-closed termination.
938    /// Return a typed Tool error instead of unwinding through a backend callback.
939    #[serde(default)]
940    #[clap(skip)]
941    pub exit_on_unsupported_syscall: bool,
942    // AUTONOMOUS-BOT-IMPLEMENTED
943    // TODO-HUMAN-REVIEW(PR-644): Review process-tree shutdown for ptrace fail-closed mode.
944    /// Terminate the whole tracer when an unsupported syscall is observed.
945    #[serde(default)]
946    #[clap(skip)]
947    pub shutdown_on_unsupported_syscall: bool,
948
949    // AUTONOMOUS-BOT-IMPLEMENTED
950    // TODO-HUMAN-REVIEW(PR-644): Review the internal cross-process warning report channel.
951    /// Internal inherited file descriptor used to aggregate unsupported syscalls.
952    #[serde(default)]
953    #[clap(skip)]
954    pub unsupported_syscall_report_fd: Option<i32>,
955
956    /// Panic when a precise PMU timer overshoots its expected RCB target instead of logging an
957    /// error and continuing through normal timer handling. Intended for Detcore debugging.
958    #[serde(default)]
959    #[clap(
960        long = "panic-on-rbc-overshoot",
961        visible_alias = "panic-on-rcb-overshoot"
962    )]
963    pub panic_on_rcb_overshoot: bool,
964
965    /// **Internal:** Set to `true` if we're inside a UTS namespace.
966    // FIXME: This can be removed once spawn_fn-based tests support namespaces.
967    #[clap(skip)]
968    pub has_uts_namespace: bool,
969
970    /// **Internal:** Print a one-time notice on Hermit's standard error when the guest binds an
971    /// abstract `AF_UNIX` name, or fails to with `EADDRINUSE`. `hermit run` sets it for a run that
972    /// asked for `--network=host` on the ptrace backend: the guest then shares the host's network
973    /// namespace, and with it the abstract names of every program on the host, other Hermit runs
974    /// included, and Detcore runs in Hermit's own process, so the notice reaches Hermit's stderr
975    /// and never the guest's. Nothing the guest observes depends on it. The DBT backend's runtime
976    /// reads the frozen legacy form ([`to_legacy_backend_json`]), which leaves this field out.
977    #[serde(default)]
978    #[clap(skip)]
979    pub notice_host_network_abstract_binds: bool,
980
981    /// How the guest's process and thread IDs are numbered
982    /// ([`GuestIds`](crate::pid::GuestIds)). The host sets it, not a flag:
983    /// `hermit run` from `--no-namespace` and `--pid-base`. Recorded schedules
984    /// store it, and a replay under another numbering is refused.
985    #[serde(default)]
986    #[clap(skip)]
987    pub guest_ids: crate::pid::GuestIds,
988
989    /// **Internal:** For each of the guest's standard streams 0, 1 and 2, the lowest of them that
990    /// is one open file description with it, as `kcmp(KCMP_FILE)` answered for the descriptors
991    /// the launcher hands the guest (Hermit's KVM launch, where the guest's output is not
992    /// captured: the stdin it reserved, and its own standard output and error, which the
993    /// executor passes through). `None` where that is not known (any other launch, a captured or
994    /// replaced stream, or a kernel that refuses kcmp): each stream is then a description of its
995    /// own. Detcore's lock ledger takes the streams of one group as the one description they are
996    /// (package K slice K8-h). The backend's own answer, a provider query naming which guest
997    /// descriptors are one host description, would also cover descriptors no launcher hands
998    /// over; no backend offers one yet. The DBT backend's runtime reads the frozen legacy form
999    /// ([`to_legacy_backend_json`]), which leaves this field out.
1000    #[serde(default)]
1001    #[clap(skip)]
1002    pub standard_stream_aliases: Option<[u8; 3]>,
1003
1004    /// **Internal:** Path to the replay data folder.
1005    #[clap(skip)]
1006    pub replay_data: Option<PathBuf>,
1007
1008    /// Kill all remaining tasks iff daemons are the only ones left.
1009    /// Disabled by default.
1010    #[clap(long)]
1011    pub kill_daemons: bool,
1012
1013    /// Serve the guest to one gdb client on 127.0.0.1:`--gdbserver-port`, and
1014    /// wait for it before the guest runs; attach with
1015    /// `gdb -ex 'target remote 127.0.0.1:PORT' PROGRAM`. The port is bound in the
1016    /// host's network namespace, so the guest keeps the network `--network`
1017    /// gives it, and a `--strict` run can be debugged with every strict refusal
1018    /// in force. The debugger is an input of the run: breakpoints, writes to the
1019    /// guest's memory or registers and the signals it sends all change it. With
1020    /// a gdbserver, Detcore also takes no reports of the guest's signal-handler
1021    /// returns, so a multithreaded guest whose handlers take scheduler turns can
1022    /// be scheduled differently than without one (docs/SIGNAL_STATE_CHECK.md).
1023    /// Only the ptrace backend (the default, and `--backend=e9patch`, whose
1024    /// rewritten guest runs under it) serves gdb: `--backend=kvm`, `dbt`,
1025    /// `sabre`, `liteinst` and `in-guest-trap`, and `--namespace-only`, refuse
1026    /// `--gdbserver` and `--dap-listen`.
1027    #[clap(long)]
1028    pub gdbserver: bool,
1029    /// The loopback TCP port `--gdbserver` serves on; 0 lets the system pick a
1030    /// free one, which the run prints.
1031    #[clap(long, value_name = "uint16", default_value = "1234")]
1032    pub gdbserver_port: u16,
1033
1034    /// Configure the maximum time a guest thread may run without returning to Detcore. This is
1035    /// measured in virtual nanoseconds and enforced with retired conditional branch (RCB)
1036    /// counting. `--preemption-timeout` is retained as a deprecated alias.
1037    ///
1038    /// Set this to `disabled` or `0` to disable PMU-backed preemption. Positive values must be at
1039    /// least one RCB (10 virtual nanoseconds at the default clock multiplier) and require
1040    /// user-space hardware performance counters.
1041    ///
1042    /// Small values are expensive. A thread that computes without returning to Detcore is stopped
1043    /// by a PMU timer event at the end of every timeslice, and on the ptrace backend a precise
1044    /// timer (the default) is placed by stepping the guest through up to the last skid margin of
1045    /// the slice: 100 to 10000 RCBs depending on the processor, or the `--skid-margin` override. A
1046    /// slice no longer than the margin is stepped through whole. A CPU-bound guest can run
1047    /// thousands of times slower than at the default (a pipeline test took 0.3 s at the default and
1048    /// 994 s at 1000), and a `--timeout` that expires may mean a slow run rather than a hung one.
1049    /// `hermit run`, `hermit analyze` and `hermit bisect` say so on stderr when a slice is at most
1050    /// ten skid margins long, so that at least a tenth of it is stepped (1000000 virtual
1051    /// nanoseconds at the default clock multiplier on a processor with a 10000-RCB margin); a
1052    /// preemption replay, whose recording sets the slices, and `--imprecise-timers`, which steps
1053    /// nothing, say nothing. `--summary` reports how many timeslices the run had.
1054    #[serde(alias = "preemption_timeout")]
1055    #[clap(
1056                long,
1057                visible_alias = "preemption-timeout",
1058                value_name = "uint64|'disabled'",
1059                default_value = "200000000",
1060                value_parser = parse_timeslice)]
1061    pub max_timeslice: MaybeTimeslice,
1062
1063    /// Target logical timeslice, in virtual nanoseconds, checked at syscall boundaries and the
1064    /// other points where the guest returns control to Detcore: signals, timer events, and
1065    /// trapped `rdtsc` and `cpuid` instructions unless `--target-timeslice-syscalls-only` is given.
1066    /// This avoids PMU preemption for workloads that enter the kernel frequently. Omit this option
1067    /// to use only `--max-timeslice`.
1068    #[serde(default)]
1069    #[clap(long, value_name = "virtual-nanoseconds")]
1070    pub target_timeslice: Option<NonZeroU64>,
1071
1072    /// Do not end an expired `--target-timeslice` at a trapped `rdtsc` or `cpuid` while a PMU
1073    /// maximum (`--max-timeslice`) is armed; end it at the next syscall, signal or timer event,
1074    /// or at the PMU maximum. A trapped instruction can sit inside a user-space spinlock: QEMU's
1075    /// multi-threaded TCG reads the host TSC while holding its `vm_clock_lock`, and a vCPU that
1076    /// yields there leaves the other vCPUs spinning for whole PMU slices. Has no effect without
1077    /// `--target-timeslice`, or with `--max-timeslice disabled`, where the trap is the only way
1078    /// out of a busy-wait on the TSC.
1079    #[serde(default)]
1080    #[clap(long, requires = "target_timeslice")]
1081    pub target_timeslice_syscalls_only: bool,
1082
1083    /// After a `FUTEX_WAKE` or `FUTEX_WAKE_BITSET` that woke at least one waiter, move the waker to
1084    /// the back of its priority band and have it yield, so that woken threads of its priority run
1085    /// before it continues. Without
1086    /// it, a thread that releases a contended lock keeps running until its slice ends and can
1087    /// re-take the lock many times before the waiter is scheduled: QEMU's multi-threaded TCG vCPUs
1088    /// starve its main loop of the big QEMU lock this way. The yield is a scheduler turn of its
1089    /// own: the waker's timeslice keeps its deadlines. Each extra thread switch is charged
1090    /// scheduler time that the guest can observe (see `--scheduler-turn-cost`), and costs wall
1091    /// time: a two-vCPU QEMU boot took 5% to 48% longer with it. The waker keeps its priority, so a
1092    /// woken thread of a lower priority still runs after it; `--chaos` redraws threads' priorities
1093    /// from their chaos PRNG, which can put a woken thread in a lower band; and a random
1094    /// `--sched-heuristic` does not select first in, first out. In those cases a woken thread is not
1095    /// guaranteed to run first. Applies to the precise futex mode while threads are sequentialized,
1096    /// on the ptrace backend (including e9patch preprocessing, which runs on it); replay applies it
1097    /// as the recorded run did.
1098    #[serde(default)]
1099    #[clap(long)]
1100    pub futex_wake_yields: bool,
1101
1102    /// Hand the turn to a thread polling a pipe or FIFO once another thread's action has readied it
1103    /// (<https://github.com/rrnewton/hermit/issues/4380>), within this many virtual nanoseconds of
1104    /// the acting thread's own time. A write that moves bytes into a scheduler-managed pipe, a read
1105    /// that makes room in one, an explicit close of one end, or an open of a FIFO end readies the
1106    /// threads polling for that: the scheduler restores their priority, and once the acting
1107    /// thread's time is this far past its first readying action, at the next point where it
1108    /// returns to Detcore (a syscall's entry or exit, or a signal or trapped instruction), it is
1109    /// moved behind them and yields, so they run before it continues. A poller is restored to its
1110    /// own priority and the acting thread moves only within its band, so where `--chaos` has drawn
1111    /// a poller a lower priority than the acting thread, that poller still runs after it and is
1112    /// not guaranteed to run first. `0` hands off at the end of
1113    /// the readying syscall. Unset (the default), nothing changes: a polled thread whose peer is
1114    /// ready waits for the acting thread's slice to end, as before. Each hand-off adds at least two
1115    /// charged scheduler turns that the guest can observe as time (see `--scheduler-turn-cost`).
1116    /// A thread that computes after its write without returning to Detcore delays the hand-off
1117    /// until it next does, or its slice ends; an implicit close (an exit, an `O_CLOEXEC` exec)
1118    /// readies nothing; and an end reopened by path (`/proc/self/fd/N`, as `/dev/fd/N` process
1119    /// substitution does) is a channel of its own, so the other side's actions do not ready it. Recorded
1120    /// schedules store the value, and a replay under another value is refused. Refused without
1121    /// sequentialized threads, on the DBT, KVM and LiteInst backends, with `--namespace-only`,
1122    /// `--strace-only` or `--debug-externalize-sockets`, and
1123    /// under a `--sched-heuristic` that does not select by priority; never applied in record mode.
1124    #[serde(default)]
1125    #[clap(long, value_name = "nanos")]
1126    pub pipe_wake_latency: Option<u64>,
1127
1128    /// How a run of consecutive scheduler turns that each end an expired timed wait (a futex wait
1129    /// answered `ETIMEDOUT`, or a sleep already due) is charged while another thread is queued:
1130    /// `once`, one scheduler turn for the whole run, or `each`, a turn per wait. Unset, `each`
1131    /// under `--chaos` or a `--sched-heuristic` that does not select by priority, where the time
1132    /// each charge adds stretches short waits as race search relies on, and `once` otherwise, so
1133    /// that a thread polling with waits shorter than a turn charge blocks and lets other threads
1134    /// run. A replay of a schedule or of preemptions that leaves this unset takes the value its
1135    /// record stores (a record that stores none, written before the option existed or by an
1136    /// `each` run, charged each wait), and an explicit value that disagrees with the record is
1137    /// refused. `hermit analyze` and `hermit bisect` run every trial under the value of the
1138    /// recording they start from, or `each` when analyze records its own chaos target. Under
1139    /// `hermit record` and `hermit replay` every wait is charged, whatever the value.
1140    #[serde(default)]
1141    #[clap(long, value_enum, value_name = "each|once")]
1142    pub expired_wait_charge: Option<ExpiredWaitCharge>,
1143
1144    /// Write a work/span report of the run's schedule to this path as JSON when the run ends
1145    /// normally: work (the sum of every thread's guest CPU time, in virtual nanoseconds), a
1146    /// lower span over the scheduler's certain happens-before edges (program order, spawn, a
1147    /// child's exit to the wait that reaps it, conflicting resources), and an upper span that
1148    /// also orders a thread that waited after everything other threads did meanwhile. The
1149    /// report lists the dependencies it does not model: what a wait waited for and who sent a
1150    /// signal, so the lower span can be optimistic; and what passes without a wait (data, an end
1151    /// of file or a peer's close found on a channel, a pending signal taken by `sigtimedwait`,
1152    /// shared memory, files and devices), which neither span orders; and the CPU time of a call a
1153    /// thread was running outside the schedule when it was killed. An observer: no scheduling
1154    /// decision reads it, so the schedule
1155    /// and the guest's results are those of a run without it, except that Hermit's own command
1156    /// line, which a guest can read as PID 1's, shows the option. Needs sequentialized threads
1157    /// and the ptrace backend. Unset (the default), nothing is gathered.
1158    #[serde(default)]
1159    #[clap(long, value_name = "path")]
1160    pub work_span_report: Option<std::path::PathBuf>,
1161
1162    /// Virtual nanoseconds that each committed scheduler turn adds to the global clock, before
1163    /// `--clock-multiplier` (default 500000). The charge keeps time moving while threads take
1164    /// turns without running. Every thread observes it as a jump in time across a thread switch,
1165    /// so a guest that times short intervals while another thread runs, such as a QEMU guest
1166    /// kernel calibrating its TSC against the emulated PIT, can see a gap far larger than any
1167    /// work done. A smaller charge makes those jumps smaller; a finite poll or futex timeout then
1168    /// takes more turns to expire. The charge, after the multiplier, must be between 1 and
1169    /// 1000000000. If a charged turn would pass the end of representable virtual time, the run
1170    /// stops with HERMIT_VIRTUAL_CLOCK_EXHAUSTED and exit status 125.
1171    #[serde(default)]
1172    #[clap(long, value_name = "virtual-nanoseconds")]
1173    pub scheduler_turn_cost: Option<NonZeroU64>,
1174
1175    /// Charge a scheduler turn that ends a timeslice because its time ran out (a preemption)
1176    /// at most the virtual time its thread ran since its previous turn, instead of the full
1177    /// turn charge (`--scheduler-turn-cost`, default 500000 virtual nanoseconds). Without it,
1178    /// every preemption adds the full charge however short the slice was, so a small
1179    /// `--max-timeslice`, or a short `--chaos` draw, multiplies the guest-visible time of
1180    /// CPU-bound work by about `1 + charge / slice`: 501 times at `--max-timeslice=1000`,
1181    /// enough for a test's own timeouts to expire before it reaches the code under test. With
1182    /// it the factor is at most 2, what a slice one full charge long already costs. A
1183    /// preemption whose thread ran at least one full charge since its previous turn is
1184    /// charged as before; a thread's turns include its syscalls, so one that follows a syscall
1185    /// by less is charged only the time since that syscall, however long the slice. A capped
1186    /// turn is charged at least one virtual nanosecond, unless its full charge after
1187    /// `--clock-multiplier` is zero. An explicit `sched_yield`, and every
1188    /// turn that does not end a timeslice, keep the full charge. This changes what the guest
1189    /// reads from `clock_gettime(CLOCK_MONOTONIC)` and the other virtual clocks, and so when
1190    /// its timers and timeouts expire: recorded schedules store the setting, and a replay
1191    /// under the other is refused.
1192    /// Refused where no timeslice ends by expiry (`--max-timeslice=disabled` without
1193    /// `--target-timeslice`) or no Detcore scheduler orders the threads (`--namespace-only`,
1194    /// `--strace-only`, `--no-sequentialize-threads`), and with the DBT backend; never applied
1195    /// in record mode.
1196    #[serde(default)]
1197    #[clap(long)]
1198    pub cap_preemption_charge: bool,
1199
1200    /// End the run in virtual time: once the run's virtual clock is more than this many virtual
1201    /// nanoseconds past the run's start, Hermit grants no further scheduler turn and the run ends
1202    /// with exit status 124 and the stderr line `HERMIT_RUN_TIMEOUT class=virtual-timeout
1203    /// bound_ns=N`. The clock is Detcore's global virtual time since the run began, the same
1204    /// virtual time `--summary` reports as elapsed (`virttime_elapsed` in `--summary-json`); it is
1205    /// checked each time the scheduler commits it at the start of a turn, before the turn's thread
1206    /// runs, and the check never moves it. A guest that has already exited, or whose remaining
1207    /// processes a committed exit, kill, in-flight exec or fatal signal has marked doomed, is not
1208    /// stopped by it. A failed exec clears its in-flight mark before the old image resumes.
1209    /// Process-group SIGKILL marks are conservative: if a marked process survives a failed group
1210    /// kill, the bound does not stop it. Unlike `--timeout`, a
1211    /// wall-clock bound in seconds that fires wherever the guest has got to when the host's clock
1212    /// runs out (`class=run-timeout`), for a guest whose input all comes from inside the
1213    /// container this bound fires at the same point of the same schedule on every host and in
1214    /// every run, so the output written before it, `--summary` and a `--verify` comparison are
1215    /// reproducible. A stopped run is not a guest result, so `--verify` compares it only under
1216    /// `--verify-allow=failure` or `both`; the default `success` refuses run 1, as it refuses a
1217    /// deadlock under Hermit's model. A thread that polls something outside the container, such
1218    /// as a pipe a host process feeds, advances virtual time with each retry, and the number of
1219    /// retries depends on the host, so such a run can stop at another turn; `--verify
1220    /// --verify-strict` reports that as a divergence. Virtual time does not pass while every thread is blocked in a call
1221    /// outside the container, so bound such a wait with `--timeout`. On the ptrace backend the
1222    /// run is then finished as for a guest that exited: its output is kept and `--summary` says
1223    /// the bound fired. On the kvm backend the run exits 124 without a summary, and `--verify`
1224    /// with the bound is refused there. Needs sequentialized threads and virtual time: refused
1225    /// with `--no-sequentialize-threads`, `--namespace-only`, `--strace-only` or
1226    /// `--no-virtualize-time`, and on the dbt, liteinst, in-guest-trap and sabre backends, where
1227    /// it has not been qualified. Also refused before guest or debugger startup with
1228    /// `--gdbserver` or `--dap-listen`: a debugger can suppress a fatal signal after Detcore
1229    /// marks its process doomed, leaving a survivor outside the bound. Remove the debugger
1230    /// option or the virtual bound, or use `--timeout` for a wall-clock bound instead.
1231    #[serde(default)]
1232    #[clap(long, value_name = "virtual-nanoseconds")]
1233    pub virtual_timeout: Option<NonZeroU64>,
1234
1235    /// Shut down immediately upon SIGINT, rather than letting the guest handle it.
1236    #[clap(long)]
1237    pub sigint_instakill: bool,
1238
1239    /// Warn if binds are non-zero.
1240    #[clap(long)]
1241    pub warn_non_zero_binds: bool,
1242
1243    /// Apply a specialized scheduling heuristic which may help exercise certain bugs.
1244    #[clap(long, default_value = "none", value_name = "str")]
1245    // TODO: Rename this to scheduler_strategy?
1246    pub sched_heuristic: SchedHeuristic,
1247
1248    /// Use this number to seed the PRNG that supplies randomness to the scheduler.
1249    #[clap(long, env = "HERMIT_SCHED_SEED", value_name = "uint64")]
1250    pub sched_seed: Option<u64>,
1251
1252    /// External network record or replay, set by `hermit run --record-networking`
1253    /// or `--replay-networking`; `Off` otherwise, in which case Detcore never
1254    /// consults it. Its perturbation seed has no fallback to `seed` or
1255    /// `sched_seed`.
1256    #[serde(default)]
1257    #[clap(skip)]
1258    pub network_trace: NetworkTraceConfig,
1259
1260    /// Configure the probability for the Sticky Random scheduler to stay in a thread.
1261    /// For value 0.0, we are behaving like Random.
1262    /// For value 1.0, we are behaving like a DFS, where the same thread is
1263    /// always picked as long as it is available in the Run queue. After
1264    /// this thread is exhausted, the next thread will be chosen randomly.
1265    /// For value 0.5, we have a 50/50 chance to pick the same thread.
1266    #[clap(long, default_value = "0.0", value_name = "double")]
1267    pub sched_sticky_random_param: f64,
1268
1269    /// **Internal:** An internal flag for indicating to Detcore whether we are in `hermit record` or
1270    /// `hermit replay` mode.  This is necessary because there are DIFFERENT global
1271    /// invariants in record mode (e.g. files dont exist).  If we move to a chroot model
1272    /// and reproduce more, recording less, then this flag should become obsolete.
1273    #[clap(skip = false)]
1274    pub recordreplay_modes: bool,
1275
1276    /// **Internal:** Set only by `hermit replay`, alongside `recordreplay_modes`. Replay serves
1277    /// socket reads, writes, accepts and connects, and `poll`, `ppoll`, `select` and
1278    /// `pselect6`, from the recording, so they cannot block. They still run as backgrounded
1279    /// blocking I/O, as during recording, but Detcore's scheduler commits no other turn until
1280    /// they finish. Otherwise the descriptor-table changes of different threads could
1281    /// interleave differently from one replay to the next.
1282    ///
1283    /// It never enters the legacy form ([`to_legacy_backend_json`]), so a standalone DBT
1284    /// run's `HERMIT_DBT_DETCONFIG` reads it as false. Record and replay run only on the
1285    /// ptrace backend.
1286    #[serde(default)]
1287    #[clap(skip = false)]
1288    pub replaying: bool,
1289
1290    /// **Internal:** debugging option to stop execution after a specific scheduler commit, aka turn number
1291    /// (non-negative integer). This only makes sense if `--sequentialize-threads` is specified, as the scheduler is otherwise not engaged.
1292    #[clap(long, value_name = "turn_N")]
1293    pub stop_after_turn: Option<u64>,
1294
1295    /// **Internal:** debugging option to stop execution after a scheduler loop iteration (non-negative integer).
1296    /// This only makes sense if `--sequentialize-threads` is specified, as the scheduler is otherwise not engaged.
1297    #[clap(long, value_name = "iter_N")]
1298    pub stop_after_iter: Option<u64>,
1299
1300    /// Send SIGNAL to the run's root process once the scheduler has committed
1301    /// turn N, as a timer expiry is sent (`turn=N:SIGNAL`, SIGNAL a name such
1302    /// as `SIGUSR1` or a number, not SIGKILL or SIGSTOP). It is sent before
1303    /// the next turn is selected, after the first `COMMIT turn M` with M >= N:
1304    /// turn numbers also count the turns the scheduler skips, so if turn N
1305    /// was skipped, after the next commit. The turn is a deterministic point
1306    /// of the run, so a process in the run that waits for the signal acts at
1307    /// the same point every time: for example a controller that stops QEMU and
1308    /// saves a snapshot there. The run itself goes on. Only with
1309    /// `--sequentialize-threads`, where the scheduler numbers turns.
1310    #[clap(long, value_name = "turn=N:SIGNAL", value_parser = parse_signal_at)]
1311    #[serde(default)]
1312    pub signal_at: Option<SignalAt>,
1313
1314    /// **Internal:** Debugging option to treat all sockets as mysterious external, nondeterministic
1315    /// entities, rather than container-internal and determinstically scheduled.
1316    #[clap(long)]
1317    pub debug_externalize_sockets: bool,
1318
1319    /// **Internal:** Debugging option to change how futexes are implemented, either precisely modeled
1320    /// by hermit, by polling the kernel with non-blocking futex operations, or treated as external
1321    /// (nondeterministic) operations which unblock at imprecise times.
1322    #[clap(
1323        long,
1324        value_name = "precise|polling|external",
1325        default_value = "precise"
1326    )]
1327    pub debug_futex_mode: BlockingMode,
1328
1329    /// Do not count the retired conditional branches (RCBs) of each thread towards its logical
1330    /// time.  Instead, count each checkin with the scheduler as a fixed increment to logical time.
1331    /// Even when this option is set, HW RCB performance counters may still be enabled if a
1332    /// max-timeslice is specified.
1333    #[clap(long)]
1334    pub no_rcb_time: bool,
1335
1336    /// An option to enable logging the hash of heap memory maps for the purpose of determinism checking
1337    #[clap(long)]
1338    pub detlog_heap: bool,
1339
1340    /// An option to enable logging the hash of stack memory maps for the purpose of determinism checking
1341    ///
1342    /// THIS HASH COVERS argv AND THE ENVIRONMENT, which the kernel places at the
1343    /// top of the initial process stack. Two runs whose command lines differ by a
1344    /// single character therefore produce different stack hashes from the first
1345    /// sample, even when the command lines are the same LENGTH and every stack
1346    /// address matches. Measured: equal-length-but-different argv diverged the
1347    /// hash 14 records in, while byte-identical argv held it for 5023 records.
1348    ///
1349    /// Holding a run-directory name to a fixed WIDTH is a sufficient control when
1350    /// only addresses matter, and is NOT sufficient here. Comparing two runs
1351    /// under this flag requires byte-identical argv and environment; otherwise
1352    /// the first divergence you find is your own input.
1353    #[clap(long)]
1354    pub detlog_stack: bool,
1355
1356    /// Log a hash of every private, writable anonymous mapping at each syscall, for memory
1357    /// determinism checking: allocator arenas (scudo's, jemalloc's, glibc's mmap chunks), thread
1358    /// stacks and TLS blocks, none of which `--detlog-heap` (the brk heap) or `--detlog-stack`
1359    /// (the main thread's stack) covers. A mapping named by `PR_SET_VMA_ANON_NAME`
1360    /// (`[anon:<name>]`) counts as anonymous. `--detlog-anon=pages` adds a short digest for each
1361    /// 4 KiB page that is not all zero, to find the page where two runs part.
1362    ///
1363    /// ptrace backend only (also with e9patch preprocessing): the mappings come from the guest
1364    /// task's own `/proc/<tid>/maps`, which an in-guest tool shares and a VMM's does not hold.
1365    /// Other backends refuse the flag. EXPENSIVE: every byte of every such mapping is read at
1366    /// every syscall, in 1 MiB pieces, so a guest with a large heap pays for its whole size each
1367    /// time; a mapping over 256 MiB (a sanitizer's shadow, a heap reserved up front) is recorded
1368    /// by its size alone. Reading an untouched page maps the shared zero page there, which
1369    /// `mincore` then reports resident, as with `--detlog-heap` and `--detlog-stack`. A thread
1370    /// stack holds thread IDs and, below its stack pointer, the scratch Hermit writes when it
1371    /// injects a call, so runs whose IDs differ differ there legitimately. Page indices count
1372    /// from the mapping's start, so a mapping that merges or moves renumbers its pages.
1373    #[clap(
1374        long,
1375        value_enum,
1376        value_name = "pages",
1377        num_args = 0..=1,
1378        require_equals = true,
1379        default_missing_value = "mappings"
1380    )]
1381    #[serde(default)]
1382    pub detlog_anon: Option<DetlogAnon>,
1383
1384    /// Log a hash of the guest REGISTER FILE at guest-logical-control points, for determinism
1385    /// checking. stdout, the INFO log, the stack and the heap are all hashed today; the register
1386    /// file is not, so two backends can differ in register state and every existing check still
1387    /// reports parity.
1388    ///
1389    /// SAMPLED ONLY AT GUEST-LOGICAL-CONTROL POINTS -- see `Detcore::detlog_registers`. Registers
1390    /// are NOT sampled inside a tool handler: a backend running its handler in-guest executes code
1391    /// the ptrace reference never executes, so a difference there is correct behaviour, not a
1392    /// determinism bug.
1393    #[clap(long)]
1394    pub detlog_regs: bool,
1395
1396    /// Log a hash of each syscall's OUTPUT BUFFER, taken at the syscall boundary from the
1397    /// address and length in the syscall's own arguments.
1398    ///
1399    /// WHAT IT SEES THAT THE MAPPING HASHES DO NOT. `--detlog-heap` and `--detlog-stack` hash a
1400    /// whole named mapping, so their coverage is decided by where the guest happened to ALLOCATE
1401    /// a buffer. Measured, three runs per cell, same netlink exchange with only the receive
1402    /// buffer's home changed: a `[stack]` buffer is missed by `--detlog-heap`, a `[heap]` buffer
1403    /// is missed by `--detlog-stack`, and a BSS/static or anonymous-`mmap` buffer is missed by
1404    /// BOTH even with both enabled. Anonymous `mmap` is where glibc puts any `malloc` above the
1405    /// 128 KiB `M_MMAP_THRESHOLD`. Reading the extent out of the syscall arguments makes the
1406    /// buffer's home irrelevant.
1407    ///
1408    /// WHY IT IS NOT REDUNDANT WITH `--verify`. A syscall whose buffer is a bare pointer in
1409    /// Reverie prints the ADDRESS, not the contents, so a `recvmsg` returning a stable
1410    /// `Ok(1468)` whose payload varies produces a character-identical record and `--verify`
1411    /// reports `bitwise_parity: true`. 44.1% of the syscalls in a QEMU/Linux boot move bytes
1412    /// through such a buffer.
1413    ///
1414    /// COST is proportional to bytes actually moved, NOT to syscall count or mapping size:
1415    /// ~0.75 s per GB of guest I/O. A QEMU/Linux boot moves 139.1 MB through these buffers,
1416    /// against the 10.9 TB `--detlog-heap` hashes over the same run.
1417    ///
1418    /// NAME IS PROVISIONAL: `io-buffers` is the owner's candidate and is not settled.
1419    ///
1420    /// ON BY DEFAULT. It was opt-in until 2026-08-24, and opt-in made the
1421    /// determinism gate weaker than its name: with the hash absent, the netlink
1422    /// `recvmsg` above compares equal and `--verify` reports success. A check
1423    /// that must be requested is not a standard. The opt-out exists for the
1424    /// deliberate case (bulk I/O where the cost matters and content parity is
1425    /// not the question), not as the ordinary setting.
1426    ///
1427    /// COST OF THE DEFAULT, measured 2026-08-24 on a 316-core x86_64 Linux
1428    /// build host: a typical small test guest pays about ONE MILLISECOND
1429    /// (`/bin/true` 0.029s -> 0.030s, `/bin/ls` 0.041s -> 0.041s, 8 runs each).
1430    /// 64 MiB through `cat` costs +0.07-0.10s in a RELEASE build, which is the
1431    /// ~1.1-1.6 s/GB matching the figure quoted above. The same workload in a
1432    /// DEBUG build costs +3.4s, roughly 50x more, because the hash loop is
1433    /// unoptimized -- so a debug-built node moving tens of megabytes is the one
1434    /// place the default is felt.
1435    #[clap(long = "no-detlog-io-buffers", action = clap::ArgAction::SetFalse)]
1436    pub detlog_io_buffers: bool,
1437
1438    /// Sampling cadence for `--detlog-regs`: hash every Nth guest-logical-control point.
1439    ///
1440    /// COST TIER. 1 (the default) is the FULL tier -- every control point hashed -- and is what a
1441    /// short test should use. Measured cost at this scale is within run-to-run noise: /bin/true
1442    /// (49 control points), `wc -l /etc/passwd` (135) and a 5-iteration shell loop (195) were
1443    /// 0.04-0.07s with the flag on and the same with it off. A larger N is the SPOT-CHECK tier for
1444    /// runs where full hashing is too expensive; it trades detection latency for cost, since a
1445    /// divergence is only seen at the next sampled point. Every emitted line records the tier it
1446    /// was produced under, so a cell can state which tier it met instead of leaving it implicit.
1447    #[clap(long, default_value = "1", value_name = "uint64")]
1448    pub detlog_regs_cadence: u64,
1449
1450    /// Configure a time offset (in seconds) between a container OS considered booted and a guest is executed
1451    /// This primarily affects 'sysinfo' syscall's 'uptime' field reporting
1452    #[clap(long, default_value = "120", value_name = "uint64")]
1453    pub sysinfo_uptime_offset: u64,
1454
1455    /// Configure memory available for the container.  Takes a number of bytes, or shorthand (e.g.
1456    /// "1GB"). Right now this doesn't enforce an upper bound, but does affect the amount of memory
1457    /// reported to the guest.
1458    #[clap(long, default_value = "1GB", value_parser = try_parse_memory, value_name = "bytesize")]
1459    pub memory: u64,
1460
1461    /// Present a machine with this many CPUs, 1 to 128, so that `nproc`,
1462    /// `sysconf(_SC_NPROCESSORS_ONLN/CONF)` and thread pools size themselves
1463    /// to it. Threads still run one at a time. The affinity mask,
1464    /// `getcpu`, CPUID's APIC IDs, `/proc/cpuinfo`, `/proc/stat`, a task's
1465    /// `Cpus_allowed` and `/sys/devices/system/cpu` (its CPU lists, `cpuN`
1466    /// directories, topology and cache files) all describe the same N
1467    /// single-core sockets on one NUMA node. `sched_setaffinity` follows
1468    /// Linux: a mask with none of the N CPUs is EINVAL. The host must list
1469    /// at least N CPUs under `/sys/devices/system/cpu`. Without this flag the
1470    /// guest sees Hermit's historical single CPU, whose `sched_setaffinity`
1471    /// accepts any mask.
1472    #[serde(default)]
1473    #[clap(long, value_name = "count", value_parser = parse_virtual_cpus)]
1474    pub virtual_cpus: Option<NonZeroU32>,
1475
1476    /// How much of `/proc` the guest sees. Every level is the same on every
1477    /// backend and host. `minimal` (the default) shows a small set, modelled
1478    /// on gVisor's; `standard` shows every system-wide name Detcore serves or
1479    /// plans to serve. A system-wide or `/proc/sys` name outside the chosen level
1480    /// does not exist: lookups give ENOENT and listings leave it out. So does
1481    /// a name in a process's or a thread's directory, except a few that Hermit
1482    /// still leaves to the kernel (`mem`, `uid_map`, `gid_map`, `setgroups`),
1483    /// as are whether a process exists and the entries of its `fd`,
1484    /// `fdinfo`, `ns` and `net`. A spelling through a symbolic link or `..` is
1485    /// checked only when it opens a file.
1486    #[serde(default)]
1487    #[clap(
1488        long = "proc",
1489        default_value = "minimal",
1490        value_name = "minimal|standard"
1491    )]
1492    pub proc_level: ProcLevel,
1493
1494    /// Configure extra interrupt points based on thread id and rcb counter. Detcore will raise a precise
1495    /// timer for this RCB whenever it detects that current current thread timeslice intercects any of the
1496    /// interrupt points specified
1497    #[clap(long, value_name = "tid:rcbs", value_parser = try_parse_numbers_with_colon)]
1498    pub interrupt_at: Vec<(DetTid, u64)>,
1499
1500    /// Resolved happens-before program: deterministic ordering edges between
1501    /// anchored events (see `detcore_model::happens_before`). This is populated
1502    /// programmatically by hermit-cli after loading and resolving a
1503    /// `--happens-before` spec against the guest binary; it is not a direct CLI
1504    /// flag and is not serialized (it is reconstructed from the spec file each
1505    /// run, so `#[serde(skip)]` avoids requiring serde on `Sysno`-bearing
1506    /// positions and keeps save-config output stable). The scheduler enforces
1507    /// these edges only when `sequentialize_threads` is set.
1508    #[serde(skip)]
1509    #[clap(skip)]
1510    pub happens_before: Option<HappensBeforeProgram>,
1511
1512    /// Whether Detcore records the host identity of each file the guest
1513    /// opens, sending each record to the global state, which writes the run's
1514    /// records to [`Self::host_input_log`] when the run ends (see
1515    /// [`crate::host_input`]). `hermit run --verify` sets it, so that a
1516    /// divergence caused by a host file replaced during a run can be named.
1517    /// The records are never part of the compared log. Serialized, so that it
1518    /// reaches the Detcore tool on every backend, including one that runs
1519    /// inside the guest and receives this `Config` over RPC; never a CLI flag.
1520    #[serde(default)]
1521    #[clap(skip)]
1522    pub record_host_inputs: bool,
1523
1524    /// Where the global state writes the run's host-input records, one
1525    /// [`crate::host_input::HostInputRecord`] JSON line each and a
1526    /// [`crate::host_input::HostInputLogEnd`] line, when the run ends.
1527    /// `hermit run --verify` sets a private file. A host path, read only by
1528    /// the global state, which every backend that hosts it builds from this
1529    /// `Config` in-process; so set programmatically, like `happens_before`,
1530    /// and never serialized.
1531    #[serde(skip)]
1532    #[clap(skip)]
1533    pub host_input_log: Option<PathBuf>,
1534
1535    /// Captured inputs that the launcher copied into private container files.
1536    /// Only the host global state consumes these identities, just like
1537    /// `host_input_log`; guest-side tools still report the actual open's stat.
1538    /// Never serialized: every backend constructs that global state from this
1539    /// in-process config. Physical identities remain necessary for deferred
1540    /// content checks even when records publish the captured input's origin.
1541    #[serde(skip)]
1542    #[clap(skip)]
1543    pub host_input_snapshots: Vec<crate::host_input::HostInputSnapshot>,
1544
1545    /// A guest address range, `[start, end)`, holding code whose syscalls
1546    /// the backend lets through without Detcore seeing them, which the guest
1547    /// cannot otherwise use without faulting. With
1548    /// [`Self::record_host_inputs`] set, a guest call that may change the
1549    /// memory there (a fixed mapping over it, a protection or advice change
1550    /// or unmapping that covers it, an open of a process-memory file for
1551    /// writing) is reported as a change at `/`, so no host input change is
1552    /// named in that run (see [`crate::host_input`]). The backend that hosts
1553    /// Detcore in-process sets it, so set programmatically, like
1554    /// `happens_before`, and never serialized.
1555    #[serde(skip)]
1556    #[clap(skip)]
1557    pub untraced_code_range: Option<(u64, u64)>,
1558
1559    /// Opt-in capture of a core file for a guest thread killed by a signal
1560    /// whose default action dumps core; `None`, the default, writes nothing.
1561    /// A host path and host-side limits, read only by the global state at a
1562    /// fatal exit stop, so set programmatically, like `host_input_log`, and
1563    /// never serialized.
1564    #[serde(skip)]
1565    #[clap(skip)]
1566    pub fatal_core_capture: Option<FatalCoreCapture>,
1567
1568    /// Whether the in-guest runtime delivers the guest's signals through
1569    /// Detcore's schedule (A5 step 3c.1, gate D35 of the design,
1570    /// <https://github.com/rrnewton/hermit/issues/4485>): while its code runs
1571    /// it blocks every signal it does not reserve, and at each return to
1572    /// guest code it predicts the kernel's deliveries, reports them in two
1573    /// phases, and makes a predicted death a scheduled one. Detcore answers
1574    /// Reverie's `Tool::in_guest_signal_delivery` from it.
1575    ///
1576    /// The host sets it, not a flag, when it prepares an in-guest run on the
1577    /// default route, and only where every condition of the gate holds:
1578    /// site patching off, sequentialized threads, no configuration in which
1579    /// the two-phase protocol cannot be kept (`detcore::two_phase_refusal`),
1580    /// and a scheduler that runs as the init of the guest's PID namespace,
1581    /// which only `hermit run`'s container launch knows. Preparing any run
1582    /// clears it first, so a caller's configuration cannot carry it in; the
1583    /// in-guest loader route never sets it. Every Tool image receives it in
1584    /// the configuration handshake, which guest code cannot change. False,
1585    /// the default, changes nothing. Like `in_guest_reserved_signals`, it
1586    /// has no legacy key.
1587    #[serde(default)]
1588    #[clap(skip)]
1589    pub in_guest_signal_delivery: bool,
1590
1591    /// Whether `--replay-preemptions-from` names a record that carries
1592    /// callback checkpoints, so priority-history replay ends each annotated
1593    /// row at its recorded callback endpoint instead of at the first stop that
1594    /// reaches the row's RCB count
1595    /// (<https://github.com/rrnewton/hermit/issues/4082>).
1596    ///
1597    /// The host sets it, not a flag, from the validated record before the
1598    /// scheduler or any Tool reads the configuration, and refuses a
1599    /// configuration it does not qualify for such a record
1600    /// (`preemption_admission::refuse_unqualified_annotated_replay`). Preparing
1601    /// any run sets it from the record, so a caller's configuration cannot
1602    /// carry it in; Detcore refuses a record whose checkpoints disagree with
1603    /// it. A record so annotated was made with `--record-preemptions`, so the
1604    /// replay observes schedule events as its recording did
1605    /// (`observes_schedevents`). False, the default, changes nothing. Like
1606    /// `in_guest_signal_delivery`, it has no legacy key.
1607    #[serde(default)]
1608    #[clap(skip)]
1609    pub annotated_priority_replay: bool,
1610}
1611
1612/// Where and how much [`Config::fatal_core_capture`] may write.
1613#[derive(Debug, Clone, PartialEq, Eq)]
1614pub struct FatalCoreCapture {
1615    /// Directory receiving the compressed cores. Its regular files, whoever
1616    /// wrote them, count against `max_total_bytes`.
1617    pub dir: PathBuf,
1618    /// Prefix of every file name this run writes, so the run can find and
1619    /// remove its own cores.
1620    pub file_prefix: String,
1621    /// Most bytes one compressed core may occupy.
1622    pub max_core_bytes: u64,
1623    /// Most bytes all regular files in `dir` may occupy together.
1624    pub max_total_bytes: u64,
1625    /// Longest one capture may take before it gives up.
1626    pub time_limit: std::time::Duration,
1627}
1628
1629fn try_parse_numbers_with_colon(from_str: &str) -> anyhow::Result<(DetTid, u64)> {
1630    if let Some((thread_id_str, time_str)) = from_str.split_once(':') {
1631        Ok((
1632            thread_id_str
1633                .parse::<DetTid>()
1634                .map_err(anyhow::Error::msg)?,
1635            time_str.parse::<u64>().map_err(anyhow::Error::msg)?,
1636        ))
1637    } else {
1638        anyhow::bail!(
1639            "unable to parse <thread_id>:<logical_time> from '{}'",
1640            from_str
1641        )
1642    }
1643}
1644
1645fn try_parse_memory(from_str: &str) -> anyhow::Result<u64> {
1646    <bytesize::ByteSize as FromStr>::from_str(from_str)
1647        .map(|res| res.as_u64())
1648        .map_err(anyhow::Error::msg)
1649}
1650
1651/// The most CPUs `--virtual-cpus` presents: the bits of the 128-bit mask
1652/// Detcore's `sched_getaffinity` answers with, whose highest CPU is the
1653/// `kernel_max` (127) it reports.
1654pub const MAX_VIRTUAL_CPUS: u32 = 128;
1655
1656fn parse_virtual_cpus(value: &str) -> Result<NonZeroU32, String> {
1657    value
1658        .parse::<u32>()
1659        .ok()
1660        .filter(|count| (1..=MAX_VIRTUAL_CPUS).contains(count))
1661        .and_then(NonZeroU32::new)
1662        .ok_or_else(|| {
1663            format!(
1664                "expected a CPU count from 1 to {MAX_VIRTUAL_CPUS}, for example --virtual-cpus 4"
1665            )
1666        })
1667}
1668
1669impl Config {
1670    /// How many CPUs the guest sees: `--virtual-cpus`, or Hermit's
1671    /// historical one.
1672    pub fn virtual_cpu_count(&self) -> u32 {
1673        self.virtual_cpus.map_or(1, NonZeroU32::get)
1674    }
1675
1676    /// Whether the scheduler models which thread the kernel gives a signal,
1677    /// from the signal masks it reads from the kernel, and keeps the
1678    /// per-process ledger of signals discarded at send time; the guest side
1679    /// publishes a waiting thread's mask and records handler-restored
1680    /// real-time signals for it (`publish_wait_mask`,
1681    /// `record_handler_restore_mask`). It needs serialized threads, so that a
1682    /// mask it reads cannot change before the kernel acts on it, and a backend
1683    /// whose kernel signal targets a tool can predict
1684    /// (`BackendCapabilities::kernel_signal_targets_are_predictable`).
1685    pub fn models_signal_targets(&self) -> bool {
1686        self.sequentialize_threads && self.backend.kernel_signal_targets_are_predictable
1687    }
1688
1689    /// Whether `signal` is one of [`Config::in_guest_reserved_signals`]. Never
1690    /// signal 0 (an existence check sends nothing), nor a number outside 1 to
1691    /// 64, which Linux refuses with EINVAL before any target is named. Always
1692    /// false while the set is empty, which it is on every backend but the
1693    /// in-guest ones' default route.
1694    pub fn reserves_in_guest_signal(&self, signal: i64) -> bool {
1695        (1..=64).contains(&signal) && self.in_guest_reserved_signals & (1_u64 << (signal - 1)) != 0
1696    }
1697
1698    /// This configuration with `change` applied to its backend capabilities.
1699    /// [`BackendCapabilities`] cannot be built field by field outside Reverie,
1700    /// so a caller that needs to adjust individual facts starts from the value
1701    /// already here.
1702    pub fn with_backend(mut self, change: impl FnOnce(&mut BackendCapabilities)) -> Self {
1703        change(&mut self.backend);
1704        self
1705    }
1706
1707    /// Whether the epoch is the stable library default omitted by `Display`.
1708    pub fn has_default_epoch(&self) -> bool {
1709        self.epoch == DEFAULT_EPOCH_STR.parse::<DateTime<Utc>>().unwrap()
1710    }
1711
1712    /// Replace the stable library/test default with the host wall clock captured
1713    /// by the outer `hermit run` invocation. The caller owns the single
1714    /// host-clock read boundary; all guest clock and metadata observations
1715    /// consume the resulting concrete epoch.
1716    pub fn capture_epoch_from_host_time(&mut self, now: SystemTime) {
1717        self.epoch = epoch_from_host_time(now);
1718    }
1719
1720    /// Virtual nanoseconds one RCB takes at this clock multiplier and, under a
1721    /// chaos per-thread slowdown, at its largest factor: the most any thread's
1722    /// RCB takes, so a timeslice of `n` virtual nanoseconds is at least
1723    /// `n / largest_nanos_per_rcb()` RCBs for every thread.
1724    ///
1725    /// The two multipliers are combined first and then applied to
1726    /// `NANOS_PER_RCB`, the order of the one-RCB PMU epsilon that Detcore's
1727    /// post-hook compares with the end of the maximum
1728    /// (`post_handler_hook_observed` and `callback_checkpoint_expiry_due` in
1729    /// `detcore/src/lib.rs`), which [`Self::minimum_max_timeslice_nanos`]
1730    /// bounds. Floating-point multiplication is not associative: at a clock
1731    /// multiplier of 2.5 and a largest factor of 4.4, `(10 * 2.5) * 4.4` is one
1732    /// unit in the last place above 110, which would raise that minimum to 111.
1733    pub fn largest_nanos_per_rcb(&self) -> f64 {
1734        let slowdown = if self.chaos && self.chaos_per_thread_slowdown {
1735            self.chaos_slowdown_max_factor
1736        } else {
1737            1.0
1738        };
1739        NANOS_PER_RCB * (self.clock_multiplier.unwrap_or(1.0) * slowdown)
1740    }
1741
1742    /// Smallest PMU-backed maximum representable by one RCB at this clock multiplier.
1743    pub fn minimum_max_timeslice_nanos(&self) -> u64 {
1744        (self.largest_nanos_per_rcb().ceil() as u64).max(NANOS_PER_RCB as u64)
1745    }
1746
1747    /// Check invariants that must hold at every execution boundary without mutating the config.
1748    pub fn validate_invariants(&self) {
1749        assert!(self.sched_sticky_random_param >= 0.0);
1750        assert!(self.sched_sticky_random_param <= 1.0);
1751        // AUTONOMOUS-BOT-IMPLEMENTED
1752        // TODO-HUMAN-REVIEW(PR-1149)
1753        assert!(
1754            self.chaos_slowdown_max_factor.is_finite()
1755                && self.chaos_slowdown_max_factor >= 1.0
1756                && self.chaos_slowdown_max_factor <= RcbTimeMultiplier::MAX,
1757            "chaos_slowdown_max_factor must be finite and in [1.0, {}], got {}",
1758            RcbTimeMultiplier::MAX,
1759            self.chaos_slowdown_max_factor
1760        );
1761        if let Some(multiplier) = self.clock_multiplier {
1762            assert!(
1763                multiplier.is_finite() && multiplier > 0.0,
1764                "clock_multiplier must be finite and positive"
1765            );
1766        }
1767        let minimum_max_timeslice = self.minimum_max_timeslice_nanos();
1768        assert!(
1769            self.max_timeslice
1770                .is_none_or(|timeslice| u64::from(timeslice) >= minimum_max_timeslice),
1771            "max_timeslice must be at least one RCB ({} virtual nanoseconds)",
1772            minimum_max_timeslice
1773        );
1774    }
1775
1776    /// Sanity check the flags, and update any wherever flag B is implied by A.
1777    pub fn validate(&mut self) {
1778        self.validate_invariants();
1779
1780        // TODO(T124429978) Restore the eprintln! calls below to tracing::warn! when the tracing
1781        // subscriber is set up early enough for these warnings to print.
1782
1783        if self.record_preemptions_to.is_some() {
1784            self.record_preemptions = true;
1785        }
1786        // TODO: separate out recording flags: --record-preemptions vs --record-schedule-trace
1787        // if self.record_preemptions && !self.chaos {
1788        //     tracing::warn!(
1789        //         "Setting --record-preemptions when not in chaos mode doesn't do anything."
1790        //     );
1791        // }
1792
1793        if self.replay_schedule_from.is_some() && self.replay_preemptions_from.is_some() {
1794            panic!("Cannot set both --replay-preemptions-from and --replay-schedule-from!!");
1795        }
1796
1797        if self.chaos {
1798            self.sequentialize_threads = true;
1799        }
1800
1801        if self.replay_preemptions_from.is_some() && self.imprecise_timers {
1802            eprintln!(
1803                "WARNING: Setting --imprecise timers with --replay-preemptions-from is probably not what you want. They won't replay precisely."
1804            );
1805        }
1806
1807        if self.stop_after_turn.is_some() && !self.sequentialize_threads {
1808            eprintln!(
1809                "WARNING: --stop-after-turn will have no effect if --no-sequentialize-threads is enabled"
1810            );
1811            self.stop_after_turn = None;
1812        }
1813        if self.stop_after_iter.is_some() && !self.sequentialize_threads {
1814            eprintln!(
1815                "WARNING: --stop-after-iter will have no effect if --no-sequentialize-threads is enabled"
1816            );
1817            self.stop_after_iter = None;
1818        }
1819        if self.signal_at.is_some() && !self.sequentialize_threads {
1820            eprintln!(
1821                "WARNING: --signal-at will have no effect if --no-sequentialize-threads is enabled"
1822            );
1823            self.signal_at = None;
1824        }
1825
1826        if self.debug_externalize_sockets && !self.sequentialize_threads {
1827            eprintln!(
1828                "WARNING: --debug-externalize-sockets will have no effect if --no-sequentialize-threads is enabled"
1829            );
1830            self.debug_externalize_sockets = false;
1831        }
1832
1833        if !self.stacktrace_event.is_empty()
1834            && !self.record_preemptions
1835            && self.replay_schedule_from.is_none()
1836        {
1837            eprintln!(
1838                "WARNING: -s/--stacktrace-event has no effect if not recording/replaying events!"
1839            );
1840        }
1841
1842        if self.preemption_stacktrace_log_file.is_some() {
1843            self.preemption_stacktrace = true;
1844        }
1845    }
1846
1847    /// Whether configured preemption can use this backend's timer mechanism.
1848    ///
1849    /// This is independent of RCB accounting and does not certify a clock.
1850    pub fn use_preemption_timer(&self) -> bool {
1851        self.max_timeslice.is_some() && self.backend.delivers_preemption_timer_events
1852    }
1853
1854    /// Whether a thread that is the only one able to run may skip its
1855    /// timeslice-end stops and have them processed when it next enters
1856    /// Detcore (see [`Config::eager_lone_preemptions`]). Only where a
1857    /// timeslice end reads nothing from the guest but its branch count, and
1858    /// nothing but the thread's next entry could act before the next one:
1859    /// precise PMU timers keyed to retired branches at one virtual-time rate,
1860    /// no recorded or replayed schedule or preemptions (they capture guest
1861    /// state at each end), no slice-boundary protocol, no stack traces at
1862    /// preemptions or events, no turn-numbered triggers (`--stop-after-turn`,
1863    /// `--stop-after-iter`, `--signal-at`), no happens-before program, no
1864    /// manual interrupt points and no syscall-boundary deadline
1865    /// (`--target-timeslice`). Never with a gdbserver either: a debugger is
1866    /// served at the stops, and between skipped ends it would see Detcore's
1867    /// slice bookkeeping lag the guest's program counter.
1868    pub fn replays_lone_preemptions(&self) -> bool {
1869        !self.eager_lone_preemptions
1870            && self.use_preemption_timer()
1871            && self.use_rcb_time()
1872            && self.sequentialize_threads
1873            && !self.imprecise_timers
1874            && !self.chaos_per_thread_slowdown
1875            && !self.record_preemptions
1876            && !self.strict_schedule_boundaries
1877            && self.replay_preemptions_from.is_none()
1878            && self.replay_schedule_from.is_none()
1879            && self.target_timeslice.is_none()
1880            && !self.preemption_stacktrace
1881            && self.stacktrace_event.is_empty()
1882            && self.stop_after_turn.is_none()
1883            && self.stop_after_iter.is_none()
1884            && self.signal_at.is_none()
1885            && self.happens_before.is_none()
1886            && self.interrupt_at.is_empty()
1887            && !self.gdbserver
1888    }
1889
1890    /// Should we use RCB in computing logical time?
1891    ///
1892    /// The answer is NO either if `--no-rcb-time` is specified or if HW counters are disabled by
1893    /// setting `--max-timeslice=disabled`.
1894    pub fn use_rcb_time(&self) -> bool {
1895        self.max_timeslice.is_some() && !self.no_rcb_time
1896    }
1897
1898    /// Whether a `FUTEX_WAKE` that woke `woken` waiters makes the waker yield
1899    /// to them (`--futex-wake-yields`).
1900    ///
1901    /// A thread that releases a contended lock usually keeps running: the
1902    /// waiter it woke is queued behind it, and the waker can re-take the lock,
1903    /// uncontended, many times before that waiter runs.
1904    /// glibc and Rust mutexes both let a releasing thread barge back in, and on
1905    /// real hardware the woken thread wins on another CPU within the release
1906    /// window. Under sequentialized threads it never gets one, so QEMU's
1907    /// multi-threaded TCG vCPUs starved its main loop of the big QEMU lock for
1908    /// whole PMU slices at a time
1909    /// (<https://github.com/rrnewton/hermit/issues/3874>). When this holds, the
1910    /// scheduler moves the waker to the back of its priority band, and the
1911    /// waker takes a yield turn at the syscall's post-hook, so woken threads
1912    /// of its priority run before it continues (it keeps its band; `--chaos`
1913    /// can redraw priorities, and a random heuristic does not select first
1914    /// in, first out). The yield leaves the waker's timeslice alone.
1915    ///
1916    /// A thread switch is not free in virtual time: every scheduler decision
1917    /// is charged, so a policy that switches more often moves time that the
1918    /// guest observes. A QEMU guest kernel that calibrates its TSC against the
1919    /// emulated PIT failed that calibration with this policy, because the
1920    /// main loop now ran in the middle of it.
1921    ///
1922    /// The decision reads only the configuration and the wake's result, the
1923    /// number of waiters the scheduler took off the futex in the waker's turn,
1924    /// which is already deterministic. A wake that woke no one keeps the
1925    /// slice. Replay applies the policy exactly as the recorded run did: a
1926    /// preemption record holds slice ends but not this queue order, so the
1927    /// requeue and the yield must happen again for the replayed run to reach
1928    /// the same schedule. Because the yield consumes no slice end and draws
1929    /// nothing from the thread's chaos PRNG, making it again keeps the replay
1930    /// aligned with its record.
1931    pub fn yields_after_futex_wake(&self, woken: u64) -> bool {
1932        self.futex_wake_yields && woken > 0
1933    }
1934
1935    /// The virtual nanoseconds each committed scheduler turn adds to the
1936    /// global clock: `--scheduler-turn-cost` (default [`NANOS_PER_SCHED`])
1937    /// times `--clock-multiplier`, truncated as it always was. A configured
1938    /// cost must give a charge of at least one nanosecond, because the charge
1939    /// is what advances the clock through turns that run no guest code, such
1940    /// as the retries of a finite poll; a scaled charge of zero would leave
1941    /// such a timeout unable to expire. A configured cost, or its scaled
1942    /// charge, above [`MAX_SCHEDULER_TURN_COST`] is refused too, so that a
1943    /// configured charge never exceeds one second per turn.
1944    pub fn scheduler_turn_charge(&self) -> Result<u64, String> {
1945        let multiplier = self.clock_multiplier.unwrap_or(1.0);
1946        let Some(cost) = self.scheduler_turn_cost else {
1947            return Ok((NANOS_PER_SCHED * multiplier) as u64);
1948        };
1949        let cost = u64::from(cost);
1950        if cost > MAX_SCHEDULER_TURN_COST {
1951            return Err(format!(
1952                "--scheduler-turn-cost must be at most {MAX_SCHEDULER_TURN_COST} virtual \
1953                 nanoseconds (received {cost})"
1954            ));
1955        }
1956        let charge = cost as f64 * multiplier;
1957        if !(1.0..=MAX_SCHEDULER_TURN_COST as f64).contains(&charge) {
1958            return Err(format!(
1959                "--scheduler-turn-cost {cost} at --clock-multiplier {multiplier} charges {charge} \
1960                 virtual nanoseconds per scheduler turn; the charge must be at least 1, so that \
1961                 timed waits keep advancing, and at most {MAX_SCHEDULER_TURN_COST}"
1962            ));
1963        }
1964        Ok(charge as u64)
1965    }
1966
1967    /// The `--pipe-wake-latency` in effect (<https://github.com/rrnewton/hermit/issues/4380>): the
1968    /// option's value in run mode, with sequentialized threads and a heuristic that selects by
1969    /// priority; `None` everywhere else, so nothing of the hand-off runs.
1970    pub fn pipe_wake_latency_in_effect(&self) -> Option<u64> {
1971        self.pipe_wake_latency.filter(|_| {
1972            self.use_nonblocking_sockets()
1973                && !self.recordreplay_modes
1974                && self.sched_heuristic.is_priority_ordered()
1975        })
1976    }
1977
1978    /// Whether a run of consecutive turns that each end an expired timed wait is charged one
1979    /// scheduler turn (`--expired-wait-charge`): the explicit value, or by default outside race
1980    /// search (`--chaos`, or a `--sched-heuristic` that does not select by priority). Never under
1981    /// `hermit record` and `hermit replay`. A replay that leaves the option unset takes its
1982    /// record's value instead (`Scheduler::new`).
1983    pub fn expired_wait_runs_charged_once(&self) -> bool {
1984        !self.recordreplay_modes
1985            && match self.expired_wait_charge {
1986                Some(charge) => charge == ExpiredWaitCharge::Once,
1987                None => !self.chaos && self.sched_heuristic.is_priority_ordered(),
1988            }
1989    }
1990
1991    /// Should we convert sockets to SOCK_NONBLOCK?
1992    pub fn use_nonblocking_sockets(&self) -> bool {
1993        self.sequentialize_threads && !self.debug_externalize_sockets
1994    }
1995
1996    /// Should we call trace_schedevent to trace each SchedEvent?
1997    /// This applies to both record and replay for scheduled events.
1998    pub fn should_trace_schedevent(&self) -> bool {
1999        self.record_preemptions || self.replay_schedule_from.is_some()
2000    }
2001
2002    /// Whether this run produces and traces schedule events: the decision
2003    /// `should_trace_schedevent` makes for recording and schedule replay, and
2004    /// also replay of a callback-annotated priority history
2005    /// (`annotated_priority_replay`). Such a record was made with
2006    /// `--record-preemptions`, so its replay makes every decision this gates
2007    /// as its recording did, whether or not it writes a record, and its
2008    /// callback checkpoints compare against the progress those events count.
2009    pub fn observes_schedevents(&self) -> bool {
2010        self.should_trace_schedevent() || self.annotated_priority_replay
2011    }
2012
2013    /// Why this effective configuration cannot replay a callback-annotated
2014    /// priority history, or `None` when it can. Its row ends are actual
2015    /// callback endpoints at exact RCB counts, guarded one RCB past each: that
2016    /// needs the backend's own Detcore preemption timer events
2017    /// (`delivers_preemption_timer_events`), sequentialized threads (the
2018    /// scheduler that grants each history and acknowledges each row), an
2019    /// active maximum timeslice, precise timers and RCB time. The answer
2020    /// names the first unmet requirement. It reads capabilities, never a
2021    /// backend's name, and never changes the configuration.
2022    pub fn annotated_priority_replay_unqualified(&self) -> Option<&'static str> {
2023        if !self.backend.delivers_preemption_timer_events {
2024            Some("cannot deliver Detcore preemption timer events")
2025        } else if !self.sequentialize_threads {
2026            Some("does not sequentialize threads (--no-sequentialize-threads)")
2027        } else if self.max_timeslice.is_none() {
2028            Some("has its preemption timer disabled (--max-timeslice=disabled)")
2029        } else if self.imprecise_timers {
2030            Some("requests imprecise timers (--imprecise-timers)")
2031        } else if self.no_rcb_time {
2032            Some("requests logical time without RCBs (--no-rcb-time)")
2033        } else {
2034            None
2035        }
2036    }
2037
2038    /// Returns manual interuption points for a given thread
2039    pub fn interrupts_for_thread(&self, thread_id: DetTid) -> BTreeSet<u64> {
2040        self.interrupt_at
2041            .iter()
2042            .filter_map(|(tid, time)| {
2043                if tid.eq(&thread_id) {
2044                    Some(*time)
2045                } else {
2046                    None
2047                }
2048            })
2049            .collect::<BTreeSet<u64>>()
2050    }
2051}
2052
2053impl fmt::Display for Config {
2054    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2055        if !self.virtualize_time {
2056            write!(f, " --no-virtualize-time")?;
2057        }
2058        if !self.virtualize_cpuid {
2059            write!(f, " --no-virtualize-cpuid")?;
2060        }
2061        if !self.cpu_profile.is_default() {
2062            write!(f, " --cpu-profile={}", self.cpu_profile)?;
2063        }
2064        if !self.virtualize_metadata {
2065            write!(f, " --no-virtualize-metadata")?;
2066        }
2067        if self.passthru_opt {
2068            write!(f, " --passthru-opt")?;
2069        }
2070        match self.runs_post_fork {
2071            RunsPostFork::Child => {}
2072            RunsPostFork::Parent => write!(f, " --runs-post-fork=parent")?,
2073            RunsPostFork::Random => write!(f, " --runs-post-fork=random")?,
2074        }
2075        if !self.has_default_epoch() {
2076            write!(f, " --epoch={}", self.epoch.to_rfc3339())?;
2077        }
2078        if self.seed != 0 {
2079            write!(f, " --seed={}", self.seed)?;
2080        }
2081
2082        if let Some(rng_seed) = self.rng_seed {
2083            write!(f, " --rng-seed={}", rng_seed)?;
2084        }
2085        if let Some(fuzz_seed) = self.fuzz_seed {
2086            write!(f, " --fuzz-seed={}", fuzz_seed)?;
2087        }
2088
2089        if self.fuzz_futexes {
2090            write!(f, " --fuzz-futexes")?;
2091        }
2092        if self.chaos_target_races {
2093            write!(f, " --chaos-target-races")?;
2094        }
2095        // AUTONOMOUS-BOT-IMPLEMENTED
2096        // TODO-HUMAN-REVIEW(PR-1149)
2097        if self.chaos_per_thread_slowdown {
2098            write!(f, " --chaos-per-thread-slowdown")?;
2099            write!(
2100                f,
2101                " --chaos-slowdown-max-factor={}",
2102                self.chaos_slowdown_max_factor
2103            )?;
2104            // AUTONOMOUS-BOT-IMPLEMENTED
2105            // TODO-HUMAN-REVIEW(PR-1151)
2106            if self.chaos_epoch_length_ns > 0 {
2107                write!(f, " --chaos-epoch-length-ns={}", self.chaos_epoch_length_ns)?;
2108            }
2109        }
2110        if let Some(m) = self.clock_multiplier {
2111            write!(f, " --clock-multiplier={}", m)?;
2112        }
2113        if self.imprecise_timers {
2114            write!(f, " --imprecise-timers")?;
2115        }
2116        if self.eager_lone_preemptions {
2117            write!(f, " --eager-lone-preemptions")?;
2118        }
2119        if !self.signal_state_copy {
2120            write!(f, " --no-signal-state-copy")?;
2121        }
2122        if self.chaos {
2123            write!(f, " --chaos")?;
2124        }
2125        if let Some(range) = self.chaos_timeslice_range {
2126            write!(f, " --chaos-timeslice-range={range}")?;
2127        }
2128        if self.chaos_timeslice_draw == ChaosTimesliceDraw::Run {
2129            write!(f, " --chaos-timeslice-draw=run")?;
2130        }
2131        if self.strict_schedule_boundaries {
2132            write!(f, " --strict-schedule-boundaries")?;
2133        }
2134        if self.record_preemptions {
2135            write!(f, " --record-preemptions")?;
2136        }
2137
2138        if let Some(p) = &self.record_preemptions_to {
2139            let s = p.to_str().expect("valid unicode path");
2140            write!(f, " --record-preemptions-to={}", shell_words::quote(s))?;
2141        }
2142        if let Some(p) = &self.replay_preemptions_from {
2143            let s = p.to_str().expect("valid unicode path");
2144            write!(f, " --replay-preemptions-from={}", shell_words::quote(s))?;
2145        }
2146        if let Some(p) = &self.replay_schedule_from {
2147            let s = p.to_str().expect("valid unicode path");
2148            write!(f, " --replay-schedule-from={}", shell_words::quote(s))?;
2149        }
2150        if self.replay_exhausted_panic {
2151            write!(f, " --replay-exhausted-panic")?;
2152        }
2153        if self.die_on_desync {
2154            write!(f, " --die-on-desync")?;
2155        }
2156        if self.replay_edited_preemptions {
2157            write!(f, " --replay-edited-preemptions")?;
2158        }
2159        if self.replay_continue_fairly {
2160            write!(f, " --replay-continue-fairly")?;
2161        }
2162        for (index, path) in &self.stacktrace_event {
2163            write!(f, " --stacktrace-event={}", index)?;
2164            if let Some(p) = path {
2165                let s = p.to_str().expect("valid unicode path");
2166                write!(f, ",{}", shell_words::quote(s))?;
2167            }
2168        }
2169        if let Some(p) = &self.access_trace {
2170            let s = p.to_str().expect("valid unicode path");
2171            write!(f, " --record-access-trace-to={}", shell_words::quote(s))?;
2172        }
2173        if self.access_trace_from != 0 {
2174            write!(f, " --access-trace-from={}", self.access_trace_from)?;
2175        }
2176        if let Some(records) = self.access_trace_max_records {
2177            write!(f, " --access-trace-max-records={}", records)?;
2178        }
2179        if let Some(bytes) = self.access_trace_max_bytes {
2180            write!(f, " --access-trace-max-bytes={}", bytes)?;
2181        }
2182        if self.preemption_stacktrace {
2183            write!(f, " --preemption-stacktrace")?;
2184        }
2185        if self.panic_on_unsupported_syscalls {
2186            write!(f, " --panic-on-unsupported-syscalls")?;
2187        }
2188        if self.panic_on_rcb_overshoot {
2189            write!(f, " --panic-on-rbc-overshoot")?;
2190        }
2191        if self.kill_daemons {
2192            write!(f, " --kill-daemons")?;
2193        }
2194        if self.gdbserver {
2195            write!(f, " --gdbserver")?;
2196        }
2197        if self.gdbserver_port != /* default */ 1234u16 {
2198            write!(f, " --gdbserver-port={}", self.gdbserver_port)?;
2199        }
2200        match &self.max_timeslice {
2201            // The range sets the maximum itself (its MAX, or the value it
2202            // drew or a replayed record gave), and refuses an explicit one.
2203            Some(x)
2204                if self.chaos_timeslice_range.is_some()
2205                    && (self.chaos_timeslice_range.map(|range| range.max) == Some(*x)
2206                        || self.chaos_timeslice == Some(*x)) => {}
2207            Some(x) => {
2208                if *x != NonZeroU64::new(200_000_000).unwrap() {
2209                    write!(f, " --max-timeslice={}", x)?;
2210                }
2211            }
2212            None => {
2213                write!(f, " --max-timeslice=disabled")?;
2214            }
2215        }
2216        if let Some(target_timeslice) = self.target_timeslice {
2217            write!(f, " --target-timeslice={}", target_timeslice)?;
2218        }
2219        if self.target_timeslice_syscalls_only {
2220            write!(f, " --target-timeslice-syscalls-only")?;
2221        }
2222        if self.futex_wake_yields {
2223            write!(f, " --futex-wake-yields")?;
2224        }
2225        if let Some(latency) = self.pipe_wake_latency {
2226            write!(f, " --pipe-wake-latency={}", latency)?;
2227        }
2228        if let Some(charge) = self.expired_wait_charge {
2229            write!(f, " --expired-wait-charge={}", charge.as_str())?;
2230        }
2231        if let Some(report) = &self.work_span_report {
2232            write!(f, " --work-span-report={}", report.display())?;
2233        }
2234        if let Some(cost) = self.scheduler_turn_cost {
2235            write!(f, " --scheduler-turn-cost={}", cost)?;
2236        }
2237        if self.cap_preemption_charge {
2238            write!(f, " --cap-preemption-charge")?;
2239        }
2240        if let Some(bound) = self.virtual_timeout {
2241            write!(f, " --virtual-timeout={}", bound)?;
2242        }
2243        if self.sigint_instakill {
2244            write!(f, " --sigint-instakill")?;
2245        }
2246        if self.warn_non_zero_binds {
2247            write!(f, " --warn-non-zero-binds")?;
2248        }
2249        match &self.sched_heuristic {
2250            SchedHeuristic::None => {}
2251            SchedHeuristic::ConnectBind => {
2252                write!(f, " --sched-heuristic=connectbind")?;
2253            }
2254            SchedHeuristic::Random => {
2255                write!(f, " --sched-heuristic=random")?;
2256            }
2257            SchedHeuristic::StickyRandom => {
2258                write!(f, " --sched-heuristic=stickyrandom")?;
2259            }
2260        }
2261        if let Some(s) = self.sched_seed {
2262            write!(f, " --sched-seed={}", s)?;
2263        }
2264        if self.sched_sticky_random_param != 0.0 {
2265            write!(
2266                f,
2267                " --sched-sticky-random-param={}",
2268                self.sched_sticky_random_param
2269            )?;
2270        }
2271        if let Some(t) = self.stop_after_turn {
2272            write!(f, " --stop-after-turn={}", t)?;
2273        }
2274        if let Some(i) = self.stop_after_iter {
2275            write!(f, " --stop-after-iter={}", i)?;
2276        }
2277        if let Some(at) = self.signal_at {
2278            write!(f, " --signal-at={}", at)?;
2279        }
2280        if self.debug_externalize_sockets {
2281            write!(f, " --debug-externalize-sockets")?;
2282        }
2283        match &self.debug_futex_mode {
2284            BlockingMode::External => {
2285                write!(f, " --debug-futex-mode=external")?;
2286            }
2287            BlockingMode::Polling => {
2288                write!(f, " --debug-futex-mode=polling")?;
2289            }
2290            BlockingMode::Precise => { /* default */ }
2291        }
2292        if self.no_rcb_time {
2293            write!(f, " --no-rcb-time")?;
2294        }
2295        if self.detlog_heap {
2296            write!(f, " --detlog-heap")?;
2297        }
2298        if self.detlog_stack {
2299            write!(f, " --detlog-stack")?;
2300        }
2301        match self.detlog_anon {
2302            Some(DetlogAnon::Mappings) => write!(f, " --detlog-anon")?,
2303            Some(DetlogAnon::Pages) => write!(f, " --detlog-anon=pages")?,
2304            None => {}
2305        }
2306        if self.detlog_regs {
2307            write!(f, " --detlog-regs")?;
2308        }
2309        if self.detlog_regs_cadence != /* default */ 1 {
2310            write!(f, " --detlog-regs-cadence={}", self.detlog_regs_cadence)?;
2311        }
2312        if !self.detlog_io_buffers {
2313            write!(f, " --no-detlog-io-buffers")?;
2314        }
2315        if self.sysinfo_uptime_offset != /* default */ 120 {
2316            write!(f, " --sysinfo-uptime-offset={}", self.sysinfo_uptime_offset)?;
2317        }
2318        if self.memory != 1_000_000_000 {
2319            write!(f, " --memory={}", self.memory)?;
2320        }
2321        if let Some(cpus) = self.virtual_cpus {
2322            write!(f, " --virtual-cpus={cpus}")?;
2323        }
2324        match self.proc_level {
2325            ProcLevel::Minimal => {}
2326            ProcLevel::Standard => write!(f, " --proc=standard")?,
2327        }
2328        for (tid, rcb) in &self.interrupt_at {
2329            write!(f, " --interrupt-at={}:{}", tid, rcb)?;
2330        }
2331        Ok(())
2332    }
2333}
2334
2335/// How much of `/proc` the guest sees (`--proc`). Each level contains the
2336/// one before it.
2337#[derive(
2338    Debug,
2339    Default,
2340    Clone,
2341    Copy,
2342    Serialize,
2343    Deserialize,
2344    Parser,
2345    PartialEq,
2346    Eq,
2347    PartialOrd,
2348    Ord
2349)]
2350pub enum ProcLevel {
2351    /// The names most programs read, modelled on gVisor's `/proc`.
2352    #[default]
2353    Minimal,
2354    /// Every name Detcore serves or plans to serve.
2355    Standard,
2356}
2357
2358impl FromStr for ProcLevel {
2359    type Err = String;
2360
2361    fn from_str(s: &str) -> Result<Self, Self::Err> {
2362        match s.to_lowercase().as_str() {
2363            "minimal" => Ok(Self::Minimal),
2364            "standard" => Ok(Self::Standard),
2365            _ => Err(format!(
2366                "Expected minimal|standard, could not parse: {:?}",
2367                s
2368            )),
2369        }
2370    }
2371}
2372
2373/// Which side of an ordinary fork/clone receives the first post-registration turn.
2374#[derive(
2375    Debug,
2376    Default,
2377    Clone,
2378    Copy,
2379    Serialize,
2380    Deserialize,
2381    Parser,
2382    PartialEq,
2383    Eq
2384)]
2385pub enum RunsPostFork {
2386    /// Run the newly registered child before its parent resumes.
2387    #[default]
2388    Child,
2389    /// Allow the parent to resume before the newly registered child starts.
2390    Parent,
2391    /// Deterministically choose child-first or parent-first from the scheduler seed.
2392    Random,
2393}
2394
2395impl FromStr for RunsPostFork {
2396    type Err = String;
2397
2398    fn from_str(s: &str) -> Result<Self, Self::Err> {
2399        match s.to_lowercase().as_str() {
2400            "child" => Ok(Self::Child),
2401            "parent" => Ok(Self::Parent),
2402            "random" => Ok(Self::Random),
2403            _ => Err(format!(
2404                "Expected Child|Parent|Random, could not parse: {:?}",
2405                s
2406            )),
2407        }
2408    }
2409}
2410
2411/// How should we handle syscalls which may block, but are internal to the hermit container?
2412/// These syscalls are determinizable, but there are multiple methods of doing so.
2413/// These choices *do not* apply to blocking syscalls that wait for external conditions outside the
2414/// container, such as network responses.
2415///
2416/// Mostly it helps to switch this as: (1) a debugging aid to figure out what is going wrong with a
2417/// given guest program, or (2) in order to find the more performant mode for a given guest program.
2418#[derive(Debug, Clone, Copy, Serialize, Deserialize, Parser, PartialEq, Eq)]
2419pub enum BlockingMode {
2420    /// Handle the internal blocking syscall as though it was external, and unblocks at an
2421    /// unpredictable nondeterministic time.  These blocked threads will be parked in the
2422    /// scheduler's BlockedPool.
2423    ///
2424    /// (TODO: In the future these scheduling decisions will be recorded, and this comment needs to
2425    /// be updated accordingly.)
2426    External,
2427    /// Transform each blocking syscall into non-blocking, and then the scheduler will use that
2428    /// non-blocking form to repeatedly poll for completion of the operation.  When polling occurs
2429    /// (and the backoff policy there on) is decided by the scheduler.
2430    /// See NOTE [Blocking Syscalls via Internal Polling] in this folder.
2431    Polling,
2432    /// Precisely model the blocking and unblocking behavior inside hermit.
2433    /// TODO: This work is not completed yet for all forms of blocking syscalls.
2434    Precise,
2435}
2436
2437impl FromStr for BlockingMode {
2438    type Err = String;
2439
2440    fn from_str(s: &str) -> Result<Self, Self::Err> {
2441        match s.to_lowercase().as_str() {
2442            "polling" => Ok(BlockingMode::Polling),
2443            "precise" => Ok(BlockingMode::Precise),
2444            "external" => Ok(BlockingMode::External),
2445            _ => Err(format!(
2446                "Expected Polling|Precise|External, could not parse: {:?}",
2447                s
2448            )),
2449        }
2450    }
2451}
2452
2453/// How a run of consecutive scheduler turns that each end an expired timed wait is charged while
2454/// another thread is queued (`--expired-wait-charge`).
2455#[derive(
2456    Debug,
2457    Clone,
2458    Copy,
2459    PartialEq,
2460    Eq,
2461    Hash,
2462    Serialize,
2463    Deserialize,
2464    clap::ValueEnum
2465)]
2466#[serde(rename_all = "lowercase")]
2467pub enum ExpiredWaitCharge {
2468    /// Every expired wait is charged a scheduler turn.
2469    Each,
2470    /// A run of expired waits is charged one scheduler turn.
2471    Once,
2472}
2473
2474impl ExpiredWaitCharge {
2475    /// The rule a run follows when it charges a run of expired waits once
2476    /// (`once`) or not (`each`).
2477    pub fn from_charged_once(once: bool) -> Self {
2478        if once { Self::Once } else { Self::Each }
2479    }
2480
2481    /// The option's value as `--expired-wait-charge` spells it.
2482    pub fn as_str(self) -> &'static str {
2483        match self {
2484            Self::Each => "each",
2485            Self::Once => "once",
2486        }
2487    }
2488}
2489
2490#[derive(
2491    Debug,
2492    Default,
2493    Clone,
2494    Copy,
2495    Serialize,
2496    Deserialize,
2497    Parser,
2498    PartialEq,
2499    Eq
2500)]
2501/// Apply a specialized scheduling heuristic which may help exercise certain bugs.
2502pub enum SchedHeuristic {
2503    /// Don't modify the scheduling algorithm.
2504    // TODO: Is the default a round robin?
2505    #[default]
2506    None,
2507    /// Prioritize connect and deprioritize bind to exercise races
2508    ConnectBind,
2509    /// Random: Randomly pick any available thread to make progress.
2510    Random,
2511    /// Sticky Random: Randomly pick any available thread. On the next round,
2512    /// and after the thread is parked, randomly choose if we will continue
2513    /// executing on the same thread, or picking another one.
2514    StickyRandom,
2515    // TODO: make all sleeps "instant".
2516}
2517
2518impl SchedHeuristic {
2519    /// Whether selection follows priority order: `None` and `ConnectBind` pop
2520    /// the first queued entry, so a backed-off poller can be starved; `Random`
2521    /// and `StickyRandom` draw a uniform index into the queue and never are.
2522    /// The scheduler's run queue and `hermit run`'s refusals share this one
2523    /// definition.
2524    pub fn is_priority_ordered(self) -> bool {
2525        matches!(self, SchedHeuristic::None | SchedHeuristic::ConnectBind)
2526    }
2527}
2528
2529// Lame to not derive this, but even `derive_more` won't do enums.
2530impl FromStr for SchedHeuristic {
2531    type Err = String;
2532
2533    fn from_str(s: &str) -> Result<Self, Self::Err> {
2534        match s.to_lowercase().as_str() {
2535            "none" | "roundrobin" => Ok(SchedHeuristic::None),
2536            "connectbind" => Ok(SchedHeuristic::ConnectBind),
2537            "random" => Ok(SchedHeuristic::Random),
2538            "stickyrandom" => Ok(SchedHeuristic::StickyRandom),
2539            _ => Err(format!(
2540                "Expected None|ConnectBind|Random|StickyRandom, could not parse: {:?}",
2541                s
2542            )),
2543        }
2544    }
2545}
2546
2547/// An optional virtual-timeslice duration. `None` disables that preemption mechanism.
2548pub type MaybeTimeslice = Option<NonZeroU64>;
2549
2550/// How `--chaos-timeslice-range` draws (`--chaos-timeslice-draw`).
2551#[derive(
2552    Debug,
2553    Clone,
2554    Copy,
2555    PartialEq,
2556    Eq,
2557    Default,
2558    Serialize,
2559    Deserialize,
2560    clap::ValueEnum
2561)]
2562#[serde(rename_all = "kebab-case")]
2563pub enum ChaosTimesliceDraw {
2564    /// Each chaos slice draws its mean from the range.
2565    #[default]
2566    Slice,
2567    /// The run draws its `--max-timeslice` from the range once.
2568    Run,
2569}
2570
2571/// The range used by `--chaos-timeslice-draw=run` when no range is given.
2572/// One microsecond to 200 milliseconds spans fine preemption windows and
2573/// the ordinary maximum. Plain `--chaos` keeps its fixed maximum: fine draws
2574/// can cost much more, so this discovery mode remains explicit.
2575pub const RECOMMENDED_CHAOS_TIMESLICE_RANGE: TimesliceRange = TimesliceRange {
2576    min: NonZeroU64::new(1_000).unwrap(),
2577    max: NonZeroU64::new(200_000_000).unwrap(),
2578};
2579
2580/// Long help for `--chaos`, including the explicit discovery mode.
2581pub fn chaos_long_help() -> String {
2582    let range = RECOMMENDED_CHAOS_TIMESLICE_RANGE;
2583    format!(
2584        "Schedule threads chaotically.\n\n\
2585         The behavior of this flag is subject to change. Current behavior is to randomize thread \
2586         priorities at every logical timeslice. Other randomization strategies are possible with \
2587         `--sched-heuristic`.\n\n\
2588         Thread scheduling remains deterministic, determined by the random seed.\n\n\
2589         By default, every chaos slice's mean is the run's `--max-timeslice` (200000000 virtual \
2590         nanoseconds unless given), so one seed sweep explores mostly one slice scale. For race \
2591         discovery, sweep seeds with `--chaos-timeslice-draw=run`: each run draws its \
2592         `--max-timeslice` once, log-uniformly from the recommended range {range} virtual \
2593         nanoseconds (or from `--chaos-timeslice-range`), as a pure function of the scheduler \
2594         seed. Fine draws explore short preemption windows, but can take orders of magnitude \
2595         longer: size time limits for them. A replay adopts a recorded drawn maximum without \
2596         drawing again. See `--chaos-timeslice-draw`."
2597    )
2598}
2599
2600/// Long help for the draw policy, sharing the recommended range with resolution.
2601pub fn chaos_timeslice_draw_long_help() -> String {
2602    let range = RECOMMENDED_CHAOS_TIMESLICE_RANGE;
2603    format!(
2604        "How a chaos run draws its timeslice. Requires `--chaos`.\n\n\
2605         `run` (the discovery mode) draws the run's `--max-timeslice` once, log-uniformly from \
2606         `--chaos-timeslice-range`, or from the recommended range {range} virtual nanoseconds \
2607         (1 us to 200 ms) without an explicit range. The draw is a pure function of the scheduler \
2608         seed; the run is then an ordinary chaos run under it, so a seed sweep spreads its seeds \
2609         over the range, each seed at one scale. Without a seed, every run uses seed 0 and draws \
2610         the same value. Fine draws can be much slower: size time limits for the fine end.\n\n\
2611         Fine draws also incur frequent scheduler-turn charges in guest-visible time, which can \
2612         trigger a guest's own watchdogs. See `--cap-preemption-charge` for the explicit option \
2613         that caps expired-timeslice charges. The range parser requires MIN to be at least \
2614         10 virtual nanoseconds and no larger than MAX.\n\n\
2615         `--summary-json` reports the drawn value (`preemption.max_timeslice_ns`), and a \
2616         `--record-preemptions-to` record stores it. A replay adopts a recorded drawn maximum \
2617         without drawing again; it needs no timeslice option. A legacy record with no drawn \
2618         value does not override the requested draw policy. `hermit analyze` and `hermit bisect` \
2619         take a recorded value and refuse `run` in their run arguments, since all trials must \
2620         share one maximum.\n\n\
2621         `slice` (the default) draws each slice's mean from an explicitly supplied range; it is \
2622         what a range alone does. Without a range or an explicit `run` draw, the ordinary fixed \
2623         maximum remains unchanged."
2624    )
2625}
2626
2627/// The bounds of `--chaos-timeslice-range`, in virtual nanoseconds, both
2628/// included: `MIN..MAX`, with `MIN` at least one RCB and no larger than `MAX`.
2629#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
2630#[serde(try_from = "String", into = "String")]
2631pub struct TimesliceRange {
2632    pub min: NonZeroU64,
2633    pub max: NonZeroU64,
2634}
2635
2636impl TimesliceRange {
2637    /// The point of the range a uniform draw `u` in `[0, 1)` maps to under a
2638    /// log-uniform law: `exp(ln MIN + u (ln MAX - ln MIN))`, in virtual
2639    /// nanoseconds. Each decade of the range gets an equal share of draws.
2640    pub fn log_uniform(&self, u: f64) -> f64 {
2641        let (low, high) = ((self.min.get() as f64).ln(), (self.max.get() as f64).ln());
2642        (low + u * (high - low))
2643            .exp()
2644            .clamp(self.min.get() as f64, self.max.get() as f64)
2645    }
2646}
2647
2648impl FromStr for TimesliceRange {
2649    type Err = String;
2650    fn from_str(src: &str) -> Result<Self, Self::Err> {
2651        let (min, max) = src
2652            .split_once("..")
2653            .ok_or_else(|| format!("expected MIN..MAX virtual nanoseconds, got {src:?}"))?;
2654        let bound = |text: &str| -> Result<NonZeroU64, String> {
2655            let value = text
2656                .parse::<u64>()
2657                .map_err(|error| format!("{text:?} is not a number of nanoseconds: {error}"))?;
2658            if value < NANOS_PER_RCB as u64 {
2659                return Err(format!(
2660                    "{value} ns is below one RCB ({} virtual nanoseconds), the shortest PMU-backed \
2661                     timeslice",
2662                    NANOS_PER_RCB as u64
2663                ));
2664            }
2665            Ok(NonZeroU64::new(value).expect("at least one RCB"))
2666        };
2667        let (min, max) = (bound(min)?, bound(max)?);
2668        if min > max {
2669            return Err(format!("the range {min}..{max} is empty: MIN exceeds MAX"));
2670        }
2671        Ok(Self { min, max })
2672    }
2673}
2674
2675// A saved or legacy configuration is checked as the command line is.
2676impl TryFrom<String> for TimesliceRange {
2677    type Error = String;
2678    fn try_from(text: String) -> Result<Self, Self::Error> {
2679        text.parse()
2680    }
2681}
2682
2683impl From<TimesliceRange> for String {
2684    fn from(range: TimesliceRange) -> Self {
2685        range.to_string()
2686    }
2687}
2688
2689impl std::fmt::Display for TimesliceRange {
2690    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2691        write!(f, "{}..{}", self.min, self.max)
2692    }
2693}
2694
2695/// Deprecated name for an optional PMU-backed virtual-timeslice duration.
2696#[deprecated(note = "use MaybeTimeslice")]
2697pub type MaybePreemptionTimeout = MaybeTimeslice;
2698
2699/// A signal sent to the run's root process at a scheduler turn
2700/// (`Config::signal_at`).
2701#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
2702pub struct SignalAt {
2703    /// The scheduler turn after whose commit the signal is sent.
2704    pub turn: u64,
2705    /// The signal's number.
2706    pub signal: i32,
2707}
2708
2709impl std::fmt::Display for SignalAt {
2710    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2711        write!(f, "turn={}:{}", self.turn, self.signal)
2712    }
2713}
2714
2715/// Parse `turn=N:SIGNAL` (`Config::signal_at`).
2716fn parse_signal_at(src: &str) -> Result<SignalAt, String> {
2717    let rest = src
2718        .strip_prefix("turn=")
2719        .ok_or_else(|| format!("expected turn=N:SIGNAL, got {src:?}"))?;
2720    let (turn, signal) = rest
2721        .split_once(':')
2722        .ok_or_else(|| format!("expected turn=N:SIGNAL, got {src:?}"))?;
2723    let turn = turn
2724        .parse::<u64>()
2725        .map_err(|e| format!("the turn in {src:?} is not a non-negative integer: {e}"))?;
2726    let signal = match signal.parse::<i32>() {
2727        Ok(number) => nix::sys::signal::Signal::try_from(number),
2728        Err(_) => signal.parse::<nix::sys::signal::Signal>(),
2729    }
2730    .map_err(|_| format!("{signal:?} in {src:?} names no signal"))?;
2731    // Neither can be caught, blocked or ignored, so Linux acts on them in the
2732    // kernel at once: SIGKILL retires the root outside the scheduler's
2733    // SIGKILL retirement protocol, and SIGSTOP stops it outside the schedule.
2734    if matches!(
2735        signal,
2736        nix::sys::signal::Signal::SIGKILL | nix::sys::signal::Signal::SIGSTOP
2737    ) {
2738        return Err(format!(
2739            "{signal} in {src:?} cannot be caught or blocked; --signal-at sends only \
2740             signals the guest can handle"
2741        ));
2742    }
2743    Ok(SignalAt {
2744        turn,
2745        signal: signal as i32,
2746    })
2747}
2748
2749fn parse_timeslice(src: &str) -> Result<MaybeTimeslice, ParseTimesliceError> {
2750    if let Ok(n) = src.parse::<u64>() {
2751        if n != 0 && n < NANOS_PER_RCB as u64 {
2752            Err(ParseTimesliceError::new(
2753                "PMU-backed timeslices must be at least one RCB (10 virtual nanoseconds)",
2754            ))
2755        } else {
2756            Ok(NonZeroU64::new(n))
2757        }
2758    } else {
2759        match src {
2760            "disabled" => Ok(None),
2761            _ => Err(ParseTimesliceError::new(
2762                "Unable to parse timeslice, expected disabled or a non-negative integer",
2763            )),
2764        }
2765    }
2766}
2767
2768fn parse_index_with_path(src: &str) -> Result<(u64, Option<PathBuf>), String> {
2769    let convert = |e| format!("Failed to parse int index before comma: {e}");
2770    if let Some((index_str, path)) = src.split_once(',') {
2771        let ix = index_str.parse::<u64>().map_err(convert)?;
2772        let pathbuf = PathBuf::from_str(path).map_err(|_| "the impossible happened")?;
2773        Ok((ix, Some(pathbuf)))
2774    } else {
2775        let ix = src.parse::<u64>().map_err(convert)?;
2776        Ok((ix, None))
2777    }
2778}
2779
2780#[derive(Debug)]
2781struct ParseTimesliceError {
2782    details: String,
2783}
2784
2785impl fmt::Display for ParseTimesliceError {
2786    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2787        write!(f, "{}", self.details)
2788    }
2789}
2790
2791impl ParseTimesliceError {
2792    fn new(msg: &str) -> ParseTimesliceError {
2793        ParseTimesliceError {
2794            details: msg.to_string(),
2795        }
2796    }
2797}
2798
2799impl std::error::Error for ParseTimesliceError {
2800    fn description(&self) -> &str {
2801        &self.details
2802    }
2803}
2804
2805/// The default epoch used by DetCore for things like initial file modtimes.
2806///
2807/// N.B. Default to a reasonable date. Some programs (like zip) have trouble with the
2808/// original unix epoch (time zero).
2809pub static DEFAULT_EPOCH_STR: &str = "2026-01-01T00:00:00Z";
2810
2811pub use crate::canonical_test_epoch::CANONICAL_TEST_EPOCH;
2812pub use crate::canonical_test_epoch::CANONICAL_TEST_EPOCH_ARG;
2813
2814/// Convert one invocation's captured host instant without losing subsecond
2815/// precision. CLI and comparison orchestration share this input conversion;
2816/// guest clock progression never reads the host clock through it.
2817pub fn epoch_from_host_time(now: SystemTime) -> DateTime<Utc> {
2818    DateTime::<Utc>::from(now)
2819}
2820
2821impl Config {
2822    /// Construct the config using environment variables only, not CLI args.
2823    pub fn from_env() -> Self {
2824        let args: [OsString; 2] = [
2825            OsString::from("CMD"), // Silly/unused.
2826            OsString::from(format!("--epoch={}", DEFAULT_EPOCH_STR)),
2827        ];
2828        Config::parse_from(args.iter())
2829    }
2830
2831    /// Returns effective "rng-seed" parameter taking in account "seed"
2832    /// parameter if former isn't specified
2833    pub fn rng_seed(&self) -> u64 {
2834        self.rng_seed.unwrap_or(self.seed)
2835    }
2836
2837    /// Returns the fuzz_seed, as specified by the user or defaulting to the primary seed if
2838    /// unspecified.
2839    pub fn fuzz_seed(&self) -> u64 {
2840        self.fuzz_seed.unwrap_or(self.seed)
2841    }
2842
2843    /// Returns effective "sched-seed" parameter taking in account "seed"
2844    /// parameter if former isn't specified
2845    pub fn sched_seed(&self) -> u64 {
2846        self.sched_seed.unwrap_or(self.seed)
2847    }
2848}
2849
2850/// The largest `--scheduler-turn-cost` accepted, in virtual nanoseconds, both
2851/// before and after the clock multiplier scales it: one second per scheduler
2852/// turn.
2853pub const MAX_SCHEDULER_TURN_COST: u64 = 1_000_000_000;
2854
2855/// N.B. we don't want to specify two different notions of "default", so we use the
2856/// `Clap` instance above.
2857/// Environment variable carrying the coordinator's fingerprint to an
2858/// out-of-process plugin: `detcore::config_wire_fingerprint`, which extends
2859/// [`config_wire_fingerprint`] with Detcore's global RPC definitions.
2860///
2861/// Named alongside the other `REVERIE_SABRE_HERMIT_*` launch variables so the
2862/// two travel together and a reader finds them in one place.
2863pub const CONFIG_FINGERPRINT_ENV: &str = "REVERIE_SABRE_HERMIT_CONFIG_FINGERPRINT";
2864
2865/// Environment variable carrying the coordinator's fingerprint
2866/// (`detcore::config_wire_fingerprint`, which extends
2867/// [`config_wire_fingerprint`] with Detcore's global RPC definitions) to the
2868/// in-guest LiteInst runtime (`libdetcore_liteinst.so`), which checks it
2869/// before it decodes the configuration the coordinator sends. Without the
2870/// check, a runtime built from another tree fails with a bare decoding error
2871/// that names neither build (<https://github.com/rrnewton/hermit/issues/3986>).
2872/// The runtime removes it once checked, as it removes its DETLOG variables:
2873/// the value is zeroed in the environment block and the variable is unset, so
2874/// the fingerprint never reaches the guest.
2875pub const LITEINST_CONFIG_FINGERPRINT_ENV: &str = "HERMIT_LITEINST_CONFIG_FINGERPRINT";
2876
2877const CONFIG_DEFINITION_SOURCES: &[&[u8]] = &[
2878    include_bytes!("config.rs"),
2879    // `RawInode`, the host identity the inode RPCs (`DeterminizeInode`,
2880    // `TouchFile`, `SetFileMtime`) carry. Its layout changed from one `u64`
2881    // to a device and an inode number, which neither encoding of `Config`
2882    // shows.
2883    include_bytes!("fd.rs"),
2884    include_bytes!("happens_before.rs"),
2885    include_bytes!("network_trace.rs"),
2886    include_bytes!("pid.rs"),
2887    include_bytes!("schedule.rs"),
2888    include_bytes!("time.rs"),
2889];
2890
2891/// A fingerprint of this build's [`Config`] payload and the configuration and
2892/// clock RPC definitions shared by a plugin and its coordinator.
2893///
2894/// # Why this exists
2895///
2896/// An out-of-process plugin such as `libdetcore_sabre.so` is a separate Cargo
2897/// artifact that lands in the same target directory as `hermit`. Changing
2898/// `Config` or `DetTime` -- or merely switching branches -- leaves the plugin
2899/// stale while everything still *looks* built. `Config` is transferred during
2900/// the RPC handshake, and `DetTime` is the first field in every Detcore request.
2901/// A stale plugin decodes either against the wrong layout and the failure
2902/// surfaces as an opaque codec error: measured, one added `bool` field
2903/// produced `Decode(InvalidBooleanValue(20))` at connect, which points nowhere
2904/// near "your plugin is from a different build" and cost a long diagnosis while
2905/// blocking every SaBRe measurement.
2906///
2907/// # What it measures
2908///
2909/// Two encodings of `Config::default()` and the source definitions for the
2910/// configuration and clock RPC fields are fingerprinted with separate domains:
2911///
2912/// - the exact legacy-bincode bytes used by Reverie RPC, which detect changes
2913///   such as `u32` to `u64` even when both default to JSON number zero; and
2914/// - the JSON encoding, which carries every field name and makes a pure rename
2915///   visible even though bincode is positional; and
2916/// - the source files defining `Config`, its local serialized field types,
2917///   `DetTime`, and the `RawInode` the inode RPCs carry, which catch
2918///   wire-incompatible changes hidden by a default such as `Option<u64>::None`
2919///   to `Option<u32>::None`, or an added clock or inode field that leaves both
2920///   encodings of `Config` unchanged.
2921///
2922/// The source and JSON domains are deliberately stricter than the wire format
2923/// strictly requires. A documentation-only edit in one of these files can
2924/// require rebuilding the plugin; missing a wire-incompatible hidden variant
2925/// can make it decode the handshake or a subsequent request at the wrong offsets.
2926fn config_wire_default() -> Config {
2927    // `Config::default()` is intentionally environment-aware through Clap.
2928    // A coordinator may therefore inherit HERMIT_EPOCH/HERMIT_PRNG,
2929    // HERMIT_SCHED_SEED or HERMIT_CPU_PROFILE even though the separately loaded plugin receives a
2930    // minimal guest environment. Those invocation values are payload data,
2931    // not wire shape, and must not make two artifacts from the same source
2932    // reject one another. Explicit arguments take precedence over Clap's
2933    // environment provider; clear the scheduler override afterward to retain
2934    // the real environment-free default of `None` in the encoded shape.
2935    let mut config = Config::parse_from([
2936        "config-wire-fingerprint",
2937        &format!("--epoch={DEFAULT_EPOCH_STR}"),
2938        "--seed=0",
2939        "--sched-seed=0",
2940        "--cpu-profile=x86-64-v2",
2941    ]);
2942    config.sched_seed = None;
2943    // `BackendCapabilities` is defined in Reverie, outside
2944    // `CONFIG_DEFINITION_SOURCES`, so the source domain cannot see a change to
2945    // it; only its encoding can. Every field encodes in one byte (a bool, or
2946    // the `u8` width of `hardware_watchpoints`), so any constant
2947    // carries every field's width (see
2948    // `config_fingerprint_sees_the_width_of_every_backend_capability`). KVM's
2949    // is kept, as it was when one field was optional and only KVM's set it.
2950    config.backend = BackendCapabilities::KVM;
2951    config
2952}
2953
2954pub fn config_wire_fingerprint() -> String {
2955    let config = config_wire_default();
2956    let wire = bincode::serde::encode_to_vec(&config, bincode::config::legacy())
2957        .expect("canonical Config wire default must encode with Reverie's bincode configuration");
2958    let named_shape = serde_json::to_string(&config)
2959        .expect("canonical Config wire default must encode as JSON for field-name checking");
2960    fingerprint_of_config_material(&wire, &named_shape, CONFIG_DEFINITION_SOURCES)
2961}
2962
2963/// What differs when a coordinator announced `expected` as its
2964/// [`config_wire_fingerprint`] and this build's is another, or `None` when
2965/// they are equal. The in-guest LiteInst runtime reports this before it
2966/// decodes the coordinator's configuration
2967/// (<https://github.com/rrnewton/hermit/issues/3986>).
2968pub fn config_fingerprint_mismatch(expected: &std::ffi::OsStr) -> Option<String> {
2969    let ours = config_wire_fingerprint();
2970    (expected.to_str() != Some(ours.as_str())).then(|| {
2971        format!(
2972            "its configuration and clock RPC fingerprint is {ours}, Hermit's is {}",
2973            expected.to_string_lossy()
2974        )
2975    })
2976}
2977
2978/// Domain-separated FNV-1a over wire bytes, named JSON, and defining source.
2979/// This is a mismatch detector, not a security boundary. Length-prefixing each
2980/// domain prevents two different source-file boundaries from hashing the same
2981/// concatenation.
2982fn fingerprint_of_config_material(
2983    wire: &[u8],
2984    named_shape: &str,
2985    definition_sources: &[&[u8]],
2986) -> String {
2987    let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
2988    let mut update = |domain: u8, bytes: &[u8]| {
2989        for byte in std::iter::once(&domain)
2990            .chain((bytes.len() as u64).to_le_bytes().iter())
2991            .chain(bytes.iter())
2992        {
2993            hash ^= u64::from(*byte);
2994            hash = hash.wrapping_mul(0x1000_0000_01b3);
2995        }
2996    };
2997    update(0, wire);
2998    update(1, named_shape.as_bytes());
2999    for source in definition_sources {
3000        update(2, source);
3001    }
3002    format!("{hash:016x}")
3003}
3004
3005/// `Config::signal_state_copy` for a serialized configuration written before
3006/// the field existed: on, the default.
3007fn signal_state_copy_default() -> bool {
3008    true
3009}
3010
3011impl Default for Config {
3012    fn default() -> Self {
3013        let v: Vec<String> = vec![];
3014        Config::parse_from(v.iter())
3015    }
3016}
3017
3018/// Serializes `config` as JSON in the form it had before [`Config::backend`]
3019/// existed.
3020///
3021/// [`Config::backend`] is left out. In its place, at the same position in
3022/// the object, are the fifteen
3023/// separate backend keys that a serialized configuration carried before,
3024/// under the names and in the order it carried them, each with a value
3025/// computed from `config`. [`Config::record_host_inputs`], which the legacy
3026/// form never had, is left out too, and reads back as false: this form serves
3027/// only DBT, whose launcher collects no host inputs. So is
3028/// [`Config::guest_may_inherit_a_terminal`], which is read only where
3029/// `BackendCapabilities::host_timed_signals_are_physical` holds, false for
3030/// DBT, and [`Config::in_guest_detlog_forward_policy`] and
3031/// [`Config::in_guest_site_patching_off`], which is unset for DBT, and
3032/// [`Config::launch_image`], which came after it. So is [`Config::target_timeslice_syscalls_only`],
3033/// added after this form froze; DBT runs without a PMU maximum, where the
3034/// option has no effect. So are [`Config::futex_wake_yields`],
3035/// [`Config::scheduler_turn_cost`], [`Config::cap_preemption_charge`] and
3036/// [`Config::virtual_timeout`], also added after this form froze, which
3037/// `hermit run` refuses with the DBT backend, and [`Config::replaying`], set
3038/// only by `hermit replay`, which runs only on the ptrace backend. So is
3039/// [`Config::inherited_seccomp`], added after this form froze, which reads
3040/// back as unknown, and [`Config::virtual_cpus`], which `hermit run`
3041/// refuses with the DBT backend and reads back as unset. So is
3042/// [`Config::in_guest_reserved_signals`], empty for DBT (it is set only for
3043/// in-guest runs), which reads back as empty, and
3044/// [`Config::in_guest_refuses_host_timed_producers`] and
3045/// [`Config::in_guest_signal_delivery`], false for DBT, which read back as
3046/// false. So is [`Config::notice_host_network_abstract_binds`], added after this form
3047/// froze, which reads back as false: DBT's runtime then prints no notice for
3048/// an abstract `AF_UNIX` bind. So is [`Config::eager_lone_preemptions`], added after this form froze,
3049/// which has no effect without a PMU timer and reads back as false. So is
3050/// [`Config::standard_stream_aliases`], added after this form froze, set only for KVM, which
3051/// reads back as unset. Every
3052/// other field is serialized exactly as `serde_json::to_string(config)`
3053/// serializes it.
3054///
3055/// A standalone DBT run (one with no coordinator) reads its configuration
3056/// from `HERMIT_DBT_DETCONFIG` in this form or in the versioned envelope of
3057/// [`encode_backend_config_json`]. `hermit run --backend=dbt` writes neither:
3058/// it launches DBT coordinated, and the runtime receives the typed `Config`
3059/// through the coordinator's first RPC frame.
3060///
3061/// Read the result with [`from_legacy_backend_json`], which recovers
3062/// [`Config::backend`] from the fifteen keys. A plain `Config`
3063/// deserialization ignores the keys and gives the field its serde default.
3064pub fn to_legacy_backend_json(config: &Config) -> serde_json::Result<String> {
3065    if config.strict_schedule_boundaries {
3066        return Err(serde::ser::Error::custom(
3067            "strict schedule boundaries cannot be represented by the legacy backend configuration",
3068        ));
3069    }
3070    let mut json = Vec::with_capacity(4096);
3071    let mut serializer = serde_json::Serializer::new(&mut json);
3072    config.serialize(legacy_backend_json::ConfigSerializer {
3073        inner: &mut serializer,
3074        config,
3075    })?;
3076    String::from_utf8(json).map_err(serde::ser::Error::custom)
3077}
3078
3079/// The JSON keys, in order, under which a serialized [`Config`] carried the
3080/// backend facts before they moved into [`Config::backend`], each with the
3081/// value it has for `config`.
3082///
3083/// `backend_is_kvm` named the backend rather than a fact; its value is
3084/// `provides_process_signal_control`, a capability only KVM reports.
3085/// `kvm_shared_dequeue_timers` was a startup policy, not a fact: the
3086/// production launcher, hermit-cli, set it exactly when that capability and
3087/// [`Config::sequentialize_threads`] held. Its value is that conjunction, and
3088/// [`from_legacy_backend_json`] refuses any other value.
3089fn legacy_backend_keys(config: &Config) -> [(&'static str, bool); 15] {
3090    let backend = &config.backend;
3091    [
3092        // No backend installs a CPUID policy of its own any more, so the key
3093        // is always written false; see `from_legacy_backend_json`.
3094        ("cpuid_virtualized_by_backend", false),
3095        // Every backend now takes every madvise advice and refuses what it does
3096        // not implement itself, so the key is always written true, its value on
3097        // DBT, the form's only reader; see `from_legacy_backend_json`.
3098        ("backend_supports_madvise", true),
3099        (
3100            "discover_live_file_metadata",
3101            backend.tool_shares_guest_descriptor_table,
3102        ),
3103        (
3104            "detect_host_clock_futex_timeouts",
3105            backend.guest_clock_reads_bypass_backend,
3106        ),
3107        // Reverie's backend contract now requires every backend to leave the
3108        // syscall-clobbered registers as a native return would (with the
3109        // exceptions it names), so the key is always written false, its value
3110        // on DBT, the form's only reader; see `from_legacy_backend_json`.
3111        ("syscall_clobbers_virtualized_by_backend", false),
3112        (
3113            "cancel_killed_thread_rpcs",
3114            backend.needs_killed_thread_rpc_cancellation,
3115        ),
3116        (
3117            "backend_reports_physical_process_exits",
3118            backend.reports_physical_process_exits,
3119        ),
3120        (
3121            "backend_tracks_process_children",
3122            backend.tracks_process_children,
3123        ),
3124        (
3125            "backend_runs_exit_robust_list",
3126            backend.runs_exit_robust_list,
3127        ),
3128        (
3129            "backend_requires_thread_directed_process_signals",
3130            backend.requires_thread_directed_process_signals,
3131        ),
3132        ("backend_is_kvm", backend.provides_process_signal_control),
3133        (
3134            "kvm_shared_dequeue_timers",
3135            backend.provides_process_signal_control && config.sequentialize_threads,
3136        ),
3137        (
3138            "backend_supports_parked_write_signal_interruption",
3139            backend.supports_parked_write_signal_interruption,
3140        ),
3141        // No backend virtualizes the capability prctls any more; they reach
3142        // the kernel on every backend. Always false; see
3143        // `from_legacy_backend_json`.
3144        ("backend_virtualizes_capability_prctls", false),
3145        // A vfork child may register before or after the parent's call returns
3146        // on every backend now, and Detcore waits for it either way, so the key
3147        // is always written false, its value on DBT, the form's only reader;
3148        // see `from_legacy_backend_json`.
3149        ("backend_defers_vfork_child_registration", false),
3150    ]
3151}
3152
3153/// Parses JSON written by [`to_legacy_backend_json`], or any JSON in the form
3154/// a serialized [`Config`] had before [`Config::backend`] existed, and recovers
3155/// [`Config::backend`] from the fifteen legacy backend keys.
3156///
3157/// The keys are read exactly as they were read while each was a `Config`
3158/// field: a key that is absent takes the default that field had, and a key of
3159/// the wrong type fails the parse. Each key then supplies every capability that
3160/// replaced a read of it, so a decoded configuration behaves as the same JSON
3161/// did before:
3162///
3163/// - `discover_live_file_metadata` supplies the three descriptor and loopback
3164///   facts that were each a read of it;
3165/// - `backend_is_kvm` supplies the two behaviours it selected that remain
3166///   capabilities, `provides_process_signal_control` and
3167///   `emulates_child_waits` (the other three are now answered by the backend
3168///   itself: refusing a non-leader exec is a typed refusal it reports per
3169///   exec, the `gettimeofday` repair runs on every backend through its
3170///   `time(2)` probe and `Guest::storable_memory_ranges`, and the user
3171///   address limit is each guest's own `Guest::user_address_limit` report);
3172/// - every other key supplies the one capability of the same meaning.
3173///
3174/// `backend_supports_madvise` supplies no capability any more: it said the
3175/// tool could forward every madvise advice to the backend, which every backend
3176/// now accepts, refusing an advice it does not implement itself. Since
3177/// `supports_madv_dontneed` was deleted, `MADV_DONTNEED` had been forwarded
3178/// whatever this key said.
3179///
3180/// This inverts [`to_legacy_backend_json`] for every capability value the keys
3181/// can express, which includes every backend's own constant except for
3182/// capabilities added after the legacy form froze: those never enter it, as
3183/// `record_host_inputs` does not, and read back as their default. There are
3184/// thirteen. `process_exits_complete_asynchronously` and
3185/// `virtualizes_guest_sigalrm` are true only for in-guest LiteInst, which
3186/// never travels through this DBT-only form.
3187/// `supports_blocked_wait_signal_interruption`,
3188/// `delivers_preemption_timer_events` and
3189/// `runs_guest_code_between_injections` read back as false, the value DBT
3190/// has and the value their earlier `Config` fields read back as; so do
3191/// `reports_sigreturns` and the four capabilities split out of
3192/// `supports_blocked_wait_signal_interruption` (`kernel_signal_state_is_guests`,
3193/// `kernel_signal_targets_are_predictable`,
3194/// `host_task_reads_reach_guest_threads`, `host_timed_signals_are_physical`),
3195/// false for DBT too. `provides_guest_backtraces` reads back as true, the
3196/// ptrace value it takes from the starting constant and DBT's value; the
3197/// two backends that report false (in-guest LiteInst and KVM) travel
3198/// through this form only inside the versioned envelope.
3199/// `places_orphans` reads back as false and `publishable_parent_death_signals`
3200/// as the empty set, DBT's values; KVM, the one backend that reports
3201/// otherwise, travels through this form only inside the versioned envelope
3202/// too.
3203///
3204/// `cpuid_virtualized_by_backend` supplies no capability either: it said the
3205/// backend installed a CPUID policy of its own, which no backend does any
3206/// more (KVM stopped with Reverie 0f4d8852). It is still written, as false,
3207/// so the legacy form keeps its bytes, and an input that
3208/// says `true` fails the parse rather than being read as a policy no backend
3209/// has.
3210///
3211/// `backend_defers_vfork_child_registration` supplies no capability either: it
3212/// said the backend registered a vfork child only after the parent's call
3213/// returned, which Reverie's backend contract now allows of every backend. It
3214/// is written as false, its value on DBT, so the legacy form keeps its bytes.
3215/// The reader still takes it as one of the fifteen keys, so it keeps its
3216/// position in the array form, must be a boolean and may appear once, and
3217/// ignores its value (KVM wrote true).
3218///
3219/// `backend_supports_madvise` is written as true, its value on DBT, so the
3220/// legacy form keeps its bytes. The reader still takes it as one of the fifteen
3221/// keys, so it keeps its position in the array form, must be a boolean and may
3222/// appear once, and ignores its value (KVM wrote false).
3223///
3224/// `syscall_clobbers_virtualized_by_backend` supplies no capability either: it
3225/// said the backend left the registers a `syscall` instruction clobbers as a
3226/// native return would, which Reverie's backend contract now requires of every
3227/// backend (with the exceptions that contract names). It is written as false,
3228/// its value on DBT, so the legacy form keeps its bytes. The reader still takes it as one of the fifteen keys, so
3229/// it keeps its position in the array form, must be a boolean and may appear
3230/// once, and ignores its value (SaBRe and KVM wrote true).
3231///
3232/// `backend_virtualizes_capability_prctls` supplies no capability either: it
3233/// said the backend answered the capability-set prctls itself, and every
3234/// backend now passes them to the kernel. It is written as false, so the
3235/// legacy form keeps its bytes, and `true` fails the parse.
3236///
3237/// `kvm_shared_dequeue_timers` supplies no capability. It was read into a
3238/// `Config` field that chose the controlled signal path, and that choice is
3239/// now made from the capabilities, [`Config::sequentialize_threads`] and the
3240/// control the backend offers. It is read as it was, a boolean that is false
3241/// when absent, and the parse then fails unless it equals `backend_is_kvm &&
3242/// sequentialize_threads`, the value the production launcher always wrote. An
3243/// accepted input whose capabilities are those of the backend that runs it
3244/// selects the path it selected before. An input that would select another,
3245/// such as `true` without `backend_is_kvm`, or absent beside `backend_is_kvm`
3246/// and sequentialized threads, fails the parse rather than being silently
3247/// reinterpreted. (`backend_is_kvm` given to a backend that offers no control,
3248/// such as DBT, already misdescribed that backend and stays outside this
3249/// guarantee; the production launcher never writes it.) A failed parse is
3250/// handled like any malformed value: the DBT standalone JSON reader falls
3251/// back to its strict default configuration and emits its
3252/// `could not parse HERMIT_DBT_DETCONFIG` warning.
3253///
3254/// Everything else is read as the legacy form was read, by `Config`'s own
3255/// derived deserializer with the legacy keys taken out before it sees them:
3256///
3257/// - In an object, a key the legacy form did not name is ignored, whatever its
3258///   value. `backend` is such a key, so it is ignored rather than read as the
3259///   field it is now; no production writer emits it. A key the legacy form
3260///   named fails the parse when it appears twice or holds the wrong type.
3261/// - A JSON array lists the fields by position, as serde reads any derived
3262///   struct. The positions are those of the legacy form, which are the key
3263///   order of [`to_legacy_backend_json`]: the fifteen legacy keys stand where
3264///   [`Config::backend`] stands. Too many elements fail the parse; too few
3265///   leave the fields
3266///   after them missing, each taking its default or failing the parse as the
3267///   derived deserializer decides for that field.
3268pub fn from_legacy_backend_json(json: &str) -> serde_json::Result<Config> {
3269    let mut deserializer = serde_json::Deserializer::from_str(json);
3270    let (mut config, legacy) = serde::Deserializer::deserialize_struct(
3271        &mut deserializer,
3272        "Config",
3273        &[],
3274        legacy_backend_json::LegacyConfigVisitor,
3275    )?;
3276    deserializer.end()?;
3277    if legacy.cpuid_virtualized_by_backend {
3278        return Err(<serde_json::Error as serde::de::Error>::custom(
3279            "cpuid_virtualized_by_backend is true, but no backend installs a CPUID \
3280             policy of its own; the key must be false or absent",
3281        ));
3282    }
3283    if legacy.backend_virtualizes_capability_prctls {
3284        return Err(<serde_json::Error as serde::de::Error>::custom(
3285            "backend_virtualizes_capability_prctls is true, but no backend answers the \
3286             capability prctls itself; the key must be false or absent",
3287        ));
3288    }
3289    let controlled = legacy.backend_is_kvm && config.sequentialize_threads;
3290    if legacy.kvm_shared_dequeue_timers != controlled {
3291        return Err(<serde_json::Error as serde::de::Error>::custom(
3292            format_args!(
3293                "kvm_shared_dequeue_timers is {} but backend_is_kvm && \
3294             sequentialize_threads is {controlled}; the key is no longer an \
3295             independent setting and must equal that conjunction",
3296                legacy.kvm_shared_dequeue_timers,
3297            ),
3298        ));
3299    }
3300    config.backend = legacy.capabilities();
3301    Ok(config)
3302}
3303
3304/// Preserve the exact backend facts alongside an already encoded legacy config.
3305///
3306/// The legacy form couples some capabilities and omits others. When it can
3307/// express `backend` exactly, return `legacy_json` byte for byte, so a reader
3308/// of the legacy form reads it unchanged. Otherwise wrap it in a versioned
3309/// envelope whose typed backend replaces the legacy projection on decoding.
3310/// Non-backend fields keep the legacy form's semantics in either case.
3311pub fn encode_backend_config_json(
3312    legacy_json: String,
3313    backend: BackendCapabilities,
3314) -> serde_json::Result<String> {
3315    if from_legacy_backend_json(&legacy_json)?.backend == backend {
3316        return Ok(legacy_json);
3317    }
3318    serde_json::to_string(&VersionedBackendConfig {
3319        dbt_config_version: 1,
3320        config: serde_json::from_str(&legacy_json)?,
3321        backend,
3322    })
3323}
3324
3325/// Read a legacy configuration or a versioned envelope with exact backend facts.
3326///
3327/// Any valid legacy object or positional array retains
3328/// [`from_legacy_backend_json`]'s behavior, including ignored unknown fields.
3329/// Otherwise the input must be an envelope: unknown versions, duplicate fields
3330/// and missing typed capabilities are parse errors.
3331/// This parser does not choose the caller's error policy. The admitted DBT
3332/// standalone JSON path retains its strict-default parse fallback; coordinated
3333/// startup receives its authoritative typed Config through the RPC handshake.
3334pub fn decode_backend_config_json(json: &str) -> serde_json::Result<Config> {
3335    // Legacy inputs take precedence: even dbt_config_version was an ignored
3336    // unknown key in that form, and its presence must not change a valid run.
3337    if let Ok(config) = from_legacy_backend_json(json) {
3338        return Ok(config);
3339    }
3340    // Deserialize the original bytes, not a Value map that would erase
3341    // duplicate keys before the typed deserializers can reject them.
3342    let envelope: VersionedBackendConfig<'_> = serde_json::from_str(json)?;
3343    if envelope.dbt_config_version != 1 {
3344        return Err(<serde_json::Error as serde::de::Error>::custom(
3345            format_args!(
3346                "unsupported DBT configuration version {}",
3347                envelope.dbt_config_version
3348            ),
3349        ));
3350    }
3351    let mut config = from_legacy_backend_json(envelope.config.get())?;
3352    config.backend = envelope.backend;
3353    Ok(config)
3354}
3355
3356#[derive(Serialize, Deserialize)]
3357#[serde(deny_unknown_fields)]
3358struct VersionedBackendConfig<'a> {
3359    dbt_config_version: u32,
3360    #[serde(borrow)]
3361    config: &'a serde_json::value::RawValue,
3362    backend: BackendCapabilities,
3363}
3364
3365/// The names of the fifteen legacy backend keys, in the order
3366/// [`legacy_backend_keys`] gives them.
3367const LEGACY_BACKEND_KEY_NAMES: [&str; 15] = [
3368    "cpuid_virtualized_by_backend",
3369    "backend_supports_madvise",
3370    "discover_live_file_metadata",
3371    "detect_host_clock_futex_timeouts",
3372    "syscall_clobbers_virtualized_by_backend",
3373    "cancel_killed_thread_rpcs",
3374    "backend_reports_physical_process_exits",
3375    "backend_tracks_process_children",
3376    "backend_runs_exit_robust_list",
3377    "backend_requires_thread_directed_process_signals",
3378    "backend_is_kvm",
3379    "kvm_shared_dequeue_timers",
3380    "backend_supports_parked_write_signal_interruption",
3381    "backend_virtualizes_capability_prctls",
3382    "backend_defers_vfork_child_registration",
3383];
3384
3385const fn legacy_backend_key_default_true() -> bool {
3386    true
3387}
3388
3389/// The fifteen legacy backend keys with the types and serde defaults they had
3390/// as `Config` fields. It is deserialized from the keys
3391/// [`legacy_backend_json::LegacyConfigVisitor`] takes out of the input, so a
3392/// key given twice fails here as it failed as a `Config` field.
3393#[derive(Deserialize)]
3394struct LegacyBackendKeys {
3395    #[serde(default)]
3396    cpuid_virtualized_by_backend: bool,
3397    // Supplies no capability; declared so that the key, like the others,
3398    // fails the parse when it is given twice.
3399    #[expect(
3400        dead_code,
3401        reason = "read only to refuse a repeated key; its value is ignored"
3402    )]
3403    #[serde(default = "legacy_backend_key_default_true")]
3404    backend_supports_madvise: bool,
3405    #[serde(default)]
3406    discover_live_file_metadata: bool,
3407    #[serde(default)]
3408    detect_host_clock_futex_timeouts: bool,
3409    // Supplies no capability; declared so that the key, like the others,
3410    // fails the parse when it is given twice.
3411    #[expect(
3412        dead_code,
3413        reason = "read only to refuse a repeated key; its value is ignored"
3414    )]
3415    #[serde(default)]
3416    syscall_clobbers_virtualized_by_backend: bool,
3417    #[serde(default)]
3418    cancel_killed_thread_rpcs: bool,
3419    #[serde(default)]
3420    backend_reports_physical_process_exits: bool,
3421    #[serde(default = "legacy_backend_key_default_true")]
3422    backend_tracks_process_children: bool,
3423    #[serde(default = "legacy_backend_key_default_true")]
3424    backend_runs_exit_robust_list: bool,
3425    #[serde(default)]
3426    backend_requires_thread_directed_process_signals: bool,
3427    #[serde(default)]
3428    backend_is_kvm: bool,
3429    /// Read as it was while it was a `Config` field, then checked by
3430    /// [`from_legacy_backend_json`] against `backend_is_kvm` and
3431    /// [`Config::sequentialize_threads`]; it supplies no capability.
3432    #[serde(default)]
3433    kvm_shared_dequeue_timers: bool,
3434    #[serde(default = "legacy_backend_key_default_true")]
3435    backend_supports_parked_write_signal_interruption: bool,
3436    #[serde(default)]
3437    backend_virtualizes_capability_prctls: bool,
3438    // Supplies no capability; declared so that the key, like the others,
3439    // fails the parse when it is given twice.
3440    #[expect(
3441        dead_code,
3442        reason = "read only to refuse a repeated key; its value is ignored"
3443    )]
3444    #[serde(default)]
3445    backend_defers_vfork_child_registration: bool,
3446}
3447
3448impl LegacyBackendKeys {
3449    /// The capabilities these keys describe; the inverse of
3450    /// [`legacy_backend_keys`]. Every field is assigned, so the starting
3451    /// constant contributes nothing a key decides.
3452    fn capabilities(&self) -> BackendCapabilities {
3453        let mut backend = BackendCapabilities::PTRACE;
3454        backend.tool_shares_guest_descriptor_table = self.discover_live_file_metadata;
3455        backend.rediscovers_descriptors_after_exec = self.discover_live_file_metadata;
3456        backend.loopback_pollers_yield_to_peers = self.discover_live_file_metadata;
3457        backend.guest_clock_reads_bypass_backend = self.detect_host_clock_futex_timeouts;
3458        backend.needs_killed_thread_rpc_cancellation = self.cancel_killed_thread_rpcs;
3459        // A legacy encoding predates this key. Only the ptrace tracer reports
3460        // child-exit publication, and it is the one backend that needs no
3461        // killed-thread RPC cancellation.
3462        backend.reports_child_exit_publication = !self.cancel_killed_thread_rpcs;
3463        // Newer than the legacy form. It took over the child-exit ledger's gates
3464        // from reports_child_exit_publication and has its value on every backend
3465        // the form can express (true for ptrace and the default, false for DBT,
3466        // the form's only reader), so it is read back the same way.
3467        backend.reports_kernel_child_exit_signals = backend.reports_child_exit_publication;
3468        backend.reports_physical_process_exits = self.backend_reports_physical_process_exits;
3469        backend.tracks_process_children = self.backend_tracks_process_children;
3470        backend.runs_exit_robust_list = self.backend_runs_exit_robust_list;
3471        backend.requires_thread_directed_process_signals =
3472            self.backend_requires_thread_directed_process_signals;
3473        backend.supports_parked_write_signal_interruption =
3474            self.backend_supports_parked_write_signal_interruption;
3475        backend.provides_process_signal_control = self.backend_is_kvm;
3476        backend.emulates_child_waits = self.backend_is_kvm;
3477        // A legacy encoding predates this key. DBT, the form's only reader,
3478        // promotes shared open-file descriptions before a native fork or exec
3479        // and alone requires thread-directed process signals (DBT's runtime
3480        // also states it from any source, `assert_dbt_execution_model`).
3481        backend.promotes_open_files_before_native_lifecycle =
3482            self.backend_requires_thread_directed_process_signals;
3483        // Added after the legacy form froze. Each was a `Config` field with no
3484        // legacy key that read back as false (or, for the four split out of
3485        // `supports_blocked_wait_signal_interruption`, a reading of one), and
3486        // each is false for DBT, the form's only reader, so a DBT
3487        // configuration stays exact.
3488        set_kernel_signal_facts(&mut backend, false);
3489        backend.reports_sigreturns = false;
3490        backend.delivers_preemption_timer_events = false;
3491        backend.runs_guest_code_between_injections = false;
3492        // Added after the legacy form froze, and false for DBT, the form's
3493        // only reader, whose guest threads are host tasks whose exits run
3494        // Linux's robust-list walk. KVM, the one backend that reports true,
3495        // is preserved exactly only by the versioned envelope of
3496        // `encode_backend_config_json`, never by these keys.
3497        backend.kernel_never_walks_robust_lists = false;
3498        // Added after the legacy form froze (Reverie 9443a6b2): it reads
3499        // back as the starting constant's true, which is also DBT's value,
3500        // the form's only reader, so a DBT configuration stays exact. The
3501        // two backends that report false (in-guest LiteInst and KVM) are
3502        // preserved exactly only by the versioned envelope of
3503        // `encode_backend_config_json`, never by these keys.
3504        backend.provides_guest_backtraces = true;
3505        // Added after the legacy form froze (Reverie f614c7c8): false and the
3506        // empty set, DBT's values, the form's only reader. KVM, the one
3507        // backend that reports otherwise, is preserved exactly only by the
3508        // versioned envelope of `encode_backend_config_json`, never by these
3509        // keys.
3510        backend.places_orphans = false;
3511        backend.publishable_parent_death_signals = 0;
3512        // Added after the legacy form froze (Reverie 1161 made it a `u8`
3513        // width). The legacy form carries no width for it: by absence it
3514        // says the bool false, and the minimal width that represents a
3515        // legacy bool false is 0, so it reads back as 0. A backend with a
3516        // nonzero width (ptrace reports 4 on x86_64) is preserved exactly
3517        // only by the versioned envelope of `encode_backend_config_json`,
3518        // never by these keys.
3519        backend.hardware_watchpoints = 0;
3520        backend
3521    }
3522}
3523
3524/// The serde adapters behind [`to_legacy_backend_json`] and
3525/// [`from_legacy_backend_json`].
3526///
3527/// The serializer adapter wraps the JSON serializer only at the top level,
3528/// where `Config`'s derived `Serialize` opens one struct and writes its fields
3529/// in declaration order. Nested values go straight to the wrapped serializer,
3530/// so they, and every field other than the two it replaces, are encoded
3531/// unchanged.
3532///
3533/// The deserializer adapters likewise wrap only the top-level object or array,
3534/// and hand every nested value to the wrapped JSON deserializer.
3535mod legacy_backend_json {
3536    use std::fmt;
3537
3538    use serde::Deserialize;
3539    use serde::Serialize;
3540    use serde::de;
3541    use serde::de::DeserializeSeed;
3542    use serde::de::IgnoredAny;
3543    use serde::de::MapAccess;
3544    use serde::de::SeqAccess;
3545    use serde::de::Visitor;
3546    use serde::de::value::BoolDeserializer;
3547    use serde::de::value::MapAccessDeserializer;
3548    use serde::de::value::MapDeserializer;
3549    use serde::de::value::SeqAccessDeserializer;
3550    use serde::de::value::StringDeserializer;
3551    use serde::de::value::U64Deserializer;
3552    use serde::ser::Error as _;
3553    use serde::ser::Impossible;
3554    use serde::ser::SerializeStruct;
3555    use serde::ser::Serializer;
3556
3557    use super::BackendCapabilities;
3558    use super::Config;
3559    use super::LEGACY_BACKEND_KEY_NAMES;
3560    use super::LegacyBackendKeys;
3561    use super::legacy_backend_keys;
3562
3563    /// The keys `Config` has now that its legacy form did not. The legacy
3564    /// form ignored them, as it ignored every key it did not name.
3565    /// `record_host_inputs` has no legacy key and no legacy value: the legacy
3566    /// form serves only DBT, whose launcher collects no host inputs, so it is
3567    /// never written and always reads as false.
3568    /// So is `guest_may_inherit_a_terminal`, which is read only where
3569    /// `BackendCapabilities::host_timed_signals_are_physical` holds;
3570    /// that is false for DBT.
3571    /// `target_timeslice_syscalls_only` came after the legacy form froze; the
3572    /// legacy form never carried it, so it is never written and reads as false,
3573    /// the behaviour every legacy reader already has.
3574    /// `in_guest_detlog_forward_policy` is unset for DBT (its runtime forwards
3575    /// no records on a socket), so it is never written and reads as unset.
3576    /// `futex_wake_yields`, `pipe_wake_latency`, `scheduler_turn_cost` and
3577    /// `cap_preemption_charge` and `virtual_timeout` came after the legacy form
3578    /// froze too, and `hermit run` refuses them with the DBT backend.
3579    /// `replaying` is never written either: only `hermit replay` sets it, and
3580    /// replay runs only on the ptrace backend.
3581    /// `in_guest_site_patching_off` is false for DBT (it is set only for
3582    /// in-guest LiteInst), so it is never written and reads as false.
3583    /// `in_guest_reserved_signals` is empty for DBT (it is set only for
3584    /// in-guest runs), so it is never written and reads as empty.
3585    /// `in_guest_refuses_host_timed_producers` is false for DBT (it is set
3586    /// only for in-guest runs), so it is never written and reads as false.
3587    /// So is `in_guest_signal_delivery`, for the same reason.
3588    /// `launch_image` came after the legacy form froze: it is never written
3589    /// and reads as unset. DBT's runtime takes it from the coordinator's
3590    /// configuration instead, which declares DynamoRIO's library as the
3591    /// launcher.
3592    /// `inherited_seccomp` came after the legacy form froze too. It is never
3593    /// written and reads as unknown.
3594    /// `cpu_profile` and `cpu_profile_host` came after it as well. DBT refuses
3595    /// the x86-64-v3 profile, so they are never written and read as v2.
3596    /// `signal_state_copy` came after it too. The copy is never kept on DBT
3597    /// (its kernel signal state is not the guest's), so it is never written
3598    /// and reads as on, its serde default.
3599    /// And `container_mount_table`, which describes one run's container: the
3600    /// frozen form omits it, so it is never written and reads as absent. The
3601    /// coordinated startup's typed configuration, which is how Hermit
3602    /// launches DBT, carries the table.
3603    /// `mountinfo_mount_namespace` too, for the same reason: it is never
3604    /// written and reads as absent. So are `directory_placeholder` and
3605    /// `container_procfs`, which describe one run's container; the
3606    /// coordinated startup's typed configuration carries them.
3607    /// `signal_delivery_check_fatal` matters only where the copy is kept, so
3608    /// it is never written and reads as false.
3609    /// `work_span_report` came after it as well: the report is written by the
3610    /// scheduler, not the in-guest runtime, so it is never written and reads
3611    /// as unset.
3612    /// `signal_at` came after the form froze; the scheduler acts on it, not
3613    /// the runtime, so it is never written and reads as unset.
3614    /// `annotated_priority_replay` came after it too, and is false for every
3615    /// backend that reads the legacy form: priority replay needs a preemption
3616    /// timer, which none of them delivers, so it is never written and reads
3617    /// as false.
3618    /// `virtual_cpus` came after it too, and `hermit run` refuses it with the
3619    /// DBT backend: it is never written and reads as unset.
3620    /// `expired_wait_charge` too: the scheduler charges turns, not the
3621    /// runtime, so it is never written and reads as unset.
3622    /// `notice_host_network_abstract_binds` came after it too. It is never written and
3623    /// reads as false, so the DBT runtime prints no notice for an abstract
3624    /// `AF_UNIX` bind under host networking.
3625    /// `replay_edited_preemptions` came after it too. It is never written and
3626    /// reads as false; it cannot matter there, since a timerless backend
3627    /// such as DBT refuses `--replay-preemptions-from`, which it requires.
3628    /// `eager_lone_preemptions` came after it too. DBT has no PMU timer, so
3629    /// the option has no effect there; it is never written and reads as
3630    /// false.
3631    /// `access_trace`, `access_trace_from`, `access_trace_max_records` and
3632    /// `access_trace_max_bytes` came after it too; the scheduler writes the
3633    /// trace, and `hermit run` refuses it on DBT, so they are never written
3634    /// and read as unset, 0, unset and unset.
3635    // `chaos_timeslice_range` came after it too: the scheduler draws the
3636    // slices, not the runtime, so it is never written and reads as unset.
3637    // `chaos_timeslice_draw` and `chaos_timeslice` came with it, for the same
3638    // reason; they read as `slice` and unset.
3639    // `guest_ids` came after it too: DBT numbers its guest itself, so it
3640    // is never written and reads as `GuestIds::Namespace`, its serde
3641    // default.
3642    // `detlog_anon` came after it too: a coordinator-side ptrace-only
3643    // diagnostic the other backends refuse, so it is never written and
3644    // reads as absent, its serde default.
3645    /// `replay_continue_fairly` came after it too; only the scheduler reads
3646    /// it, so it is never written and reads as false.
3647    /// `standard_stream_aliases` came after it too, and is set only for KVM;
3648    /// it is never written and reads as unset.
3649    const FIELDS_WITHOUT_A_LEGACY_KEY: [&str; 46] = [
3650        "backend",
3651        "strict_schedule_boundaries",
3652        "record_host_inputs",
3653        "guest_may_inherit_a_terminal",
3654        "in_guest_detlog_forward_policy",
3655        "in_guest_site_patching_off",
3656        "in_guest_reserved_signals",
3657        "in_guest_refuses_host_timed_producers",
3658        "launch_image",
3659        "target_timeslice_syscalls_only",
3660        "futex_wake_yields",
3661        "pipe_wake_latency",
3662        "expired_wait_charge",
3663        "replaying",
3664        "scheduler_turn_cost",
3665        "cap_preemption_charge",
3666        "virtual_timeout",
3667        "inherited_seccomp",
3668        "cpu_profile",
3669        "cpu_profile_host",
3670        "cpu_profile_source",
3671        "signal_state_copy",
3672        "container_mount_table",
3673        "mountinfo_mount_namespace",
3674        "directory_placeholder",
3675        "container_procfs",
3676        "signal_delivery_check_fatal",
3677        "work_span_report",
3678        "signal_at",
3679        "in_guest_signal_delivery",
3680        "annotated_priority_replay",
3681        "virtual_cpus",
3682        "notice_host_network_abstract_binds",
3683        "replay_edited_preemptions",
3684        "eager_lone_preemptions",
3685        "chaos_timeslice_range",
3686        "chaos_timeslice_draw",
3687        "chaos_timeslice",
3688        "guest_ids",
3689        "detlog_anon",
3690        "access_trace",
3691        "access_trace_from",
3692        "access_trace_max_records",
3693        "access_trace_max_bytes",
3694        "replay_continue_fairly",
3695        "standard_stream_aliases",
3696    ];
3697
3698    /// Reads the top-level object or array of a legacy configuration. The
3699    /// derived `Config` deserializer reads every field; this takes the legacy
3700    /// backend keys out of the input before it sees them and collects them for
3701    /// [`LegacyBackendKeys`].
3702    pub(super) struct LegacyConfigVisitor;
3703
3704    impl<'de> Visitor<'de> for LegacyConfigVisitor {
3705        type Value = (Config, LegacyBackendKeys);
3706
3707        fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
3708            formatter.write_str("struct Config")
3709        }
3710
3711        fn visit_map<A: MapAccess<'de>>(self, map: A) -> Result<Self::Value, A::Error> {
3712            let mut legacy = Vec::new();
3713            let config = Config::deserialize(MapAccessDeserializer::new(LegacyKeys {
3714                inner: map,
3715                legacy: &mut legacy,
3716            }))?;
3717            Ok((config, legacy_backend_keys_from(legacy)?))
3718        }
3719
3720        fn visit_seq<A: SeqAccess<'de>>(self, seq: A) -> Result<Self::Value, A::Error> {
3721            let fields = config_field_order().map_err(<A::Error as de::Error>::custom)?;
3722            let mut legacy = Vec::new();
3723            let config = Config::deserialize(SeqAccessDeserializer::new(LegacyPositions {
3724                inner: seq,
3725                fields: fields.iter(),
3726                legacy: &mut legacy,
3727            }))?;
3728            Ok((config, legacy_backend_keys_from(legacy)?))
3729        }
3730    }
3731
3732    /// Reads the collected legacy keys with their old types and defaults. A
3733    /// key that appeared twice is collected twice, and fails here as a
3734    /// duplicate field.
3735    fn legacy_backend_keys_from<E: de::Error>(
3736        legacy: Vec<(&'static str, bool)>,
3737    ) -> Result<LegacyBackendKeys, E> {
3738        LegacyBackendKeys::deserialize(MapDeserializer::<_, E>::new(legacy.into_iter()))
3739    }
3740
3741    /// The top-level object's entries, with each legacy backend key read as the
3742    /// boolean it was and collected rather than passed on, and each key of
3743    /// [`FIELDS_WITHOUT_A_LEGACY_KEY`] skipped as the unknown key it was.
3744    struct LegacyKeys<'l, A> {
3745        inner: A,
3746        legacy: &'l mut Vec<(&'static str, bool)>,
3747    }
3748
3749    impl<'de, A: MapAccess<'de>> MapAccess<'de> for LegacyKeys<'_, A> {
3750        type Error = A::Error;
3751
3752        fn next_key_seed<K: DeserializeSeed<'de>>(
3753            &mut self,
3754            seed: K,
3755        ) -> Result<Option<K::Value>, A::Error> {
3756            while let Some(key) = self.inner.next_key::<String>()? {
3757                if let Some(name) = LEGACY_BACKEND_KEY_NAMES
3758                    .into_iter()
3759                    .find(|name| *name == key)
3760                {
3761                    let value = self.inner.next_value::<bool>()?;
3762                    self.legacy.push((name, value));
3763                } else if FIELDS_WITHOUT_A_LEGACY_KEY.contains(&key.as_str()) {
3764                    self.inner.next_value::<IgnoredAny>()?;
3765                } else {
3766                    return seed.deserialize(StringDeserializer::new(key)).map(Some);
3767                }
3768            }
3769            Ok(None)
3770        }
3771
3772        fn next_value_seed<V: DeserializeSeed<'de>>(
3773            &mut self,
3774            seed: V,
3775        ) -> Result<V::Value, A::Error> {
3776            self.inner.next_value_seed(seed)
3777        }
3778    }
3779
3780    /// The top-level array's elements, in the legacy form's positions. The
3781    /// derived `Config` deserializer asks for one element per field in
3782    /// declaration order; at [`Config::backend`] this reads up to fifteen
3783    /// elements as the legacy backend keys, and neither the field it asks for
3784    /// there nor [`Config::record_host_inputs`],
3785    /// [`Config::guest_may_inherit_a_terminal`],
3786    /// [`Config::in_guest_detlog_forward_policy`],
3787    /// [`Config::in_guest_site_patching_off`],
3788    /// [`Config::in_guest_reserved_signals`],
3789    /// [`Config::in_guest_refuses_host_timed_producers`],
3790    /// [`Config::in_guest_signal_delivery`], [`Config::launch_image`],
3791    /// [`Config::target_timeslice_syscalls_only`],
3792    /// [`Config::futex_wake_yields`], [`Config::replaying`],
3793    /// [`Config::scheduler_turn_cost`], [`Config::cap_preemption_charge`],
3794    /// [`Config::virtual_timeout`], [`Config::inherited_seccomp`] or [`Config::replay_edited_preemptions`] takes
3795    /// an element. Each gets a
3796    /// placeholder; [`super::from_legacy_backend_json`] replaces the first.
3797    struct LegacyPositions<'f, 'l, A> {
3798        inner: A,
3799        fields: std::slice::Iter<'f, String>,
3800        legacy: &'l mut Vec<(&'static str, bool)>,
3801    }
3802
3803    impl<'de, A: SeqAccess<'de>> SeqAccess<'de> for LegacyPositions<'_, '_, A> {
3804        type Error = A::Error;
3805
3806        fn next_element_seed<T: DeserializeSeed<'de>>(
3807            &mut self,
3808            seed: T,
3809        ) -> Result<Option<T::Value>, A::Error> {
3810            match self.fields.next().map(String::as_str) {
3811                Some("backend") => {
3812                    for name in LEGACY_BACKEND_KEY_NAMES {
3813                        match self.inner.next_element::<bool>()? {
3814                            Some(value) => self.legacy.push((name, value)),
3815                            // A short array; the rest take their defaults.
3816                            None => break,
3817                        }
3818                    }
3819                    let placeholder = serde_json::to_value(BackendCapabilities::PTRACE)
3820                        .map_err(<A::Error as de::Error>::custom)?;
3821                    seed.deserialize(placeholder)
3822                        .map(Some)
3823                        .map_err(<A::Error as de::Error>::custom)
3824                }
3825                Some(
3826                    "strict_schedule_boundaries"
3827                    | "record_host_inputs"
3828                    | "guest_may_inherit_a_terminal"
3829                    | "in_guest_site_patching_off"
3830                    | "in_guest_refuses_host_timed_producers"
3831                    | "in_guest_signal_delivery"
3832                    | "annotated_priority_replay"
3833                    | "target_timeslice_syscalls_only"
3834                    | "futex_wake_yields"
3835                    | "cap_preemption_charge"
3836                    | "replaying"
3837                    | "signal_delivery_check_fatal"
3838                    | "container_procfs"
3839                    | "notice_host_network_abstract_binds"
3840                    | "replay_edited_preemptions"
3841                    | "eager_lone_preemptions"
3842                    | "replay_continue_fairly",
3843                ) => seed.deserialize(BoolDeserializer::new(false)).map(Some),
3844                // Empty, as its serde default is; and 0, the default.
3845                Some("in_guest_reserved_signals" | "access_trace_from") => {
3846                    seed.deserialize(U64Deserializer::new(0)).map(Some)
3847                }
3848                // On, as its serde default is.
3849                Some("signal_state_copy") => {
3850                    seed.deserialize(BoolDeserializer::new(true)).map(Some)
3851                }
3852                // Unset, as its serde default is.
3853                Some(
3854                    "in_guest_detlog_forward_policy"
3855                    | "launch_image"
3856                    | "detlog_anon"
3857                    | "scheduler_turn_cost"
3858                    | "virtual_timeout"
3859                    | "pipe_wake_latency"
3860                    | "expired_wait_charge"
3861                    | "container_mount_table"
3862                    | "mountinfo_mount_namespace"
3863                    | "directory_placeholder"
3864                    | "work_span_report"
3865                    | "signal_at"
3866                    | "virtual_cpus"
3867                    | "chaos_timeslice_range"
3868                    | "chaos_timeslice"
3869                    | "access_trace"
3870                    | "access_trace_max_records"
3871                    | "access_trace_max_bytes"
3872                    | "standard_stream_aliases",
3873                ) => seed
3874                    .deserialize(serde_json::Value::Null)
3875                    .map(Some)
3876                    .map_err(<A::Error as de::Error>::custom),
3877                // Per-slice, as its serde default is.
3878                Some("chaos_timeslice_draw") => {
3879                    let slice = serde_json::to_value(super::ChaosTimesliceDraw::Slice)
3880                        .map_err(<A::Error as de::Error>::custom)?;
3881                    seed.deserialize(slice)
3882                        .map(Some)
3883                        .map_err(<A::Error as de::Error>::custom)
3884                }
3885                // x86-64-v2, as their serde defaults are.
3886                Some("cpu_profile") => {
3887                    let v2 = serde_json::to_value(crate::cpu_profile::CpuProfile::X86_64V2)
3888                        .map_err(<A::Error as de::Error>::custom)?;
3889                    seed.deserialize(v2)
3890                        .map(Some)
3891                        .map_err(<A::Error as de::Error>::custom)
3892                }
3893                Some("cpu_profile_host" | "cpu_profile_source") => seed
3894                    .deserialize(serde_json::Value::Null)
3895                    .map(Some)
3896                    .map_err(<A::Error as de::Error>::custom),
3897                // Numbered by a PID namespace, as its serde default is.
3898                Some("guest_ids") => {
3899                    let namespace = serde_json::to_value(crate::pid::GuestIds::Namespace)
3900                        .map_err(<A::Error as de::Error>::custom)?;
3901                    seed.deserialize(namespace)
3902                        .map(Some)
3903                        .map_err(<A::Error as de::Error>::custom)
3904                }
3905                // Unknown, as its serde default is.
3906                Some("inherited_seccomp") => {
3907                    let unknown = serde_json::to_value(super::InheritedSeccomp::Unknown)
3908                        .map_err(<A::Error as de::Error>::custom)?;
3909                    seed.deserialize(unknown)
3910                        .map(Some)
3911                        .map_err(<A::Error as de::Error>::custom)
3912                }
3913                _ => self.inner.next_element_seed(seed),
3914            }
3915        }
3916    }
3917
3918    /// The names of `Config`'s serialized fields in declaration order, which
3919    /// is the order its derived deserializer reads an array in: no field is
3920    /// serialized without being deserialized or the reverse.
3921    ///
3922    /// The configuration serialized to learn them is built by Clap from no
3923    /// arguments, with every environment binding removed before parsing.
3924    /// Building `Config::command()` still reads the bound variables (Clap's
3925    /// `Arg::env` reads a variable's value when the binding is made), but
3926    /// removing the bindings discards those values, so no ambient value
3927    /// reaches the parse. `Config::default()` would instead parse
3928    /// HERMIT_EPOCH, HERMIT_PRNG, HERMIT_SCHED_SEED and HERMIT_CPU_PROFILE, and exit the process
3929    /// when one of them does not parse; the legacy decoder was not affected
3930    /// by any of them.
3931    fn config_field_order() -> serde_json::Result<Vec<String>> {
3932        let config = environment_free_config().map_err(<serde_json::Error as de::Error>::custom)?;
3933        let json = serde_json::to_string(&config)?;
3934        Ok(serde_json::from_str::<KeyOrder>(&json)?.0)
3935    }
3936
3937    /// `Config` as Clap builds it from no arguments and no environment.
3938    fn environment_free_config() -> Result<Config, clap::Error> {
3939        use clap::CommandFactory;
3940        use clap::FromArgMatches;
3941
3942        let matches = Config::command()
3943            .mut_args(|arg| arg.env(None::<&'static str>))
3944            .try_get_matches_from(["config-field-order"])?;
3945        Config::from_arg_matches(&matches)
3946    }
3947
3948    /// The keys of a JSON object, in the order they appear.
3949    pub(super) struct KeyOrder(pub(super) Vec<String>);
3950
3951    impl<'de> Deserialize<'de> for KeyOrder {
3952        fn deserialize<D: de::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
3953            struct KeyOrderVisitor;
3954
3955            impl<'de> Visitor<'de> for KeyOrderVisitor {
3956                type Value = KeyOrder;
3957
3958                fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
3959                    formatter.write_str("a JSON object")
3960                }
3961
3962                fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<KeyOrder, A::Error> {
3963                    let mut keys = Vec::new();
3964                    while let Some(key) = map.next_key::<String>()? {
3965                        map.next_value::<IgnoredAny>()?;
3966                        keys.push(key);
3967                    }
3968                    Ok(KeyOrder(keys))
3969                }
3970            }
3971
3972            deserializer.deserialize_map(KeyOrderVisitor)
3973        }
3974    }
3975
3976    pub(super) struct ConfigSerializer<'c, S> {
3977        pub(super) inner: S,
3978        pub(super) config: &'c Config,
3979    }
3980
3981    pub(super) struct ConfigFields<'c, S> {
3982        inner: S,
3983        config: &'c Config,
3984        wrote_backend_keys: bool,
3985    }
3986
3987    fn not_a_config<E: serde::ser::Error>() -> E {
3988        E::custom("the legacy backend JSON encoding applies only to a Detcore Config")
3989    }
3990
3991    impl<'c, S: Serializer> Serializer for ConfigSerializer<'c, S> {
3992        type Ok = S::Ok;
3993        type Error = S::Error;
3994        type SerializeSeq = Impossible<S::Ok, S::Error>;
3995        type SerializeTuple = Impossible<S::Ok, S::Error>;
3996        type SerializeTupleStruct = Impossible<S::Ok, S::Error>;
3997        type SerializeTupleVariant = Impossible<S::Ok, S::Error>;
3998        type SerializeMap = Impossible<S::Ok, S::Error>;
3999        type SerializeStruct = ConfigFields<'c, S::SerializeStruct>;
4000        type SerializeStructVariant = Impossible<S::Ok, S::Error>;
4001
4002        fn is_human_readable(&self) -> bool {
4003            self.inner.is_human_readable()
4004        }
4005
4006        fn serialize_struct(
4007            self,
4008            name: &'static str,
4009            len: usize,
4010        ) -> Result<Self::SerializeStruct, S::Error> {
4011            // Two fields are replaced by fifteen keys.
4012            let len = len + legacy_backend_keys(self.config).len() - 2;
4013            Ok(ConfigFields {
4014                inner: self.inner.serialize_struct(name, len)?,
4015                config: self.config,
4016                wrote_backend_keys: false,
4017            })
4018        }
4019
4020        fn serialize_bool(self, _: bool) -> Result<S::Ok, S::Error> {
4021            Err(not_a_config())
4022        }
4023        fn serialize_i8(self, _: i8) -> Result<S::Ok, S::Error> {
4024            Err(not_a_config())
4025        }
4026        fn serialize_i16(self, _: i16) -> Result<S::Ok, S::Error> {
4027            Err(not_a_config())
4028        }
4029        fn serialize_i32(self, _: i32) -> Result<S::Ok, S::Error> {
4030            Err(not_a_config())
4031        }
4032        fn serialize_i64(self, _: i64) -> Result<S::Ok, S::Error> {
4033            Err(not_a_config())
4034        }
4035        fn serialize_u8(self, _: u8) -> Result<S::Ok, S::Error> {
4036            Err(not_a_config())
4037        }
4038        fn serialize_u16(self, _: u16) -> Result<S::Ok, S::Error> {
4039            Err(not_a_config())
4040        }
4041        fn serialize_u32(self, _: u32) -> Result<S::Ok, S::Error> {
4042            Err(not_a_config())
4043        }
4044        fn serialize_u64(self, _: u64) -> Result<S::Ok, S::Error> {
4045            Err(not_a_config())
4046        }
4047        fn serialize_f32(self, _: f32) -> Result<S::Ok, S::Error> {
4048            Err(not_a_config())
4049        }
4050        fn serialize_f64(self, _: f64) -> Result<S::Ok, S::Error> {
4051            Err(not_a_config())
4052        }
4053        fn serialize_char(self, _: char) -> Result<S::Ok, S::Error> {
4054            Err(not_a_config())
4055        }
4056        fn serialize_str(self, _: &str) -> Result<S::Ok, S::Error> {
4057            Err(not_a_config())
4058        }
4059        fn serialize_bytes(self, _: &[u8]) -> Result<S::Ok, S::Error> {
4060            Err(not_a_config())
4061        }
4062        fn serialize_none(self) -> Result<S::Ok, S::Error> {
4063            Err(not_a_config())
4064        }
4065        fn serialize_some<T: ?Sized + Serialize>(self, _: &T) -> Result<S::Ok, S::Error> {
4066            Err(not_a_config())
4067        }
4068        fn serialize_unit(self) -> Result<S::Ok, S::Error> {
4069            Err(not_a_config())
4070        }
4071        fn serialize_unit_struct(self, _: &'static str) -> Result<S::Ok, S::Error> {
4072            Err(not_a_config())
4073        }
4074        fn serialize_unit_variant(
4075            self,
4076            _: &'static str,
4077            _: u32,
4078            _: &'static str,
4079        ) -> Result<S::Ok, S::Error> {
4080            Err(not_a_config())
4081        }
4082        fn serialize_newtype_struct<T: ?Sized + Serialize>(
4083            self,
4084            _: &'static str,
4085            _: &T,
4086        ) -> Result<S::Ok, S::Error> {
4087            Err(not_a_config())
4088        }
4089        fn serialize_newtype_variant<T: ?Sized + Serialize>(
4090            self,
4091            _: &'static str,
4092            _: u32,
4093            _: &'static str,
4094            _: &T,
4095        ) -> Result<S::Ok, S::Error> {
4096            Err(not_a_config())
4097        }
4098        fn serialize_seq(self, _: Option<usize>) -> Result<Self::SerializeSeq, S::Error> {
4099            Err(not_a_config())
4100        }
4101        fn serialize_tuple(self, _: usize) -> Result<Self::SerializeTuple, S::Error> {
4102            Err(not_a_config())
4103        }
4104        fn serialize_tuple_struct(
4105            self,
4106            _: &'static str,
4107            _: usize,
4108        ) -> Result<Self::SerializeTupleStruct, S::Error> {
4109            Err(not_a_config())
4110        }
4111        fn serialize_tuple_variant(
4112            self,
4113            _: &'static str,
4114            _: u32,
4115            _: &'static str,
4116            _: usize,
4117        ) -> Result<Self::SerializeTupleVariant, S::Error> {
4118            Err(not_a_config())
4119        }
4120        fn serialize_map(self, _: Option<usize>) -> Result<Self::SerializeMap, S::Error> {
4121            Err(not_a_config())
4122        }
4123        fn serialize_struct_variant(
4124            self,
4125            _: &'static str,
4126            _: u32,
4127            _: &'static str,
4128            _: usize,
4129        ) -> Result<Self::SerializeStructVariant, S::Error> {
4130            Err(not_a_config())
4131        }
4132    }
4133
4134    impl<S: SerializeStruct> SerializeStruct for ConfigFields<'_, S> {
4135        type Ok = S::Ok;
4136        type Error = S::Error;
4137
4138        fn serialize_field<T: ?Sized + Serialize>(
4139            &mut self,
4140            key: &'static str,
4141            value: &T,
4142        ) -> Result<(), S::Error> {
4143            match key {
4144                "backend" => {
4145                    for (legacy_key, legacy_value) in legacy_backend_keys(self.config) {
4146                        self.inner.serialize_field(legacy_key, &legacy_value)?;
4147                    }
4148                    self.wrote_backend_keys = true;
4149                    Ok(())
4150                }
4151                // No legacy key; see FIELDS_WITHOUT_A_LEGACY_KEY.
4152                "strict_schedule_boundaries"
4153                | "record_host_inputs"
4154                | "guest_may_inherit_a_terminal"
4155                | "in_guest_detlog_forward_policy"
4156                | "in_guest_site_patching_off"
4157                | "in_guest_reserved_signals"
4158                | "in_guest_refuses_host_timed_producers"
4159                | "launch_image"
4160                | "target_timeslice_syscalls_only"
4161                | "futex_wake_yields"
4162                | "pipe_wake_latency"
4163                | "expired_wait_charge"
4164                | "replaying"
4165                | "scheduler_turn_cost"
4166                | "cap_preemption_charge"
4167                | "virtual_timeout"
4168                | "inherited_seccomp"
4169                | "cpu_profile"
4170                | "cpu_profile_host"
4171                | "cpu_profile_source"
4172                | "signal_state_copy"
4173                | "container_mount_table"
4174                | "mountinfo_mount_namespace"
4175                | "directory_placeholder"
4176                | "container_procfs"
4177                | "signal_delivery_check_fatal"
4178                | "work_span_report"
4179                | "signal_at"
4180                | "in_guest_signal_delivery"
4181                | "annotated_priority_replay"
4182                | "virtual_cpus"
4183                | "notice_host_network_abstract_binds"
4184                | "replay_edited_preemptions"
4185                | "eager_lone_preemptions"
4186                | "chaos_timeslice_range"
4187                | "chaos_timeslice_draw"
4188                | "chaos_timeslice"
4189                | "guest_ids"
4190                | "detlog_anon"
4191                | "standard_stream_aliases" => Ok(()),
4192                "access_trace"
4193                | "access_trace_from"
4194                | "access_trace_max_records"
4195                | "access_trace_max_bytes" => Ok(()),
4196                "replay_continue_fairly" => Ok(()),
4197                _ => self.inner.serialize_field(key, value),
4198            }
4199        }
4200
4201        fn skip_field(&mut self, key: &'static str) -> Result<(), S::Error> {
4202            self.inner.skip_field(key)
4203        }
4204
4205        fn end(self) -> Result<S::Ok, S::Error> {
4206            if !self.wrote_backend_keys {
4207                return Err(S::Error::custom(
4208                    "Config serialized no `backend` field to replace with the legacy backend keys",
4209                ));
4210            }
4211            self.inner.end()
4212        }
4213    }
4214}
4215
4216#[cfg(test)]
4217mod tests {
4218    use super::*;
4219
4220    /// `--chaos-timeslice-range` takes MIN..MAX virtual nanoseconds, both at
4221    /// least one RCB and MIN no larger than MAX, prints back as given, needs
4222    /// `--chaos`, and maps uniform draws log-uniformly onto the range.
4223    fn range_max(config: &Config) -> NonZeroU64 {
4224        config.chaos_timeslice_range.unwrap().max
4225    }
4226
4227    #[test]
4228    fn a_chaos_timeslice_range_is_min_to_max_nanoseconds_drawn_log_uniformly() {
4229        let range: TimesliceRange = "1000..1000000000".parse().unwrap();
4230        assert_eq!((range.min.get(), range.max.get()), (1_000, 1_000_000_000));
4231        assert_eq!(range.to_string(), "1000..1000000000");
4232        assert_eq!("10..10".parse::<TimesliceRange>().unwrap().max.get(), 10);
4233        for refused in ["1000", "9..1000", "2000..1000", "a..b", "1000..", "..1000"] {
4234            assert!(refused.parse::<TimesliceRange>().is_err(), "{refused}");
4235        }
4236        assert!(
4237            Config::try_parse_from(["config", "--chaos-timeslice-range=1000..2000"]).is_err(),
4238            "a range without --chaos was accepted"
4239        );
4240        let config =
4241            Config::try_parse_from(["config", "--chaos", "--chaos-timeslice-range=1000..2000"])
4242                .unwrap();
4243        assert_eq!(
4244            config.chaos_timeslice_range,
4245            Some("1000..2000".parse().unwrap())
4246        );
4247        assert!(
4248            Config::try_parse_from([
4249                "config",
4250                "--chaos",
4251                "--chaos-timeslice-range=1000..2000",
4252                "--target-timeslice=1000",
4253            ])
4254            .is_err(),
4255            "a range beside --target-timeslice was accepted"
4256        );
4257        // The rendered command line carries the range, not the maximum it
4258        // sets, and parses back to the same range.
4259        let mut resolved = config.clone();
4260        resolved.max_timeslice = Some(range_max(&config));
4261        let rendered = resolved.to_string();
4262        assert!(
4263            rendered.contains(" --chaos-timeslice-range=1000..2000"),
4264            "{rendered}"
4265        );
4266        assert!(!rendered.contains("--max-timeslice"), "{rendered}");
4267        let reparsed = Config::parse_from(
4268            std::iter::once("detcore".to_string())
4269                .chain(rendered.split_whitespace().map(String::from)),
4270        );
4271        assert_eq!(reparsed.chaos_timeslice_range, config.chaos_timeslice_range);
4272        // A saved configuration is checked as the command line is.
4273        assert_eq!(
4274            serde_json::to_value(config.chaos_timeslice_range).unwrap(),
4275            "1000..2000"
4276        );
4277        assert!(serde_json::from_str::<TimesliceRange>("\"2000..1000\"").is_err());
4278        assert_eq!(config.chaos_timeslice_draw, ChaosTimesliceDraw::Slice);
4279        assert_eq!(config.chaos_timeslice, None);
4280        let config = Config::try_parse_from([
4281            "config",
4282            "--chaos",
4283            "--chaos-timeslice-range=1000..2000",
4284            "--chaos-timeslice-draw=run",
4285        ])
4286        .unwrap();
4287        assert_eq!(config.chaos_timeslice_draw, ChaosTimesliceDraw::Run);
4288        let bare =
4289            Config::try_parse_from(["config", "--chaos", "--chaos-timeslice-draw=run"]).unwrap();
4290        assert_eq!(bare.chaos_timeslice_draw, ChaosTimesliceDraw::Run);
4291        assert_eq!(
4292            bare.chaos_timeslice_range, None,
4293            "the CLI resolves the range"
4294        );
4295        assert_eq!(
4296            Config::default().chaos_timeslice_draw,
4297            ChaosTimesliceDraw::Slice
4298        );
4299        for args in [
4300            vec!["config", "--chaos-timeslice-draw=run"],
4301            vec!["config", "--chaos", "--chaos-timeslice-draw=slice"],
4302            vec!["config", "--chaos", "--chaos-timeslice-draw=fixed"],
4303            vec![
4304                "config",
4305                "--chaos",
4306                "--chaos-timeslice-draw=run",
4307                "--target-timeslice=1000",
4308            ],
4309        ] {
4310            assert!(Config::try_parse_from(&args).is_err(), "{args:?}");
4311        }
4312        let rendered = bare.to_string();
4313        assert!(
4314            rendered.contains(" --chaos-timeslice-draw=run"),
4315            "{rendered}"
4316        );
4317        assert!(!rendered.contains("--chaos-timeslice-range"), "{rendered}");
4318        let reparsed =
4319            Config::parse_from(std::iter::once("detcore").chain(rendered.split_whitespace()));
4320        assert_eq!(reparsed.chaos_timeslice_draw, ChaosTimesliceDraw::Run);
4321        assert_eq!(reparsed.chaos_timeslice_range, None);
4322        assert_eq!(
4323            RECOMMENDED_CHAOS_TIMESLICE_RANGE,
4324            "1000..200000000".parse().unwrap()
4325        );
4326        let command = <Config as clap::CommandFactory>::command();
4327        for id in ["chaos", "chaos_timeslice_draw"] {
4328            let text = command
4329                .get_arguments()
4330                .find(|arg| arg.get_id() == id)
4331                .unwrap()
4332                .get_long_help()
4333                .unwrap()
4334                .to_string();
4335            assert!(
4336                text.contains(&RECOMMENDED_CHAOS_TIMESLICE_RANGE.to_string()),
4337                "{text}"
4338            );
4339            assert!(text.contains("time limits"), "{text}");
4340        }
4341        // A drawn maximum renders as the range and the draw, which parse
4342        // back to the same request.
4343        let mut drawn = config.clone();
4344        drawn.max_timeslice = NonZeroU64::new(1234);
4345        drawn.chaos_timeslice = NonZeroU64::new(1234);
4346        let rendered = drawn.to_string();
4347        assert!(
4348            rendered.contains(" --chaos-timeslice-range=1000..2000 --chaos-timeslice-draw=run"),
4349            "{rendered}"
4350        );
4351        assert!(!rendered.contains("--max-timeslice"), "{rendered}");
4352        let reparsed = Config::parse_from(
4353            std::iter::once("detcore".to_string())
4354                .chain(rendered.split_whitespace().map(String::from)),
4355        );
4356        assert_eq!(reparsed.chaos_timeslice_range, config.chaos_timeslice_range);
4357        assert_eq!(reparsed.chaos_timeslice_draw, ChaosTimesliceDraw::Run);
4358
4359        // Six decades, an equal share each: u = k/6 lands on 10^(3+k).
4360        for k in 0..=6 {
4361            let expected = 10f64.powi(3 + k);
4362            let point = range.log_uniform(f64::from(k) / 6.0);
4363            assert!((point / expected - 1.0).abs() < 1e-9, "{k}: {point}");
4364        }
4365        let point: TimesliceRange = "4321..4321".parse().unwrap();
4366        assert_eq!(point.log_uniform(0.7), 4321.0);
4367    }
4368
4369    /// The four capabilities split out of
4370    /// `supports_blocked_wait_signal_interruption` in A5 step 3c.0 equal it
4371    /// on every backend, as the readings they replace did, so no backend's
4372    /// behaviour moves; and `models_signal_targets` is serialized threads and
4373    /// `kernel_signal_targets_are_predictable`, under both thread modes.
4374    #[test]
4375    fn split_signal_facts_equal_their_former_bundle_on_every_backend() {
4376        // Only ptrace and e9patch set the bundle today.
4377        let backends = [
4378            ("ptrace", BackendCapabilities::PTRACE, true),
4379            ("e9patch", BackendCapabilities::E9PATCH, true),
4380            ("liteinst", BackendCapabilities::LITEINST_IN_GUEST, false),
4381            ("sabre", BackendCapabilities::SABRE, false),
4382            ("dbt", BackendCapabilities::DBT, false),
4383            ("kvm", BackendCapabilities::KVM, false),
4384            ("unprepared", DEFAULT_BACKEND_CAPABILITIES, false),
4385        ];
4386        for (name, backend, expected) in backends {
4387            assert_eq!(
4388                backend.supports_blocked_wait_signal_interruption, expected,
4389                "{name}"
4390            );
4391            assert_eq!(backend.kernel_signal_state_is_guests, expected, "{name}");
4392            assert_eq!(
4393                backend.kernel_signal_targets_are_predictable, expected,
4394                "{name}"
4395            );
4396            assert_eq!(
4397                backend.host_task_reads_reach_guest_threads, expected,
4398                "{name}"
4399            );
4400            assert_eq!(backend.host_timed_signals_are_physical, expected, "{name}");
4401            // It sets exactly those five: the whole struct after the call
4402            // equals `backend` with only them flipped by hand.
4403            let mut flipped = backend;
4404            flipped.supports_blocked_wait_signal_interruption = !expected;
4405            flipped.kernel_signal_state_is_guests = !expected;
4406            flipped.kernel_signal_targets_are_predictable = !expected;
4407            flipped.host_task_reads_reach_guest_threads = !expected;
4408            flipped.host_timed_signals_are_physical = !expected;
4409            let mut toggled = backend;
4410            set_kernel_signal_facts(&mut toggled, !expected);
4411            assert_eq!(toggled, flipped, "{name}");
4412            set_kernel_signal_facts(&mut toggled, expected);
4413            assert_eq!(toggled, backend, "{name}");
4414            for sequentialize_threads in [false, true] {
4415                let cfg = Config {
4416                    sequentialize_threads,
4417                    backend,
4418                    ..Config::default()
4419                };
4420                assert_eq!(
4421                    cfg.models_signal_targets(),
4422                    sequentialize_threads && expected,
4423                    "{name}, sequentialize_threads={sequentialize_threads}"
4424                );
4425            }
4426        }
4427    }
4428
4429    #[test]
4430    fn default_epoch_is_2026() {
4431        assert_eq!(DEFAULT_EPOCH_STR, "2026-01-01T00:00:00Z");
4432        let epoch = DEFAULT_EPOCH_STR.parse::<DateTime<Utc>>().unwrap();
4433        assert_eq!(epoch.timestamp(), 1_767_225_600);
4434    }
4435
4436    #[test]
4437    fn resolved_epoch_is_serialized_as_an_exact_config_input() {
4438        let epoch = "2000-12-31T23:59:59.123456789Z"
4439            .parse::<DateTime<Utc>>()
4440            .unwrap();
4441        let config = Config {
4442            epoch,
4443            ..Config::default()
4444        };
4445        let encoded = serde_json::to_value(&config).unwrap();
4446        assert_eq!(
4447            encoded["epoch"],
4448            serde_json::json!("2000-12-31T23:59:59.123456789Z")
4449        );
4450        let decoded: Config = serde_json::from_value(encoded).unwrap();
4451        assert_eq!(decoded.epoch, epoch);
4452    }
4453
4454    #[test]
4455    fn default_backend_capabilities_match_instrumented_backends() {
4456        let config = Config::default();
4457        assert_eq!(config.backend, DEFAULT_BACKEND_CAPABILITIES);
4458        assert!(!config.backend.reports_physical_process_exits);
4459        assert!(config.backend.tracks_process_children);
4460        assert!(config.backend.runs_exit_robust_list);
4461        assert!(!config.backend.requires_thread_directed_process_signals);
4462        assert!(config.backend.supports_parked_write_signal_interruption);
4463        assert!(!config.sequentialize_threads);
4464        assert!(
4465            legacy_backend_keys(&config).contains(&("kvm_shared_dequeue_timers", false)),
4466            "the default configuration must not write a controlled-run legacy key"
4467        );
4468        assert!(!config.backend.supports_blocked_wait_signal_interruption);
4469        assert!(!config.backend.kernel_signal_state_is_guests);
4470        assert!(!config.backend.kernel_signal_targets_are_predictable);
4471        assert!(!config.backend.host_task_reads_reach_guest_threads);
4472        assert!(!config.backend.host_timed_signals_are_physical);
4473        assert!(!config.backend.delivers_preemption_timer_events);
4474        assert!(!config.backend.runs_guest_code_between_injections);
4475        assert!(!config.guest_may_inherit_a_terminal);
4476        assert!(config.in_guest_detlog_forward_policy.is_none());
4477        assert!(!config.in_guest_site_patching_off);
4478        assert_eq!(config.in_guest_reserved_signals, 0);
4479        assert!(!config.reserves_in_guest_signal(libc::SIGSYS.into()));
4480        assert!(!config.in_guest_refuses_host_timed_producers);
4481        assert!(!config.in_guest_signal_delivery);
4482        assert!(config.launch_image.is_none());
4483    }
4484
4485    #[test]
4486    fn a_config_without_backend_capabilities_deserializes_as_ptrace() {
4487        let mut encoded = serde_json::to_value(Config::default()).unwrap();
4488        let fields = encoded.as_object_mut().unwrap();
4489        assert!(fields.remove("backend").is_some());
4490        assert!(!fields.contains_key("shared_dequeue_timers"));
4491        let decoded: Config = serde_json::from_value(encoded).unwrap();
4492        assert_eq!(decoded.backend, DEFAULT_BACKEND_CAPABILITIES);
4493    }
4494
4495    /// `Config`'s own derived deserializer ignores a key it does not name, and
4496    /// `shared_dequeue_timers` is no longer one it names, so JSON saved while
4497    /// it was a field still reads and the key decides nothing, whatever it
4498    /// holds and however often it appears. This is the whole compatibility
4499    /// policy for the named form, and it is safe because no production code
4500    /// reads a `Config` by field name: the standalone DBT form is read by
4501    /// [`from_legacy_backend_json`], which checks the key, and the bincode form
4502    /// that crosses process boundaries is positional. Removing the field
4503    /// changed that positional form and [`config_wire_fingerprint`]. SaBRe's
4504    /// plugin compares the fingerprint and refuses a coordinator from another
4505    /// build; LiteInst's in-guest runtime does not, so a runtime library built
4506    /// before the removal and loaded by a newer coordinator can misdecode the
4507    /// Config and fail its initialization (exit 127). That mixed-build hazard
4508    /// is not new: every change to the positional form has it.
4509    #[test]
4510    fn a_saved_shared_dequeue_timers_field_is_ignored() {
4511        let baseline = serde_json::to_string(&Config::default()).unwrap();
4512        for extra in [
4513            r#""shared_dequeue_timers":false"#,
4514            r#""shared_dequeue_timers":true"#,
4515            r#""shared_dequeue_timers":"yes""#,
4516            r#""shared_dequeue_timers":true,"shared_dequeue_timers":false"#,
4517        ] {
4518            let edited = baseline.replacen('{', &format!("{{{extra},"), 1);
4519            let decoded: Config = serde_json::from_str(&edited).unwrap();
4520            assert_eq!(
4521                serde_json::to_string(&decoded).unwrap(),
4522                baseline,
4523                "{edited}"
4524            );
4525        }
4526    }
4527
4528    /// Ptrace's capabilities with the robust-list transition and parked-write
4529    /// signal interruption both left to Detcore: the value the retired
4530    /// ptrace-hosted LiteInst runtime reported. No backend reports it now, but
4531    /// the legacy keys can still express it, so the round trips keep it.
4532    fn ptrace_without_exit_transitions() -> BackendCapabilities {
4533        let mut backend = BackendCapabilities::PTRACE;
4534        backend.runs_exit_robust_list = false;
4535        backend.supports_parked_write_signal_interruption = false;
4536        backend
4537    }
4538
4539    /// `backend` as the legacy form can carry it: capabilities added after the
4540    /// form froze never enter it and read back as their default (see
4541    /// [`process_exits_complete_asynchronously_never_enters_the_legacy_form`]).
4542    fn legacy_expressible(mut backend: BackendCapabilities) -> BackendCapabilities {
4543        backend.process_exits_complete_asynchronously = false;
4544        backend.virtualizes_guest_sigalrm = false;
4545        set_kernel_signal_facts(&mut backend, false);
4546        backend.delivers_preemption_timer_events = false;
4547        backend.runs_guest_code_between_injections = false;
4548        backend.reports_sigreturns = false;
4549        // Added after the legacy form froze: it reads back as false (see
4550        // `LegacyBackendKeys::capabilities`).
4551        backend.kernel_never_walks_robust_lists = false;
4552        // Added after the legacy form froze: it reads back as true (see
4553        // `LegacyBackendKeys::capabilities`).
4554        backend.provides_guest_backtraces = true;
4555        // Added after the legacy form froze: they read back as false and the
4556        // empty set (see `LegacyBackendKeys::capabilities`).
4557        backend.places_orphans = false;
4558        backend.publishable_parent_death_signals = 0;
4559        backend.reports_kernel_child_exit_signals = backend.reports_child_exit_publication;
4560        // The legacy discover_live_file_metadata bit couples three facts.
4561        backend.rediscovers_descriptors_after_exec = backend.tool_shares_guest_descriptor_table;
4562        // The legacy form derives promotion from the thread-directed signal
4563        // bit, which only DBT, the form's only reader, sets.
4564        backend.promotes_open_files_before_native_lifecycle =
4565            backend.requires_thread_directed_process_signals;
4566        backend.loopback_pollers_yield_to_peers = backend.tool_shares_guest_descriptor_table;
4567        // The legacy form carries no width for `hardware_watchpoints`: it
4568        // reads back as 0, the minimal width (see `LegacyBackendKeys::capabilities`).
4569        backend.hardware_watchpoints = 0;
4570        backend
4571    }
4572
4573    #[test]
4574    fn legacy_backend_json_round_trips_every_backend_capabilities_constant() {
4575        for (name, backend) in [
4576            ("PTRACE", BackendCapabilities::PTRACE),
4577            ("E9PATCH", BackendCapabilities::E9PATCH),
4578            (
4579                "ptrace without exit transitions",
4580                ptrace_without_exit_transitions(),
4581            ),
4582            ("LITEINST_IN_GUEST", BackendCapabilities::LITEINST_IN_GUEST),
4583            ("SABRE", BackendCapabilities::SABRE),
4584            ("DBT", BackendCapabilities::DBT),
4585            ("KVM", BackendCapabilities::KVM),
4586        ] {
4587            for sequentialize_threads in [false, true] {
4588                let sent = Config {
4589                    backend,
4590                    sequentialize_threads,
4591                    ..Config::default()
4592                };
4593                let json = to_legacy_backend_json(&sent).unwrap();
4594                let received = from_legacy_backend_json(&json).unwrap();
4595                assert_eq!(received.backend, legacy_expressible(backend), "{name}");
4596                assert_eq!(
4597                    received.sequentialize_threads, sequentialize_threads,
4598                    "{name}"
4599                );
4600                assert_controlled_key(&json, &backend, sequentialize_threads, name);
4601                let expressible = Config {
4602                    backend: legacy_expressible(backend),
4603                    ..sent.clone()
4604                };
4605                assert_eq!(
4606                    serde_json::to_string(&received).unwrap(),
4607                    serde_json::to_string(&expressible).unwrap(),
4608                    "{name}"
4609                );
4610                assert_eq!(to_legacy_backend_json(&received).unwrap(), json, "{name}");
4611                assert_controlled_key_is_checked(&json, &sent, name);
4612            }
4613        }
4614    }
4615
4616    #[test]
4617    fn backend_config_json_preserves_every_backend_and_representable_bytes() {
4618        for (name, backend, needs_envelope) in [
4619            ("PTRACE", BackendCapabilities::PTRACE, true),
4620            ("E9PATCH", BackendCapabilities::E9PATCH, true),
4621            (
4622                "LITEINST_IN_GUEST",
4623                BackendCapabilities::LITEINST_IN_GUEST,
4624                true,
4625            ),
4626            ("SABRE", BackendCapabilities::SABRE, true),
4627            ("DBT", BackendCapabilities::DBT, true),
4628            // KVM's `provides_guest_backtraces` is false, which the legacy
4629            // form cannot express (it reads back true), so KVM needs the
4630            // envelope like the others.
4631            ("KVM", BackendCapabilities::KVM, true),
4632        ] {
4633            for sequentialize_threads in [false, true] {
4634                let sent = Config {
4635                    backend,
4636                    sequentialize_threads,
4637                    ..Config::default()
4638                };
4639                let legacy = to_legacy_backend_json(&sent).unwrap();
4640                let encoded = encode_backend_config_json(legacy.clone(), backend).unwrap();
4641                assert_eq!(encoded != legacy, needs_envelope, "{name}");
4642                assert_eq!(
4643                    from_legacy_backend_json(&encoded).is_err(),
4644                    needs_envelope,
4645                    "{name}: an envelope must not be accepted as a legacy config"
4646                );
4647                let received = decode_backend_config_json(&encoded).unwrap();
4648                assert_eq!(received.backend, backend, "{name}");
4649                assert_eq!(
4650                    serde_json::to_string(&received).unwrap(),
4651                    serde_json::to_string(&sent).unwrap(),
4652                    "{name}"
4653                );
4654            }
4655        }
4656    }
4657
4658    #[test]
4659    fn backend_config_json_round_trips_each_independent_capability() {
4660        let baseline = serde_json::to_value(BackendCapabilities::DBT).unwrap();
4661        for (name, value) in baseline.as_object().unwrap() {
4662            let mut edited = baseline.clone();
4663            // Every capability is a bool except `hardware_watchpoints`, a
4664            // `u8` width, and `publishable_parent_death_signals`, a `u64`
4665            // set of signal bits: flip a bool, and move a number to another
4666            // value (DBT's is 0 for both: width 1, and KVM's set).
4667            edited[name] = match name.as_str() {
4668                "hardware_watchpoints" => serde_json::Value::from(1u8),
4669                "publishable_parent_death_signals" => serde_json::Value::from(
4670                    BackendCapabilities::KVM.publishable_parent_death_signals,
4671                ),
4672                _ => serde_json::Value::Bool(!value.as_bool().unwrap()),
4673            };
4674            let backend = serde_json::from_value(edited).unwrap();
4675            let sent = Config {
4676                backend,
4677                ..Config::default()
4678            };
4679            let legacy = to_legacy_backend_json(&sent).unwrap();
4680            let encoded = encode_backend_config_json(legacy, backend).unwrap();
4681            let received = decode_backend_config_json(&encoded).unwrap();
4682            assert_eq!(received.backend, backend, "{name}");
4683        }
4684    }
4685
4686    #[test]
4687    fn backend_config_json_requires_each_typed_capability_once() {
4688        let sent = Config {
4689            backend: BackendCapabilities::LITEINST_IN_GUEST,
4690            ..Config::default()
4691        };
4692        let encoded =
4693            encode_backend_config_json(to_legacy_backend_json(&sent).unwrap(), sent.backend)
4694                .unwrap();
4695        let envelope: serde_json::Value = serde_json::from_str(&encoded).unwrap();
4696        for (name, value) in envelope["backend"].as_object().unwrap() {
4697            let mut missing = envelope.clone();
4698            missing["backend"].as_object_mut().unwrap().remove(name);
4699            assert!(
4700                decode_backend_config_json(&missing.to_string()).is_err(),
4701                "missing {name}"
4702            );
4703            let backend = serde_json::to_string(&sent.backend).unwrap();
4704            let duplicate = backend.replacen('{', &format!("{{\"{name}\":{value},"), 1);
4705            let duplicate = encoded.replace(&backend, &duplicate);
4706            assert_ne!(duplicate, encoded);
4707            assert!(
4708                decode_backend_config_json(&duplicate).is_err(),
4709                "duplicate {name}"
4710            );
4711        }
4712    }
4713
4714    #[test]
4715    fn backend_config_json_keeps_legacy_object_and_array_semantics() {
4716        let legacy = to_legacy_backend_json(&Config::default()).unwrap();
4717        let unknown_duplicates = legacy.replacen(
4718            '{',
4719            r#"{"unrecognized":1,"unrecognized":null,"backend":null,"backend":true,"dbt_config_version":null,"dbt_config_version":2,"#,
4720            1,
4721        );
4722        let known_duplicate = legacy.replacen('{', r#"{"seed":1,"#, 1);
4723        let mistyped = legacy.replacen("\"backend_is_kvm\":false", "\"backend_is_kvm\":null", 1);
4724        assert_ne!(mistyped, legacy);
4725        for json in [
4726            legacy.clone(),
4727            positional(&legacy),
4728            unknown_duplicates,
4729            known_duplicate,
4730            mistyped,
4731            format!("{legacy} trailing"),
4732            "[]".to_owned(),
4733        ] {
4734            let old = from_legacy_backend_json(&json);
4735            let new = decode_backend_config_json(&json);
4736            match (old, new) {
4737                (Ok(old), Ok(new)) => assert_eq!(
4738                    serde_json::to_string(&old).unwrap(),
4739                    serde_json::to_string(&new).unwrap()
4740                ),
4741                (Err(_), Err(_)) => {}
4742                _ => panic!("legacy parse outcome changed for {json}"),
4743            }
4744        }
4745    }
4746
4747    /// `kvm_shared_dequeue_timers` is read independently of the other keys,
4748    /// absent, false or true, and the parse succeeds exactly when the value
4749    /// read (false when absent) equals `backend_is_kvm &&
4750    /// sequentialize_threads`. Under that condition the accepted value is the
4751    /// one the decoded capabilities and `sequentialize_threads` imply, so the
4752    /// decoded configuration is the one sent; otherwise the error names the
4753    /// key.
4754    fn assert_controlled_key_is_checked(json: &str, sent: &Config, name: &str) {
4755        let controlled = sent.backend.provides_process_signal_control && sent.sequentialize_threads;
4756        let written = format!("\"kvm_shared_dequeue_timers\":{controlled}");
4757        assert_eq!(json.matches(&written).count(), 1, "{name}");
4758        for value in [None, Some(false), Some(true)] {
4759            let edited = match value {
4760                None => json.replacen(&format!(",{written}"), "", 1),
4761                Some(value) => json.replacen(
4762                    &written,
4763                    &format!("\"kvm_shared_dequeue_timers\":{value}"),
4764                    1,
4765                ),
4766            };
4767            assert_eq!(
4768                edited.contains("kvm_shared_dequeue_timers"),
4769                value.is_some(),
4770                "{name} {value:?}"
4771            );
4772            let case = format!(
4773                "{name} sequentialize_threads={} key={value:?}",
4774                sent.sequentialize_threads
4775            );
4776            match from_legacy_backend_json(&edited) {
4777                Ok(received) => {
4778                    assert_eq!(value.unwrap_or(false), controlled, "{case} was accepted");
4779                    // Compared with what the legacy form can carry; see
4780                    // `legacy_expressible`.
4781                    let expressible = Config {
4782                        backend: legacy_expressible(sent.backend),
4783                        ..sent.clone()
4784                    };
4785                    assert_eq!(
4786                        serde_json::to_string(&received).unwrap(),
4787                        serde_json::to_string(&expressible).unwrap(),
4788                        "{case}"
4789                    );
4790                }
4791                Err(error) => {
4792                    assert_ne!(value.unwrap_or(false), controlled, "{case}: {error}");
4793                    assert!(
4794                        error.to_string().contains("kvm_shared_dequeue_timers"),
4795                        "{case}: {error}"
4796                    );
4797                }
4798            }
4799        }
4800    }
4801
4802    /// The four controlled-key outcomes that differ from simply ignoring the
4803    /// key, spelled out: each would otherwise run a configuration on a path
4804    /// other than the one the same JSON selected before.
4805    #[test]
4806    fn a_controlled_key_that_contradicts_the_capabilities_fails_the_parse() {
4807        let kvm = Config {
4808            backend: BackendCapabilities::KVM,
4809            sequentialize_threads: true,
4810            ..Config::default()
4811        };
4812        let kvm_json = to_legacy_backend_json(&kvm).unwrap();
4813        assert!(from_legacy_backend_json(&kvm_json).is_ok());
4814        let dbt = Config {
4815            backend: BackendCapabilities::DBT,
4816            sequentialize_threads: true,
4817            ..Config::default()
4818        };
4819        let dbt_json = to_legacy_backend_json(&dbt).unwrap();
4820        assert!(from_legacy_backend_json(&dbt_json).is_ok());
4821        for edited in [
4822            // KVM, sequentialized, key false: the control was left uninstalled.
4823            kvm_json.replacen(
4824                r#""kvm_shared_dequeue_timers":true"#,
4825                r#""kvm_shared_dequeue_timers":false"#,
4826                1,
4827            ),
4828            // KVM, sequentialized, key absent: read as false, as before.
4829            kvm_json.replacen(r#","kvm_shared_dequeue_timers":true"#, "", 1),
4830            // No control capability, key true: the controlled loop was chosen.
4831            dbt_json.replacen(
4832                r#""kvm_shared_dequeue_timers":false"#,
4833                r#""kvm_shared_dequeue_timers":true"#,
4834                1,
4835            ),
4836            // KVM without sequentialized threads, key true.
4837            to_legacy_backend_json(&Config {
4838                sequentialize_threads: false,
4839                ..kvm.clone()
4840            })
4841            .unwrap()
4842            .replacen(
4843                r#""kvm_shared_dequeue_timers":false"#,
4844                r#""kvm_shared_dequeue_timers":true"#,
4845                1,
4846            ),
4847        ] {
4848            assert_ne!(edited, kvm_json);
4849            assert_ne!(edited, dbt_json);
4850            let error = from_legacy_backend_json(&edited).unwrap_err();
4851            assert!(
4852                error.to_string().contains("kvm_shared_dequeue_timers"),
4853                "{error}"
4854            );
4855        }
4856    }
4857
4858    /// Each legacy key decodes as the `Config` field it was: absent, it takes
4859    /// that field's default, and set alone, it gives exactly the capabilities
4860    /// that replaced Detcore's reads of that field.
4861    #[test]
4862    fn legacy_backend_keys_decode_as_the_fields_they_were() {
4863        let ptrace_json = to_legacy_backend_json(&Config::default()).unwrap();
4864        let ptrace: serde_json::Map<String, serde_json::Value> =
4865            serde_json::from_str(&ptrace_json).unwrap();
4866        let keys = legacy_backend_keys(&Config::default());
4867
4868        let mut absent = ptrace.clone();
4869        for (key, _) in keys {
4870            assert!(absent.remove(key).is_some(), "{key}");
4871        }
4872        let decoded = from_legacy_backend_json(&serde_json::to_string(&absent).unwrap()).unwrap();
4873        // The capabilities added after the legacy form froze read back as
4874        // false (`legacy_expressible`), whatever ptrace reports.
4875        let legacy_ptrace = legacy_expressible(BackendCapabilities::PTRACE);
4876        assert_eq!(decoded.backend, legacy_ptrace);
4877        let unflipped =
4878            serde_json::to_string(&from_legacy_backend_json(&ptrace_json).unwrap()).unwrap();
4879        assert_eq!(serde_json::to_string(&decoded).unwrap(), unflipped);
4880
4881        for (key, ptrace_value) in keys {
4882            let mut expected = legacy_ptrace;
4883            match key {
4884                "cpuid_virtualized_by_backend" => {
4885                    // No backend installs a CPUID policy of its own, so `true`
4886                    // fails the parse.
4887                    let mut flipped = ptrace.clone();
4888                    flipped.insert(key.to_owned(), serde_json::Value::Bool(!ptrace_value));
4889                    let error = from_legacy_backend_json(&serde_json::to_string(&flipped).unwrap())
4890                        .unwrap_err();
4891                    assert!(error.to_string().contains(key), "{error}");
4892                    continue;
4893                }
4894                "syscall_clobbers_virtualized_by_backend"
4895                | "backend_supports_madvise"
4896                | "backend_defers_vfork_child_registration" => {
4897                    // None supplies a capability: either value reads back the
4898                    // same configuration.
4899                    let mut flipped = ptrace.clone();
4900                    flipped.insert(key.to_owned(), serde_json::Value::Bool(!ptrace_value));
4901                    let decoded =
4902                        from_legacy_backend_json(&serde_json::to_string(&flipped).unwrap())
4903                            .unwrap();
4904                    assert_eq!(serde_json::to_string(&decoded).unwrap(), unflipped, "{key}");
4905                    continue;
4906                }
4907                "discover_live_file_metadata" => {
4908                    expected.tool_shares_guest_descriptor_table = true;
4909                    expected.rediscovers_descriptors_after_exec = true;
4910                    expected.loopback_pollers_yield_to_peers = true;
4911                }
4912                "detect_host_clock_futex_timeouts" => {
4913                    expected.guest_clock_reads_bypass_backend = true
4914                }
4915                "cancel_killed_thread_rpcs" => {
4916                    expected.needs_killed_thread_rpc_cancellation = true;
4917                    expected.reports_child_exit_publication = false;
4918                    expected.reports_kernel_child_exit_signals = false;
4919                }
4920                "backend_reports_physical_process_exits" => {
4921                    expected.reports_physical_process_exits = true
4922                }
4923                "backend_tracks_process_children" => expected.tracks_process_children = false,
4924                "backend_runs_exit_robust_list" => expected.runs_exit_robust_list = false,
4925                "backend_requires_thread_directed_process_signals" => {
4926                    expected.requires_thread_directed_process_signals = true;
4927                    expected.promotes_open_files_before_native_lifecycle = true;
4928                }
4929                "backend_is_kvm" => {
4930                    expected.provides_process_signal_control = true;
4931                    expected.emulates_child_waits = true;
4932                }
4933                "kvm_shared_dequeue_timers" => {
4934                    // Set alone it contradicts `backend_is_kvm`, so the parse
4935                    // fails; see
4936                    // `a_controlled_key_that_contradicts_the_capabilities_fails_the_parse`.
4937                    let mut flipped = ptrace.clone();
4938                    flipped.insert(key.to_owned(), serde_json::Value::Bool(!ptrace_value));
4939                    assert!(
4940                        from_legacy_backend_json(&serde_json::to_string(&flipped).unwrap())
4941                            .is_err(),
4942                        "{key}"
4943                    );
4944                    continue;
4945                }
4946                "backend_supports_parked_write_signal_interruption" => {
4947                    expected.supports_parked_write_signal_interruption = false
4948                }
4949                "backend_virtualizes_capability_prctls" => {
4950                    // No backend answers the capability prctls itself, so
4951                    // `true` fails the parse.
4952                    let mut flipped = ptrace.clone();
4953                    flipped.insert(key.to_owned(), serde_json::Value::Bool(!ptrace_value));
4954                    let error = from_legacy_backend_json(&serde_json::to_string(&flipped).unwrap())
4955                        .unwrap_err();
4956                    assert!(error.to_string().contains(key), "{error}");
4957                    continue;
4958                }
4959                other => panic!("no expectation for legacy key {other}"),
4960            }
4961            let mut flipped = ptrace.clone();
4962            flipped.insert(key.to_owned(), serde_json::Value::Bool(!ptrace_value));
4963            let decoded =
4964                from_legacy_backend_json(&serde_json::to_string(&flipped).unwrap()).unwrap();
4965            assert_eq!(decoded.backend, expected, "{key}");
4966            assert_ne!(serde_json::to_string(&decoded).unwrap(), unflipped, "{key}");
4967        }
4968    }
4969
4970    /// The legacy `kvm_shared_dequeue_timers` key holds exactly when the
4971    /// backend provides process signal control and threads are sequentialized,
4972    /// the condition under which Detcore installs that control.
4973    fn assert_controlled_key(
4974        json: &str,
4975        backend: &BackendCapabilities,
4976        sequentialize_threads: bool,
4977        name: &str,
4978    ) {
4979        let object: serde_json::Map<String, serde_json::Value> =
4980            serde_json::from_str(json).unwrap();
4981        assert_eq!(
4982            object["kvm_shared_dequeue_timers"],
4983            serde_json::Value::Bool(
4984                backend.provides_process_signal_control && sequentialize_threads
4985            ),
4986            "{name} sequentialize_threads={sequentialize_threads}"
4987        );
4988    }
4989
4990    /// A legacy key failed the parse when it held the wrong type, or appeared
4991    /// twice, while it was a `Config` field; it still does, so a reader falls
4992    /// back as it did.
4993    #[test]
4994    fn a_legacy_backend_key_of_the_wrong_type_fails_the_parse() {
4995        let json = to_legacy_backend_json(&Config::default()).unwrap();
4996        assert!(from_legacy_backend_json(&json).is_ok());
4997        for (key, value) in legacy_backend_keys(&Config::default()) {
4998            let field = format!("\"{key}\":{value}");
4999            assert_eq!(json.matches(&field).count(), 1, "{field}");
5000            let mistyped = json.replacen(&field, &format!("\"{key}\":0"), 1);
5001            assert!(from_legacy_backend_json(&mistyped).is_err(), "{mistyped}");
5002            let repeated = json.replacen(&field, &format!("{field},{field}"), 1);
5003            assert!(from_legacy_backend_json(&repeated).is_err(), "{repeated}");
5004        }
5005    }
5006
5007    /// The entries of a JSON object, in the order they appear.
5008    fn ordered_entries(json: &str) -> Vec<(String, serde_json::Value)> {
5009        struct Entries(Vec<(String, serde_json::Value)>);
5010
5011        impl<'de> Deserialize<'de> for Entries {
5012            fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
5013                struct EntriesVisitor;
5014
5015                impl<'de> serde::de::Visitor<'de> for EntriesVisitor {
5016                    type Value = Entries;
5017
5018                    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
5019                        formatter.write_str("a JSON object")
5020                    }
5021
5022                    fn visit_map<A: serde::de::MapAccess<'de>>(
5023                        self,
5024                        mut map: A,
5025                    ) -> Result<Entries, A::Error> {
5026                        let mut entries = Vec::new();
5027                        while let Some(entry) = map.next_entry()? {
5028                            entries.push(entry);
5029                        }
5030                        Ok(Entries(entries))
5031                    }
5032                }
5033
5034                deserializer.deserialize_map(EntriesVisitor)
5035            }
5036        }
5037
5038        serde_json::from_str::<Entries>(json).unwrap().0
5039    }
5040
5041    /// The JSON array of `json`'s values in its key order: the same
5042    /// configuration, listed by position.
5043    fn positional(json: &str) -> String {
5044        let values: Vec<serde_json::Value> = ordered_entries(json)
5045            .into_iter()
5046            .map(|(_, value)| value)
5047            .collect();
5048        serde_json::to_string(&values).unwrap()
5049    }
5050
5051    /// The legacy form's positions are `Config`'s fields in declaration order
5052    /// with the fifteen legacy keys where `backend` stands and no
5053    /// `record_host_inputs`, `guest_may_inherit_a_terminal`,
5054    /// `in_guest_detlog_forward_policy`, `in_guest_site_patching_off`,
5055    /// `in_guest_reserved_signals`, `in_guest_refuses_host_timed_producers`,
5056    /// `in_guest_signal_delivery`,
5057    /// `launch_image`, `target_timeslice_syscalls_only`, `futex_wake_yields`,
5058    /// `replaying`,
5059    /// `scheduler_turn_cost`, `cap_preemption_charge`,
5060    /// `inherited_seccomp`, `replay_edited_preemptions` or `detlog_anon`,
5061    /// which is the key order the encoder writes.
5062    #[test]
5063    fn legacy_positions_are_the_encoded_key_order() {
5064        let names = legacy_backend_keys(&Config::default()).map(|(name, _)| name);
5065        assert_eq!(names, LEGACY_BACKEND_KEY_NAMES);
5066
5067        let fields = serde_json::from_str::<legacy_backend_json::KeyOrder>(
5068            &serde_json::to_string(&Config::default()).unwrap(),
5069        )
5070        .unwrap()
5071        .0;
5072        let mut expected = Vec::new();
5073        for field in &fields {
5074            match field.as_str() {
5075                "backend" => expected.extend(names.map(str::to_owned)),
5076                "strict_schedule_boundaries"
5077                | "record_host_inputs"
5078                | "guest_may_inherit_a_terminal"
5079                | "in_guest_detlog_forward_policy"
5080                | "in_guest_site_patching_off"
5081                | "in_guest_reserved_signals"
5082                | "in_guest_refuses_host_timed_producers"
5083                | "launch_image"
5084                | "target_timeslice_syscalls_only"
5085                | "futex_wake_yields"
5086                | "pipe_wake_latency"
5087                | "expired_wait_charge"
5088                | "replaying"
5089                | "scheduler_turn_cost"
5090                | "cap_preemption_charge"
5091                | "virtual_timeout"
5092                | "inherited_seccomp"
5093                | "cpu_profile"
5094                | "cpu_profile_host"
5095                | "cpu_profile_source"
5096                | "signal_state_copy"
5097                | "container_mount_table"
5098                | "mountinfo_mount_namespace"
5099                | "directory_placeholder"
5100                | "container_procfs"
5101                | "signal_delivery_check_fatal"
5102                | "work_span_report"
5103                | "signal_at"
5104                | "in_guest_signal_delivery"
5105                | "annotated_priority_replay"
5106                | "virtual_cpus"
5107                | "notice_host_network_abstract_binds"
5108                | "replay_edited_preemptions"
5109                | "eager_lone_preemptions"
5110                | "chaos_timeslice_range"
5111                | "chaos_timeslice_draw"
5112                | "chaos_timeslice"
5113                | "guest_ids"
5114                | "detlog_anon"
5115                | "standard_stream_aliases" => {}
5116                "access_trace"
5117                | "access_trace_from"
5118                | "access_trace_max_records"
5119                | "access_trace_max_bytes" => {}
5120                "replay_continue_fairly" => {}
5121                other => expected.push(other.to_owned()),
5122            }
5123        }
5124        let encoded = to_legacy_backend_json(&Config::default()).unwrap();
5125        let keys: Vec<String> = ordered_entries(&encoded)
5126            .into_iter()
5127            .map(|(key, _)| key)
5128            .collect();
5129        assert_eq!(keys, expected);
5130        // `backend` becomes fifteen keys; `record_host_inputs`,
5131        // `guest_may_inherit_a_terminal`,
5132        // `in_guest_detlog_forward_policy`, `in_guest_site_patching_off`,
5133        // `in_guest_reserved_signals`, `in_guest_refuses_host_timed_producers`,
5134        // `launch_image`, `target_timeslice_syscalls_only`, `futex_wake_yields`,
5135        // `pipe_wake_latency`, `expired_wait_charge`, `replaying`, `scheduler_turn_cost`,
5136        // `cap_preemption_charge`, `virtual_timeout`, `inherited_seccomp`, `cpu_profile`,
5137        // `cpu_profile_host`, `cpu_profile_source`,
5138        // `signal_state_copy`, `container_mount_table`,
5139        // `guest_may_inherit_a_terminal`, `in_guest_detlog_forward_policy`,
5140        // `in_guest_site_patching_off`, `in_guest_reserved_signals`,
5141        // `in_guest_refuses_host_timed_producers`, `launch_image`,
5142        // `target_timeslice_syscalls_only`, `futex_wake_yields`,
5143        // `pipe_wake_latency`, `replaying`, `scheduler_turn_cost`,
5144        // `inherited_seccomp`, `cpu_profile`, `cpu_profile_host`,
5145        // `cpu_profile_source`, `signal_state_copy`, `container_mount_table`,
5146        // `mountinfo_mount_namespace`, `directory_placeholder`,
5147        // `container_procfs`, `signal_delivery_check_fatal`,
5148        // `work_span_report`, `signal_at`, `in_guest_signal_delivery`,
5149        // `annotated_priority_replay`, `virtual_cpus`, `replay_edited_preemptions`,
5150        // `notice_host_network_abstract_binds`, `eager_lone_preemptions`,
5151        // `chaos_timeslice_range`, `chaos_timeslice_draw`, `chaos_timeslice`,
5152        // `guest_ids`, `virtual_timeout`, `detlog_anon`,
5153        // `access_trace`, `access_trace_from`, `access_trace_max_records`,
5154        // `access_trace_max_bytes`, `replay_continue_fairly`,
5155        // `standard_stream_aliases` and `strict_schedule_boundaries` none:
5156        // fourteen more keys and forty-five fewer, so thirty-one keys fewer
5157        // than fields.
5158        assert!(!fields.iter().any(|field| field == "shared_dequeue_timers"));
5159        assert_eq!(fields.len(), keys.len() + 31);
5160    }
5161
5162    /// `record_host_inputs` never enters the legacy form, whatever its value:
5163    /// the legacy string keeps its bytes, and it reads back as
5164    /// false from both the object and the array form.
5165    #[test]
5166    fn record_host_inputs_never_enters_the_legacy_form() {
5167        let off = Config {
5168            backend: BackendCapabilities::DBT,
5169            ..Config::default()
5170        };
5171        let on = Config {
5172            record_host_inputs: true,
5173            ..off.clone()
5174        };
5175        let json = to_legacy_backend_json(&on).unwrap();
5176        assert_eq!(json, to_legacy_backend_json(&off).unwrap());
5177        assert!(!json.contains("record_host_inputs"), "{json}");
5178        assert!(!from_legacy_backend_json(&json).unwrap().record_host_inputs);
5179        let values: Vec<serde_json::Value> = ordered_entries(&json)
5180            .into_iter()
5181            .map(|(_, value)| value)
5182            .collect();
5183        let array = serde_json::to_string(&values).unwrap();
5184        assert!(!from_legacy_backend_json(&array).unwrap().record_host_inputs);
5185    }
5186
5187    /// `process_exits_complete_asynchronously`, added after the legacy form
5188    /// froze, never enters it, whatever its value, exactly as
5189    /// `record_host_inputs` does not: the legacy string keeps its bytes, and
5190    /// it reads back as false from the object and the array form.
5191    #[test]
5192    fn process_exits_complete_asynchronously_never_enters_the_legacy_form() {
5193        let on = Config {
5194            backend: BackendCapabilities::LITEINST_IN_GUEST,
5195            ..Config::default()
5196        };
5197        assert!(on.backend.process_exits_complete_asynchronously);
5198        assert!(on.backend.virtualizes_guest_sigalrm);
5199        let off = Config {
5200            backend: legacy_expressible(on.backend),
5201            ..on.clone()
5202        };
5203        let json = to_legacy_backend_json(&on).unwrap();
5204        assert_eq!(json, to_legacy_backend_json(&off).unwrap());
5205        assert!(
5206            !json.contains("process_exits_complete_asynchronously"),
5207            "{json}"
5208        );
5209        assert!(!json.contains("virtualizes_guest_sigalrm"), "{json}");
5210        let decoded = from_legacy_backend_json(&json).unwrap();
5211        assert!(!decoded.backend.process_exits_complete_asynchronously);
5212        assert!(!decoded.backend.virtualizes_guest_sigalrm);
5213        let array = positional(&json);
5214        assert!(
5215            !from_legacy_backend_json(&array)
5216                .unwrap()
5217                .backend
5218                .process_exits_complete_asynchronously
5219        );
5220    }
5221
5222    /// The capabilities added after the legacy form froze that a legacy
5223    /// configuration reads back as false
5224    /// (`supports_blocked_wait_signal_interruption` and the four split out of
5225    /// it, `delivers_preemption_timer_events`,
5226    /// `runs_guest_code_between_injections`, `reports_sigreturns`,
5227    /// `kernel_never_walks_robust_lists`, `places_orphans`, and
5228    /// `publishable_parent_death_signals`, which reads back as the empty set)
5229    /// never enter it, whatever their
5230    /// value: the legacy string keeps its bytes, and they read
5231    /// back as false from both the object and the array form.
5232    #[test]
5233    fn post_freeze_capabilities_never_enter_the_legacy_form() {
5234        let off = Config {
5235            backend: BackendCapabilities::DBT,
5236            ..Config::default()
5237        };
5238        let mut on = off.clone();
5239        set_kernel_signal_facts(&mut on.backend, true);
5240        on.backend.delivers_preemption_timer_events = true;
5241        on.backend.runs_guest_code_between_injections = true;
5242        on.backend.reports_sigreturns = true;
5243        on.backend.kernel_never_walks_robust_lists = true;
5244        on.backend.places_orphans = true;
5245        on.backend.publishable_parent_death_signals =
5246            BackendCapabilities::KVM.publishable_parent_death_signals;
5247        let json = to_legacy_backend_json(&on).unwrap();
5248        assert_eq!(json, to_legacy_backend_json(&off).unwrap());
5249        for key in [
5250            "supports_blocked_wait_signal_interruption",
5251            "kernel_signal_state_is_guests",
5252            "kernel_signal_targets_are_predictable",
5253            "host_task_reads_reach_guest_threads",
5254            "host_timed_signals_are_physical",
5255            "delivers_preemption_timer_events",
5256            "runs_guest_code_between_injections",
5257            "reports_sigreturns",
5258            "kernel_never_walks_robust_lists",
5259            "places_orphans",
5260            "publishable_parent_death_signals",
5261        ] {
5262            assert!(!json.contains(key), "{json}");
5263        }
5264        let values: Vec<serde_json::Value> = ordered_entries(&json)
5265            .into_iter()
5266            .map(|(_, value)| value)
5267            .collect();
5268        let array = serde_json::to_string(&values).unwrap();
5269        for decoded in [&json, &array] {
5270            let backend = from_legacy_backend_json(decoded).unwrap().backend;
5271            assert!(!backend.supports_blocked_wait_signal_interruption);
5272            assert!(!backend.kernel_signal_state_is_guests);
5273            assert!(!backend.kernel_signal_targets_are_predictable);
5274            assert!(!backend.host_task_reads_reach_guest_threads);
5275            assert!(!backend.host_timed_signals_are_physical);
5276            assert!(!backend.delivers_preemption_timer_events);
5277            assert!(!backend.runs_guest_code_between_injections);
5278            assert!(!backend.reports_sigreturns);
5279            assert!(!backend.kernel_never_walks_robust_lists);
5280            assert!(!backend.places_orphans);
5281            assert_eq!(backend.publishable_parent_death_signals, 0);
5282            assert_eq!(backend, legacy_expressible(BackendCapabilities::DBT));
5283        }
5284    }
5285
5286    /// `guest_may_inherit_a_terminal` never enters the legacy form, whatever
5287    /// its value: the legacy string keeps its bytes, and it reads
5288    /// back as false from both the object and the array form.
5289    /// `in_guest_detlog_forward_policy` is unset for DBT, so the legacy form
5290    /// never carries it, and it reads back as unset.
5291    #[test]
5292    fn in_guest_detlog_forward_policy_never_enters_the_legacy_form() {
5293        let off = Config {
5294            backend: BackendCapabilities::DBT,
5295            ..Config::default()
5296        };
5297        let on = Config {
5298            in_guest_detlog_forward_policy: Some("1".to_owned()),
5299            ..off.clone()
5300        };
5301        let json = to_legacy_backend_json(&on).unwrap();
5302        assert_eq!(json, to_legacy_backend_json(&off).unwrap());
5303        assert!(!json.contains("in_guest_detlog_forward_policy"), "{json}");
5304        assert!(
5305            from_legacy_backend_json(&json)
5306                .unwrap()
5307                .in_guest_detlog_forward_policy
5308                .is_none()
5309        );
5310    }
5311
5312    /// `launch_image` has no legacy key, so the legacy form never carries
5313    /// it, and it reads back as unset from both the object and the array
5314    /// form.
5315    #[test]
5316    fn launch_image_never_enters_the_legacy_form() {
5317        let off = Config {
5318            backend: BackendCapabilities::DBT,
5319            ..Config::default()
5320        };
5321        let on = Config {
5322            launch_image: Some(LaunchImage {
5323                exe: Some(PathBuf::from("/usr/bin/true")),
5324                launcher: Some(PathBuf::from("/usr/bin/env")),
5325            }),
5326            ..off.clone()
5327        };
5328        let json = to_legacy_backend_json(&on).unwrap();
5329        assert_eq!(json, to_legacy_backend_json(&off).unwrap());
5330        assert!(!json.contains("launch_image"), "{json}");
5331        assert!(
5332            from_legacy_backend_json(&json)
5333                .unwrap()
5334                .launch_image
5335                .is_none()
5336        );
5337        let values: Vec<serde_json::Value> = ordered_entries(&json)
5338            .into_iter()
5339            .map(|(_, value)| value)
5340            .collect();
5341        let array = serde_json::to_string(&values).unwrap();
5342        assert!(
5343            from_legacy_backend_json(&array)
5344                .unwrap()
5345                .launch_image
5346                .is_none()
5347        );
5348    }
5349
5350    /// `in_guest_site_patching_off` is false for DBT, so the legacy form never
5351    /// carries it, and it reads back as false from both the object and the
5352    /// array form.
5353    #[test]
5354    fn in_guest_site_patching_off_never_enters_the_legacy_form() {
5355        let off = Config {
5356            backend: BackendCapabilities::DBT,
5357            ..Config::default()
5358        };
5359        let on = Config {
5360            in_guest_site_patching_off: true,
5361            ..off.clone()
5362        };
5363        let json = to_legacy_backend_json(&on).unwrap();
5364        assert_eq!(json, to_legacy_backend_json(&off).unwrap());
5365        assert!(!json.contains("in_guest_site_patching_off"), "{json}");
5366        assert!(
5367            !from_legacy_backend_json(&json)
5368                .unwrap()
5369                .in_guest_site_patching_off
5370        );
5371        let values: Vec<serde_json::Value> = ordered_entries(&json)
5372            .into_iter()
5373            .map(|(_, value)| value)
5374            .collect();
5375        let array = serde_json::to_string(&values).unwrap();
5376        assert!(
5377            !from_legacy_backend_json(&array)
5378                .unwrap()
5379                .in_guest_site_patching_off
5380        );
5381    }
5382
5383    #[test]
5384    fn guest_may_inherit_a_terminal_never_enters_the_legacy_form() {
5385        let off = Config {
5386            backend: BackendCapabilities::DBT,
5387            ..Config::default()
5388        };
5389        let on = Config {
5390            guest_may_inherit_a_terminal: true,
5391            ..off.clone()
5392        };
5393        let json = to_legacy_backend_json(&on).unwrap();
5394        assert_eq!(json, to_legacy_backend_json(&off).unwrap());
5395        assert!(!json.contains("guest_may_inherit_a_terminal"), "{json}");
5396        assert!(
5397            !from_legacy_backend_json(&json)
5398                .unwrap()
5399                .guest_may_inherit_a_terminal
5400        );
5401        let values: Vec<serde_json::Value> = ordered_entries(&json)
5402            .into_iter()
5403            .map(|(_, value)| value)
5404            .collect();
5405        let array = serde_json::to_string(&values).unwrap();
5406        assert!(
5407            !from_legacy_backend_json(&array)
5408                .unwrap()
5409                .guest_may_inherit_a_terminal
5410        );
5411    }
5412
5413    /// `guest_ids` never enters the legacy form, whatever its value: the
5414    /// legacy string keeps its bytes, and it reads back as numbered by a PID
5415    /// namespace from both the object and the array form.
5416    #[test]
5417    fn the_guest_numbering_never_enters_the_legacy_form() {
5418        let namespace = Config {
5419            backend: BackendCapabilities::DBT,
5420            ..Config::default()
5421        };
5422        let chosen = Config {
5423            guest_ids: crate::pid::GuestIds::Chosen {
5424                base: 5000,
5425                refuse_host_ids: true,
5426            },
5427            ..namespace.clone()
5428        };
5429        let json = to_legacy_backend_json(&chosen).unwrap();
5430        assert_eq!(json, to_legacy_backend_json(&namespace).unwrap());
5431        assert!(!json.contains("guest_ids"), "{json}");
5432        assert_eq!(
5433            from_legacy_backend_json(&json).unwrap().guest_ids,
5434            crate::pid::GuestIds::Namespace
5435        );
5436        let values: Vec<serde_json::Value> = ordered_entries(&json)
5437            .into_iter()
5438            .map(|(_, value)| value)
5439            .collect();
5440        let array = serde_json::to_string(&values).unwrap();
5441        assert_eq!(
5442            from_legacy_backend_json(&array).unwrap().guest_ids,
5443            crate::pid::GuestIds::Namespace
5444        );
5445    }
5446
5447    /// `inherited_seccomp` never enters the legacy form, whatever its value:
5448    /// the legacy string keeps its bytes, and it reads back as
5449    /// unknown from both the object and the array form.
5450    #[test]
5451    fn the_cpu_profile_never_enters_the_legacy_form() {
5452        let v2 = Config {
5453            backend: BackendCapabilities::DBT,
5454            ..Config::default()
5455        };
5456        let v3 = Config {
5457            cpu_profile: crate::cpu_profile::CpuProfile::X86_64V3,
5458            cpu_profile_source: Some(17),
5459            ..v2.clone()
5460        };
5461        let json = to_legacy_backend_json(&v3).unwrap();
5462        assert_eq!(json, to_legacy_backend_json(&v2).unwrap());
5463        assert!(!json.contains("cpu_profile"), "{json}");
5464        assert_eq!(
5465            from_legacy_backend_json(&json).unwrap().cpu_profile,
5466            crate::cpu_profile::CpuProfile::X86_64V2
5467        );
5468        assert_eq!(
5469            from_legacy_backend_json(&json).unwrap().cpu_profile_source,
5470            None
5471        );
5472        // The bincode form the tool reads carries all profile fields.
5473        let wire = bincode::serde::encode_to_vec(&v3, bincode::config::legacy()).unwrap();
5474        let (decoded, _): (Config, usize) =
5475            bincode::serde::decode_from_slice(&wire, bincode::config::legacy()).unwrap();
5476        assert_eq!(
5477            decoded.cpu_profile,
5478            crate::cpu_profile::CpuProfile::X86_64V3
5479        );
5480        assert_eq!(decoded.cpu_profile_source, Some(17));
5481    }
5482
5483    #[test]
5484    fn inherited_seccomp_never_enters_the_legacy_form() {
5485        use crate::config::InheritedSeccomp;
5486        let unknown = Config {
5487            backend: BackendCapabilities::DBT,
5488            ..Config::default()
5489        };
5490        assert_eq!(unknown.inherited_seccomp, InheritedSeccomp::Unknown);
5491        let measured = Config {
5492            inherited_seccomp: InheritedSeccomp::Filter(Some(2)),
5493            ..unknown.clone()
5494        };
5495        let json = to_legacy_backend_json(&measured).unwrap();
5496        assert_eq!(json, to_legacy_backend_json(&unknown).unwrap());
5497        assert!(!json.contains("inherited_seccomp"), "{json}");
5498        assert_eq!(
5499            from_legacy_backend_json(&json).unwrap().inherited_seccomp,
5500            InheritedSeccomp::Unknown
5501        );
5502        let values: Vec<serde_json::Value> = ordered_entries(&json)
5503            .into_iter()
5504            .map(|(_, value)| value)
5505            .collect();
5506        let array = serde_json::to_string(&values).unwrap();
5507        assert_eq!(
5508            from_legacy_backend_json(&array).unwrap().inherited_seccomp,
5509            InheritedSeccomp::Unknown
5510        );
5511    }
5512
5513    /// `Config` crosses Reverie RPC as legacy bincode, which is positional, so
5514    /// `inherited_seccomp` must be encoded whatever its value, the number of
5515    /// filters included.
5516    #[test]
5517    fn inherited_seccomp_round_trips_through_reverie_bincode() {
5518        use crate::config::InheritedSeccomp;
5519        for inherited_seccomp in [
5520            InheritedSeccomp::Unknown,
5521            InheritedSeccomp::Disabled,
5522            InheritedSeccomp::Strict,
5523            InheritedSeccomp::Filter(None),
5524            InheritedSeccomp::Filter(Some(3)),
5525        ] {
5526            let config = Config {
5527                inherited_seccomp,
5528                ..Config::default()
5529            };
5530            let wire = bincode::serde::encode_to_vec(&config, bincode::config::legacy()).unwrap();
5531            let (decoded, read): (Config, usize) =
5532                bincode::serde::decode_from_slice(&wire, bincode::config::legacy()).unwrap();
5533            assert_eq!(read, wire.len());
5534            assert_eq!(decoded.inherited_seccomp, inherited_seccomp);
5535            assert_eq!(
5536                bincode::serde::encode_to_vec(&decoded, bincode::config::legacy()).unwrap(),
5537                wire
5538            );
5539        }
5540    }
5541
5542    /// `Seccomp` gives the mode, and `Seccomp_filters` the number of filters in
5543    /// filter mode.
5544    #[test]
5545    fn inherited_seccomp_is_read_from_a_status_file() {
5546        use crate::config::InheritedSeccomp;
5547        let status = |lines: &str| {
5548            InheritedSeccomp::parse(&format!(
5549                "Name:\tguest\nNoNewPrivs:\t1\n{lines}Speculation_Store_Bypass:\tthread vulnerable\n"
5550            ))
5551        };
5552        assert_eq!(
5553            status("Seccomp:\t0\nSeccomp_filters:\t0\n"),
5554            InheritedSeccomp::Disabled
5555        );
5556        assert_eq!(
5557            status("Seccomp:\t1\nSeccomp_filters:\t0\n"),
5558            InheritedSeccomp::Strict
5559        );
5560        assert_eq!(
5561            status("Seccomp:\t2\nSeccomp_filters:\t3\n"),
5562            InheritedSeccomp::Filter(Some(3))
5563        );
5564        assert_eq!(status("Seccomp:\t2\n"), InheritedSeccomp::Filter(None));
5565        assert_eq!(status(""), InheritedSeccomp::Unknown);
5566        assert_eq!(status("Seccomp:\t7\n"), InheritedSeccomp::Unknown);
5567        // This thread's own state is readable.
5568        assert_ne!(InheritedSeccomp::measure(), InheritedSeccomp::Unknown);
5569    }
5570
5571    /// `target_timeslice_syscalls_only` never enters the legacy form, whatever
5572    /// its value: the legacy string keeps its bytes, and it reads
5573    /// back as false from both the object and the array form.
5574    #[test]
5575    fn target_timeslice_syscalls_only_never_enters_the_legacy_form() {
5576        let off = Config {
5577            backend: BackendCapabilities::DBT,
5578            target_timeslice: NonZeroU64::new(1_000_000),
5579            ..Config::default()
5580        };
5581        let on = Config {
5582            target_timeslice_syscalls_only: true,
5583            ..off.clone()
5584        };
5585        let json = to_legacy_backend_json(&on).unwrap();
5586        assert_eq!(json, to_legacy_backend_json(&off).unwrap());
5587        assert!(!json.contains("target_timeslice_syscalls_only"), "{json}");
5588        assert!(
5589            !from_legacy_backend_json(&json)
5590                .unwrap()
5591                .target_timeslice_syscalls_only
5592        );
5593        let values: Vec<serde_json::Value> = ordered_entries(&json)
5594            .into_iter()
5595            .map(|(_, value)| value)
5596            .collect();
5597        let array = serde_json::to_string(&values).unwrap();
5598        assert!(
5599            !from_legacy_backend_json(&array)
5600                .unwrap()
5601                .target_timeslice_syscalls_only
5602        );
5603    }
5604
5605    #[test]
5606    fn a_futex_wake_yields_only_when_it_woke_a_waiter() {
5607        let config = Config {
5608            futex_wake_yields: true,
5609            ..Config::default()
5610        };
5611        assert!(config.yields_after_futex_wake(1));
5612        assert!(config.yields_after_futex_wake(3));
5613        // A wake that found no waiter hands nothing off and keeps the slice.
5614        assert!(!config.yields_after_futex_wake(0));
5615        // Off by default.
5616        assert!(!Config::default().futex_wake_yields);
5617        assert!(!Config::default().yields_after_futex_wake(1));
5618    }
5619
5620    #[test]
5621    fn a_futex_wake_yields_under_replay_too() {
5622        // Neither record holds the queue order the policy creates, so replay
5623        // must make the same decision as the recorded run.
5624        let config = Config {
5625            futex_wake_yields: true,
5626            ..Config::default()
5627        };
5628        let preemption_replay = Config {
5629            replay_preemptions_from: Some(PathBuf::from("/nonexistent/preemptions.json")),
5630            ..config.clone()
5631        };
5632        assert!(preemption_replay.yields_after_futex_wake(1));
5633        assert!(!preemption_replay.yields_after_futex_wake(0));
5634        let schedule_replay = Config {
5635            replay_schedule_from: Some(PathBuf::from("/nonexistent/schedule.json")),
5636            ..config
5637        };
5638        assert!(schedule_replay.yields_after_futex_wake(1));
5639    }
5640
5641    #[test]
5642    fn scheduler_turn_charge_refuses_a_scaled_charge_below_one_nanosecond() {
5643        let default = Config::default();
5644        assert_eq!(default.scheduler_turn_charge(), Ok(500_000));
5645        let scaled = Config {
5646            clock_multiplier: Some(0.1),
5647            ..Config::default()
5648        };
5649        assert_eq!(scaled.scheduler_turn_charge(), Ok(50_000));
5650        let cost = |cost: u64, multiplier: f64| Config {
5651            scheduler_turn_cost: NonZeroU64::new(cost),
5652            clock_multiplier: Some(multiplier),
5653            ..Config::default()
5654        };
5655        assert_eq!(cost(10_000, 0.1).scheduler_turn_charge(), Ok(1_000));
5656        assert_eq!(cost(10, 0.1).scheduler_turn_charge(), Ok(1));
5657        // Truncation would make these zero: a finite poll would never expire.
5658        assert!(cost(9, 0.1).scheduler_turn_charge().is_err());
5659        assert!(cost(1, 0.1).scheduler_turn_charge().is_err());
5660        assert_eq!(
5661            cost(MAX_SCHEDULER_TURN_COST, 1.0).scheduler_turn_charge(),
5662            Ok(MAX_SCHEDULER_TURN_COST)
5663        );
5664        assert!(
5665            cost(MAX_SCHEDULER_TURN_COST + 1, 1.0)
5666                .scheduler_turn_charge()
5667                .is_err()
5668        );
5669        assert!(cost(u64::MAX, 1.0).scheduler_turn_charge().is_err());
5670        assert!(
5671            cost(MAX_SCHEDULER_TURN_COST, 1e12)
5672                .scheduler_turn_charge()
5673                .is_err()
5674        );
5675        // The scaled charge has the same upper bound as the cost: one charge
5676        // of 9e18 ns would exhaust the clock in two turns.
5677        assert!(
5678            cost(MAX_SCHEDULER_TURN_COST, 9e9)
5679                .scheduler_turn_charge()
5680                .is_err()
5681        );
5682        assert!(cost(1_000_000, 1_001.0).scheduler_turn_charge().is_err());
5683        assert_eq!(
5684            cost(1_000_000, 1_000.0).scheduler_turn_charge(),
5685            Ok(MAX_SCHEDULER_TURN_COST)
5686        );
5687        let message = cost(MAX_SCHEDULER_TURN_COST, 9e9)
5688            .scheduler_turn_charge()
5689            .unwrap_err();
5690        assert!(message.contains("at least 1"), "{message}");
5691        assert!(message.contains("at most 1000000000"), "{message}");
5692    }
5693
5694    /// `futex_wake_yields` never enters the legacy form, whatever its
5695    /// value: the legacy string keeps its bytes, and it reads
5696    /// back as false from both the object and the array form.
5697    #[test]
5698    fn futex_wake_yields_never_enters_the_legacy_form() {
5699        let off = Config {
5700            backend: BackendCapabilities::DBT,
5701            ..Config::default()
5702        };
5703        let on = Config {
5704            futex_wake_yields: true,
5705            ..off.clone()
5706        };
5707        let json = to_legacy_backend_json(&on).unwrap();
5708        assert_eq!(json, to_legacy_backend_json(&off).unwrap());
5709        assert!(!json.contains("futex_wake_yields"), "{json}");
5710        assert!(!from_legacy_backend_json(&json).unwrap().futex_wake_yields);
5711        let values: Vec<serde_json::Value> = ordered_entries(&json)
5712            .into_iter()
5713            .map(|(_, value)| value)
5714            .collect();
5715        let array = serde_json::to_string(&values).unwrap();
5716        assert!(!from_legacy_backend_json(&array).unwrap().futex_wake_yields);
5717    }
5718
5719    /// `scheduler_turn_cost` never enters the legacy form, whatever its value:
5720    /// the legacy string keeps its bytes, and it reads back as
5721    /// unset from both the object and the array form.
5722    #[test]
5723    fn scheduler_turn_cost_never_enters_the_legacy_form() {
5724        let off = Config {
5725            backend: BackendCapabilities::DBT,
5726            ..Config::default()
5727        };
5728        let on = Config {
5729            scheduler_turn_cost: NonZeroU64::new(10_000),
5730            ..off.clone()
5731        };
5732        let json = to_legacy_backend_json(&on).unwrap();
5733        assert_eq!(json, to_legacy_backend_json(&off).unwrap());
5734        assert!(!json.contains("scheduler_turn_cost"), "{json}");
5735        assert_eq!(
5736            from_legacy_backend_json(&json).unwrap().scheduler_turn_cost,
5737            None
5738        );
5739        let values: Vec<serde_json::Value> = ordered_entries(&json)
5740            .into_iter()
5741            .map(|(_, value)| value)
5742            .collect();
5743        let array = serde_json::to_string(&values).unwrap();
5744        assert_eq!(
5745            from_legacy_backend_json(&array)
5746                .unwrap()
5747                .scheduler_turn_cost,
5748            None
5749        );
5750    }
5751
5752    /// `virtual_timeout` crosses Reverie RPC in the positional bincode form,
5753    /// set or not, and is rendered into the reproduce command; it never enters
5754    /// DBT's legacy form, whose runtime `hermit run` refuses it for.
5755    #[test]
5756    fn virtual_timeout_round_trips_but_never_enters_the_legacy_form() {
5757        let off = Config {
5758            backend: BackendCapabilities::DBT,
5759            ..Config::default()
5760        };
5761        let on = Config {
5762            virtual_timeout: NonZeroU64::new(3_500_000_000),
5763            ..off.clone()
5764        };
5765        assert!(
5766            on.to_string().contains(" --virtual-timeout=3500000000"),
5767            "{on}"
5768        );
5769        assert!(!off.to_string().contains("--virtual-timeout"), "{off}");
5770        for config in [&off, &on] {
5771            let wire = bincode::serde::encode_to_vec(config, bincode::config::legacy()).unwrap();
5772            let (decoded, read): (Config, usize) =
5773                bincode::serde::decode_from_slice(&wire, bincode::config::legacy()).unwrap();
5774            assert_eq!(read, wire.len());
5775            assert_eq!(decoded.virtual_timeout, config.virtual_timeout);
5776        }
5777        let json = to_legacy_backend_json(&on).unwrap();
5778        assert_eq!(json, to_legacy_backend_json(&off).unwrap());
5779        assert!(!json.contains("virtual_timeout"), "{json}");
5780        assert_eq!(
5781            from_legacy_backend_json(&json).unwrap().virtual_timeout,
5782            None
5783        );
5784        let values: Vec<serde_json::Value> = ordered_entries(&json)
5785            .into_iter()
5786            .map(|(_, value)| value)
5787            .collect();
5788        let array = serde_json::to_string(&values).unwrap();
5789        assert_eq!(
5790            from_legacy_backend_json(&array).unwrap().virtual_timeout,
5791            None
5792        );
5793    }
5794
5795    /// `cap_preemption_charge` never enters the legacy form, whatever its
5796    /// value: the legacy string keeps its bytes, and it reads
5797    /// back as false from both the object and the array form.
5798    #[test]
5799    fn cap_preemption_charge_never_enters_the_legacy_form() {
5800        let off = Config {
5801            backend: BackendCapabilities::DBT,
5802            ..Config::default()
5803        };
5804        let on = Config {
5805            cap_preemption_charge: true,
5806            ..off.clone()
5807        };
5808        let json = to_legacy_backend_json(&on).unwrap();
5809        assert_eq!(json, to_legacy_backend_json(&off).unwrap());
5810        assert!(!json.contains("cap_preemption_charge"), "{json}");
5811        assert!(
5812            !from_legacy_backend_json(&json)
5813                .unwrap()
5814                .cap_preemption_charge
5815        );
5816        let values: Vec<serde_json::Value> = ordered_entries(&json)
5817            .into_iter()
5818            .map(|(_, value)| value)
5819            .collect();
5820        let array = serde_json::to_string(&values).unwrap();
5821        assert!(
5822            !from_legacy_backend_json(&array)
5823                .unwrap()
5824                .cap_preemption_charge
5825        );
5826    }
5827
5828    /// `--cap-preemption-charge` is off by default, parses, and survives the
5829    /// rendered command line and serde, so a recorded configuration replays
5830    /// with the rule it ran with.
5831    #[test]
5832    fn cap_preemption_charge_is_opt_in_and_round_trips() {
5833        let default = Config::default();
5834        assert!(!default.cap_preemption_charge);
5835        assert!(!default.to_string().contains("--cap-preemption-charge"));
5836        let config = Config::parse_from(["detcore", "--cap-preemption-charge"]);
5837        assert!(config.cap_preemption_charge);
5838        let rendered = config.to_string();
5839        assert!(rendered.contains(" --cap-preemption-charge"), "{rendered}");
5840        let reparsed =
5841            Config::parse_from(std::iter::once("detcore").chain(rendered.split_whitespace()));
5842        assert!(reparsed.cap_preemption_charge);
5843        let json = serde_json::to_string(&config).unwrap();
5844        let back: Config = serde_json::from_str(&json).unwrap();
5845        assert!(back.cap_preemption_charge);
5846        let mut legacy: serde_json::Value = serde_json::to_value(&default).unwrap();
5847        legacy
5848            .as_object_mut()
5849            .unwrap()
5850            .remove("cap_preemption_charge");
5851        let old: Config = serde_json::from_value(legacy).unwrap();
5852        assert!(
5853            !old.cap_preemption_charge,
5854            "a configuration from before the option reads as off"
5855        );
5856    }
5857
5858    /// `replaying` never enters the legacy form, whatever its value: the
5859    /// legacy string keeps its bytes, and it reads back as false
5860    /// from both the object and the array form.
5861    #[test]
5862    fn replaying_never_enters_the_legacy_form() {
5863        let off = Config {
5864            backend: BackendCapabilities::DBT,
5865            ..Config::default()
5866        };
5867        let on = Config {
5868            replaying: true,
5869            ..off.clone()
5870        };
5871        let json = to_legacy_backend_json(&on).unwrap();
5872        assert_eq!(json, to_legacy_backend_json(&off).unwrap());
5873        assert!(!json.contains("replaying"), "{json}");
5874        assert!(!from_legacy_backend_json(&json).unwrap().replaying);
5875        let values: Vec<serde_json::Value> = ordered_entries(&json)
5876            .into_iter()
5877            .map(|(_, value)| value)
5878            .collect();
5879        let array = serde_json::to_string(&values).unwrap();
5880        assert!(!from_legacy_backend_json(&array).unwrap().replaying);
5881    }
5882
5883    /// `Config` crosses Reverie RPC as legacy bincode, which is positional, so
5884    /// `replaying` must be encoded whatever its value: a field left out when
5885    /// false would shift every later field for the decoder.
5886    #[test]
5887    fn replaying_round_trips_through_reverie_bincode() {
5888        for replaying in [false, true] {
5889            let config = Config {
5890                replaying,
5891                ..Config::default()
5892            };
5893            let wire = bincode::serde::encode_to_vec(&config, bincode::config::legacy()).unwrap();
5894            let (decoded, read): (Config, usize) =
5895                bincode::serde::decode_from_slice(&wire, bincode::config::legacy()).unwrap();
5896            assert_eq!(read, wire.len());
5897            assert_eq!(decoded.replaying, replaying);
5898            assert_eq!(
5899                bincode::serde::encode_to_vec(&decoded, bincode::config::legacy()).unwrap(),
5900                wire
5901            );
5902        }
5903    }
5904
5905    /// `signal_at` came after the legacy form froze, so it never enters it,
5906    /// and both the object and the array form read it back as unset.
5907    #[test]
5908    fn signal_at_never_enters_the_legacy_form() {
5909        let off = Config {
5910            backend: BackendCapabilities::DBT,
5911            ..Config::default()
5912        };
5913        let on = Config {
5914            signal_at: Some(SignalAt {
5915                turn: 500,
5916                signal: libc::SIGUSR1,
5917            }),
5918            ..off.clone()
5919        };
5920        let json = to_legacy_backend_json(&on).unwrap();
5921        assert_eq!(json, to_legacy_backend_json(&off).unwrap());
5922        assert!(!json.contains("signal_at"), "{json}");
5923        assert_eq!(from_legacy_backend_json(&json).unwrap().signal_at, None);
5924        let values: Vec<serde_json::Value> = ordered_entries(&json)
5925            .into_iter()
5926            .map(|(_, value)| value)
5927            .collect();
5928        let array = serde_json::to_string(&values).unwrap();
5929        assert_eq!(from_legacy_backend_json(&array).unwrap().signal_at, None);
5930    }
5931
5932    /// `virtual_cpus` came after the legacy form froze, so it never enters
5933    /// it: the DBT runtime's environment keeps its bytes, and the option
5934    /// reads back as unset (`hermit run` refuses it with DBT).
5935    #[test]
5936    fn virtual_cpus_never_enter_the_legacy_form() {
5937        let off = Config {
5938            backend: BackendCapabilities::DBT,
5939            ..Config::default()
5940        };
5941        let on = Config {
5942            virtual_cpus: NonZeroU32::new(4),
5943            ..off.clone()
5944        };
5945        let json = to_legacy_backend_json(&on).unwrap();
5946        assert_eq!(json, to_legacy_backend_json(&off).unwrap());
5947        assert!(!json.contains("virtual_cpus"), "{json}");
5948        assert_eq!(from_legacy_backend_json(&json).unwrap().virtual_cpus, None);
5949    }
5950
5951    /// `--virtual-cpus` takes a count from 1 to the 128 CPUs Detcore's mask
5952    /// holds, and names the range when refused; it reaches the reproducer
5953    /// command line.
5954    #[test]
5955    fn virtual_cpus_parse_from_1_to_128() {
5956        let parse = |count: &str| {
5957            Config::try_parse_from(["detcore".to_owned(), format!("--virtual-cpus={count}")])
5958        };
5959        assert_eq!(parse("1").unwrap().virtual_cpus, NonZeroU32::new(1));
5960        assert_eq!(parse("128").unwrap().virtual_cpus, NonZeroU32::new(128));
5961        for refused in ["0", "129", "-1", "four"] {
5962            let error = parse(refused).unwrap_err().to_string();
5963            assert!(error.contains("from 1 to 128"), "{refused}: {error}");
5964        }
5965        let config = parse("8").unwrap();
5966        assert_eq!(config.virtual_cpu_count(), 8);
5967        assert!(config.to_string().contains(" --virtual-cpus=8"));
5968        assert_eq!(Config::default().virtual_cpus, None);
5969        assert_eq!(Config::default().virtual_cpu_count(), 1);
5970        assert!(!Config::default().to_string().contains("virtual-cpus"));
5971    }
5972
5973    /// `eager_lone_preemptions` came after the legacy form froze, so it never
5974    /// enters it (DBT has no PMU timer, where it would act), and both the
5975    /// object and the array form read it back as false.
5976    #[test]
5977    fn eager_lone_preemptions_never_enters_the_legacy_form() {
5978        let off = Config {
5979            backend: BackendCapabilities::DBT,
5980            ..Config::default()
5981        };
5982        let on = Config {
5983            eager_lone_preemptions: true,
5984            ..off.clone()
5985        };
5986        let json = to_legacy_backend_json(&on).unwrap();
5987        assert_eq!(json, to_legacy_backend_json(&off).unwrap());
5988        assert!(!json.contains("eager_lone_preemptions"), "{json}");
5989        assert!(
5990            !from_legacy_backend_json(&json)
5991                .unwrap()
5992                .eager_lone_preemptions
5993        );
5994        let values: Vec<serde_json::Value> = ordered_entries(&json)
5995            .into_iter()
5996            .map(|(_, value)| value)
5997            .collect();
5998        let array = serde_json::to_string(&values).unwrap();
5999        assert!(
6000            !from_legacy_backend_json(&array)
6001                .unwrap()
6002                .eager_lone_preemptions
6003        );
6004    }
6005
6006    /// `Config` crosses Reverie RPC as positional legacy bincode, so
6007    /// `signal_at` is encoded whether set or not.
6008    #[test]
6009    fn signal_at_round_trips_through_reverie_bincode() {
6010        for signal_at in [
6011            None,
6012            Some(SignalAt {
6013                turn: 3000,
6014                signal: libc::SIGUSR1,
6015            }),
6016        ] {
6017            let config = Config {
6018                signal_at,
6019                ..Config::default()
6020            };
6021            let wire = bincode::serde::encode_to_vec(&config, bincode::config::legacy()).unwrap();
6022            let (decoded, read): (Config, usize) =
6023                bincode::serde::decode_from_slice(&wire, bincode::config::legacy()).unwrap();
6024            assert_eq!(read, wire.len());
6025            assert_eq!(decoded.signal_at, signal_at);
6026        }
6027    }
6028
6029    /// `notice_host_network_abstract_binds` came after the legacy form froze, so it never
6030    /// enters it, and both the object and the array form read it back as
6031    /// false.
6032    #[test]
6033    fn notice_host_network_abstract_binds_never_enters_the_legacy_form() {
6034        let off = Config {
6035            backend: BackendCapabilities::DBT,
6036            ..Config::default()
6037        };
6038        let on = Config {
6039            notice_host_network_abstract_binds: true,
6040            ..off.clone()
6041        };
6042        let json = to_legacy_backend_json(&on).unwrap();
6043        assert_eq!(json, to_legacy_backend_json(&off).unwrap());
6044        assert!(
6045            !json.contains("notice_host_network_abstract_binds"),
6046            "{json}"
6047        );
6048        assert!(
6049            !from_legacy_backend_json(&json)
6050                .unwrap()
6051                .notice_host_network_abstract_binds
6052        );
6053        let values: Vec<serde_json::Value> = ordered_entries(&json)
6054            .into_iter()
6055            .map(|(_, value)| value)
6056            .collect();
6057        let array = serde_json::to_string(&values).unwrap();
6058        assert!(
6059            !from_legacy_backend_json(&array)
6060                .unwrap()
6061                .notice_host_network_abstract_binds
6062        );
6063    }
6064
6065    /// `Config` crosses Reverie RPC as positional legacy bincode, so
6066    /// `notice_host_network_abstract_binds` is encoded whether set or not.
6067    #[test]
6068    fn notice_host_network_abstract_binds_round_trips_through_reverie_bincode() {
6069        for notice_host_network_abstract_binds in [false, true] {
6070            let config = Config {
6071                notice_host_network_abstract_binds,
6072                ..Config::default()
6073            };
6074            let wire = bincode::serde::encode_to_vec(&config, bincode::config::legacy()).unwrap();
6075            let (decoded, read): (Config, usize) =
6076                bincode::serde::decode_from_slice(&wire, bincode::config::legacy()).unwrap();
6077            assert_eq!(read, wire.len());
6078            assert_eq!(
6079                decoded.notice_host_network_abstract_binds,
6080                notice_host_network_abstract_binds
6081            );
6082        }
6083    }
6084
6085    /// The access trace's four fields came after the legacy form froze, so
6086    /// they never enter it, and both the object and the array form read them
6087    /// back as unset, 0, unset and unset. Set or not, they cross Reverie RPC's positional
6088    /// bincode and the CLI spelling (`Display`) unchanged.
6089    #[test]
6090    fn access_trace_never_enters_the_legacy_form_and_round_trips() {
6091        let off = Config {
6092            backend: BackendCapabilities::DBT,
6093            ..Config::default()
6094        };
6095        let on = Config {
6096            access_trace: Some(PathBuf::from("/var/run 1.access")),
6097            access_trace_from: 7586,
6098            access_trace_max_records: Some(40),
6099            access_trace_max_bytes: Some(4096),
6100            ..off.clone()
6101        };
6102        let json = to_legacy_backend_json(&on).unwrap();
6103        assert_eq!(json, to_legacy_backend_json(&off).unwrap());
6104        assert!(!json.contains("access_trace"), "{json}");
6105        let decoded = from_legacy_backend_json(&json).unwrap();
6106        assert_eq!((decoded.access_trace, decoded.access_trace_from), (None, 0));
6107        assert_eq!(decoded.access_trace_max_records, None);
6108        assert_eq!(decoded.access_trace_max_bytes, None);
6109        let values: Vec<serde_json::Value> = ordered_entries(&json)
6110            .into_iter()
6111            .map(|(_, value)| value)
6112            .collect();
6113        let array = serde_json::to_string(&values).unwrap();
6114        let decoded = from_legacy_backend_json(&array).unwrap();
6115        assert_eq!((decoded.access_trace, decoded.access_trace_from), (None, 0));
6116        assert_eq!(
6117            (
6118                decoded.access_trace_max_records,
6119                decoded.access_trace_max_bytes
6120            ),
6121            (None, None)
6122        );
6123
6124        for config in [Config::default(), on.clone()] {
6125            let wire = bincode::serde::encode_to_vec(&config, bincode::config::legacy()).unwrap();
6126            let (decoded, read): (Config, usize) =
6127                bincode::serde::decode_from_slice(&wire, bincode::config::legacy()).unwrap();
6128            assert_eq!(read, wire.len());
6129            assert_eq!(decoded.access_trace, config.access_trace);
6130            assert_eq!(decoded.access_trace_from, config.access_trace_from);
6131            assert_eq!(
6132                decoded.access_trace_max_records,
6133                config.access_trace_max_records
6134            );
6135            assert_eq!(
6136                decoded.access_trace_max_bytes,
6137                config.access_trace_max_bytes
6138            );
6139        }
6140
6141        let spelled = on.to_string();
6142        assert!(
6143            spelled.contains(
6144                " --record-access-trace-to='/var/run 1.access' --access-trace-from=7586 \
6145                 --access-trace-max-records=40 --access-trace-max-bytes=4096"
6146            ),
6147            "{spelled}"
6148        );
6149        assert!(!Config::default().to_string().contains("access-trace"));
6150        let reparsed = Config::parse_from([
6151            "hermit",
6152            "--record-access-trace-to=/var/run 1.access",
6153            "--access-trace-from=7586",
6154            "--access-trace-max-records=40",
6155            "--access-trace-max-bytes=4096",
6156        ]);
6157        assert_eq!(reparsed.access_trace, on.access_trace);
6158        assert_eq!(reparsed.access_trace_from, 7586);
6159        assert_eq!(reparsed.access_trace_max_records, Some(40));
6160        assert_eq!(reparsed.access_trace_max_bytes, Some(4096));
6161    }
6162
6163    /// Anonymous-memory logging is newer than the frozen DBT configuration.
6164    /// Omit every mode from its object and positional forms while preserving
6165    /// the exact mode in the current serde and bincode encodings.
6166    #[test]
6167    fn anonymous_mapping_logs_never_enter_the_legacy_form() {
6168        let off = Config {
6169            detlog_stack: true,
6170            detlog_regs: true,
6171            detlog_io_buffers: false,
6172            detlog_regs_cadence: 7,
6173            ..Config::default()
6174        };
6175        let legacy = to_legacy_backend_json(&off).unwrap();
6176        assert!(!legacy.contains("detlog_anon"), "{legacy}");
6177        for mode in [None, Some(DetlogAnon::Mappings), Some(DetlogAnon::Pages)] {
6178            let config = Config {
6179                detlog_anon: mode,
6180                ..off.clone()
6181            };
6182            let object = to_legacy_backend_json(&config).unwrap();
6183            assert_eq!(object, legacy);
6184            let values: Vec<serde_json::Value> = ordered_entries(&object)
6185                .into_iter()
6186                .map(|(_, value)| value)
6187                .collect();
6188            let array = serde_json::to_string(&values).unwrap();
6189            for encoded in [object, array] {
6190                let decoded = from_legacy_backend_json(&encoded).unwrap();
6191                assert_eq!(decoded.detlog_anon, None);
6192                assert_eq!(to_legacy_backend_json(&decoded).unwrap(), legacy);
6193            }
6194
6195            let current = serde_json::to_value(&config).unwrap();
6196            assert_eq!(current["detlog_anon"], serde_json::to_value(mode).unwrap());
6197            let decoded: Config = serde_json::from_value(current).unwrap();
6198            assert_eq!(decoded.detlog_anon, mode);
6199            let wire = bincode::serde::encode_to_vec(&config, bincode::config::legacy()).unwrap();
6200            let (decoded, read): (Config, usize) =
6201                bincode::serde::decode_from_slice(&wire, bincode::config::legacy()).unwrap();
6202            assert_eq!(read, wire.len());
6203            assert_eq!(decoded.detlog_anon, mode);
6204        }
6205    }
6206
6207    /// The in-guest reserved-signal set as an in-guest default-route run
6208    /// carries it: SIGSYS, SIGSEGV and SIGTRAP.
6209    fn in_guest_reserved_set() -> u64 {
6210        [libc::SIGSYS, libc::SIGSEGV, libc::SIGTRAP]
6211            .into_iter()
6212            .fold(0, |set, signal| set | (1_u64 << (signal - 1)))
6213    }
6214
6215    /// `in_guest_reserved_signals` is empty for DBT, so the legacy form never
6216    /// carries it, and it reads back as empty from both the object and the
6217    /// array form.
6218    #[test]
6219    fn in_guest_reserved_signals_never_enters_the_legacy_form() {
6220        let empty = Config {
6221            backend: BackendCapabilities::DBT,
6222            ..Config::default()
6223        };
6224        let reserved = Config {
6225            in_guest_reserved_signals: in_guest_reserved_set(),
6226            ..empty.clone()
6227        };
6228        let json = to_legacy_backend_json(&reserved).unwrap();
6229        assert_eq!(json, to_legacy_backend_json(&empty).unwrap());
6230        assert!(!json.contains("in_guest_reserved_signals"), "{json}");
6231        assert_eq!(
6232            from_legacy_backend_json(&json)
6233                .unwrap()
6234                .in_guest_reserved_signals,
6235            0
6236        );
6237        let values: Vec<serde_json::Value> = ordered_entries(&json)
6238            .into_iter()
6239            .map(|(_, value)| value)
6240            .collect();
6241        let array = serde_json::to_string(&values).unwrap();
6242        assert_eq!(
6243            from_legacy_backend_json(&array)
6244                .unwrap()
6245                .in_guest_reserved_signals,
6246            0
6247        );
6248    }
6249
6250    /// The set reaches every Tool image through Reverie's bincode handshake
6251    /// unchanged, empty or not, and a JSON configuration without the key reads
6252    /// as empty (its serde default).
6253    #[test]
6254    fn in_guest_reserved_signals_round_trips_and_defaults_to_empty() {
6255        for set in [0, in_guest_reserved_set()] {
6256            let config = Config {
6257                in_guest_reserved_signals: set,
6258                ..Config::default()
6259            };
6260            let wire = bincode::serde::encode_to_vec(&config, bincode::config::legacy()).unwrap();
6261            let (decoded, read): (Config, usize) =
6262                bincode::serde::decode_from_slice(&wire, bincode::config::legacy()).unwrap();
6263            assert_eq!(read, wire.len());
6264            assert_eq!(decoded.in_guest_reserved_signals, set);
6265            let json: Config =
6266                serde_json::from_str(&serde_json::to_string(&config).unwrap()).unwrap();
6267            assert_eq!(json.in_guest_reserved_signals, set);
6268        }
6269        let mut encoded = serde_json::to_value(Config::default()).unwrap();
6270        encoded
6271            .as_object_mut()
6272            .unwrap()
6273            .remove("in_guest_reserved_signals")
6274            .expect("the field is serialized");
6275        let decoded: Config = serde_json::from_value(encoded).unwrap();
6276        assert_eq!(decoded.in_guest_reserved_signals, 0);
6277    }
6278
6279    /// `reserves_in_guest_signal` answers from the set alone: each reserved
6280    /// signal and no other, never signal 0 or a number outside 1 to 64, and
6281    /// nothing at all while the set is empty.
6282    #[test]
6283    fn reserves_in_guest_signal_reads_only_the_set() {
6284        let empty = Config::default();
6285        let reserved = Config {
6286            in_guest_reserved_signals: in_guest_reserved_set(),
6287            ..Config::default()
6288        };
6289        for signal in -1..=66 {
6290            assert!(!empty.reserves_in_guest_signal(signal), "{signal}");
6291            let expected = [libc::SIGSYS, libc::SIGSEGV, libc::SIGTRAP]
6292                .into_iter()
6293                .any(|reserved| i64::from(reserved) == signal);
6294            assert_eq!(
6295                reserved.reserves_in_guest_signal(signal),
6296                expected,
6297                "{signal}"
6298            );
6299        }
6300        // The top bit is signal 64, the last one Linux accepts.
6301        let top = Config {
6302            in_guest_reserved_signals: 1 << 63,
6303            ..Config::default()
6304        };
6305        assert!(top.reserves_in_guest_signal(64));
6306        assert!(!top.reserves_in_guest_signal(65));
6307        assert!(!top.reserves_in_guest_signal(0));
6308    }
6309
6310    /// `in_guest_refuses_host_timed_producers` is false for DBT, so the legacy
6311    /// form never carries it, and it reads back as false from both the object
6312    /// and the array form.
6313    #[test]
6314    fn in_guest_refuses_host_timed_producers_never_enters_the_legacy_form() {
6315        let off = Config {
6316            backend: BackendCapabilities::DBT,
6317            ..Config::default()
6318        };
6319        let on = Config {
6320            in_guest_refuses_host_timed_producers: true,
6321            ..off.clone()
6322        };
6323        let json = to_legacy_backend_json(&on).unwrap();
6324        assert_eq!(json, to_legacy_backend_json(&off).unwrap());
6325        assert!(
6326            !json.contains("in_guest_refuses_host_timed_producers"),
6327            "{json}"
6328        );
6329        assert!(
6330            !from_legacy_backend_json(&json)
6331                .unwrap()
6332                .in_guest_refuses_host_timed_producers
6333        );
6334        let values: Vec<serde_json::Value> = ordered_entries(&json)
6335            .into_iter()
6336            .map(|(_, value)| value)
6337            .collect();
6338        let array = serde_json::to_string(&values).unwrap();
6339        assert!(
6340            !from_legacy_backend_json(&array)
6341                .unwrap()
6342                .in_guest_refuses_host_timed_producers
6343        );
6344    }
6345
6346    /// The setting reaches every Tool image through Reverie's bincode
6347    /// handshake unchanged, on or off, and a JSON configuration without the
6348    /// key reads as off (its serde default).
6349    #[test]
6350    fn in_guest_refuses_host_timed_producers_round_trips_and_defaults_to_off() {
6351        for refuses in [false, true] {
6352            let config = Config {
6353                in_guest_refuses_host_timed_producers: refuses,
6354                ..Config::default()
6355            };
6356            let wire = bincode::serde::encode_to_vec(&config, bincode::config::legacy()).unwrap();
6357            let (decoded, read): (Config, usize) =
6358                bincode::serde::decode_from_slice(&wire, bincode::config::legacy()).unwrap();
6359            assert_eq!(read, wire.len());
6360            assert_eq!(decoded.in_guest_refuses_host_timed_producers, refuses);
6361            let json: Config =
6362                serde_json::from_str(&serde_json::to_string(&config).unwrap()).unwrap();
6363            assert_eq!(json.in_guest_refuses_host_timed_producers, refuses);
6364        }
6365        let mut encoded = serde_json::to_value(Config::default()).unwrap();
6366        encoded
6367            .as_object_mut()
6368            .unwrap()
6369            .remove("in_guest_refuses_host_timed_producers")
6370            .expect("the field is serialized");
6371        let decoded: Config = serde_json::from_value(encoded).unwrap();
6372        assert!(!decoded.in_guest_refuses_host_timed_producers);
6373    }
6374
6375    /// `in_guest_signal_delivery` is false for DBT, so the legacy form never
6376    /// carries it, and it reads back as false from both the object and the
6377    /// array form.
6378    #[test]
6379    fn in_guest_signal_delivery_never_enters_the_legacy_form() {
6380        let off = Config {
6381            backend: BackendCapabilities::DBT,
6382            ..Config::default()
6383        };
6384        let on = Config {
6385            in_guest_signal_delivery: true,
6386            ..off.clone()
6387        };
6388        let json = to_legacy_backend_json(&on).unwrap();
6389        assert_eq!(json, to_legacy_backend_json(&off).unwrap());
6390        assert!(!json.contains("in_guest_signal_delivery"), "{json}");
6391        assert!(
6392            !from_legacy_backend_json(&json)
6393                .unwrap()
6394                .in_guest_signal_delivery
6395        );
6396        let values: Vec<serde_json::Value> = ordered_entries(&json)
6397            .into_iter()
6398            .map(|(_, value)| value)
6399            .collect();
6400        let array = serde_json::to_string(&values).unwrap();
6401        assert!(
6402            !from_legacy_backend_json(&array)
6403                .unwrap()
6404                .in_guest_signal_delivery
6405        );
6406    }
6407
6408    /// `--replay-edited-preemptions` stays out of the legacy (DBT) form, and
6409    /// reads as false from it.
6410    #[test]
6411    fn replay_edited_preemptions_never_enters_the_legacy_form() {
6412        let off = Config {
6413            backend: BackendCapabilities::DBT,
6414            replay_preemptions_from: Some("record.preempts".into()),
6415            ..Config::default()
6416        };
6417        let on = Config {
6418            replay_edited_preemptions: true,
6419            ..off.clone()
6420        };
6421        let json = to_legacy_backend_json(&on).unwrap();
6422        assert_eq!(json, to_legacy_backend_json(&off).unwrap());
6423        assert!(!json.contains("replay_edited_preemptions"), "{json}");
6424        assert!(
6425            !from_legacy_backend_json(&json)
6426                .unwrap()
6427                .replay_edited_preemptions
6428        );
6429        let values: Vec<serde_json::Value> = ordered_entries(&json)
6430            .into_iter()
6431            .map(|(_, value)| value)
6432            .collect();
6433        let array = serde_json::to_string(&values).unwrap();
6434        assert!(
6435            !from_legacy_backend_json(&array)
6436                .unwrap()
6437                .replay_edited_preemptions
6438        );
6439    }
6440
6441    /// `--replay-edited-preemptions` describes the record
6442    /// `--replay-preemptions-from` reads, so it needs one. It round-trips
6443    /// through the rendered options and through Reverie's bincode handshake,
6444    /// and a JSON configuration without the key reads as off.
6445    #[test]
6446    fn replay_edited_preemptions_needs_a_replayed_record_and_round_trips() {
6447        assert!(Config::try_parse_from(["detcore", "--replay-edited-preemptions"]).is_err());
6448        let config = Config::try_parse_from([
6449            "detcore",
6450            "--replay-preemptions-from=record.preempts",
6451            "--replay-edited-preemptions",
6452        ])
6453        .unwrap();
6454        assert!(config.replay_edited_preemptions);
6455        // The rendered options parse back to the same replay settings.
6456        let rendered = config.to_string();
6457        let words: Vec<String> = shell_words::split(&rendered)
6458            .unwrap()
6459            .into_iter()
6460            .filter(|word| word.starts_with("--replay-"))
6461            .collect();
6462        let reparsed =
6463            Config::try_parse_from(std::iter::once("detcore".to_owned()).chain(words)).unwrap();
6464        assert!(reparsed.replay_edited_preemptions, "{rendered}");
6465        assert_eq!(
6466            reparsed.replay_preemptions_from, config.replay_preemptions_from,
6467            "{rendered}"
6468        );
6469        let strict =
6470            Config::try_parse_from(["detcore", "--replay-preemptions-from=record.preempts"])
6471                .unwrap();
6472        assert!(!strict.to_string().contains("--replay-edited-preemptions"));
6473
6474        for config in [&config, &strict] {
6475            let wire = bincode::serde::encode_to_vec(config, bincode::config::legacy()).unwrap();
6476            let (decoded, read): (Config, usize) =
6477                bincode::serde::decode_from_slice(&wire, bincode::config::legacy()).unwrap();
6478            assert_eq!(read, wire.len());
6479            assert_eq!(
6480                decoded.replay_edited_preemptions,
6481                config.replay_edited_preemptions
6482            );
6483        }
6484        let mut encoded = serde_json::to_value(&config).unwrap();
6485        encoded
6486            .as_object_mut()
6487            .unwrap()
6488            .remove("replay_edited_preemptions")
6489            .expect("the field is serialized");
6490        let decoded: Config = serde_json::from_value(encoded).unwrap();
6491        assert!(!decoded.replay_edited_preemptions);
6492    }
6493
6494    /// The gate reaches every Tool image through Reverie's bincode handshake
6495    /// unchanged, on or off, and a JSON configuration without the key reads
6496    /// as off (its serde default).
6497    #[test]
6498    fn in_guest_signal_delivery_round_trips_and_defaults_to_off() {
6499        for on in [false, true] {
6500            let config = Config {
6501                in_guest_signal_delivery: on,
6502                ..Config::default()
6503            };
6504            let wire = bincode::serde::encode_to_vec(&config, bincode::config::legacy()).unwrap();
6505            let (decoded, read): (Config, usize) =
6506                bincode::serde::decode_from_slice(&wire, bincode::config::legacy()).unwrap();
6507            assert_eq!(read, wire.len());
6508            assert_eq!(decoded.in_guest_signal_delivery, on);
6509            let json: Config =
6510                serde_json::from_str(&serde_json::to_string(&config).unwrap()).unwrap();
6511            assert_eq!(json.in_guest_signal_delivery, on);
6512        }
6513        let mut encoded = serde_json::to_value(Config::default()).unwrap();
6514        encoded
6515            .as_object_mut()
6516            .unwrap()
6517            .remove("in_guest_signal_delivery")
6518            .expect("the field is serialized");
6519        let decoded: Config = serde_json::from_value(encoded).unwrap();
6520        assert!(!decoded.in_guest_signal_delivery);
6521    }
6522
6523    /// `annotated_priority_replay` never enters the legacy form, and reads
6524    /// back as false from both its object and array forms; it crosses
6525    /// Reverie's positional bincode and JSON unchanged, and a JSON
6526    /// configuration without the key reads as off.
6527    #[test]
6528    fn annotated_priority_replay_never_enters_the_legacy_form_and_round_trips() {
6529        let off = Config {
6530            backend: BackendCapabilities::DBT,
6531            ..Config::default()
6532        };
6533        let on = Config {
6534            annotated_priority_replay: true,
6535            ..off.clone()
6536        };
6537        let json = to_legacy_backend_json(&on).unwrap();
6538        assert_eq!(json, to_legacy_backend_json(&off).unwrap());
6539        assert!(!json.contains("annotated_priority_replay"), "{json}");
6540        assert!(
6541            !from_legacy_backend_json(&json)
6542                .unwrap()
6543                .annotated_priority_replay
6544        );
6545        let values: Vec<serde_json::Value> = ordered_entries(&json)
6546            .into_iter()
6547            .map(|(_, value)| value)
6548            .collect();
6549        let array = serde_json::to_string(&values).unwrap();
6550        assert!(
6551            !from_legacy_backend_json(&array)
6552                .unwrap()
6553                .annotated_priority_replay
6554        );
6555        for annotated in [false, true] {
6556            let config = Config {
6557                annotated_priority_replay: annotated,
6558                ..Config::default()
6559            };
6560            let wire = bincode::serde::encode_to_vec(&config, bincode::config::legacy()).unwrap();
6561            let (decoded, read): (Config, usize) =
6562                bincode::serde::decode_from_slice(&wire, bincode::config::legacy()).unwrap();
6563            assert_eq!(read, wire.len());
6564            assert_eq!(decoded.annotated_priority_replay, annotated);
6565            let json: Config =
6566                serde_json::from_str(&serde_json::to_string(&config).unwrap()).unwrap();
6567            assert_eq!(json.annotated_priority_replay, annotated);
6568        }
6569        let mut encoded = serde_json::to_value(Config::default()).unwrap();
6570        encoded
6571            .as_object_mut()
6572            .unwrap()
6573            .remove("annotated_priority_replay")
6574            .expect("the field is serialized");
6575        let decoded: Config = serde_json::from_value(encoded).unwrap();
6576        assert!(!decoded.annotated_priority_replay);
6577    }
6578
6579    /// Schedule events are observed exactly where they are traced, and also
6580    /// in replay of a callback-annotated priority history, whose recording
6581    /// traced them, whether or not that replay records.
6582    #[test]
6583    fn annotated_priority_replay_observes_schedule_events() {
6584        let plain = Config::default();
6585        assert!(!plain.should_trace_schedevent() && !plain.observes_schedevents());
6586        let recording = Config {
6587            record_preemptions: true,
6588            ..Config::default()
6589        };
6590        assert!(recording.should_trace_schedevent() && recording.observes_schedevents());
6591        let replay = Config {
6592            replay_preemptions_from: Some("history.json".into()),
6593            ..Config::default()
6594        };
6595        assert!(!replay.observes_schedevents(), "an unannotated history");
6596        let annotated = Config {
6597            annotated_priority_replay: true,
6598            ..replay
6599        };
6600        assert!(!annotated.should_trace_schedevent());
6601        assert!(annotated.observes_schedevents());
6602    }
6603
6604    /// An annotated priority history replays only on an effective
6605    /// configuration with the backend's own preemption timer events,
6606    /// sequentialized threads, an active maximum, precise timers and RCB time.
6607    /// A configuration nobody prepared for a backend has no timer events and
6608    /// is refused; each other unmet requirement is named in turn, and the
6609    /// answer never changes the configuration.
6610    #[test]
6611    fn annotated_priority_replay_requires_a_qualified_effective_configuration() {
6612        let unprepared = Config {
6613            annotated_priority_replay: true,
6614            replay_preemptions_from: Some("history.json".into()),
6615            ..Config::default()
6616        };
6617        assert_eq!(
6618            unprepared.annotated_priority_replay_unqualified(),
6619            Some("cannot deliver Detcore preemption timer events")
6620        );
6621        let qualified = Config {
6622            backend: BackendCapabilities::PTRACE,
6623            sequentialize_threads: true,
6624            max_timeslice: NonZeroU64::new(1_000_000),
6625            imprecise_timers: false,
6626            no_rcb_time: false,
6627            ..unprepared.clone()
6628        };
6629        assert_eq!(qualified.annotated_priority_replay_unqualified(), None);
6630        let before = serde_json::to_value(&qualified).unwrap();
6631        for (config, setting) in [
6632            (
6633                Config {
6634                    sequentialize_threads: false,
6635                    ..qualified.clone()
6636                },
6637                "does not sequentialize threads (--no-sequentialize-threads)",
6638            ),
6639            (
6640                Config {
6641                    max_timeslice: None,
6642                    ..qualified.clone()
6643                },
6644                "has its preemption timer disabled (--max-timeslice=disabled)",
6645            ),
6646            (
6647                Config {
6648                    imprecise_timers: true,
6649                    ..qualified.clone()
6650                },
6651                "requests imprecise timers (--imprecise-timers)",
6652            ),
6653            (
6654                Config {
6655                    no_rcb_time: true,
6656                    ..qualified.clone()
6657                },
6658                "requests logical time without RCBs (--no-rcb-time)",
6659            ),
6660        ] {
6661            assert_eq!(
6662                config.annotated_priority_replay_unqualified(),
6663                Some(setting)
6664            );
6665        }
6666        assert_eq!(serde_json::to_value(&qualified).unwrap(), before);
6667    }
6668
6669    /// `--signal-at` takes `turn=N:SIGNAL` with SIGNAL a name or a number.
6670    /// SIGKILL and SIGSTOP are refused, by name or number: the kernel acts on
6671    /// them at once, outside the scheduler's protocols for them.
6672    #[test]
6673    fn signal_at_parses_a_turn_and_a_catchable_signal() {
6674        let usr1 = SignalAt {
6675            turn: 500,
6676            signal: libc::SIGUSR1,
6677        };
6678        assert_eq!(parse_signal_at("turn=500:SIGUSR1"), Ok(usr1));
6679        assert_eq!(parse_signal_at("turn=500:10"), Ok(usr1));
6680        for refused in [
6681            "turn=500:SIGKILL",
6682            "turn=500:9",
6683            "turn=500:SIGSTOP",
6684            "turn=500:19",
6685        ] {
6686            let error = parse_signal_at(refused).unwrap_err();
6687            assert!(
6688                error.contains("cannot be caught or blocked"),
6689                "{refused}: {error}"
6690            );
6691        }
6692        for malformed in [
6693            "turn=500",
6694            "500:SIGUSR1",
6695            "turn=-1:SIGUSR1",
6696            "turn=5:SIGNOPE",
6697        ] {
6698            assert!(parse_signal_at(malformed).is_err(), "{malformed}");
6699        }
6700    }
6701
6702    /// Serde reads a derived struct from a JSON array by position, so the
6703    /// legacy form could be given as one. Each array reads as the object
6704    /// whose values it lists in order, every backend's capabilities included.
6705    #[test]
6706    fn a_legacy_positional_array_reads_as_the_object_it_lists() {
6707        for (name, backend) in [
6708            ("PTRACE", BackendCapabilities::PTRACE),
6709            ("E9PATCH", BackendCapabilities::E9PATCH),
6710            (
6711                "ptrace without exit transitions",
6712                ptrace_without_exit_transitions(),
6713            ),
6714            ("LITEINST_IN_GUEST", BackendCapabilities::LITEINST_IN_GUEST),
6715            ("SABRE", BackendCapabilities::SABRE),
6716            ("DBT", BackendCapabilities::DBT),
6717            ("KVM", BackendCapabilities::KVM),
6718        ] {
6719            for sequentialize_threads in [false, true] {
6720                let sent = Config {
6721                    backend,
6722                    sequentialize_threads,
6723                    seed: 7,
6724                    chaos: true,
6725                    ..Config::default()
6726                };
6727                let json = to_legacy_backend_json(&sent).unwrap();
6728                assert_controlled_key(&json, &backend, sequentialize_threads, name);
6729                let array = positional(&json);
6730                assert!(array.starts_with('['), "{array}");
6731                let received = from_legacy_backend_json(&array).unwrap();
6732                assert_eq!(received.backend, legacy_expressible(backend), "{name}");
6733                assert_eq!(
6734                    received.sequentialize_threads, sequentialize_threads,
6735                    "{name}"
6736                );
6737                assert_eq!(received.seed, 7, "{name}");
6738                assert!(received.chaos, "{name}");
6739                assert_eq!(to_legacy_backend_json(&received).unwrap(), json, "{name}");
6740            }
6741        }
6742    }
6743
6744    /// An array fails the parse where the legacy form's array did: with an
6745    /// element past the last position, with an element of the wrong type, or
6746    /// short of a field that has no default. The last position,
6747    /// `interrupt_at`, has none, so every shorter array fails.
6748    #[test]
6749    fn a_malformed_legacy_positional_array_fails_the_parse() {
6750        let json = to_legacy_backend_json(&Config {
6751            backend: BackendCapabilities::DBT,
6752            ..Config::default()
6753        })
6754        .unwrap();
6755        let entries = ordered_entries(&json);
6756        let values: Vec<serde_json::Value> =
6757            entries.iter().map(|(_, value)| value.clone()).collect();
6758        let parse = |values: &[serde_json::Value]| {
6759            from_legacy_backend_json(&serde_json::to_string(values).unwrap())
6760        };
6761        assert!(parse(&values).is_ok());
6762
6763        let mut longer = values.clone();
6764        longer.push(serde_json::Value::Bool(false));
6765        assert!(parse(&longer).is_err());
6766
6767        assert_eq!(entries.last().unwrap().0, "interrupt_at");
6768        for len in 0..values.len() {
6769            assert!(parse(&values[..len]).is_err(), "{len} elements");
6770        }
6771
6772        for (position, (key, _)) in entries.iter().enumerate() {
6773            if LEGACY_BACKEND_KEY_NAMES.contains(&key.as_str()) || key == "seed" {
6774                let mut mistyped = values.clone();
6775                mistyped[position] = serde_json::json!("0");
6776                assert!(parse(&mistyped).is_err(), "{key}");
6777            }
6778        }
6779    }
6780
6781    /// Selects the child half of
6782    /// [`a_legacy_configuration_decodes_whatever_the_ambient_hermit_settings`] and
6783    /// carries the input it decodes.
6784    const LEGACY_INPUT_IN_CHILD: &str = "DETCORE_MODEL_TEST_LEGACY_INPUT";
6785    /// The legacy object the child's decoded configuration must re-encode as.
6786    const LEGACY_EXPECTED_IN_CHILD: &str = "DETCORE_MODEL_TEST_LEGACY_EXPECTED";
6787    /// Printed by a child that decoded its input, so that a parent can tell
6788    /// that run from one that selected no test.
6789    const LEGACY_DECODED_IN_CHILD: &str = "legacy configuration decoded under the ambient settings";
6790
6791    /// Clap reads HERMIT_EPOCH, HERMIT_PRNG, HERMIT_SCHED_SEED and HERMIT_CPU_PROFILE when it
6792    /// parses a `Config`, and exits the process when one of them does not
6793    /// parse. The legacy decoder read none of them, so a valid legacy object
6794    /// or array decodes to the configuration it lists whatever they hold. Each
6795    /// decode runs in a child process given those variables unset, set to
6796    /// text that does not parse, or set to bytes that are not UTF-8. The child
6797    /// builds no `Config` before decoding, which would read them itself.
6798    #[test]
6799    fn a_legacy_configuration_decodes_whatever_the_ambient_hermit_settings() {
6800        use std::ffi::OsStr;
6801        use std::os::unix::ffi::OsStrExt;
6802
6803        if let Some(input) = std::env::var_os(LEGACY_INPUT_IN_CHILD) {
6804            let expected = std::env::var(LEGACY_EXPECTED_IN_CHILD).unwrap();
6805            let received = from_legacy_backend_json(input.to_str().unwrap()).unwrap();
6806            assert_eq!(
6807                received.backend,
6808                legacy_expressible(BackendCapabilities::DBT)
6809            );
6810            assert_eq!(received.seed, 7);
6811            assert_eq!(received.sched_seed, Some(11));
6812            assert_eq!(to_legacy_backend_json(&received).unwrap(), expected);
6813            println!("{LEGACY_DECODED_IN_CHILD}");
6814            return;
6815        }
6816
6817        const VARIABLES: [&str; 4] = [
6818            "HERMIT_EPOCH",
6819            "HERMIT_PRNG",
6820            "HERMIT_SCHED_SEED",
6821            "HERMIT_CPU_PROFILE",
6822        ];
6823        let text = OsStr::new("not-a-value");
6824        let bytes = OsStr::from_bytes(b"\xff");
6825        let mut settings: Vec<Vec<(&str, &OsStr)>> = vec![Vec::new()];
6826        for variable in VARIABLES {
6827            settings.push(vec![(variable, text)]);
6828            settings.push(vec![(variable, bytes)]);
6829        }
6830        settings.push(VARIABLES.map(|variable| (variable, text)).to_vec());
6831        settings.push(VARIABLES.map(|variable| (variable, bytes)).to_vec());
6832
6833        let object = to_legacy_backend_json(&Config {
6834            backend: BackendCapabilities::DBT,
6835            seed: 7,
6836            sched_seed: Some(11),
6837            ..Config::default()
6838        })
6839        .unwrap();
6840        let array = positional(&object);
6841        assert!(array.starts_with('['), "{array}");
6842        let test = format!(
6843            "{}::a_legacy_configuration_decodes_whatever_the_ambient_hermit_settings",
6844            module_path!().split_once("::").unwrap().1
6845        );
6846        for input in [&object, &array] {
6847            for setting in &settings {
6848                let mut child = std::process::Command::new(std::env::current_exe().unwrap());
6849                child
6850                    .args(["--exact", &test, "--nocapture"])
6851                    .env(LEGACY_INPUT_IN_CHILD, input)
6852                    .env(LEGACY_EXPECTED_IN_CHILD, &object);
6853                for variable in VARIABLES {
6854                    child.env_remove(variable);
6855                }
6856                for (variable, value) in setting {
6857                    child.env(variable, value);
6858                }
6859                let output = child.output().unwrap();
6860                let stdout = String::from_utf8_lossy(&output.stdout);
6861                let stderr = String::from_utf8_lossy(&output.stderr);
6862                assert!(
6863                    output.status.success()
6864                        && stdout.contains(LEGACY_DECODED_IN_CHILD)
6865                        && stdout.contains("test result: ok. 1 passed; 0 failed;"),
6866                    "decoding {} under {setting:?} did not succeed exactly once: {}\n{stdout}\n{stderr}",
6867                    if input.starts_with('[') {
6868                        "the array"
6869                    } else {
6870                        "the object"
6871                    },
6872                    output.status,
6873                );
6874            }
6875        }
6876    }
6877
6878    /// Set in the child process of
6879    /// [`hermit_cpu_profile_supplies_the_profile_only_when_the_flag_is_absent`]:
6880    /// the arguments it parses, one per line.
6881    const CPU_PROFILE_ARGS_IN_CHILD: &str = "DETCORE_MODEL_TEST_CPU_PROFILE_ARGS";
6882
6883    /// HERMIT_CPU_PROFILE gives `--cpu-profile` its value when the flag is
6884    /// absent: unset is x86-64-v2, an explicit flag wins over the variable,
6885    /// and a value that names no profile is refused. The wire default stays
6886    /// x86-64-v2 whatever the variable holds. Each parse runs in a child
6887    /// process given exactly the variable under test, because Clap reads the
6888    /// process environment.
6889    #[test]
6890    fn hermit_cpu_profile_supplies_the_profile_only_when_the_flag_is_absent() {
6891        use clap::Parser;
6892
6893        if let Some(args) = std::env::var_os(CPU_PROFILE_ARGS_IN_CHILD) {
6894            let args = args.into_string().unwrap();
6895            let argv = std::iter::once("config").chain(args.lines());
6896            match Config::try_parse_from(argv) {
6897                Ok(config) => println!(
6898                    "cpu-profile={} wire={}",
6899                    config.cpu_profile.name(),
6900                    config_wire_default().cpu_profile.name()
6901                ),
6902                Err(error) => println!("refused: {:?}", error.kind()),
6903            }
6904            return;
6905        }
6906
6907        let test = format!(
6908            "{}::hermit_cpu_profile_supplies_the_profile_only_when_the_flag_is_absent",
6909            module_path!().split_once("::").unwrap().1
6910        );
6911        let parse = |variable: Option<&str>, args: &[&str]| -> String {
6912            let mut child = std::process::Command::new(std::env::current_exe().unwrap());
6913            child
6914                .args(["--exact", &test, "--nocapture"])
6915                .env(CPU_PROFILE_ARGS_IN_CHILD, args.join("\n"));
6916            for name in [
6917                "HERMIT_EPOCH",
6918                "HERMIT_PRNG",
6919                "HERMIT_SCHED_SEED",
6920                "HERMIT_CPU_PROFILE",
6921            ] {
6922                child.env_remove(name);
6923            }
6924            if let Some(value) = variable {
6925                child.env("HERMIT_CPU_PROFILE", value);
6926            }
6927            let output = child.output().unwrap();
6928            let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
6929            assert!(
6930                output.status.success() && stdout.contains("test result: ok. 1 passed; 0 failed;"),
6931                "the child for {variable:?} {args:?} did not run exactly once: {}\n{stdout}",
6932                output.status,
6933            );
6934            stdout
6935                .lines()
6936                .find(|line| line.starts_with("cpu-profile=") || line.starts_with("refused: "))
6937                .unwrap_or_else(|| panic!("the child printed no verdict:\n{stdout}"))
6938                .to_owned()
6939        };
6940
6941        assert_eq!(parse(None, &[]), "cpu-profile=x86-64-v2 wire=x86-64-v2");
6942        assert_eq!(
6943            parse(Some("x86-64-v3"), &[]),
6944            "cpu-profile=x86-64-v3 wire=x86-64-v2"
6945        );
6946        assert_eq!(
6947            parse(Some("host-no-rdrand"), &[]),
6948            "cpu-profile=host-no-rdrand wire=x86-64-v2"
6949        );
6950        assert_eq!(
6951            parse(None, &["--cpu-profile=x86-64-v3"]),
6952            "cpu-profile=x86-64-v3 wire=x86-64-v2"
6953        );
6954        assert_eq!(
6955            parse(Some("x86-64-v3"), &["--cpu-profile=x86-64-v2"]),
6956            "cpu-profile=x86-64-v2 wire=x86-64-v2"
6957        );
6958        assert_eq!(parse(Some("x86-64-v4"), &[]), "refused: InvalidValue");
6959        assert_eq!(
6960            parse(Some("x86-64-v4"), &["--cpu-profile=x86-64-v2"]),
6961            "cpu-profile=x86-64-v2 wire=x86-64-v2"
6962        );
6963    }
6964
6965    /// `backend` and `shared_dequeue_timers` were not keys of the legacy form,
6966    /// which ignored every key it did not name. They are still ignored,
6967    /// whatever they hold, however often they appear and wherever they stand,
6968    /// so the legacy keys alone decide the capabilities. A key the legacy form
6969    /// named still fails the parse when it appears twice.
6970    #[test]
6971    fn keys_the_legacy_form_did_not_name_are_ignored() {
6972        let sent = Config {
6973            backend: legacy_expressible(BackendCapabilities::DBT),
6974            seed: 7,
6975            ..Config::default()
6976        };
6977        let json = to_legacy_backend_json(&sent).unwrap();
6978        let kvm = serde_json::to_string(&BackendCapabilities::KVM).unwrap();
6979        for extra in [
6980            r#""backend":null"#.to_owned(),
6981            r#""shared_dequeue_timers":null"#.to_owned(),
6982            r#""backend":null,"shared_dequeue_timers":null"#.to_owned(),
6983            format!(r#""backend":{kvm},"shared_dequeue_timers":true"#),
6984            r#""backend":7,"backend":{"tracks_process_children":false}"#.to_owned(),
6985            r#""shared_dequeue_timers":"yes","shared_dequeue_timers":true"#.to_owned(),
6986            r#""no_such_field":[1,{"x":null}]"#.to_owned(),
6987        ] {
6988            for edited in [
6989                json.replacen('{', &format!("{{{extra},"), 1),
6990                format!("{},{extra}}}", &json[..json.len() - 1]),
6991            ] {
6992                let received = from_legacy_backend_json(&edited).unwrap();
6993                assert_eq!(received.backend, sent.backend, "{edited}");
6994                assert_eq!(
6995                    serde_json::to_string(&received).unwrap(),
6996                    serde_json::to_string(&sent).unwrap(),
6997                    "{edited}"
6998                );
6999                assert_eq!(to_legacy_backend_json(&received).unwrap(), json, "{edited}");
7000            }
7001        }
7002
7003        let repeated = format!("{},\"seed\":7}}", &json[..json.len() - 1]);
7004        assert!(from_legacy_backend_json(&repeated).is_err());
7005    }
7006
7007    #[test]
7008    fn network_perturbation_seed_never_falls_back_to_scheduler_seeds() {
7009        let mut config = Config {
7010            seed: 41,
7011            sched_seed: Some(42),
7012            ..Config::default()
7013        };
7014        assert_eq!(config.network_trace.network_perturb_seed, None);
7015
7016        config.network_trace = NetworkTraceConfig {
7017            mode: crate::network_trace::NetworkTraceMode::Replay,
7018            path: Some("network.trace".into()),
7019            network_perturb_seed: Some(43),
7020        };
7021        assert_eq!(config.seed, 41);
7022        assert_eq!(config.sched_seed(), 42);
7023        assert_eq!(config.network_trace.network_perturb_seed, Some(43));
7024    }
7025
7026    #[test]
7027    fn missing_mountinfo_provenance_deserializes_as_empty() {
7028        let mut value = serde_json::to_value(Config::default()).unwrap();
7029        value
7030            .as_object_mut()
7031            .unwrap()
7032            .remove("mountinfo_root_rewrites");
7033        value
7034            .as_object_mut()
7035            .unwrap()
7036            .remove("mountinfo_device_rewrites");
7037        value.as_object_mut().unwrap().remove("mountinfo_mount_ids");
7038        value
7039            .as_object_mut()
7040            .unwrap()
7041            .remove("mountinfo_mount_ids_captured");
7042        value
7043            .as_object_mut()
7044            .unwrap()
7045            .remove("mount_id_assignment_order");
7046        value
7047            .as_object_mut()
7048            .unwrap()
7049            .remove("container_mount_table");
7050        value
7051            .as_object_mut()
7052            .unwrap()
7053            .remove("mountinfo_mount_namespace");
7054        value
7055            .as_object_mut()
7056            .unwrap()
7057            .remove("directory_placeholder");
7058        value.as_object_mut().unwrap().remove("container_procfs");
7059        let restored: Config = serde_json::from_value(value).unwrap();
7060        assert!(restored.mountinfo_root_rewrites.is_empty());
7061        assert!(restored.mountinfo_device_rewrites.is_empty());
7062        assert!(restored.mountinfo_mount_ids.is_empty());
7063        assert!(!restored.mountinfo_mount_ids_captured);
7064        assert!(restored.mount_id_assignment_order.is_empty());
7065        assert!(restored.container_mount_table.is_none());
7066        assert!(restored.mountinfo_mount_namespace.is_none());
7067        assert!(restored.directory_placeholder.is_none());
7068        assert!(!restored.container_procfs);
7069    }
7070
7071    /// Lone slice ends are processed without stops only where a slice end
7072    /// reads nothing from the guest but its branch count: each listed option
7073    /// turns it off.
7074    #[test]
7075    fn lone_preemptions_are_replayed_only_where_a_slice_end_reads_nothing_else() {
7076        let base = Config {
7077            sequentialize_threads: true,
7078            backend: reverie::BackendCapabilities::PTRACE,
7079            ..Config::parse_from(["detcore"])
7080        };
7081        assert!(base.replays_lone_preemptions());
7082        let off: Vec<(&str, Config)> = vec![
7083            (
7084                "--eager-lone-preemptions",
7085                Config {
7086                    eager_lone_preemptions: true,
7087                    ..base.clone()
7088                },
7089            ),
7090            (
7091                "no sequentialization",
7092                Config {
7093                    sequentialize_threads: false,
7094                    ..base.clone()
7095                },
7096            ),
7097            (
7098                "--max-timeslice=disabled",
7099                Config {
7100                    max_timeslice: None,
7101                    ..base.clone()
7102                },
7103            ),
7104            (
7105                "--no-rcb-time",
7106                Config {
7107                    no_rcb_time: true,
7108                    ..base.clone()
7109                },
7110            ),
7111            (
7112                "--imprecise-timers",
7113                Config {
7114                    imprecise_timers: true,
7115                    ..base.clone()
7116                },
7117            ),
7118            (
7119                "--chaos-per-thread-slowdown",
7120                Config {
7121                    chaos_per_thread_slowdown: true,
7122                    ..base.clone()
7123                },
7124            ),
7125            (
7126                "--record-preemptions",
7127                Config {
7128                    record_preemptions: true,
7129                    ..base.clone()
7130                },
7131            ),
7132            (
7133                "--strict-schedule-boundaries",
7134                Config {
7135                    strict_schedule_boundaries: true,
7136                    ..base.clone()
7137                },
7138            ),
7139            (
7140                "--replay-preemptions-from",
7141                Config {
7142                    replay_preemptions_from: Some("p".into()),
7143                    ..base.clone()
7144                },
7145            ),
7146            (
7147                "--replay-schedule-from",
7148                Config {
7149                    replay_schedule_from: Some("s".into()),
7150                    ..base.clone()
7151                },
7152            ),
7153            (
7154                "--target-timeslice",
7155                Config {
7156                    target_timeslice: NonZeroU64::new(1_000),
7157                    ..base.clone()
7158                },
7159            ),
7160            (
7161                "--preemption-stacktrace",
7162                Config {
7163                    preemption_stacktrace: true,
7164                    ..base.clone()
7165                },
7166            ),
7167            (
7168                "--stacktrace-event",
7169                Config {
7170                    stacktrace_event: vec![(1, None)],
7171                    ..base.clone()
7172                },
7173            ),
7174            (
7175                "--stop-after-turn",
7176                Config {
7177                    stop_after_turn: Some(1),
7178                    ..base.clone()
7179                },
7180            ),
7181            (
7182                "--stop-after-iter",
7183                Config {
7184                    stop_after_iter: Some(1),
7185                    ..base.clone()
7186                },
7187            ),
7188            (
7189                "--interrupt-at",
7190                Config {
7191                    interrupt_at: vec![(DetTid::from_raw(3), 1)],
7192                    ..base.clone()
7193                },
7194            ),
7195            (
7196                "--signal-at",
7197                Config {
7198                    signal_at: Some(parse_signal_at("turn=1:SIGUSR1").unwrap()),
7199                    ..base.clone()
7200                },
7201            ),
7202            (
7203                "--happens-before",
7204                Config {
7205                    happens_before: Some(crate::happens_before::HappensBeforeProgram {
7206                        version: 1,
7207                        anchors: Default::default(),
7208                        edges: Vec::new(),
7209                        max_hold_ns: None,
7210                    }),
7211                    ..base.clone()
7212                },
7213            ),
7214            (
7215                "--gdbserver",
7216                Config {
7217                    gdbserver: true,
7218                    ..base.clone()
7219                },
7220            ),
7221        ];
7222        for (option, config) in off {
7223            assert!(!config.replays_lone_preemptions(), "{option}");
7224        }
7225    }
7226
7227    #[test]
7228    fn runs_post_fork_parses_all_modes_and_defaults_to_child() {
7229        assert_eq!(Config::default().runs_post_fork, RunsPostFork::Child);
7230        assert_eq!(
7231            Config::parse_from(["detcore", "--runs-post-fork=parent"]).runs_post_fork,
7232            RunsPostFork::Parent
7233        );
7234        assert_eq!(
7235            Config::parse_from(["detcore", "--runs-post-fork=random"]).runs_post_fork,
7236            RunsPostFork::Random
7237        );
7238        assert!(Config::try_parse_from(["detcore", "--runs-post-fork=invalid"]).is_err());
7239    }
7240
7241    #[test]
7242    fn proc_level_parses_both_levels_defaults_to_minimal_and_round_trips() {
7243        assert_eq!(Config::default().proc_level, ProcLevel::Minimal);
7244        assert!(!Config::default().to_string().contains("--proc"));
7245        let standard = Config::parse_from(["detcore", "--proc=standard"]);
7246        assert_eq!(standard.proc_level, ProcLevel::Standard);
7247        assert!(standard.to_string().contains(" --proc=standard"));
7248        assert_eq!(
7249            Config::parse_from(["detcore", "--proc=minimal"]).proc_level,
7250            ProcLevel::Minimal
7251        );
7252        assert!(Config::try_parse_from(["detcore", "--proc=host"]).is_err());
7253        assert!(ProcLevel::Minimal < ProcLevel::Standard);
7254    }
7255
7256    #[test]
7257    fn panic_on_rcb_overshoot_is_opt_in_and_round_trips() {
7258        assert!(!Config::default().panic_on_rcb_overshoot);
7259
7260        let config = Config::parse_from(["detcore", "--panic-on-rbc-overshoot"]);
7261        assert!(config.panic_on_rcb_overshoot);
7262        assert!(config.to_string().contains(" --panic-on-rbc-overshoot"));
7263
7264        let alias = Config::parse_from(["detcore", "--panic-on-rcb-overshoot"]);
7265        assert!(alias.panic_on_rcb_overshoot);
7266    }
7267
7268    #[test]
7269    fn config_display_preserves_nondefault_post_fork_modes() {
7270        let mut config = Config {
7271            runs_post_fork: RunsPostFork::Parent,
7272            ..Config::default()
7273        };
7274        assert!(config.to_string().contains(" --runs-post-fork=parent"));
7275
7276        config.runs_post_fork = RunsPostFork::Random;
7277        assert!(config.to_string().contains(" --runs-post-fork=random"));
7278    }
7279
7280    /// `--expired-wait-charge` is unset by default, parses both values, is
7281    /// printed by `Display` (reproducer commands are built from it) only when
7282    /// set, round-trips through serde, and never enters the legacy form.
7283    #[test]
7284    fn the_expired_wait_charge_is_printed_when_set_and_never_enters_the_legacy_form() {
7285        assert_eq!(Config::default().expired_wait_charge, None);
7286        assert!(
7287            !Config::default()
7288                .to_string()
7289                .contains("--expired-wait-charge")
7290        );
7291        for (value, charge) in [
7292            ("each", ExpiredWaitCharge::Each),
7293            ("once", ExpiredWaitCharge::Once),
7294        ] {
7295            let set =
7296                Config::try_parse_from(["detcore", &format!("--expired-wait-charge={value}")])
7297                    .unwrap();
7298            assert_eq!(set.expired_wait_charge, Some(charge));
7299            assert!(
7300                set.to_string()
7301                    .contains(&format!(" --expired-wait-charge={value}")),
7302                "{set}"
7303            );
7304            let reparsed: Config =
7305                serde_json::from_str(&serde_json::to_string(&set).unwrap()).unwrap();
7306            assert_eq!(reparsed.expired_wait_charge, Some(charge));
7307            let legacy = to_legacy_backend_json(&set).unwrap();
7308            assert!(!legacy.contains("expired_wait_charge"), "{legacy}");
7309            assert_eq!(
7310                from_legacy_backend_json(&legacy)
7311                    .unwrap()
7312                    .expired_wait_charge,
7313                None
7314            );
7315        }
7316    }
7317
7318    #[test]
7319    fn the_work_span_report_is_off_by_default_and_never_enters_the_legacy_form() {
7320        assert_eq!(Config::default().work_span_report, None);
7321        let on = Config::try_parse_from(["detcore", "--work-span-report=/tmp/ws.json"]).unwrap();
7322        assert_eq!(
7323            on.work_span_report.as_deref(),
7324            Some(std::path::Path::new("/tmp/ws.json"))
7325        );
7326        assert!(on.to_string().contains(" --work-span-report=/tmp/ws.json"));
7327        let reparsed: Config = serde_json::from_str(&serde_json::to_string(&on).unwrap()).unwrap();
7328        assert_eq!(reparsed.work_span_report, on.work_span_report);
7329        let mut old = serde_json::to_value(Config::default()).unwrap();
7330        old.as_object_mut().unwrap().remove("work_span_report");
7331        let old: Config = serde_json::from_value(old).unwrap();
7332        assert_eq!(old.work_span_report, None);
7333        let legacy = to_legacy_backend_json(&on).unwrap();
7334        assert!(!legacy.contains("work_span_report"), "{legacy}");
7335        assert_eq!(
7336            from_legacy_backend_json(&legacy).unwrap().work_span_report,
7337            None
7338        );
7339    }
7340
7341    #[test]
7342    fn a_fatal_delivery_check_is_off_by_default_and_never_enters_the_legacy_form() {
7343        assert!(!Config::default().signal_delivery_check_fatal);
7344        let fatal = Config {
7345            signal_delivery_check_fatal: true,
7346            ..Config::default()
7347        };
7348        let json = serde_json::to_string(&fatal).unwrap();
7349        let reparsed: Config = serde_json::from_str(&json).unwrap();
7350        assert!(reparsed.signal_delivery_check_fatal);
7351        let mut old = serde_json::to_value(Config::default()).unwrap();
7352        old.as_object_mut()
7353            .unwrap()
7354            .remove("signal_delivery_check_fatal");
7355        let old: Config = serde_json::from_value(old).unwrap();
7356        assert!(!old.signal_delivery_check_fatal);
7357        let legacy = to_legacy_backend_json(&fatal).unwrap();
7358        assert!(!legacy.contains("signal_delivery_check_fatal"), "{legacy}");
7359        assert!(
7360            !from_legacy_backend_json(&legacy)
7361                .unwrap()
7362                .signal_delivery_check_fatal
7363        );
7364    }
7365
7366    #[test]
7367    fn the_signal_state_copy_is_on_by_default_and_its_off_switch_round_trips() {
7368        let dflt = Config::default();
7369        assert!(dflt.signal_state_copy);
7370        assert!(!dflt.to_string().contains("signal-state-copy"));
7371        // A configuration serialized before the field existed reads as on.
7372        let mut json = serde_json::to_value(&dflt).unwrap();
7373        json.as_object_mut().unwrap().remove("signal_state_copy");
7374        let old: Config = serde_json::from_value(json).unwrap();
7375        assert!(old.signal_state_copy);
7376        // Off renders as the flag, so that a command rebuilt from a
7377        // configuration (analyze's reproduction commands) keeps it off.
7378        let off = Config::parse_from(["detcore", "--no-signal-state-copy"]);
7379        assert!(!off.signal_state_copy);
7380        let rendered = off.to_string();
7381        assert!(rendered.contains(" --no-signal-state-copy"), "{rendered}");
7382        let reparsed = Config::parse_from(
7383            std::iter::once("detcore".to_string())
7384                .chain(rendered.split_whitespace().map(String::from)),
7385        );
7386        assert!(!reparsed.signal_state_copy);
7387    }
7388
7389    // AUTONOMOUS-BOT-IMPLEMENTED
7390    // TODO-HUMAN-REVIEW(PR-1149)
7391    #[test]
7392    fn chaos_per_thread_slowdown_is_opt_in_and_round_trips() {
7393        // Off by default; the factor default is present but inert.
7394        let dflt = Config::default();
7395        assert!(!dflt.chaos_per_thread_slowdown);
7396        assert_eq!(dflt.chaos_slowdown_max_factor, 10.0);
7397        // Default (disabled) config does not emit the flags.
7398        assert!(!dflt.to_string().contains("--chaos-per-thread-slowdown"));
7399
7400        let config = Config::parse_from([
7401            "detcore",
7402            "--chaos",
7403            "--chaos-per-thread-slowdown",
7404            "--chaos-slowdown-max-factor=4.5",
7405        ]);
7406        assert!(config.chaos_per_thread_slowdown);
7407        assert_eq!(config.chaos_slowdown_max_factor, 4.5);
7408
7409        // The Display round-trips both flags into the recorded schedule artifact.
7410        let rendered = config.to_string();
7411        assert!(rendered.contains(" --chaos-per-thread-slowdown"));
7412        assert!(rendered.contains(" --chaos-slowdown-max-factor=4.5"));
7413        let reparsed = Config::parse_from(
7414            std::iter::once("detcore".to_string())
7415                .chain(rendered.split_whitespace().map(String::from)),
7416        );
7417        assert!(reparsed.chaos_per_thread_slowdown);
7418        assert_eq!(reparsed.chaos_slowdown_max_factor, 4.5);
7419    }
7420
7421    // AUTONOMOUS-BOT-IMPLEMENTED
7422    // TODO-HUMAN-REVIEW(PR-1151)
7423    #[test]
7424    fn chaos_epoch_length_is_opt_in_and_round_trips() {
7425        // Off by default (single stable factor == plain per-thread-slowdown).
7426        let dflt = Config::default();
7427        assert_eq!(dflt.chaos_epoch_length_ns, 0);
7428        assert!(!dflt.to_string().contains("--chaos-epoch-length-ns"));
7429
7430        // Epochs are only emitted alongside per-thread-slowdown.
7431        let config = Config::parse_from([
7432            "detcore",
7433            "--chaos",
7434            "--chaos-per-thread-slowdown",
7435            "--chaos-epoch-length-ns=100000",
7436        ]);
7437        assert_eq!(config.chaos_epoch_length_ns, 100000);
7438
7439        let rendered = config.to_string();
7440        assert!(rendered.contains(" --chaos-epoch-length-ns=100000"));
7441        let reparsed = Config::parse_from(
7442            std::iter::once("detcore".to_string())
7443                .chain(rendered.split_whitespace().map(String::from)),
7444        );
7445        assert_eq!(reparsed.chaos_epoch_length_ns, 100000);
7446
7447        // Without per-thread-slowdown the epoch flag is inert and not rendered.
7448        let no_slowdown = Config::parse_from(["detcore", "--chaos", "--chaos-epoch-length-ns=100"]);
7449        assert_eq!(no_slowdown.chaos_epoch_length_ns, 100);
7450        assert!(!no_slowdown.to_string().contains("--chaos-epoch-length-ns"));
7451    }
7452
7453    // AUTONOMOUS-BOT-IMPLEMENTED
7454    // TODO-HUMAN-REVIEW(PR-1149)
7455    #[test]
7456    #[should_panic(expected = "chaos_slowdown_max_factor must be finite and in")]
7457    fn validate_rejects_chaos_slowdown_max_factor_below_one() {
7458        let mut config = Config {
7459            chaos_slowdown_max_factor: 0.5,
7460            ..Default::default()
7461        };
7462        config.validate();
7463    }
7464
7465    #[test]
7466    #[should_panic(expected = "max_timeslice must be at least one RCB")]
7467    fn validate_rejects_max_timeslice_below_one_rcb() {
7468        let mut config = Config {
7469            max_timeslice: NonZeroU64::new(NANOS_PER_RCB as u64 - 1),
7470            ..Default::default()
7471        };
7472
7473        config.validate();
7474    }
7475
7476    #[test]
7477    fn validate_accepts_one_rcb_max_timeslice() {
7478        let mut config = Config {
7479            max_timeslice: NonZeroU64::new(NANOS_PER_RCB as u64),
7480            ..Default::default()
7481        };
7482
7483        config.validate();
7484    }
7485
7486    #[test]
7487    #[should_panic(expected = "clock_multiplier must be finite and positive")]
7488    fn validate_rejects_invalid_clock_multiplier() {
7489        let mut config = Config {
7490            clock_multiplier: Some(0.0),
7491            ..Default::default()
7492        };
7493        config.validate();
7494    }
7495
7496    #[test]
7497    #[should_panic(expected = "max_timeslice must be at least one RCB")]
7498    fn validate_scales_one_rcb_minimum_with_clock_multiplier() {
7499        let mut config = Config {
7500            max_timeslice: NonZeroU64::new(10),
7501            clock_multiplier: Some(2.0),
7502            ..Default::default()
7503        };
7504        config.validate();
7505    }
7506
7507    #[test]
7508    fn config_fingerprint_sees_the_width_of_every_backend_capability() {
7509        // `BackendCapabilities` lives in Reverie, outside the fingerprinted
7510        // sources, so only its encoding can reveal a change to it. Every
7511        // field encodes at a fixed width whatever its value (a bool, or the
7512        // `u8` width of `hardware_watchpoints`, in one byte; the `u64`
7513        // signal set of `publishable_parent_death_signals` in eight), so the
7514        // encoded default carries every field, and adding, removing or
7515        // widening one changes the bytes. A field that is optional would have
7516        // to be present in the default, or a change to its width would encode
7517        // as the same bare `None`; this fails first.
7518        let config = config_wire_default();
7519        let fields = serde_json::to_value(config.backend).unwrap();
7520        let fields = fields.as_object().unwrap();
7521        // The true widths from the capability table: every field is a bool
7522        // except `hardware_watchpoints`, a `u8` count, and
7523        // `publishable_parent_death_signals`, a `u64` set of signal bits.
7524        for (name, value) in fields {
7525            match name.as_str() {
7526                "hardware_watchpoints" => {
7527                    let width = value.as_u64().expect("a u8 count encodes as a number");
7528                    assert!(width <= u64::from(u8::MAX), "{name}={width} exceeds a u8");
7529                }
7530                "publishable_parent_death_signals" => {
7531                    value
7532                        .as_u64()
7533                        .expect("a u64 signal set encodes as a number");
7534                }
7535                _ => assert!(value.is_boolean(), "{name}={value} is not a bool"),
7536            }
7537        }
7538        let encode = |config: &Config| {
7539            bincode::serde::encode_to_vec(config, bincode::config::legacy()).unwrap()
7540        };
7541        // One byte per field, and seven more for the `u64` signal set.
7542        assert_eq!(
7543            bincode::serde::encode_to_vec(config.backend, bincode::config::legacy())
7544                .unwrap()
7545                .len(),
7546            fields.len() + 7
7547        );
7548        // The backend's values are part of the encoded default.
7549        let flipped = config.clone().with_backend(|backend| {
7550            backend.tracks_process_children = !backend.tracks_process_children
7551        });
7552        assert_eq!(encode(&flipped).len(), encode(&config).len());
7553        assert_ne!(encode(&flipped), encode(&config));
7554        // The one numeric capability: another width within a `u8` changes
7555        // the bytes at the same length; a wider type would change the length
7556        // asserted above.
7557        let widened = config.clone().with_backend(|backend| {
7558            backend.hardware_watchpoints = backend.hardware_watchpoints.wrapping_add(1)
7559        });
7560        assert_eq!(encode(&widened).len(), encode(&config).len());
7561        assert_ne!(encode(&widened), encode(&config));
7562        // The signal set: another set changes the bytes at the same length.
7563        let resigned = config
7564            .clone()
7565            .with_backend(|backend| backend.publishable_parent_death_signals ^= 1 << 63);
7566        assert_eq!(encode(&resigned).len(), encode(&config).len());
7567        assert_ne!(encode(&resigned), encode(&config));
7568    }
7569
7570    #[test]
7571    fn config_fingerprint_includes_clock_rpc_definitions() {
7572        let config = config_wire_default();
7573        let wire = bincode::serde::encode_to_vec(&config, bincode::config::legacy()).unwrap();
7574        let named_shape = serde_json::to_string(&config).unwrap();
7575        let current = config_wire_fingerprint();
7576        let clock_source = include_bytes!("time.rs").as_slice();
7577
7578        // The previous guard covered these same Config bytes and definitions,
7579        // but omitted DetTime. Adding a positional clock field could therefore
7580        // pass the handshake guard and corrupt the following request on decode.
7581        let without_clock: Vec<_> = CONFIG_DEFINITION_SOURCES
7582            .iter()
7583            .copied()
7584            .filter(|source| *source != clock_source)
7585            .collect();
7586        assert_ne!(
7587            fingerprint_of_config_material(&wire, &named_shape, &without_clock),
7588            current,
7589            "the published fingerprint must reject source inputs that omit the RPC clock"
7590        );
7591
7592        // Hold all Config material fixed and remove only the added serialized
7593        // clock field from its definition. A future clock-only change must also
7594        // invalidate the existing artifact guard, independently of config.rs.
7595        let changed_clock =
7596            include_str!("time.rs").replacen("    inherited_nanos: LogicalDuration,", "", 1);
7597        assert_ne!(changed_clock.as_bytes(), clock_source);
7598        let changed_sources: Vec<_> = CONFIG_DEFINITION_SOURCES
7599            .iter()
7600            .map(|source| {
7601                if *source == clock_source {
7602                    changed_clock.as_bytes()
7603                } else {
7604                    *source
7605                }
7606            })
7607            .collect();
7608        assert_ne!(
7609            fingerprint_of_config_material(&wire, &named_shape, &changed_sources),
7610            current,
7611            "a clock-only serialized field change must invalidate the fingerprint"
7612        );
7613    }
7614
7615    /// The inode RPCs carry `RawInode`, whose layout neither encoding of
7616    /// `Config` shows. A plugin built before `RawInode` gained its device
7617    /// sends one `u64` where the coordinator decodes two, so a change to its
7618    /// definition must invalidate the fingerprint.
7619    #[test]
7620    fn config_fingerprint_includes_the_inode_rpc_identity() {
7621        let config = config_wire_default();
7622        let wire = bincode::serde::encode_to_vec(&config, bincode::config::legacy()).unwrap();
7623        let named_shape = serde_json::to_string(&config).unwrap();
7624        let current = config_wire_fingerprint();
7625        let inode_source = include_bytes!("fd.rs").as_slice();
7626        assert!(CONFIG_DEFINITION_SOURCES.contains(&inode_source));
7627        let without_device = include_str!("fd.rs").replacen("    pub dev: u64,\n", "", 1);
7628        assert_ne!(without_device.as_bytes(), inode_source);
7629        let changed_sources: Vec<_> = CONFIG_DEFINITION_SOURCES
7630            .iter()
7631            .map(|source| {
7632                if *source == inode_source {
7633                    without_device.as_bytes()
7634                } else {
7635                    *source
7636                }
7637            })
7638            .collect();
7639        assert_ne!(
7640            fingerprint_of_config_material(&wire, &named_shape, &changed_sources),
7641            current,
7642            "an inode-identity layout change must invalidate the fingerprint"
7643        );
7644    }
7645
7646    #[test]
7647    fn a_config_fingerprint_mismatch_names_both_fingerprints() {
7648        let ours = config_wire_fingerprint();
7649        assert_eq!(
7650            config_fingerprint_mismatch(std::ffi::OsStr::new(&ours)),
7651            None
7652        );
7653        let other = config_fingerprint_mismatch(std::ffi::OsStr::new("0123456789abcdef"))
7654            .expect("another fingerprint is a mismatch");
7655        assert!(other.contains(&ours), "{other}");
7656        assert!(other.contains("0123456789abcdef"), "{other}");
7657        use std::os::unix::ffi::OsStrExt;
7658        assert!(config_fingerprint_mismatch(std::ffi::OsStr::from_bytes(b"\xff")).is_some());
7659    }
7660
7661    #[test]
7662    fn config_fingerprint_is_stable_and_shape_sensitive() {
7663        // STABLE: a build must agree with itself, or the guard would reject a
7664        // MATCHED pair -- which would be worse than having no guard at all.
7665        assert_eq!(config_wire_fingerprint(), config_wire_fingerprint());
7666        assert_eq!(config_wire_fingerprint().len(), 16);
7667
7668        let config = config_wire_default();
7669        let base = serde_json::to_string(&config).unwrap();
7670        let wire = bincode::serde::encode_to_vec(&config, bincode::config::legacy()).unwrap();
7671        assert_eq!(
7672            fingerprint_of_config_material(&wire, &base, CONFIG_DEFINITION_SOURCES),
7673            config_wire_fingerprint()
7674        );
7675
7676        // SHAPE-SENSITIVE, checked on the same mechanism the real function uses.
7677        // One added field is exactly the change that caused the outage.
7678        let with_extra_field = format!("{},\"a_new_flag\":false}}", &base[..base.len() - 1]);
7679        assert_ne!(
7680            fingerprint_of_config_material(&wire, &with_extra_field, CONFIG_DEFINITION_SOURCES),
7681            config_wire_fingerprint()
7682        );
7683        // A removed field.
7684        let removed = base.replacen("\"virtualize_time\":true,", "", 1);
7685        assert_ne!(
7686            fingerprint_of_config_material(&wire, &removed, CONFIG_DEFINITION_SOURCES),
7687            config_wire_fingerprint()
7688        );
7689        // A pure rename, which bincode would tolerate but which we still refuse.
7690        let renamed = base.replacen("\"virtualize_time\"", "\"virtualise_time\"", 1);
7691        assert_ne!(
7692            fingerprint_of_config_material(&wire, &renamed, CONFIG_DEFINITION_SOURCES),
7693            config_wire_fingerprint()
7694        );
7695
7696        // The counterexample the JSON-only fingerprint missed: serde_json emits
7697        // the same text for integer zero regardless of width, but legacy bincode
7698        // changes the payload width. A stale peer would decode every following
7699        // field at the wrong offset.
7700        #[derive(Serialize)]
7701        struct U32Field {
7702            field: u32,
7703        }
7704        #[derive(Serialize)]
7705        struct U64Field {
7706            field: u64,
7707        }
7708        let u32_value = U32Field { field: 0 };
7709        let u64_value = U64Field { field: 0 };
7710        let u32_json = serde_json::to_string(&u32_value).unwrap();
7711        let u64_json = serde_json::to_string(&u64_value).unwrap();
7712        assert_eq!(
7713            u32_json, u64_json,
7714            "the planted JSON collision must be real"
7715        );
7716        let u32_wire =
7717            bincode::serde::encode_to_vec(&u32_value, bincode::config::legacy()).unwrap();
7718        let u64_wire =
7719            bincode::serde::encode_to_vec(&u64_value, bincode::config::legacy()).unwrap();
7720        assert_ne!(u32_wire, u64_wire, "the planted wire retype must be real");
7721        assert_ne!(
7722            fingerprint_of_config_material(&u32_wire, &u32_json, &[b"struct S { field: u32 }"]),
7723            fingerprint_of_config_material(&u64_wire, &u64_json, &[b"struct S { field: u64 }"]),
7724            "a wire-incompatible integer retype must change the fingerprint"
7725        );
7726
7727        // Defaults can hide an incompatible inner type in BOTH value encodings.
7728        // The definition source is therefore load-bearing, not decorative.
7729        #[derive(Serialize)]
7730        struct OptionalU32 {
7731            field: Option<u32>,
7732        }
7733        #[derive(Serialize)]
7734        struct OptionalU64 {
7735            field: Option<u64>,
7736        }
7737        let optional_u32 = OptionalU32 { field: None };
7738        let optional_u64 = OptionalU64 { field: None };
7739        let optional_u32_json = serde_json::to_string(&optional_u32).unwrap();
7740        let optional_u64_json = serde_json::to_string(&optional_u64).unwrap();
7741        assert_eq!(optional_u32_json, optional_u64_json);
7742        let optional_u32_wire =
7743            bincode::serde::encode_to_vec(&optional_u32, bincode::config::legacy()).unwrap();
7744        let optional_u64_wire =
7745            bincode::serde::encode_to_vec(&optional_u64, bincode::config::legacy()).unwrap();
7746        assert_eq!(
7747            optional_u32_wire, optional_u64_wire,
7748            "the planted default must be invisible in both value encodings"
7749        );
7750        assert_ne!(
7751            fingerprint_of_config_material(
7752                &optional_u32_wire,
7753                &optional_u32_json,
7754                &[b"struct S { field: Option<u32> }"]
7755            ),
7756            fingerprint_of_config_material(
7757                &optional_u64_wire,
7758                &optional_u64_json,
7759                &[b"struct S { field: Option<u64> }"]
7760            ),
7761            "a hidden wire-incompatible inner-type change must alter the fingerprint"
7762        );
7763    }
7764
7765    #[test]
7766    fn config_fingerprint_uses_environment_free_wire_defaults() {
7767        let environment_derived = Config {
7768            epoch: "2042-03-04T05:06:07.890123456Z".parse().unwrap(),
7769            seed: 41,
7770            sched_seed: Some(42),
7771            cpu_profile: crate::cpu_profile::CpuProfile::X86_64V3,
7772            ..Config::default()
7773        };
7774
7775        let canonical = config_wire_default();
7776
7777        assert_ne!(
7778            serde_json::to_string(&environment_derived).unwrap(),
7779            serde_json::to_string(&canonical).unwrap()
7780        );
7781        assert_eq!(
7782            canonical.epoch,
7783            DEFAULT_EPOCH_STR.parse::<DateTime<Utc>>().unwrap()
7784        );
7785        assert_eq!(canonical.seed, 0);
7786        assert_eq!(canonical.sched_seed, None);
7787        assert_eq!(
7788            canonical.cpu_profile,
7789            crate::cpu_profile::CpuProfile::X86_64V2
7790        );
7791    }
7792
7793    // AUTONOMOUS-BOT-IMPLEMENTED
7794    // TODO-HUMAN-REVIEW(PR-1151)
7795    #[test]
7796    #[should_panic(expected = "max_timeslice must be at least one RCB")]
7797    fn validate_scales_one_rcb_minimum_with_chaos_slowdown() {
7798        let mut config = Config {
7799            chaos: true,
7800            chaos_per_thread_slowdown: true,
7801            chaos_slowdown_max_factor: 4.0,
7802            max_timeslice: NonZeroU64::new(39),
7803            ..Default::default()
7804        };
7805        config.validate();
7806    }
7807
7808    /// The one-RCB minimum multiplies like the PMU epsilon of Detcore's
7809    /// post-hook, `NANOS_PER_RCB * (clock_multiplier * factor)` in
7810    /// `detcore/src/lib.rs`, bit for bit. Every pair below rounds differently
7811    /// when multiplied left to right; in the first three that moves the
7812    /// minimum: at `--clock-multiplier=2.5` and
7813    /// `--chaos-slowdown-max-factor=4.4` the left-to-right product refused
7814    /// `--max-timeslice=110` as below "at least 111".
7815    #[test]
7816    fn one_rcb_minimum_multiplies_like_detcores_pmu_epsilon() {
7817        let slowed = |clock_multiplier: f64, factor: f64, max_timeslice: u64| Config {
7818            chaos: true,
7819            chaos_per_thread_slowdown: true,
7820            chaos_slowdown_max_factor: factor,
7821            clock_multiplier: Some(clock_multiplier),
7822            max_timeslice: NonZeroU64::new(max_timeslice),
7823            ..Default::default()
7824        };
7825
7826        let mut config = slowed(2.5, 4.4, 110);
7827        assert_eq!(config.minimum_max_timeslice_nanos(), 110);
7828        config.validate();
7829
7830        for (clock_multiplier, factor, minimum) in [
7831            (2.5, 4.4, 110),
7832            (2.5, 2.2, 55),
7833            (7.0, 1.1, 78),
7834            (0.7, 3.0, 21),
7835            (1.1, 5.5, 61),
7836        ] {
7837            let detcore = NANOS_PER_RCB * (clock_multiplier * factor);
7838            assert_ne!(
7839                (NANOS_PER_RCB * clock_multiplier * factor).to_bits(),
7840                detcore.to_bits(),
7841                "{clock_multiplier} x {factor} does not tell the orders apart"
7842            );
7843            let config = slowed(clock_multiplier, factor, 1_000_000);
7844            assert_eq!(
7845                config.largest_nanos_per_rcb().to_bits(),
7846                detcore.to_bits(),
7847                "{clock_multiplier} x {factor}"
7848            );
7849            assert_eq!(
7850                config.minimum_max_timeslice_nanos(),
7851                minimum,
7852                "{clock_multiplier} x {factor}"
7853            );
7854        }
7855    }
7856
7857    // AUTONOMOUS-BOT-IMPLEMENTED
7858    // TODO-HUMAN-REVIEW(PR-1151)
7859    #[test]
7860    #[should_panic(expected = "chaos_slowdown_max_factor must be finite and in")]
7861    fn validate_rejects_unrepresentable_chaos_slowdown_factor() {
7862        let mut config = Config {
7863            chaos_slowdown_max_factor: RcbTimeMultiplier::MAX * 2.0,
7864            ..Default::default()
7865        };
7866        config.validate();
7867    }
7868}
7869
7870#[cfg(test)]
7871mod strict_boundary_mode_tests {
7872    use super::Config;
7873
7874    #[test]
7875    fn strict_boundary_policy_is_explicit_and_round_trips_the_actual_wire() {
7876        let ordinary = Config::default();
7877        assert!(!ordinary.strict_schedule_boundaries);
7878        let mut old_shape = serde_json::to_value(&ordinary).unwrap();
7879        assert_eq!(
7880            old_shape
7881                .as_object_mut()
7882                .unwrap()
7883                .remove("strict_schedule_boundaries"),
7884            Some(false.into())
7885        );
7886        assert!(
7887            !serde_json::from_value::<Config>(old_shape)
7888                .unwrap()
7889                .strict_schedule_boundaries
7890        );
7891        let explicit = Config {
7892            strict_schedule_boundaries: true,
7893            ..ordinary
7894        };
7895        let encoded = serde_json::to_value(&explicit).unwrap();
7896        assert_eq!(encoded["strict_schedule_boundaries"], true);
7897        assert!(
7898            serde_json::from_value::<Config>(encoded)
7899                .unwrap()
7900                .strict_schedule_boundaries
7901        );
7902        assert!(
7903            explicit
7904                .to_string()
7905                .contains("--strict-schedule-boundaries")
7906        );
7907        for config in [&Config::default(), &explicit] {
7908            let bytes = bincode::serde::encode_to_vec(config, bincode::config::legacy()).unwrap();
7909            let (decoded, consumed): (Config, usize) =
7910                bincode::serde::decode_from_slice(&bytes, bincode::config::legacy()).unwrap();
7911            assert_eq!(consumed, bytes.len());
7912            assert_eq!(
7913                decoded.strict_schedule_boundaries,
7914                config.strict_schedule_boundaries
7915            );
7916            assert_eq!(
7917                bincode::serde::encode_to_vec(&decoded, bincode::config::legacy()).unwrap(),
7918                bytes
7919            );
7920        }
7921        let legacy = super::to_legacy_backend_json(&Config::default()).unwrap();
7922        assert!(!legacy.contains("strict_schedule_boundaries"));
7923        assert!(
7924            !super::from_legacy_backend_json(&legacy)
7925                .unwrap()
7926                .strict_schedule_boundaries
7927        );
7928        assert!(super::to_legacy_backend_json(&explicit).is_err());
7929    }
7930}