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}