Skip to main content

hermit/
lib.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9// Treat all Clippy warnings as errors.
10#![doc = include_str!("../README.md")]
11#![deny(clippy::all)]
12#![allow(clippy::uninlined_format_args)]
13// hermit-cli/clippy.toml sends every container clone through
14// bin/hermit/container_clone.rs, which checks that a unit test of the hermit
15// binary clones alone in its process. This library's own unit tests, a test
16// binary of their own, still clone directly.
17#![cfg_attr(test, allow(clippy::disallowed_methods))]
18
19// The application and the programmatic API share one library implementation.
20extern crate self as hermit;
21
22#[path = "bin/hermit/mod.rs"]
23pub mod app;
24
25// The actual included native consumer keeps its existing crate-root test hook.
26#[cfg(all(test, feature = "native-exec"))]
27pub(crate) use app::exec_initial;
28
29// This host library retains LiteInst code without becoming an in-guest Tool
30// image. Declare its historical backing explicitly and keep the host allocator;
31// LiteInst's allocation preflight still refuses non-Tool allocations.
32#[cfg(feature = "liteinst")]
33reverie_liteinst::declare_legacy_tool_backing!();
34
35pub mod backend_stats;
36pub mod build_info;
37pub mod canonical_verdict;
38pub mod chaos_timeslice;
39mod chroot;
40pub mod compatibility;
41use hermit_record_format::consts;
42pub mod logdiff_report;
43// TODO-HUMAN-REVIEW(PR-594): Review the public e9patch preprocessing API.
44pub mod cpu_profile_admission;
45mod error;
46use hermit_record_format::event;
47use hermit_record_format::event_stream;
48pub mod gdbserver;
49pub mod happens_before;
50pub mod host_input_change;
51mod id;
52#[cfg(feature = "liteinst")]
53pub mod in_guest_exits;
54pub mod instruction_map;
55pub mod kvm_address_space;
56#[cfg(feature = "liteinst")]
57use hermit_record_format::interp;
58pub mod liteinst_bootstrap;
59mod liteinst_constructor;
60pub mod liteinst_record;
61mod metadata;
62#[cfg(target_arch = "x86_64")]
63pub mod xstate_probe;
64/// Diagnostic writes that never wait for a reader; they live in detcore, whose
65/// own stderr writer reads the same flag.
66pub use detcore::nonwaiting_write;
67#[cfg(test)]
68mod chosen_input_tests;
69pub mod physical_exit_watch;
70pub mod preemption_admission;
71pub mod proc_mount;
72
73/// Re-runs the named test alone in a fresh process of this test binary and
74/// requires it to pass; returns false in that fresh process so the caller
75/// runs its body there. Used by tests that fork, clone or spawn children:
76/// under the threaded harness a child forked from this process can inherit
77/// a malloc arena whose lock another harness thread held at the fork, and
78/// then wait on that lock forever (seen as `__malloc_fork_lock_parent` in
79/// `__fork`). A fresh process has no other allocating threads, so its
80/// children cannot inherit a held arena lock. The fresh process still runs
81/// under libtest with one test thread, so no assertion changes.
82#[cfg(test)]
83pub(crate) fn run_in_fresh_process(name: &str) -> bool {
84    const INNER: &str = "HERMIT_LIB_TEST_FRESH_PROCESS";
85    if std::env::var_os(INNER).is_some_and(|value| value == name) {
86        return false;
87    }
88    let output = std::process::Command::new(std::env::current_exe().unwrap())
89        .args(["--exact", name, "--nocapture", "--test-threads=1"])
90        .env(INNER, name)
91        .output()
92        .unwrap();
93    let stdout = String::from_utf8_lossy(&output.stdout);
94    let stderr = String::from_utf8_lossy(&output.stderr);
95    assert!(
96        output.status.success(),
97        "inner run of {name} failed:\n{stdout}\n{stderr}"
98    );
99    assert!(
100        stdout.contains("1 passed"),
101        "inner run of {name} did not execute the test:\n{stdout}\n{stderr}"
102    );
103    true
104}
105mod ptrace_completion;
106#[cfg(test)]
107mod replay_departure_completion_tests;
108#[cfg(test)]
109mod replay_departure_test_fixture;
110mod timeout_survivors;
111
112pub use canonical_verdict::Verdict;
113
114/// Whether record/replay verification hashes syscall output buffers, and
115/// therefore whether its log comparison can see buffer CONTENT at all.
116///
117/// ONE CONSTANT BECAUSE TWO INDEPENDENT SWITCHES HAD TO AGREE AND SILENTLY DID
118/// NOT MATTER. Record/replay carried this decision twice: `metadata.rs`'s
119/// `record_or_replay_config` decided whether the recorder EMITS the hash, and
120/// `record_start.rs`'s `ComparisonOptions` decided whether the comparator READS
121/// it. Both were hard-coded `false`, neither was reachable from the command
122/// line, and nothing related them. Setting either alone achieves nothing: hash
123/// without compare is dead weight, compare without hash finds nothing. Naming
124/// the decision once makes the pair impossible to desynchronize, and makes
125/// changing it a one-line edit in a place a reader can find.
126pub const RECORD_REPLAY_HASHES_IO_BUFFERS: bool = true;
127
128/// Whether record/replay virtualizes time. FALSE, DELIBERATELY, AND THAT IS WHY
129/// A GREEN RECORD/REPLAY VERDICT IS NOT A DETERMINISM RESULT.
130///
131/// Named for the same reason as [`RECORD_REPLAY_HASHES_IO_BUFFERS`] directly
132/// above: the decision was about to be carried twice. `metadata.rs`'s
133/// `record_or_replay_config` sets the config the recorder and replayer run
134/// under, and `record_start.rs`'s `ComparisonOptions` now discloses that setting
135/// in the verdict. If those two disagreed, the report would describe a time
136/// policy the run did not use — which is precisely the class of defect the
137/// disclosure exists to close, reintroduced one layer up.
138///
139/// `hermit run --verify` reads its value from the live `det_config` instead,
140/// because `--no-virtualize-time` makes it a genuine runtime choice there. Only
141/// the record/replay path is a fixed decision, so only it gets a constant.
142pub const RECORD_REPLAY_VIRTUALIZES_TIME: bool = false;
143
144/// Exit status for a completed comparison that found a divergence.
145///
146/// This is the conventional `diff` result: zero means equal and one means
147/// different. The verification JSON distinguishes it from a guest that exits
148/// one of its own accord.
149pub const HERMIT_VERIFICATION_DIVERGENCE_EXIT: i32 = 1;
150
151/// Exit status for a failure of HERMIT ITSELF, as distinct from the guest's.
152///
153/// ⚠️ EVERY CLI ERROR USED TO BE `Exited(1)`, WHICH IS THE SINGLE MOST COMMON
154/// GUEST FAILURE CODE. A tracer panic and a guest that exited 1 of its own
155/// accord were therefore INDISTINGUISHABLE from `$?` alone — and every harness,
156/// gate and script on this project decides pass/fail from exactly that value.
157/// The information was not missing, only discarded: reverie's container reports
158/// the child's real status as a typed `ExitStatus`, and hermit collapsed it to
159/// prose and then to `1`.
160///
161/// WHY THIS DOES NOT BREAK THE "hermit's exit IS the guest's exit" CONTRACT.
162/// That contract describes the case where the guest RAN AND EXITED, and it is
163/// untouched: a guest status still flows through `raise_or_exit` unchanged, and
164/// only the `Err` arm — where there is NO guest exit code, because hermit failed
165/// before or instead of producing one — uses this value. The contract did not
166/// cover that case; it does now.
167///
168/// WHY 125. It is the established convention for "the wrapper tool itself
169/// failed" — GNU `env`, `chroot` and `timeout` all reserve 125 for exactly this,
170/// leaving 126/127 for exec-level problems. It is not a value hermit emits
171/// anywhere else.
172///
173/// ⚠️ THIS REDUCES A COLLISION, IT DOES NOT REMOVE ONE. A guest is free to exit
174/// 125 deliberately, and every value in 0..=255 is a legal guest status, so no
175/// code can be reserved outright. What it removes is the GUARANTEED collision
176/// with the most common failure code. For an unambiguous signal, read the
177/// `HERMIT_TASK_PANIC` marker on stderr or the verification JSON; this makes the
178/// cheap check — `$?` — stop actively lying.
179///
180/// ⚠️ IT LIVES IN THE LIBRARY, NOT IN `main.rs`, AND THAT PLACEMENT IS THE FIX
181/// FOR HOW IT WENT STALE. It was a private `const` inside the binary, so
182/// `tests/cli.rs` — the only thing that asserts on it — could not name it and
183/// wrote the number out by hand instead. When hermit#2558 moved the value from
184/// `1` to `125`, the definition moved and the sixteen copies did not: eight cli
185/// tests went red on main and stayed red, each failing an assertion about the
186/// exit code rather than about the behaviour it exists to check. Same shape as
187/// [`RECORD_REPLAY_HASHES_IO_BUFFERS`] above — a decision carried in two places
188/// that had to agree and had no way to. Exported here, the tests move with it.
189///
190/// ⚠️ DO NOT FLIP EVERY `Some(1)` YOU FIND. `tests/stress_suite.rs`
191/// asserts `Some(1)` as THE GUEST'S OWN SIGNAL, not as a stale copy of this
192/// constant: there `Some(1) => GuestOutcome::Exposed`, and
193/// `status.code() == Some(1)` IS the `exposes_bug` predicate. It is
194/// pattern-identical to the tests that DID go stale, and it is the OPPOSITE
195/// case -- the assertion is CORRECT and must not move with this constant.
196///
197/// ⚠️ ITS FAILURE MODE IS SILENT SUCCESS. Changing it to 125 would not
198/// turn the stress suite red; it would make `exposes_bug` NEVER FIRE, so the
199/// suite would report no bug exposed forever and pass while blind. A red is
200/// recoverable; a green that cannot fail is not.
201///
202/// REVIEWER RULE: a change to `tests/stress_suite.rs` inside an exit-code head
203/// SHOULD BE REFUSED ON SIGHT AND QUESTIONED.
204pub const HERMIT_INTERNAL_FAILURE_EXIT: i32 = 125;
205
206/// A deadline hermit itself was asked to enforce expired: **124**.
207///
208/// The status the container init exits with when a hermit-owned bound ends the
209/// run -- `run --timeout`'s SIGALRM fallback here, and `record --record-timeout`'s
210/// handler in `record_start.rs`, which has always used this value. 124 is the
211/// established code for "a deadline fired" across GNU `timeout`, `safehermit`'s
212/// wall bound and both hermit spellings, so this names an existing convention
213/// rather than claiming a new number.
214///
215/// ⚠️ IT MUST BE READ AT THE CONTAINER BOUNDARY OR IT IS SILENTLY REWRITTEN TO
216/// 125. Found only by forcing the fallback to fire, 2026-08-26: the init exited
217/// 124, `classify_container_result` had no arm for it, and it fell through to
218/// `ContainerChildExit` -- so the run reported `exit 125` and
219/// `class=container-child-exit`, "the child died with a status it did not
220/// pick", for a deadline hermit chose to enforce. Identical in shape to the
221/// refusal that arrives as 122 and needs its own arm for the same reason.
222pub const HERMIT_DEADLINE_EXIT: i32 = 124;
223
224// `--virtual-timeout` ends a run with the same deadline status, from detcore
225// (`TerminalOutcome::exit_status`); its class line, `class=virtual-timeout`,
226// tells it from `--timeout`'s `class=run-timeout`.
227const _: () = assert!(
228    detcore_model::TerminalOutcome::VirtualTimeout { bound_ns: 1 }.exit_status()
229        == HERMIT_DEADLINE_EXIT,
230    "a virtual timeout exits the deadline status"
231);
232
233/// Hermit's own log output exceeded `--max-log-bytes`: **123**.
234///
235/// The status hermit exits with when the byte budget on its tracing output
236/// (stderr, or `--log-file`) is exhausted and it deliberately tears the run
237/// down. 123 was already reserved below for "a LOG BYTE CAP fired" on behalf of
238/// dev-hermit's `bin/safehermit`; this is the same event enforced one layer
239/// lower, exactly as 124 is shared by hermit's `--timeout` and safehermit's wall
240/// deadline. It is not 125 because the cap working is not hermit breaking, and
241/// not 122 because no fail-closed determinism policy was involved: the remedy
242/// is "lower the log level or raise the cap", not "change the program".
243///
244/// Like 124, it must be read at the container boundary
245/// (`classify_container_result`) or it is rewritten to 125.
246pub const HERMIT_LOG_CAP_EXIT: i32 = 123;
247
248/// The guest program could not be found at all: **127**.
249///
250/// Exported for the same reason as [`HERMIT_INTERNAL_FAILURE_EXIT`] and with the
251/// same history one step behind it. `GuestProgramFault::exit_code` wrote `127`
252/// and `126` as bare literals, and `tests/cli.rs` had no way to name either, so
253/// a test meaning "not found" and a test meaning "hermit refused" were asserted
254/// with the same helper and the same number until the values diverged.
255///
256/// ⚠️ THIS IS NOT A SUBDIVISION OF HERMIT'S OWN FAILURE, AND THAT IS THE WHOLE
257/// DISTINCTION. 125 says *hermit* broke; 127 and 126 say *the guest program* was
258/// unusable and hermit correctly declined to start it. A caller that cannot tell
259/// those apart cannot tell "fix my tooling" from "fix my command line".
260pub const GUEST_PROGRAM_NOT_FOUND_EXIT: i32 = 127;
261
262/// The guest program exists but cannot be executed as given: **126**.
263///
264/// See [`GUEST_PROGRAM_NOT_FOUND_EXIT`]. 127/126 are the GNU `env`/`chroot`/
265/// `timeout` convention that 125 itself came from, so the three sit in one
266/// scheme rather than being three unrelated numbers.
267pub const GUEST_PROGRAM_NOT_EXECUTABLE_EXIT: i32 = 126;
268
269// ⚠️ THE VALUES THEMSELVES ARE PINNED, NOT ONLY THEIR NAMES.
270//
271// Naming a constant makes every consumer agree with the definition; it does not
272// make the definition right. Once `tests/cli.rs` asserts `Some(THE_CONSTANT)`
273// everywhere, the whole suite is self-consistent and completely unanchored -- a
274// one-character edit here would move all sixteen assertions with it and nothing
275// would fail. That is the exact shape of the defect this scheme exists to close,
276// displaced one level up, and it is why these are compile-time assertions rather
277// than a comment: a deliberate edit "would be noticed" is the argument that
278// already failed once, when hermit#2558 moved 1 -> 125 and sixteen copies went
279// stale.
280//
281// WHY EACH VALUE AND NOT ANOTHER. GNU `env`, `chroot` and `timeout` reserve 125
282// for "the wrapper itself failed", 127 for "command not found" and 126 for
283// "found but not executable". Borrowing the whole scheme rather than one number
284// is what keeps the three answers distinguishable from each other.
285//
286// ⚠️ THE EXIT-CODE ALLOCATION FOR THE `hermit` BINARY. ONE TABLE, HERE.
287//
288// Five separate exit-code defects were found in one evening and every one of
289// them came from a value being chosen, copied or rewritten somewhere that could
290// not see the others. This is the single place the space is allocated; anything
291// that emits or rewrites a hermit exit status is a consumer OF this table and
292// must not invent its own reading of it.
293//
294//   0          success.
295//   1          HERMIT_VERIFICATION_DIVERGENCE_EXIT, matching `diff`; also a
296//              legal guest status. Read the verification JSON to distinguish.
297//   2 ..= 120  THE GUEST'S OWN STATUS, passed through untouched. (The range
298//              stops at 120 because 121 is reserved below; a guest may still
299//              CHOOSE any value, see the caveat at the end.)
300//   121        HERMIT_GUEST_DEADLOCK_EXIT -- DEADLOCK UNDER HERMIT'S MODEL:
301//              Detcore's scheduler found every remaining thread blocked with
302//              no wake it models and printed its "Deadlock detected" report.
303//              NOT a guest result: it is a native hang only when the model is
304//              complete for the program, and modeling gaps have produced it
305//              before. Prints `HERMIT_MODEL_DEADLOCK
306//              class=deadlock-under-hermit-model`. The scheduler's own
307//              `HERMIT_DEADLOCK:` verdicts are NOT this: they stay 125.
308//              Defined in `detcore-model` for the reason 122 is.
309//   122        HERMIT_POLICY_REFUSAL_EXIT -- hermit REFUSED the run under a
310//              fail-closed policy. Hermit WORKED; the run was stopped on
311//              purpose and the reason was printed. Distinct from 125 because
312//              "hermit refused" and "hermit broke" demand opposite responses:
313//              read the refusal and change your program or flags, versus file a
314//              bug. Defined in `detcore-model` because `detcore` emits it and
315//              this crate recognises it, and that is the only crate both
316//              depend on.
317//   123        HERMIT_LOG_CAP_EXIT -- a LOG BYTE CAP fired. Hermit's own
318//              `--max-log-bytes` budget on its tracing output, and the value
319//              reserved for dev-hermit's `bin/safehermit` log-cap kill (which,
320//              as of 2026-09-26, still exits 125 -- see the 125 note below).
321//              Hermit's spelling prints `HERMIT_LOG_CAP class=log-cap`.
322//   124        DO NOT USE. GNU `timeout`'s deadline, and dev-hermit's
323//              `bin/safehermit` WALL DEADLINE kill. `tests/cli.rs` asserts
324//              `assert_ne!(code, Some(124))` on the awk-mincore probe.
325//   125        HERMIT_INTERNAL_FAILURE_EXIT -- hermit itself failed, no guest
326//              ran. NOT YET THE SOLE MEANING through dev-hermit's
327//              `bin/safehermit`: that wrapper has exited 125 for its
328//              log-byte-cap kill since it was created on 2026-08-20 and still
329//              did on 2026-10-05, so a run through it produces one number for
330//              two faults with opposite remedies (see "125 ⚠️ NOT FREE" below).
331//              An earlier version of this table said safehermit had already
332//              moved off 125; it never did. The intended fix is that SAFEHERMIT
333//              MOVES -- to the 123 reserved for it above -- NOT HERMIT, because
334//              that wrapper is the only layer that knows which happened (it
335//              relays hermit's status otherwise) and because 125/126/127 is one
336//              GNU scheme that cannot be broken up. Tracked as
337//              `hermit_s_125_collides`.
338//   126        GUEST_PROGRAM_NOT_EXECUTABLE_EXIT.
339//   127        GUEST_PROGRAM_NOT_FOUND_EXIT -- the ABSOLUTE-PATH form. The
340//              bare-name-on-guest-PATH form currently exits 125 instead, which
341//              is an inconsistency tracked as `hermit_reports_a_missing`, not a
342//              second meaning for 127.
343//   128 + N    killed by signal N (shell convention). Hermit does not emit these
344//              deliberately; a wrapper reporting a signal death must use them
345//              rather than borrow a code that already means something.
346//
347// ⚠️ NO CODE IS EXCLUSIVELY HERMIT'S. Every value in 0..=255 is a legal guest
348// status, so a guest may return 125 or 127 of its own accord and this table
349// cannot stop it. The table removes GUARANTEED collisions, not possible ones.
350// The only unambiguous discriminator is the `HERMIT_INTERNAL_FAILURE` marker on
351// stderr, present exactly when hermit itself failed. Any consumer deciding on
352// `$?` alone is guessing, and should say so.
353//
354// ⚠️ THE SAME ARGUMENT APPLIES TO 124, AND MORE SHARPLY, BECAUSE FIVE
355// MECHANISMS PRODUCE IT. `hermit run --timeout`, its unwind fallback, `hermit
356// record --record-timeout`, the `timeout(1)` wrapped around a manifest cell, and
357// `safehermit`'s wall deadline all exit 124, and GNU `timeout` uses it for the
358// same event. They demand different responses -- a slow guest, a wedged
359// teardown inside hermit, a cell bound set too low, a run that escaped every
360// inner bound -- so `$?` cannot route the reader to any of them.
361//
362// The discriminator is again the marker on stderr, and it is a CONTRACT rather
363// than a diagnostic nicety: `class=run-timeout` for hermit's own bound (which
364// also states the bound in seconds), `HERMIT_RUN_TIMEOUT_FALLBACK` for the
365// unwind failing to complete, `safehermit: bound.wall=` for the cgroup reap.
366// EXIT 124 WITH NO MARKER AT ALL MEANS NO INNER BOUND FIRED -- something outside
367// hermit killed the run -- which is a configuration error and not a slow guest.
368// The full picture, including which rung bounds which quantity and the
369// strict-inequality invariant between them, is docs/TIMEOUT_LADDER.md.
370//
371// ⚠️ DO NOT CLAMP INTO THIS RANGE. `scripts/hermit-code-coverage.rs` used
372// `clamp(1, 125)` and so rewrote 127 and 126 into 125 -- turning "your program is
373// missing" into "hermit broke" and pointing the reader at the wrong project.
374// Truncating a status does not lose information here, it MANUFACTURES a false
375// attribution, because the ceiling is itself a meaning.
376//
377// ⚠️ WHAT EACH VALUE MUST NOT COLLIDE WITH -- AND 125 ALREADY DOES.
378//
379//   0    success. Never a failure code; see the nonzero pin below.
380//   1    the commonest guest exit status. Sharing it is the collision
381//        hermit#2558 introduced 125 to escape, so 125 must never drift back.
382//   124  GNU `timeout`'s "deadline fired", and dev-hermit's `bin/safehermit`
383//        uses it for its WALL DEADLINE kill. `tests/cli.rs` asserts
384//        `assert_ne!(code, Some(124))` on the awk-mincore probe for that reason.
385//   126  hermit's own GUEST_PROGRAM_NOT_EXECUTABLE_EXIT.
386//   127  hermit's own GUEST_PROGRAM_NOT_FOUND_EXIT -- already in use for the
387//        ABSOLUTE-PATH form of a missing guest program. Note the bare-name form
388//        resolved against the guest PATH exits 125 instead, which is an
389//        inconsistency filed separately, not a licence to reuse 127.
390//
391//   125  ⚠️ NOT FREE. dev-hermit's `bin/safehermit` exits 125 to mean "the LOG
392//        BYTE CAP fired: safehermit killed the run through its cgroup"
393//        (`demos/lib/demo_common.py`'s SAFEHERMIT_EXIT_REASON). Any hermit run
394//        launched through that wrapper -- which is how the demos and the repeat
395//        harness run it -- can therefore produce 125 for two unrelated reasons:
396//        hermit refused, or the wrapper killed it for log volume. They demand
397//        opposite responses (fix the invocation vs raise the cap), and `$?`
398//        cannot tell them apart. This is a LIVE ambiguity, not a hypothetical:
399//        demo 5 has been observed reporting `exited with status 125` when the
400//        cap fired. It is recorded here rather than resolved because changing
401//        either value is a cross-repository decision.
402//
403//        The discriminator meanwhile is the same one that separates hermit's 125
404//        from a guest's own 125: the `HERMIT_INTERNAL_FAILURE` marker on stderr,
405//        present only when hermit itself failed. safehermit writes its verdict
406//        to its `--sh-report` file instead. Read one of those, never the number
407//        alone.
408//
409// ⚠️ 0 IS THE DANGEROUS DRIFT, NOT 126 OR 127. Every one of these is asserted as
410// `assert_eq!(code, Some(CONST))`. At 0 those assertions would stop demanding a
411// FAILURE and start demanding a SUCCESS, and would still pass -- sixteen tests
412// silently inverted. `tests/cli.rs` also asserts `!status.success()` beside the
413// code for that reason; this pin is the second of the two locks.
414const _: () = assert!(
415    HERMIT_VERIFICATION_DIVERGENCE_EXIT == 1,
416    "1 is the established diff-style result for a completed comparison that diverged"
417);
418const _: () = assert!(
419    HERMIT_INTERNAL_FAILURE_EXIT == 125,
420    "125 is the GNU wrapper-failed convention; 1 is the commonest guest status and 0 is success"
421);
422const _: () = assert!(
423    GUEST_PROGRAM_NOT_FOUND_EXIT == 127,
424    "127 is the GNU command-not-found convention and must stay distinct from 125 and 126"
425);
426const _: () = assert!(
427    GUEST_PROGRAM_NOT_EXECUTABLE_EXIT == 126,
428    "126 is the GNU found-but-not-executable convention and must stay distinct from 125 and 127"
429);
430const _: () = assert!(
431    HERMIT_LOG_CAP_EXIT == 123,
432    "123 is Hermit's log-cap code, reserved for bin/safehermit's log-cap kill as well; see the allocation table"
433);
434const _: () = assert!(
435    HERMIT_LOG_CAP_EXIT != 0
436        && HERMIT_VERIFICATION_DIVERGENCE_EXIT != 0
437        && HERMIT_INTERNAL_FAILURE_EXIT != 0
438        && GUEST_PROGRAM_NOT_FOUND_EXIT != 0
439        && GUEST_PROGRAM_NOT_EXECUTABLE_EXIT != 0,
440    "a failure code of 0 would invert every assert_eq!(code, Some(CONST)) into demanding success"
441);
442mod record;
443use hermit_record_format::record_replay_path;
444mod replay;
445
446use hermit_record_format::script;
447mod tracer_thread;
448
449use std::fs;
450use std::io;
451use std::io::Seek;
452use std::io::SeekFrom;
453use std::io::Write;
454use std::os::fd::AsRawFd;
455use std::os::fd::FromRawFd;
456#[cfg(all(test, feature = "liteinst"))]
457use std::os::unix::ffi::OsStrExt;
458#[cfg(feature = "liteinst")]
459use std::os::unix::fs::PermissionsExt;
460use std::path::Path;
461use std::path::PathBuf;
462use std::sync::Mutex;
463use std::time::Duration;
464#[cfg(target_arch = "x86_64")]
465use std::time::Instant;
466
467use anyhow::anyhow;
468use clap::ValueEnum;
469pub use consts::LAST;
470pub use consts::LAST_RUN;
471use consts::MANIFEST_NAME as RUN_MANIFEST_NAME;
472pub use consts::MANIFEST_NAME;
473use consts::METADATA_NAME;
474pub use consts::METADATA_NAME as RECORDING_METADATA_NAME;
475pub use detcore::Config as DetConfig;
476pub use detcore::Detcore;
477pub use detcore::RecordOrReplay;
478#[doc(hidden)]
479#[doc(hidden)]
480#[doc(hidden)]
481#[doc(hidden)]
482#[doc(hidden)]
483#[doc(hidden)]
484#[doc(hidden)]
485#[doc(hidden)]
486pub use error::Context;
487pub use error::Error;
488pub use error::FailureKind;
489pub use error::SerializableError;
490use goblin::elf::Elf;
491use goblin::elf::header;
492use goblin::elf::section_header;
493pub use id::Id;
494use metadata::Metadata;
495pub use metadata::RecordOptions;
496pub use metadata::RecordedSchedule;
497pub use ptrace_completion::HermitCleanupStage;
498pub use ptrace_completion::HermitCleanupUnconfirmed;
499pub use ptrace_completion::HermitPtraceFailure;
500pub use ptrace_completion::RecoveryRefusal;
501use record::Record;
502use replay::Replay;
503pub use reverie::ExitStatus;
504pub use reverie::process;
505pub use reverie::process::Command;
506pub use reverie::process::Mount;
507pub use reverie::process::Namespace;
508pub use reverie::process::Output;
509pub use reverie::process::Stdio;
510pub use script::Shebang;
511use serde::Deserialize;
512use serde::Serialize;
513pub use tracer_thread::launch_on_calling_thread;
514
515enum KvmStdinReservation {
516    Open(
517        #[cfg_attr(
518            not(target_arch = "x86_64"),
519            allow(
520                dead_code,
521                reason = "only the x86_64 KVM backend reads the reserved stdin"
522            )
523        )]
524        fs::File,
525    ),
526    Closed,
527}
528
529static KVM_STDIN_RESERVATION: Mutex<Option<KvmStdinReservation>> = Mutex::new(None);
530
531/// Saves stdin captured before Rust's process startup can reuse a closed fd 0.
532pub fn reserve_kvm_stdin(stdin: Option<fs::File>) -> io::Result<()> {
533    let mut reservation = KVM_STDIN_RESERVATION
534        .lock()
535        .map_err(|_| io::Error::other("KVM stdin reservation lock is poisoned"))?;
536    if reservation.is_some() {
537        return Err(io::Error::new(
538            io::ErrorKind::AlreadyExists,
539            "KVM stdin is already reserved",
540        ));
541    }
542    *reservation = Some(match stdin {
543        Some(file) => KvmStdinReservation::Open(file),
544        None => KvmStdinReservation::Closed,
545    });
546    Ok(())
547}
548
549fn duplicate_current_stdin() -> io::Result<Option<fs::File>> {
550    // SAFETY: F_DUPFD_CLOEXEC duplicates fd 0 without taking ownership of it.
551    let duplicate = unsafe { libc::fcntl(libc::STDIN_FILENO, libc::F_DUPFD_CLOEXEC, 3) };
552    if duplicate >= 0 {
553        // SAFETY: F_DUPFD_CLOEXEC returned a new owned descriptor.
554        return Ok(Some(unsafe { fs::File::from_raw_fd(duplicate) }));
555    }
556    let error = io::Error::last_os_error();
557    if error.raw_os_error() == Some(libc::EBADF) {
558        Ok(None)
559    } else {
560        Err(error)
561    }
562}
563
564/// For each of the three descriptors `fds` a launch hands a guest as its
565/// standard input, output and error (`None` for a closed one), the lowest of
566/// them that is one open file description with it, as `kcmp(KCMP_FILE)`
567/// answers for this process's own descriptors; `None` where kcmp gives no
568/// answer for a pair (a kernel without it, a seccomp profile that refuses it,
569/// or a descriptor that is not open). Detcore's lock ledger takes the streams
570/// of one group as the one description they are
571/// (`DetConfig::standard_stream_aliases`).
572#[cfg_attr(
573    not(target_arch = "x86_64"),
574    allow(
575        dead_code,
576        reason = "only the x86_64 KVM launch groups the standard streams"
577    )
578)]
579fn standard_stream_aliases(fds: [Option<std::os::fd::RawFd>; 3]) -> Option<[u8; 3]> {
580    const KCMP_FILE: libc::c_long = 0;
581    // SAFETY: getpid reads only this process's identity.
582    let pid = libc::c_long::from(unsafe { libc::getpid() });
583    let mut aliases = [0u8, 1, 2];
584    for later in 1..fds.len() {
585        let Some(b) = fds[later] else {
586            continue;
587        };
588        for earlier in 0..later {
589            let Some(a) = fds[earlier] else {
590                continue;
591            };
592            // SAFETY: kcmp only compares two descriptors of this process.
593            let answer = unsafe {
594                libc::syscall(
595                    libc::SYS_kcmp,
596                    pid,
597                    pid,
598                    KCMP_FILE,
599                    libc::c_long::from(a),
600                    libc::c_long::from(b),
601                )
602            };
603            match answer {
604                0 => {
605                    aliases[later] = aliases[earlier];
606                    break;
607                }
608                1..=3 => {}
609                _ => return None,
610            }
611        }
612    }
613    Some(aliases)
614}
615
616fn ensure_kvm_stdin_reserved() -> io::Result<()> {
617    let mut reservation = KVM_STDIN_RESERVATION
618        .lock()
619        .map_err(|_| io::Error::other("KVM stdin reservation lock is poisoned"))?;
620    if reservation.is_none() {
621        *reservation = Some(match duplicate_current_stdin()? {
622            Some(file) => KvmStdinReservation::Open(file),
623            None => KvmStdinReservation::Closed,
624        });
625    }
626    Ok(())
627}
628
629#[cfg(target_arch = "x86_64")]
630fn reserved_kvm_stdin() -> Result<Option<fs::File>, Error> {
631    ensure_kvm_stdin_reserved()?;
632    let reservation = KVM_STDIN_RESERVATION
633        .lock()
634        .map_err(|_| io::Error::other("KVM stdin reservation lock is poisoned"))?;
635    match reservation.as_ref() {
636        Some(KvmStdinReservation::Open(file)) => Ok(Some(file.try_clone()?)),
637        Some(KvmStdinReservation::Closed) => Ok(None),
638        None => unreachable!("stdin reservation was initialized above"),
639    }
640}
641
642/// A replayable snapshot of the guest's stdin for the output-capturing backends.
643///
644/// `hermit run --verify` executes the guest twice and compares the two runs. The
645/// output-capturing backends historically fed the guest `Stdio::null()`, so any
646/// data piped into hermit (`echo prog | hermit run --strict --verify -- ...`)
647/// was silently dropped. Both runs then saw identical *empty* input and hermit
648/// reported a false "deterministic" success even though the guest never received
649/// its input. `Seekable` holds a read-only, rewindable file so both runs receive
650/// the exact same bytes without being able to modify the caller's input; `None`
651/// means stdin should be `/dev/null` (nothing to replay, or a terminal that
652/// cannot be replayed identically to two runs).
653enum StdinSnapshot {
654    Seekable(fs::File),
655    None,
656}
657
658static OUTPUT_STDIN_SNAPSHOT: Mutex<Option<StdinSnapshot>> = Mutex::new(None);
659
660/// Records a rewindable snapshot of the process stdin so the output-capturing
661/// backends can replay identical input to each run of `hermit run --verify`.
662///
663/// `stdin` is the descriptor captured before Rust startup could reuse a closed
664/// fd 0 (see the binary's `startup_stdin`). A regular-file redirect is reopened
665/// read-only; a pipe/fifo/socket is drained once into a seekable temporary file
666/// and that file is reopened read-only. A terminal (or absent stdin) is treated
667/// as `/dev/null` because a live terminal cannot be replayed identically to two
668/// runs.
669pub fn reserve_output_stdin_snapshot(stdin: Option<fs::File>) -> io::Result<()> {
670    let snapshot = match stdin {
671        None => StdinSnapshot::None,
672        Some(mut file) => {
673            // SAFETY: as_raw_fd borrows the descriptor without taking ownership.
674            let is_tty = unsafe { libc::isatty(file.as_raw_fd()) } == 1;
675            if is_tty {
676                StdinSnapshot::None
677            } else {
678                let replay = if file.stream_position().is_ok() {
679                    // Already seekable (e.g. `< file`): retain it below through
680                    // a read-only descriptor with an independent file offset.
681                    file
682                } else {
683                    // Non-seekable (pipe/fifo/socket): buffer once into a
684                    // seekable temporary file so both --verify runs receive
685                    // identical input.
686                    //
687                    // NAME THE WAIT BEFORE ENTERING IT. This `io::copy` reads to
688                    // EOF, so a stdin that never reaches EOF -- an inherited socket,
689                    // a fifo whose writer stays open -- blocks here FOREVER, before
690                    // the guest has started. Measured: the process wedges with ZERO
691                    // bytes on stdout and stderr and is only cleared by killing it,
692                    // which is indistinguishable from a slow run. That
693                    // indistinguishability is the whole harm; an unbounded wait that
694                    // says what it is waiting for is merely slow, and a reader can
695                    // act on it.
696                    //
697                    // Deliberately UNCONDITIONAL rather than emitted after a delay:
698                    // a message that appears only once N seconds have passed makes
699                    // hermit's own stderr depend on timing, and this project does
700                    // not accept timing-dependent output. Deliberately NOT a
701                    // deadline either -- a 12s slow producer is legitimate and
702                    // delivers, and nothing can separate "slow" from "never" except
703                    // by waiting. This changes no control flow: every input that
704                    // worked before still works, byte for byte, on stdout.
705                    const BUFFERING_NOTE: &str = "hermit: --verify is buffering stdin from a non-seekable stream so both \
706                         runs receive identical input. If this appears to hang, stdin has not \
707                         reached end-of-file; pass `< /dev/null` when the guest needs no input.";
708                    if nonwaiting_write::diagnostics_must_not_wait() {
709                        // Under --max-log-bytes, termination must not depend
710                        // on this write; see `nonwaiting_write`.
711                        nonwaiting_write::write_without_waiting(
712                            libc::STDERR_FILENO,
713                            format!("{BUFFERING_NOTE}\n").as_bytes(),
714                        );
715                    } else {
716                        eprintln!("{BUFFERING_NOTE}");
717                    }
718                    let mut buffered = tempfile::tempfile()?;
719                    io::copy(&mut file, &mut buffered)?;
720                    buffered.seek(SeekFrom::Start(0))?;
721                    buffered
722                };
723
724                // A duplicate would keep the original access mode. Reopen the
725                // reserved descriptor read-only so neither verification run
726                // can modify the caller's stdin or the next run's input.
727                let replay_path = format!("/proc/self/fd/{}", replay.as_raw_fd());
728                StdinSnapshot::Seekable(fs::File::open(replay_path)?)
729            }
730        }
731    };
732    let mut reservation = OUTPUT_STDIN_SNAPSHOT
733        .lock()
734        .map_err(|_| io::Error::other("output stdin snapshot lock is poisoned"))?;
735    if reservation.is_some() {
736        return Err(io::Error::new(
737            io::ErrorKind::AlreadyExists,
738            "output stdin snapshot is already reserved",
739        ));
740    }
741    *reservation = Some(snapshot);
742    Ok(())
743}
744
745/// Returns a rewound descriptor for the reserved stdin snapshot, if any.
746///
747/// Each call rewinds the snapshot and hands back a fresh descriptor, so
748/// repeated `--verify` runs read identical input from the start. Returns `None`
749/// when no snapshot was reserved or the reserved stdin is a terminal/absent
750/// (nothing to replay). Runs are sequential (each executes in its own forked
751/// container child), so resetting the shared open file description's offset here
752/// is safe.
753fn output_backend_stdin_reservation() -> Result<(bool, Option<fs::File>), Error> {
754    let mut reservation = OUTPUT_STDIN_SNAPSHOT
755        .lock()
756        .map_err(|_| io::Error::other("output stdin snapshot lock is poisoned"))?;
757    match reservation.as_mut() {
758        Some(StdinSnapshot::Seekable(file)) => {
759            file.seek(SeekFrom::Start(0))?;
760            Ok((true, Some(file.try_clone()?)))
761        }
762        Some(StdinSnapshot::None) => Ok((true, None)),
763        None => Ok((false, None)),
764    }
765}
766
767fn output_backend_stdin_file() -> Result<Option<fs::File>, Error> {
768    Ok(output_backend_stdin_reservation()?.1)
769}
770
771/// Returns the stdin to hand a guest run in an output-capturing backend.
772///
773/// When [`reserve_output_stdin_snapshot`] has stored a replayable snapshot the
774/// file is rewound and a fresh descriptor handed to the guest, so each
775/// `--verify` run reads identical input. Otherwise (no snapshot reserved, or a
776/// terminal/absent stdin) the guest gets `/dev/null`, matching the previous
777/// behavior for callers that do not reserve a snapshot.
778pub fn output_backend_stdin() -> Result<Stdio, Error> {
779    match output_backend_stdin_file()? {
780        Some(file) => Ok(Stdio::from(file)),
781        None => Ok(Stdio::null()),
782    }
783}
784
785/// The result of recording a command.
786#[derive(Debug, Serialize, Deserialize)]
787pub struct Recording {
788    /// The unique ID of the recording.
789    pub id: Id,
790
791    /// The exit code of the command.
792    pub exit_status: ExitStatus,
793}
794
795#[derive(Clone, Copy, Debug, PartialEq, Eq)]
796enum CapabilityProbe {
797    Namespaces,
798    Ptrace,
799    Seccomp,
800}
801
802fn run_capability_probe(probe: CapabilityProbe) -> Result<bool, Error> {
803    // SAFETY: The child calls only async-signal-safe syscalls and exits immediately.
804    let pid = unsafe { libc::fork() };
805    if pid == -1 {
806        return Err(std::io::Error::last_os_error()).context("Failed to fork capability probe");
807    }
808    if pid == 0 {
809        let supported = match probe {
810            CapabilityProbe::Namespaces => unsafe {
811                libc::unshare(libc::CLONE_NEWUSER | libc::CLONE_NEWPID | libc::CLONE_NEWIPC) == 0
812            },
813            CapabilityProbe::Ptrace => {
814                // SAFETY: PTRACE_TRACEME ignores the pid and address arguments.
815                unsafe {
816                    libc::ptrace(
817                        libc::PTRACE_TRACEME,
818                        0,
819                        std::ptr::null_mut::<libc::c_void>(),
820                        std::ptr::null_mut::<libc::c_void>(),
821                    ) != -1
822                }
823            }
824            CapabilityProbe::Seccomp => {
825                let mut filter = libc::sock_filter {
826                    code: 0x06, // BPF_RET | BPF_K
827                    jt: 0,
828                    jf: 0,
829                    k: 0x7fff0000, // SECCOMP_RET_ALLOW
830                };
831                let program = libc::sock_fprog {
832                    len: 1,
833                    filter: &mut filter,
834                };
835                // SAFETY: The filter is an allow-all program with a valid one-element lifetime.
836                unsafe {
837                    libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == 0
838                        && libc::syscall(
839                            libc::SYS_seccomp,
840                            1, // SECCOMP_SET_MODE_FILTER
841                            0,
842                            &program,
843                        ) == 0
844                }
845            }
846        };
847        // SAFETY: Avoid running Rust destructors after fork.
848        unsafe { libc::_exit(i32::from(!supported)) }
849    }
850
851    let mut status = 0;
852    loop {
853        // SAFETY: pid is the child created above and status points to valid storage.
854        let result = unsafe { libc::waitpid(pid, &mut status, 0) };
855        if result == pid {
856            return Ok(libc::WIFEXITED(status) && libc::WEXITSTATUS(status) == 0);
857        }
858        if result == -1 {
859            let error = std::io::Error::last_os_error();
860            if error.kind() == std::io::ErrorKind::Interrupted {
861                continue;
862            }
863            return Err(error).context("Failed to wait for capability probe");
864        }
865    }
866}
867
868/// Whether a run creates Hermit's user, PID and IPC namespaces. A `--no-namespace`
869/// run starts the guest as a plain child and never calls `unshare`, so the
870/// namespace probe says nothing about whether it can run
871/// (<https://github.com/rrnewton/hermit/issues/4515>).
872#[derive(Clone, Copy, Debug, PartialEq, Eq)]
873pub enum NamespaceUse {
874    /// The run unshares its user, PID and IPC namespaces (the default).
875    Creates,
876    /// The run creates no namespace (`--no-namespace`).
877    None,
878}
879
880/// Check the host mechanisms needed by a retained exec-stop launcher or a
881/// ptrace run, without assuming which runtime artifact the launcher uses.
882pub fn validate_tracing_environment(namespaces: NamespaceUse) -> Result<(), Error> {
883    check_tracing_environment(namespaces, run_capability_probe)
884}
885
886/// Refuses a ptrace run that `probe` says this environment cannot host: the
887/// namespace probe for a run that creates namespaces, then the ptrace and
888/// seccomp probes for every run, which a `--no-namespace` run needs too.
889fn check_tracing_environment(
890    namespaces: NamespaceUse,
891    mut probe: impl FnMut(CapabilityProbe) -> Result<bool, Error>,
892) -> Result<(), Error> {
893    if namespaces == NamespaceUse::Creates && !probe(CapabilityProbe::Namespaces)? {
894        anyhow::bail!(
895            "Hermit cannot create its required user, PID and IPC namespaces: \
896             unshare(CLONE_NEWUSER | CLONE_NEWPID | CLONE_NEWIPC) was denied. Allow unprivileged \
897             user namespaces, IPC namespaces (user.max_ipc_namespaces) and the unshare syscall in \
898             the host/container policy."
899        );
900    }
901    if !probe(CapabilityProbe::Ptrace)? {
902        anyhow::bail!(
903            "Hermit cannot use ptrace in this environment: a child PTRACE_TRACEME probe was \
904             denied. Allow same-UID parent-child ptrace in the container seccomp and host \
905             Yama/LSM policy; CAP_SYS_PTRACE is normally not required. Use --namespace-only for \
906             a sandbox smoke test without ptrace (it intercepts only kill, through a seccomp \
907             filter)."
908        );
909    }
910    if !probe(CapabilityProbe::Seccomp)? {
911        anyhow::bail!(
912            "Hermit cannot install its tracee seccomp filter: \
913             seccomp(SECCOMP_SET_MODE_FILTER) was denied. Allow seccomp and \
914             prctl(PR_SET_NO_NEW_PRIVS) in the container policy. --namespace-only also \
915             installs a seccomp filter (on kill), so it cannot run here either."
916        );
917    }
918    Ok(())
919}
920
921// AUTONOMOUS-BOT-IMPLEMENTED
922// TODO-HUMAN-REVIEW(#688): Review LiteInst runtime discovery.
923/// Checks that `path` is an x86-64 shared object that exports every name in
924/// `required` and registers `initializer_name` in its constructor array, so
925/// that preloading it runs that initializer. `label` names the library in every
926/// error message. Returns the canonical path.
927/// Detcore requires its protected root and sole local RELATIVE registration;
928/// this is minimal discovery, separate from complete artifact qualification.
929fn validate_preload_constructor_library(
930    path: &Path,
931    label: &str,
932    required: &[&str],
933    initializer_name: &str,
934) -> io::Result<PathBuf> {
935    let bytes = fs::read(path).map_err(|error| {
936        io::Error::new(
937            error.kind(),
938            format!("failed to read {label} {}: {error}", path.display()),
939        )
940    })?;
941    let elf = Elf::parse(&bytes).map_err(|error| {
942        io::Error::new(
943            io::ErrorKind::InvalidData,
944            format!("{label} {} is not an ELF DSO: {error}", path.display()),
945        )
946    })?;
947    if elf.header.e_type != header::ET_DYN || elf.header.e_machine != header::EM_X86_64 {
948        return Err(io::Error::new(
949            io::ErrorKind::InvalidData,
950            format!("{label} {} is not an x86-64 shared object", path.display()),
951        ));
952    }
953
954    for &name in required {
955        if !elf
956            .dynsyms
957            .iter()
958            .any(|symbol| elf.dynstrtab.get_at(symbol.st_name) == Some(name))
959        {
960            return Err(io::Error::new(
961                io::ErrorKind::InvalidData,
962                format!(
963                    "{label} {} is missing required export {name}",
964                    path.display()
965                ),
966            ));
967        }
968    }
969    if initializer_name == liteinst_constructor::ROOT {
970        liteinst_constructor::validate(&bytes, &elf).map_err(|error| {
971            io::Error::new(
972                error.kind(),
973                format!(
974                    "{label} {} has invalid Detcore constructor binding: {error}",
975                    path.display()
976                ),
977            )
978        })?;
979        return path.canonicalize();
980    }
981    let (initializer_index, initializer) = elf
982        .dynsyms
983        .iter()
984        .enumerate()
985        .find(|(_, symbol)| elf.dynstrtab.get_at(symbol.st_name) == Some(initializer_name))
986        .ok_or_else(|| {
987            io::Error::other(format!(
988                "checked {label} initializer {initializer_name} disappeared"
989            ))
990        })?;
991    let init_array = elf
992        .section_headers
993        .iter()
994        .find(|section| section.sh_type == section_header::SHT_INIT_ARRAY)
995        .ok_or_else(|| {
996            io::Error::new(
997                io::ErrorKind::InvalidData,
998                format!("{label} {} has no constructor array", path.display()),
999            )
1000        })?;
1001    let init_start = init_array.sh_addr;
1002    let init_end = init_start.saturating_add(init_array.sh_size);
1003    let relocated_initializer = elf
1004        .dynrelas
1005        .iter()
1006        .chain(elf.dynrels.iter())
1007        .any(|relocation| {
1008            (init_start..init_end).contains(&relocation.r_offset)
1009                && relocation.r_sym == initializer_index
1010        });
1011    let init_bytes = usize::try_from(init_array.sh_offset)
1012        .ok()
1013        .and_then(|start| {
1014            usize::try_from(init_array.sh_size)
1015                .ok()
1016                .and_then(|size| bytes.get(start..start.checked_add(size)?))
1017        })
1018        .unwrap_or_default();
1019    let direct_initializer = init_bytes
1020        .as_chunks::<8>()
1021        .0
1022        .iter()
1023        .any(|entry| u64::from_le_bytes(*entry) == initializer.st_value);
1024    if !relocated_initializer && !direct_initializer {
1025        return Err(io::Error::new(
1026            io::ErrorKind::InvalidData,
1027            format!(
1028                "{label} {} does not register {initializer_name} as a preload constructor",
1029                path.display()
1030            ),
1031        ));
1032    }
1033    path.canonicalize()
1034}
1035
1036/// Environment variable naming the in-guest Detcore preload
1037/// (`libdetcore_liteinst.so`) explicitly.
1038pub const LITEINST_TOOL_RUNTIME_ENV: &str = "HERMIT_LITEINST_TOOL_RUNTIME";
1039
1040// AUTONOMOUS-BOT-IMPLEMENTED
1041// TODO-HUMAN-REVIEW(PR-3635): Review in-guest Detcore runtime discovery.
1042/// Returns the in-guest Detcore preload: the one [`LITEINST_TOOL_RUNTIME_ENV`]
1043/// names; otherwise the packaged `rsrcs/libdetcore_liteinst.so` of the selected
1044/// Hermit installation (`hermit-install` stages it, as it stages the DBT and
1045/// SaBRe Detcore runtimes, and a standalone installation or published bundle
1046/// carries it). A selected installation missing that resource is incomplete
1047/// and is refused. When no installation is selected, the development fallback
1048/// is the one `cargo build -p detcore-liteinst` built beside the Hermit binary
1049/// or in its `deps/` directory.
1050///
1051/// A runtime built from another tree is refused when it starts: Hermit passes
1052/// it [`detcore::config_wire_fingerprint`], which it compares with its own
1053/// before decoding anything (<https://github.com/rrnewton/hermit/issues/3986>).
1054#[doc(hidden)]
1055pub fn liteinst_tool_runtime_library_path() -> io::Result<PathBuf> {
1056    if let Some(path) = std::env::var_os(LITEINST_TOOL_RUNTIME_ENV) {
1057        let path = PathBuf::from(path);
1058        if !path.is_file() {
1059            return Err(io::Error::new(
1060                io::ErrorKind::NotFound,
1061                format!("{LITEINST_TOOL_RUNTIME_ENV} does not name a regular file"),
1062            ));
1063        }
1064        return validate_liteinst_tool_runtime_library(&path).map_err(|error| {
1065            io::Error::new(
1066                error.kind(),
1067                format!("{LITEINST_TOOL_RUNTIME_ENV} is invalid: {error}"),
1068            )
1069        });
1070    }
1071
1072    if let Some(path) = hermit_resources::resource("libdetcore_liteinst.so")? {
1073        if !path.is_file() {
1074            return Err(io::Error::new(
1075                io::ErrorKind::NotFound,
1076                "selected Hermit installation has no finalized in-guest Detcore runtime; \
1077                 run `make install-deps` to build and stage the qualified LiteInst runtime",
1078            ));
1079        }
1080        return validate_liteinst_tool_runtime_library(&path);
1081    }
1082
1083    let executable = std::env::current_exe()?;
1084    let directory = executable.parent().ok_or_else(|| {
1085        io::Error::new(
1086            io::ErrorKind::NotFound,
1087            "Hermit executable has no parent directory",
1088        )
1089    })?;
1090    if let Some(path) = [
1091        directory.join("libdetcore_liteinst.so"),
1092        directory.join("deps/libdetcore_liteinst.so"),
1093    ]
1094    .into_iter()
1095    .find(|path| path.is_file())
1096    {
1097        return validate_liteinst_tool_runtime_library(&path);
1098    }
1099    Err(io::Error::new(
1100        io::ErrorKind::NotFound,
1101        format!(
1102            "libdetcore_liteinst.so was not built beside {} or staged as an installed resource",
1103            executable.display()
1104        ),
1105    ))
1106}
1107
1108fn validate_liteinst_tool_runtime_library(path: &Path) -> io::Result<PathBuf> {
1109    validate_preload_constructor_library(
1110        path,
1111        "in-guest Detcore runtime",
1112        &["detcore_liteinst_initialize"],
1113        "detcore_liteinst_initialize",
1114    )
1115}
1116
1117// AUTONOMOUS-BOT-IMPLEMENTED
1118// TODO-HUMAN-REVIEW(PR-3635): Review the in-guest LiteInst library refusals.
1119/// In-guest LiteInst refused a run before starting its guest, because the
1120/// runtime cannot honour the request or could not monitor the program.
1121///
1122/// A type rather than a message, because it crosses the container error
1123/// boundary as [`error::FailureKind::PolicyRefusal`] (exit 122).
1124#[derive(Debug, Clone, PartialEq, Eq)]
1125pub struct LiteinstInGuestRefusal {
1126    backend: Backend,
1127    reason: String,
1128}
1129
1130impl LiteinstInGuestRefusal {
1131    /// A refusal of a `--backend=liteinst` run.
1132    pub fn new(reason: impl Into<String>) -> Self {
1133        Self::for_backend(Backend::Liteinst, reason)
1134    }
1135
1136    /// A refusal of a run of `backend`, one of the in-guest LiteInst backends
1137    /// ([`Backend::is_in_guest_liteinst`]), so the message names the backend
1138    /// the caller selected.
1139    pub fn for_backend(backend: Backend, reason: impl Into<String>) -> Self {
1140        debug_assert!(backend.is_in_guest_liteinst(), "{backend:?}");
1141        Self {
1142            backend,
1143            reason: reason.into(),
1144        }
1145    }
1146}
1147
1148impl std::fmt::Display for LiteinstInGuestRefusal {
1149    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1150        let runtime = match self.backend {
1151            Backend::InGuestTrap => "in-guest LiteInst, site patching off",
1152            _ => "in-guest LiteInst",
1153        };
1154        write!(
1155            f,
1156            "--backend={} ({runtime}) refuses this run: {}",
1157            self.backend.as_str(),
1158            self.reason
1159        )
1160    }
1161}
1162
1163impl std::error::Error for LiteinstInGuestRefusal {}
1164
1165/// Refuses, before the guest starts, an in-guest LiteInst run that the runtime
1166/// cannot honour or could not monitor.
1167///
1168/// - A maximum timeslice. Reverie's in-guest Tool host has no preemption timer:
1169///   its `set_timer` refuses with `ENOSYS`, and Detcore treats a failed
1170///   `set_timer` as fatal (`.expect("Failed to set timer")`), so the guest
1171///   would panic at its first scheduling point. The CLI refuses
1172///   `--max-timeslice` earlier; this covers library callers.
1173/// - A kernel older than Linux 6.5 ([`in_guest_liteinst_kernel_gap`]).
1174/// - A guest program that would run code before, or without, the runtime's
1175///   constructor ([`in_guest_liteinst_program_gap`]).
1176/// - For `in-guest-trap`, a guest environment that sets
1177///   `REVERIE_LITEINST_SITE_PATCHING` to anything but `0`
1178///   ([`in_guest_trap_site_patching_conflict`]).
1179///
1180/// This runs where the guest is spawned (inside the container for the CLI),
1181/// so it resolves the program in the filesystem the guest sees. It pins the
1182/// resolved program on `command` ([`pin_spawn_program`]), so the launch
1183/// executes the same path this check inspected. For `in-guest-trap` it then
1184/// sets `REVERIE_LITEINST_SITE_PATCHING=0` on `command`
1185/// ([`force_in_guest_site_patching`]).
1186#[cfg(feature = "liteinst")]
1187fn refuse_in_guest_liteinst_run(
1188    command: &mut Command,
1189    config: &DetConfig,
1190    backend: Backend,
1191) -> Result<(), Error> {
1192    let reason = if let Some(conflict) = in_guest_trap_site_patching_conflict(command, backend) {
1193        Some(conflict)
1194    } else if config.max_timeslice.is_some() {
1195        Some(
1196            "it cannot deliver Detcore's preemption timer yet (the in-guest Tool host refuses \
1197             set_timer with ENOSYS), so the run would fail at its first timeslice; pass \
1198             --max-timeslice=disabled"
1199                .to_owned(),
1200        )
1201    } else if let Some(gap) = in_guest_liteinst_kernel_gap(&running_kernel_release()) {
1202        Some(gap)
1203    } else {
1204        in_guest_liteinst_program_gap(command)?
1205    };
1206    match reason {
1207        Some(reason) => Err(Error::new(LiteinstInGuestRefusal::for_backend(
1208            backend, reason,
1209        ))),
1210        None => {
1211            force_in_guest_site_patching(command, backend);
1212            Ok(())
1213        }
1214    }
1215}
1216
1217/// The value of `REVERIE_LITEINST_SITE_PATCHING` (`reverie_liteinst::SITE_PATCHING_ENV`)
1218/// that the guest sees under `backend`, when Hermit decides it: `0` for
1219/// `in-guest-trap`, which exists to run with no syscall site patched, and
1220/// `None` for `liteinst`, which leaves the variable to the guest environment
1221/// (unset means patching on).
1222pub fn forced_site_patching_value(backend: Backend) -> Option<&'static str> {
1223    match backend {
1224        Backend::InGuestTrap => Some("0"),
1225        _ => None,
1226    }
1227}
1228
1229/// Whether LiteInst's SIGTRAP guard router can be installed in the in-guest
1230/// Tool images of a default-route run of `backend` configured by `config`:
1231/// on an in-guest backend, unless every image runs with syscall site
1232/// patching off (`config.in_guest_site_patching_off`, set for
1233/// `--backend=in-guest-trap`).
1234///
1235/// At the pinned Reverie, the shared Tool host's preparation
1236/// (`LiteinstPatchHost::prepare_patcher`, reverie-liteinst `src/runtime.rs`)
1237/// installs the router only when the site-patching setting its runtime
1238/// captured is on: the router takes the breakpoints LiteInst plants while it
1239/// publishes a patched site, and with patching off it publishes none
1240/// (A5 step 3c slice 0c). Every in-guest-trap image checks that its runtime
1241/// captured patching off and refuses to run otherwise
1242/// (`detcore::in_guest_site_patching`), so no image of such a run executes
1243/// guest code with the router. On `--backend=liteinst` Hermit leaves the setting to the guest's
1244/// environment, so an image can run with patching off and no router; the
1245/// answer is then `true` all the same, which reserves SIGTRAP where it is
1246/// not needed: a refusal Linux would not make, never a missed one. The
1247/// loader route (Reverie's private-image installer) keeps the router
1248/// whatever its setting, and never calls this (D26).
1249pub const fn in_guest_guard_router_installed(config: &DetConfig, backend: Backend) -> bool {
1250    backend.is_in_guest_liteinst() && !config.in_guest_site_patching_off
1251}
1252
1253/// The signals the in-guest runtime keeps for itself in a run of `backend`
1254/// configured by `config` (`detcore::Config::in_guest_reserved_signals`), bit
1255/// `n - 1` for signal `n`, for Hermit's default in-guest route: SIGSYS, its
1256/// syscall trap, always; SIGSEGV while it subscribes instructions, when it
1257/// installs its instruction-fault handler for SIGSEGV; and SIGTRAP while
1258/// LiteInst's guard router can be installed, which is site patching on
1259/// ([`in_guest_guard_router_installed`]): never on `in-guest-trap`, whose
1260/// `config.in_guest_site_patching_off` the caller has set first.
1261/// The subscription is Detcore's own decision from the same configuration
1262/// (`Tool::subscriptions`), which each in-guest Tool image reads to choose
1263/// its instruction subscriptions (reverie-inguest `tool_install.rs:191-195`).
1264/// Empty (0) for every other backend. The in-guest loader route never calls
1265/// this, so its set stays empty: today's behaviour there (D13(a)).
1266pub fn in_guest_reserved_signals(config: &DetConfig, backend: Backend) -> u64 {
1267    if !backend.is_in_guest_liteinst() {
1268        return 0;
1269    }
1270    let bit = |signal: libc::c_int| 1_u64 << (signal - 1);
1271    let subscription = <Detcore as reverie::Tool>::subscriptions(config);
1272    let mut reserved = bit(libc::SIGSYS);
1273    if subscription.has_rdtsc() || subscription.has_cpuid() {
1274        reserved |= bit(libc::SIGSEGV);
1275    }
1276    if in_guest_guard_router_installed(config, backend) {
1277        reserved |= bit(libc::SIGTRAP);
1278    }
1279    reserved
1280}
1281
1282/// Whether Detcore refuses host-timed signal producers in a run of `backend`
1283/// (`detcore::Config::in_guest_refuses_host_timed_producers`, A5 step 3c
1284/// slice 0b): on Hermit's default in-guest route, for both in-guest backends,
1285/// and never on another backend. The in-guest loader route never calls this,
1286/// so the setting stays off there: today's behaviour (D13(a), D26).
1287pub const fn in_guest_refuses_host_timed_producers(backend: Backend) -> bool {
1288    backend.is_in_guest_liteinst()
1289}
1290
1291/// Whether this process, which runs the scheduler of the in-guest run it
1292/// launches, owns the guests' PID namespace (A5 step 3c.1, D51 and D52): it
1293/// is PID 1 of its PID namespace, and every guest it launches descends from
1294/// it, so is a member of that namespace. When a PID namespace's init exits,
1295/// the kernel sends SIGKILL to every task with a PID in the namespace and
1296/// waits for them (`zap_pid_ns_processes`, kernel/pid_namespace.c v6.12
1297/// lines 170-222). That covers a guest in a nested PID namespace too: a task
1298/// has a PID in its own namespace and in every ancestor (`alloc_pid`,
1299/// kernel/pid.c v6.12 lines 194-253), and `setns` enters only the caller's
1300/// namespace or a descendant of it (`pidns_install`, kernel/pid_namespace.c
1301/// lines 390-397), so no guest leaves the namespace. So a refusal that exits
1302/// the scheduler (`GlobalRequest::StoppingRefusal`) then leaves no guest
1303/// process running; the in-guest delivery gate requires that
1304/// ([`in_guest_signal_delivery_gate`]).
1305///
1306/// A fact read from the process, never a setting a caller supplies, so the
1307/// run-wide-exit claim cannot be forged: `hermit run`'s container launch,
1308/// which runs the scheduler as the init of the namespace it creates, owns it,
1309/// and `hermit run --no-namespace` does not (its scheduler is a plain
1310/// child). A library caller owns it exactly when it really is that init.
1311pub fn scheduler_owns_guest_pid_namespace() -> bool {
1312    // SAFETY: getpid has no preconditions.
1313    unsafe { libc::getpid() == 1 }
1314}
1315
1316/// Whether the pinned in-guest runtime delivers the guest's signals itself
1317/// under the gate (its runtime mask, final-read loop and predicted deaths,
1318/// A5 step 3c.1e). Not yet: until the flip (3c.1f), the gate stays off on
1319/// every route, whatever [`in_guest_signal_delivery_gate`] answers.
1320#[cfg(feature = "liteinst")]
1321const IN_GUEST_RUNTIME_DELIVERS_SIGNALS: bool = false;
1322
1323/// Set only in a test child that re-executed this test binary as PID 1 of its
1324/// own user and PID namespaces (`in_guest_delivery_harness`, design D60): the
1325/// gate's set points then take the runtime as delivering signals, so the dark
1326/// 3c.1 mechanisms run end to end before the flip. No shipped build has it.
1327#[cfg(all(test, feature = "liteinst"))]
1328static FORCE_IN_GUEST_DELIVERY: std::sync::atomic::AtomicBool =
1329    std::sync::atomic::AtomicBool::new(false);
1330
1331/// Whether the in-guest runtime delivers signals itself, at the gate's set
1332/// points: [`IN_GUEST_RUNTIME_DELIVERS_SIGNALS`], or, in a test child,
1333/// [`FORCE_IN_GUEST_DELIVERY`].
1334#[cfg(feature = "liteinst")]
1335fn runtime_delivers_signals() -> bool {
1336    #[cfg(test)]
1337    if FORCE_IN_GUEST_DELIVERY.load(std::sync::atomic::Ordering::Acquire) {
1338        return true;
1339    }
1340    IN_GUEST_RUNTIME_DELIVERS_SIGNALS
1341}
1342
1343/// How many launches in this test process computed the in-guest delivery
1344/// gate on at one of its two set points. The forced-gate harness
1345/// (`in_guest_delivery_harness`, design D60) reads it after a row's run, so
1346/// that the row proves its own run was gated, not only that the force flag
1347/// was set: another conjunct of [`in_guest_signal_delivery_gate`] turning
1348/// the gate off would otherwise leave a row whose output Linux also prints
1349/// passing with the gate off. Test builds only.
1350#[cfg(all(test, feature = "liteinst"))]
1351static GATED_LAUNCHES: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
1352
1353/// The line a test child prints when a set point computes the gate on, for a
1354/// row whose run ends the process (a stopping refusal) before it can read
1355/// [`GATED_LAUNCHES`]. Test builds only.
1356#[cfg(all(test, feature = "liteinst"))]
1357const GATED_LAUNCH_WITNESS: &str = "IN-GUEST-DELIVERY-GATE-ON";
1358
1359/// The delivery gate as a set point computed it, recorded in test builds
1360/// (`GATED_LAUNCHES`, `GATED_LAUNCH_WITNESS`) when on.
1361#[cfg(feature = "liteinst")]
1362fn record_delivery_gate(gate: bool) -> bool {
1363    #[cfg(test)]
1364    if gate {
1365        GATED_LAUNCHES.fetch_add(1, std::sync::atomic::Ordering::AcqRel);
1366        println!("{GATED_LAUNCH_WITNESS}");
1367    }
1368    gate
1369}
1370
1371/// The in-guest delivery gate (`detcore::Config::in_guest_signal_delivery`,
1372/// D35 of the A5 step 3c design) for a run of `backend` on Hermit's default
1373/// in-guest route, from its final prepared configuration and the launcher's
1374/// PID-namespace ownership: an in-guest backend, site patching off,
1375/// sequentialized threads, no configuration in which the two-phase delivery
1376/// protocol cannot be kept (`detcore::two_phase_refusal`: a timeslice, a
1377/// recorded or replayed schedule, an in-effect pipe hand-off, and the rows no
1378/// in-guest backend reaches), and a scheduler that owns the guest's PID
1379/// namespace (`owns_guest_pid_namespace`, which the launch sites compute
1380/// with [`scheduler_owns_guest_pid_namespace`]). The in-guest loader route
1381/// never asks (D26, D43): its configuration keeps the gate off.
1382pub fn in_guest_signal_delivery_gate(
1383    config: &DetConfig,
1384    backend: Backend,
1385    owns_guest_pid_namespace: bool,
1386) -> bool {
1387    backend.is_in_guest_liteinst()
1388        && config.in_guest_site_patching_off
1389        && config.sequentialize_threads
1390        && detcore::two_phase_refusal(config).is_none()
1391        && owns_guest_pid_namespace
1392}
1393
1394/// `RLIMIT_CPU`'s value as Hermit's log and refusal print it: `unlimited`, or
1395/// a number of seconds.
1396#[cfg(feature = "liteinst")]
1397fn rlimit_seconds(value: libc::rlim_t) -> String {
1398    if value == libc::RLIM_INFINITY {
1399        "unlimited".to_owned()
1400    } else {
1401        format!("{value}s")
1402    }
1403}
1404
1405/// The INFO line for the physical `RLIMIT_CPU` the guest's own check found,
1406/// `old`, just before its program started, and set to unlimited, the value
1407/// Detcore reports (`syscalls/sysinfo.rs`): D25 of A5 step 3c.
1408#[cfg(feature = "liteinst")]
1409fn rlimit_cpu_report_line(old: libc::rlimit) -> String {
1410    if old.rlim_cur == libc::RLIM_INFINITY && old.rlim_max == libc::RLIM_INFINITY {
1411        format!(
1412            "HERMIT_IN_GUEST_RLIMIT_CPU: the guest's physical RLIMIT_CPU was soft {} hard {}, the \
1413             value Detcore reports; the guest starts with it unchanged (A5 step 3c, D25)",
1414            rlimit_seconds(old.rlim_cur),
1415            rlimit_seconds(old.rlim_max),
1416        )
1417    } else {
1418        format!(
1419            "HERMIT_IN_GUEST_RLIMIT_CPU: the guest's physical RLIMIT_CPU was soft {} hard {}; \
1420             Hermit sets it to soft unlimited hard unlimited as the guest starts, the value \
1421             Detcore reports, so the kernel never sends SIGXCPU at a moment host CPU time decides \
1422             (A5 step 3c, D25)",
1423            rlimit_seconds(old.rlim_cur),
1424            rlimit_seconds(old.rlim_max),
1425        )
1426    }
1427}
1428
1429/// Why the guest cannot start with the finite physical `RLIMIT_CPU` its own
1430/// check found, `old`, which it could not raise to unlimited (`errno`; raising
1431/// a hard limit needs `CAP_SYS_RESOURCE` in the initial user namespace,
1432/// `do_prlimit`, which Hermit's container never has). A finite limit would
1433/// make the kernel send SIGXCPU at the soft limit and SIGKILL at the hard
1434/// one, at moments host CPU time decides.
1435#[cfg(feature = "liteinst")]
1436fn rlimit_cpu_refusal(old: libc::rlimit, errno: nix::errno::Errno) -> String {
1437    format!(
1438        "the guest would inherit a finite physical RLIMIT_CPU (soft {}, hard {}), and Hermit \
1439         cannot raise it ({errno}), so the kernel would send SIGXCPU or SIGKILL at a moment host \
1440         CPU time decides; run Hermit with an unlimited hard CPU-time limit \
1441         (A5 step 3c, D25, https://github.com/rrnewton/hermit/issues/4485)",
1442        rlimit_seconds(old.rlim_cur),
1443        rlimit_seconds(old.rlim_max),
1444    )
1445}
1446
1447/// Why descriptor `fd`, which the guest would inherit, cannot be inherited by
1448/// an in-guest guest: it is already a complete host-timed signal producer
1449/// (`state`, D25 of A5 step 3c), holds a lease (`lease`), or has an owner
1450/// without `O_ASYNC` on a file that is not a socket. A directory
1451/// notification (`F_NOTIFY`) sets such an owner and signals it for its
1452/// events, and cannot be read back (there is no `F_GETNOTIFY`, and fdinfo
1453/// does not show dnotify), so any owner fails closed. `None` when it has
1454/// none of these.
1455#[cfg(feature = "liteinst")]
1456fn inherited_producer_refusal(
1457    fd: std::os::fd::RawFd,
1458    state: detcore::host_timed_producers::ProducerState,
1459    lease: bool,
1460) -> Option<String> {
1461    let what = if lease {
1462        "it holds a lease, whose owner Linux sends SIGIO when another process opens the file"
1463    } else if state.is_complete() && state.o_async {
1464        "it has O_ASYNC and an owner, which Linux sends SIGIO when it becomes ready"
1465    } else if state.is_complete() {
1466        "it is a socket with an owner, which Linux sends SIGURG when out-of-band data arrives"
1467    } else if state.owner {
1468        "it has an owner, which a directory notification (F_NOTIFY) on it would make Linux send \
1469         SIGIO for the directory's changes, and Hermit cannot tell whether one is set"
1470    } else {
1471        return None;
1472    };
1473    Some(format!(
1474        "the guest would inherit descriptor {fd}, which is already a signal producer: {what}, at \
1475         a moment host timing decides; close it, or clear its owner and O_ASYNC, before running \
1476         Hermit (A5 step 3c, D25, https://github.com/rrnewton/hermit/issues/4485)"
1477    ))
1478}
1479
1480/// Why descriptor `fd`, which the guest would inherit and whose state cannot
1481/// be read (`errno`), is not inherited unchecked.
1482#[cfg(feature = "liteinst")]
1483fn unreadable_descriptor_refusal(fd: std::os::fd::RawFd, errno: nix::errno::Errno) -> String {
1484    format!(
1485        "the guest would inherit descriptor {fd}, whose signal owner and flags cannot be read \
1486         ({errno}), so Hermit cannot tell whether it is a signal producer \
1487         (A5 step 3c, D25, https://github.com/rrnewton/hermit/issues/4485)"
1488    )
1489}
1490
1491/// Why the guest's descriptor table could not be listed (`errno`), so is not
1492/// inherited unchecked.
1493#[cfg(feature = "liteinst")]
1494fn unlisted_descriptors_refusal(errno: nix::errno::Errno) -> String {
1495    format!(
1496        "the guest's descriptor table could not be listed from /proc/self/fd ({errno}), so \
1497         Hermit cannot tell whether a descriptor it would inherit is a signal producer \
1498         (A5 step 3c, D25, https://github.com/rrnewton/hermit/issues/4485)"
1499    )
1500}
1501
1502/// The guest's own check, in its process just before its program starts
1503/// (D25 of A5 step 3c): a `pre_exec` callback Hermit installs after every
1504/// other one that can change what it checks, so after
1505/// the spawner has put the command's configured standard streams in place
1506/// (whatever descriptor each came from, close-on-exec or not) and after every
1507/// callback the caller installed (one of which could clear close-on-exec on a
1508/// descriptor of its own, or change a limit). Hermit's own process cannot see
1509/// that state. The callback
1510/// - lists the whole descriptor table from `/proc/self/fd`, and fails the
1511///   launch, by name, when it cannot ([`audit_inherited_descriptors`]);
1512/// - reads each descriptor without close-on-exec, and fails the launch for a
1513///   complete producer, a lease or any owner, or one it cannot read
1514///   ([`check_inherited_descriptor`]);
1515/// - reads `RLIMIT_CPU` and sets it to unlimited, and fails the launch when it
1516///   cannot.
1517///
1518/// Every outcome is written to a pipe this process reads ([`LaunchReport`]):
1519/// a failure's reason, from which the caller makes the named refusal, or the
1520/// limit the check found, which is logged at INFO at the end of the run, on
1521/// every exit path (`detcore::host_timed_producers::defer_run_end_log`).
1522#[cfg(feature = "liteinst")]
1523struct LaunchAudit {
1524    pipe: std::sync::Arc<LaunchPipe>,
1525}
1526
1527/// The pipe the guest's check reports through, and its report once read.
1528#[cfg(feature = "liteinst")]
1529struct LaunchPipe {
1530    read: std::os::fd::OwnedFd,
1531    _write: std::os::fd::OwnedFd,
1532    report: std::sync::OnceLock<Option<LaunchReport>>,
1533}
1534
1535#[cfg(feature = "liteinst")]
1536impl LaunchPipe {
1537    /// The next report in the pipe, if the check wrote one.
1538    fn read_report(&self) -> Option<LaunchReport> {
1539        let mut bytes = [0u8; LaunchReport::SIZE];
1540        let read = nix::unistd::read(&self.read, &mut bytes).ok()?;
1541        (read == bytes.len()).then(|| LaunchReport::decode(bytes))
1542    }
1543
1544    /// The launch's report, read once.
1545    fn report(&self) -> Option<LaunchReport> {
1546        *self.report.get_or_init(|| self.read_report())
1547    }
1548
1549    /// Logs at INFO the `RLIMIT_CPU` a check that passed found and set to
1550    /// unlimited (D25); nothing for a check that failed or never ran.
1551    fn log_rlimit(&self) {
1552        match self.report() {
1553            Some(report) if report.kind == audit_bits::PASSED => {
1554                tracing::info!("{}", rlimit_cpu_report_line(report.limit()));
1555            }
1556            // A refusal is reported where the launch fails, not here.
1557            Some(_) => {}
1558            None => tracing::warn!(
1559                "HERMIT_IN_GUEST_RLIMIT_CPU: the guest's launch check reported nothing \
1560                 (A5 step 3c, D25)"
1561            ),
1562        }
1563    }
1564}
1565
1566/// The report's kinds and state bits.
1567#[cfg(feature = "liteinst")]
1568mod audit_bits {
1569    /// The check passed; the report carries the limit it found.
1570    pub const PASSED: u8 = 0;
1571    /// A descriptor is a complete producer; the bits say what it holds.
1572    pub const PRODUCER: u8 = 1;
1573    /// A descriptor's state cannot be read; the errno says why.
1574    pub const UNREADABLE: u8 = 2;
1575    /// The descriptor table cannot be listed; the errno says why.
1576    pub const UNLISTED: u8 = 3;
1577    /// `RLIMIT_CPU` cannot be raised; the report carries the limit and errno.
1578    pub const RLIMIT_CPU: u8 = 4;
1579
1580    pub const O_ASYNC: u8 = 1;
1581    pub const OWNER: u8 = 2;
1582    pub const SOCKET: u8 = 4;
1583    pub const LEASE: u8 = 8;
1584}
1585
1586/// What the guest's check reports ([`LaunchAudit`]): its kind
1587/// ([`audit_bits`]), state bits, errno, descriptor, and the `RLIMIT_CPU` it
1588/// found, in 24 native-endian bytes.
1589#[cfg(feature = "liteinst")]
1590#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1591struct LaunchReport {
1592    kind: u8,
1593    bits: u8,
1594    errno: u8,
1595    fd: libc::c_int,
1596    rlimit: (libc::rlim_t, libc::rlim_t),
1597}
1598
1599#[cfg(feature = "liteinst")]
1600impl LaunchReport {
1601    const SIZE: usize = 24;
1602
1603    fn encode(self) -> [u8; Self::SIZE] {
1604        let mut bytes = [0u8; Self::SIZE];
1605        bytes[0] = self.kind;
1606        bytes[1] = self.bits;
1607        bytes[2] = self.errno;
1608        bytes[4..8].copy_from_slice(&self.fd.to_ne_bytes());
1609        bytes[8..16].copy_from_slice(&self.rlimit.0.to_ne_bytes());
1610        bytes[16..24].copy_from_slice(&self.rlimit.1.to_ne_bytes());
1611        bytes
1612    }
1613
1614    fn decode(bytes: [u8; Self::SIZE]) -> Self {
1615        let word = |range: std::ops::Range<usize>| {
1616            libc::rlim_t::from_ne_bytes(bytes[range].try_into().expect("eight bytes"))
1617        };
1618        Self {
1619            kind: bytes[0],
1620            bits: bytes[1],
1621            errno: bytes[2],
1622            fd: libc::c_int::from_ne_bytes(bytes[4..8].try_into().expect("four bytes")),
1623            rlimit: (word(8..16), word(16..24)),
1624        }
1625    }
1626
1627    fn errno(self) -> nix::errno::Errno {
1628        nix::errno::Errno::from_raw(self.errno.into())
1629    }
1630
1631    fn limit(self) -> libc::rlimit {
1632        libc::rlimit {
1633            rlim_cur: self.rlimit.0,
1634            rlim_max: self.rlimit.1,
1635        }
1636    }
1637
1638    /// The refusal this report names, `None` for a check that passed.
1639    fn refusal(self) -> Option<String> {
1640        match self.kind {
1641            audit_bits::PRODUCER => {
1642                let state = detcore::host_timed_producers::ProducerState {
1643                    o_async: self.bits & audit_bits::O_ASYNC != 0,
1644                    owner: self.bits & audit_bits::OWNER != 0,
1645                    socket: self.bits & audit_bits::SOCKET != 0,
1646                };
1647                inherited_producer_refusal(self.fd, state, self.bits & audit_bits::LEASE != 0)
1648            }
1649            audit_bits::UNREADABLE => Some(unreadable_descriptor_refusal(self.fd, self.errno())),
1650            audit_bits::UNLISTED => Some(unlisted_descriptors_refusal(self.errno())),
1651            audit_bits::RLIMIT_CPU => Some(rlimit_cpu_refusal(self.limit(), self.errno())),
1652            _ => None,
1653        }
1654    }
1655}
1656
1657/// The report for descriptor `fd` of the calling process if the guest must
1658/// not inherit it, `None` if it may: one with close-on-exec, which it does
1659/// not inherit, or `report`, the audit's own pipe, are skipped.
1660/// Async-signal-safe: fcntl and fstat only, no allocation.
1661#[cfg(feature = "liteinst")]
1662fn check_inherited_descriptor(fd: libc::c_int, report: libc::c_int) -> Option<LaunchReport> {
1663    // SAFETY: F_GETFD reads a descriptor flag and writes no memory.
1664    let fd_flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };
1665    if fd == report || fd_flags < 0 || fd_flags & libc::FD_CLOEXEC != 0 {
1666        return None;
1667    }
1668    let (kind, bits, errno) = match detcore::host_timed_producers::host_descriptor_state(fd) {
1669        Ok((state, lease)) if lease || state.owner || state.is_complete() => (
1670            audit_bits::PRODUCER,
1671            (if state.o_async {
1672                audit_bits::O_ASYNC
1673            } else {
1674                0
1675            }) | (if state.owner { audit_bits::OWNER } else { 0 })
1676                | (if state.socket { audit_bits::SOCKET } else { 0 })
1677                | (if lease { audit_bits::LEASE } else { 0 }),
1678            0,
1679        ),
1680        Ok(_) | Err(nix::errno::Errno::EBADF) => return None,
1681        Err(errno) => (audit_bits::UNREADABLE, 0, errno as i32 as u8),
1682    };
1683    Some(LaunchReport {
1684        kind,
1685        bits,
1686        errno,
1687        fd,
1688        rlimit: (0, 0),
1689    })
1690}
1691
1692/// The first report [`check_inherited_descriptor`] makes for the calling
1693/// process's descriptors, every one `/proc/self/fd` lists, read with
1694/// `getdents64` into a stack buffer. Where that directory cannot be listed
1695/// (its open or a read fails, for example with EMFILE after a caller lowered
1696/// `RLIMIT_NOFILE` below its open descriptors), the report says so
1697/// (`UNLISTED`, with the errno): no number-range scan stands in for it, since
1698/// no bound below the largest open descriptor is sure. Async-signal-safe:
1699/// open, getdents64, close, fcntl and fstat only, no allocation.
1700#[cfg(feature = "liteinst")]
1701fn audit_inherited_descriptors(report: libc::c_int) -> Option<LaunchReport> {
1702    let unlisted = || LaunchReport {
1703        kind: audit_bits::UNLISTED,
1704        bits: 0,
1705        errno: nix::errno::Errno::last_raw() as u8,
1706        fd: -1,
1707        rlimit: (0, 0),
1708    };
1709    // SAFETY: plain system calls on this process's descriptor table, writing
1710    // only into the local buffer.
1711    unsafe {
1712        let directory = libc::open(
1713            c"/proc/self/fd".as_ptr(),
1714            libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC,
1715        );
1716        if directory < 0 {
1717            return Some(unlisted());
1718        }
1719        let mut buffer = [0u8; 4096];
1720        loop {
1721            let read = libc::syscall(
1722                libc::SYS_getdents64,
1723                directory,
1724                buffer.as_mut_ptr(),
1725                buffer.len(),
1726            );
1727            if read < 0 {
1728                let failed = unlisted();
1729                libc::close(directory);
1730                return Some(failed);
1731            }
1732            if read == 0 {
1733                break;
1734            }
1735            let mut offset = 0;
1736            while offset + 19 < read as usize {
1737                // struct linux_dirent64: d_ino (8), d_off (8), d_reclen (2),
1738                // d_type (1), d_name.
1739                let length =
1740                    u16::from_ne_bytes([buffer[offset + 16], buffer[offset + 17]]) as usize;
1741                if length == 0 {
1742                    break;
1743                }
1744                let mut fd: libc::c_int = 0;
1745                let mut digits = 0;
1746                for &byte in &buffer[offset + 19..offset + length] {
1747                    if !byte.is_ascii_digit() {
1748                        break;
1749                    }
1750                    fd = fd
1751                        .saturating_mul(10)
1752                        .saturating_add((byte - b'0') as libc::c_int);
1753                    digits += 1;
1754                }
1755                if digits > 0
1756                    && fd != directory
1757                    && let Some(found) = check_inherited_descriptor(fd, report)
1758                {
1759                    libc::close(directory);
1760                    return Some(found);
1761                }
1762                offset += length;
1763            }
1764        }
1765        libc::close(directory);
1766        None
1767    }
1768}
1769
1770/// The guest's whole check ([`LaunchAudit`]), in its process: the descriptor
1771/// table, then `RLIMIT_CPU`, which it sets to unlimited. Every outcome is a
1772/// report; a failing one fails the launch. Async-signal-safe.
1773#[cfg(feature = "liteinst")]
1774fn launch_check(report: libc::c_int) -> LaunchReport {
1775    if let Some(failed) = audit_inherited_descriptors(report) {
1776        return failed;
1777    }
1778    // SAFETY: getrlimit and setrlimit read and write only the local limits.
1779    unsafe {
1780        let mut old: libc::rlimit = std::mem::zeroed();
1781        if libc::getrlimit(libc::RLIMIT_CPU, &mut old) != 0 {
1782            return LaunchReport {
1783                kind: audit_bits::RLIMIT_CPU,
1784                bits: 0,
1785                errno: nix::errno::Errno::last_raw() as u8,
1786                fd: -1,
1787                rlimit: (0, 0),
1788            };
1789        }
1790        let mut found = LaunchReport {
1791            kind: audit_bits::PASSED,
1792            bits: 0,
1793            errno: 0,
1794            fd: -1,
1795            rlimit: (old.rlim_cur, old.rlim_max),
1796        };
1797        if old.rlim_cur != libc::RLIM_INFINITY || old.rlim_max != libc::RLIM_INFINITY {
1798            let unlimited = libc::rlimit {
1799                rlim_cur: libc::RLIM_INFINITY,
1800                rlim_max: libc::RLIM_INFINITY,
1801            };
1802            if libc::setrlimit(libc::RLIMIT_CPU, &unlimited) != 0 {
1803                found.kind = audit_bits::RLIMIT_CPU;
1804                found.errno = nix::errno::Errno::last_raw() as u8;
1805            }
1806        }
1807        found
1808    }
1809}
1810
1811#[cfg(feature = "liteinst")]
1812impl LaunchAudit {
1813    /// Installs the check on `command`, after every `pre_exec` callback
1814    /// already installed on it, so a library caller's own run first. It is
1815    /// the last callback that can change what it checks: Reverie's in-guest
1816    /// launcher then adds two of its own (reverie-inguest
1817    /// `src/launcher/mod.rs` at a214eb85): `personality(ADDR_NO_RANDOMIZE)`
1818    /// (`:619`, called at `:1048` and `:1092`), and clearing close-on-exec on
1819    /// its own sealed bootstrap memfd (`:1061-1068`, `bootstrap.rs`). Neither
1820    /// touches `RLIMIT_CPU` or creates a signal producer.
1821    /// Also keeps the INFO line of the limit it finds to log at the end of
1822    /// the run, on every exit path (`detcore::host_timed_producers`).
1823    fn install(command: &mut Command) -> Result<Self, Error> {
1824        use std::os::fd::AsRawFd;
1825        let (read, write) =
1826            nix::unistd::pipe2(nix::fcntl::OFlag::O_CLOEXEC | nix::fcntl::OFlag::O_NONBLOCK)?;
1827        let report = write.as_raw_fd();
1828        // SAFETY: the callback is async-signal-safe (see `launch_check`) and
1829        // allocates nothing.
1830        unsafe {
1831            command.pre_exec(move || {
1832                let found = launch_check(report);
1833                let bytes = found.encode();
1834                libc::write(report, bytes.as_ptr().cast(), bytes.len());
1835                if found.kind == audit_bits::PASSED {
1836                    Ok(())
1837                } else {
1838                    Err(reverie::Errno::EPERM)
1839                }
1840            });
1841        }
1842        let pipe = std::sync::Arc::new(LaunchPipe {
1843            read,
1844            _write: write,
1845            report: std::sync::OnceLock::new(),
1846        });
1847        let logged = pipe.clone();
1848        detcore::host_timed_producers::defer_run_end_log(move || logged.log_rlimit());
1849        Ok(Self { pipe })
1850    }
1851
1852    /// `error`, the launch's, or the named refusal the guest's check reported
1853    /// for `backend`, which is why the launch failed. A check that passed
1854    /// before the backend failed has its limit logged first, as on every
1855    /// other exit path.
1856    fn launch_error(&self, backend: Backend, error: reverie::Error) -> Error {
1857        match self.pipe.report().and_then(LaunchReport::refusal) {
1858            Some(reason) => Error::new(LiteinstInGuestRefusal::for_backend(backend, reason)),
1859            None => {
1860                detcore::host_timed_producers::emit_run_end_log();
1861                Error::from(error)
1862            }
1863        }
1864    }
1865}
1866
1867/// Before an in-guest launch on the default route, installs the guest's own
1868/// check of what it inherits (D25 of A5 step 3c, design section 16.32,
1869/// [`LaunchAudit`]): it refuses the launch by name for an inherited
1870/// descriptor that is already a host-timed signal producer, a descriptor
1871/// table it cannot list, or a finite `RLIMIT_CPU` it cannot raise, and
1872/// otherwise sets the guest's physical `RLIMIT_CPU` to unlimited as the guest
1873/// starts. The caller turns a failed launch's report into the refusal; a
1874/// passed one's limit is logged at INFO at the end of the run, on every exit
1875/// path. `None` for a backend
1876/// that does not refuse host-timed producers.
1877#[cfg(feature = "liteinst")]
1878fn neutralize_inherited_signal_producers(
1879    command: &mut Command,
1880    backend: Backend,
1881) -> Result<Option<LaunchAudit>, Error> {
1882    if !in_guest_refuses_host_timed_producers(backend) {
1883        return Ok(None);
1884    }
1885    Ok(Some(LaunchAudit::install(command)?))
1886}
1887
1888/// Why an `in-guest-trap` run cannot start with the guest environment of
1889/// `command`: the environment (explicit or inherited) sets
1890/// `REVERIE_LITEINST_SITE_PATCHING` to a value other than the one Hermit
1891/// forces. Hermit refuses such a run rather than overriding the value
1892/// silently, because the caller asked for two contradictory things: site
1893/// patching (or a value the runtime would reject) and the backend that exists
1894/// to run without it. A value equal to the forced one is accepted. `None`
1895/// for every other backend.
1896#[cfg(feature = "liteinst")]
1897fn in_guest_trap_site_patching_conflict(command: &Command, backend: Backend) -> Option<String> {
1898    let forced = forced_site_patching_value(backend)?;
1899    let name = reverie_liteinst::SITE_PATCHING_ENV;
1900    let value = command.get_env(name)?;
1901    (*value != *std::ffi::OsStr::new(forced)).then(|| {
1902        format!(
1903            "the guest environment sets {name}={}, but --backend={} runs with syscall site \
1904             patching off and sets {name}={forced} itself; drop the variable, or select \
1905             --backend=liteinst to patch syscall sites",
1906            value.to_string_lossy(),
1907            backend.as_str()
1908        )
1909    })
1910}
1911
1912/// Sets the guest's `REVERIE_LITEINST_SITE_PATCHING` to the value Hermit
1913/// forces for `backend` ([`forced_site_patching_value`]), leaving the
1914/// environment unchanged for a backend that forces none. The guest's
1915/// descendants inherit it unless the guest changes its own environment; a
1916/// descendant that turns patching back on shows in the run's dispatch record
1917/// as patched sites.
1918#[cfg(feature = "liteinst")]
1919fn force_in_guest_site_patching(command: &mut Command, backend: Backend) {
1920    if let Some(value) = forced_site_patching_value(backend) {
1921        command.env(reverie_liteinst::SITE_PATCHING_ENV, value);
1922    }
1923}
1924
1925/// Whether every in-guest Tool image of a `backend` run checks that its runtime
1926/// captured site patching off and statistics on
1927/// (`detcore::Config::in_guest_site_patching_off`): true for `in-guest-trap`.
1928#[cfg(feature = "liteinst")]
1929fn in_guest_site_patching_off(backend: Backend) -> bool {
1930    forced_site_patching_value(backend) == Some("0")
1931}
1932
1933/// Whether an in-guest run of `backend` collects LiteInst's statistics: when
1934/// something reads them ([`backend_stats::request`]), and on every
1935/// `in-guest-trap` run, whose result is checked against them
1936/// ([`in_guest_trap_patching_evidence`]).
1937#[cfg(feature = "liteinst")]
1938fn in_guest_stats_request(
1939    backend: Backend,
1940    summary_json: &Option<PathBuf>,
1941) -> reverie::BackendStatsRequest {
1942    if backend == Backend::InGuestTrap {
1943        reverie::BackendStatsRequest::ENABLED
1944    } else {
1945        backend_stats::request(summary_json)
1946    }
1947}
1948
1949/// Reports a refusal the fail-closed policy decided inside the guest as
1950/// Hermit's own refusal. The in-guest Detcore that refuses tells the
1951/// coordinator why (`GlobalState::policy_refusal`) and exits with
1952/// `HERMIT_POLICY_REFUSAL_EXIT`, which ends only the guest process. Without
1953/// this the run ended with that as the guest's exit status and no reason, or
1954/// with whatever postcondition the early exit then tripped (an unregistered
1955/// exit, or in-guest-trap's missing statistics). Checked before those.
1956#[cfg(feature = "liteinst")]
1957fn refuse_in_guest_policy(backend: Backend, refusal: Option<String>) -> Result<(), Error> {
1958    match refusal {
1959        Some(reason) => Err(Error::new(LiteinstInGuestRefusal::for_backend(
1960            backend,
1961            format!("{reason}; --backend=ptrace runs this program"),
1962        ))),
1963        None => Ok(()),
1964    }
1965}
1966
1967/// Why a finished `in-guest-trap` run did not show that it ran with syscall
1968/// site patching off, from its dispatch record and the number of guest
1969/// processes that reported LiteInst statistics: a site the runtime patched or
1970/// considered patching (with patching off it considers none), no record, or no
1971/// process report at all, in which case the record's zeros are not
1972/// measurements (a run whose guests collected no statistics still yields an
1973/// empty record). `None` for every other backend.
1974///
1975/// This is the coordinator's postcondition. Each Tool image also refuses to
1976/// run its program when the runtime started it with patching on
1977/// (`detcore::in_guest_site_patching`), which holds for every image whatever
1978/// it reports; this one sees only the images that report, because an image
1979/// reports its statistics when it exits (`exit` or `exit_group`), so an image
1980/// that `execve`s, or is killed by a signal, sends none, and the coordinator
1981/// cannot tell that from a report that was suppressed. It refuses a run in
1982/// which no process reported; it does not detect a run in which some did and
1983/// others did not. Each image's own check covers that: an image whose runtime
1984/// is not collecting statistics refuses to run its program.
1985#[cfg(feature = "liteinst")]
1986fn in_guest_trap_patching_evidence(
1987    backend: Backend,
1988    dispatch: Option<&reverie::DispatchStats>,
1989    process_reports: u64,
1990) -> Option<String> {
1991    if backend != Backend::InGuestTrap {
1992        return None;
1993    }
1994    let Some(record) = dispatch else {
1995        return Some(
1996            "the run produced no LiteInst dispatch record, so it cannot show that no syscall \
1997             site was patched"
1998                .to_owned(),
1999        );
2000    };
2001    if process_reports == 0 {
2002        return Some(
2003            "no guest process reported LiteInst statistics, so the run's zero patched sites \
2004             are not a measurement and it cannot show that no syscall site was patched"
2005                .to_owned(),
2006        );
2007    }
2008    let shown =
2009        |count: Option<u64>| count.map_or_else(|| "unmeasured".to_owned(), |n| n.to_string());
2010    (record.sites.patched != Some(0) || record.sites.candidates != Some(0)).then(|| {
2011        format!(
2012            "its guests' LiteInst statistics report {} patched and {} candidate syscall sites, \
2013             so it did not run with syscall site patching off and its result is not an \
2014             in-guest-trap result",
2015            shown(record.sites.patched),
2016            shown(record.sites.candidates)
2017        )
2018    })
2019}
2020
2021/// Refuses a finished `in-guest-trap` run whose dispatch record shows site
2022/// patching ([`in_guest_trap_patching_evidence`]): records a determinism loss,
2023/// so verification refuses to compare it, and returns the typed in-guest
2024/// refusal (exit 122).
2025#[cfg(feature = "liteinst")]
2026fn refuse_in_guest_trap_patching(backend: Backend, evidence: Option<String>) -> Result<(), Error> {
2027    match evidence {
2028        Some(reason) => {
2029            detcore::detlog::record_determinism_loss(&reason);
2030            Err(Error::new(LiteinstInGuestRefusal::for_backend(
2031                backend, reason,
2032            )))
2033        }
2034        None => Ok(()),
2035    }
2036}
2037
2038/// Use the existing actual subscriber-derived forwarding policy for the
2039/// shared native path. Call inside the same verification tracing scope.
2040#[cfg(any(
2041    feature = "native-exec",
2042    all(feature = "exec-stop-launcher", feature = "liteinst")
2043))]
2044#[doc(hidden)]
2045pub fn in_guest_detlog_forward_policy_for_native() -> detcore::detlog::ForwardPolicy {
2046    in_guest_detlog_forward_policy()
2047}
2048
2049/// Report the shared native trap runtime through the existing statistics
2050/// comparator and require a real guest report with site patching disabled.
2051/// This never converts a patching-off execution into LiteInst evidence.
2052#[cfg(feature = "liteinst")]
2053#[doc(hidden)]
2054pub fn native_trap_dispatch_stats(
2055    source: &reverie_liteinst::LiteinstBackendStatsSource,
2056) -> Result<Option<reverie::DispatchStats>, Error> {
2057    let backend = Backend::InGuestTrap;
2058    let record = backend_stats::report(backend, reverie::BackendStatsRequest::ENABLED, source);
2059    let evidence = in_guest_trap_patching_evidence(
2060        backend,
2061        record.as_ref(),
2062        source.snapshot().process_reports(),
2063    );
2064    refuse_in_guest_trap_patching(backend, evidence)?;
2065    Ok(record)
2066}
2067
2068/// The oldest kernel in-guest LiteInst runs on, as (major, minor). Linux 6.5
2069/// added `SO_PEERPIDFD`: the coordinator admits each guest process through the
2070/// pidfd of the socket's peer and watches that pidfd for the process's exit
2071/// (`in_guest_exits`). On an older kernel every admission would fail.
2072#[cfg(feature = "liteinst")]
2073const IN_GUEST_LITEINST_KERNEL_FLOOR: (u32, u32) = (6, 5);
2074
2075/// The running kernel's release string (`uname -r`), or empty if unreadable.
2076#[cfg(feature = "liteinst")]
2077fn running_kernel_release() -> String {
2078    let mut name: libc::utsname = unsafe { std::mem::zeroed() };
2079    // SAFETY: uname fills the struct it is given.
2080    if unsafe { libc::uname(&mut name) } != 0 {
2081        return String::new();
2082    }
2083    // SAFETY: uname NUL-terminates each field.
2084    unsafe { std::ffi::CStr::from_ptr(name.release.as_ptr()) }
2085        .to_string_lossy()
2086        .into_owned()
2087}
2088
2089/// Why kernel `release` cannot run in-guest LiteInst, or `None` when it is at
2090/// least [`IN_GUEST_LITEINST_KERNEL_FLOOR`]. A release whose version cannot be
2091/// read is refused.
2092#[cfg(feature = "liteinst")]
2093fn in_guest_liteinst_kernel_gap(release: &str) -> Option<String> {
2094    let mut numbers = release
2095        .split(|c: char| !c.is_ascii_digit())
2096        .map(|part| part.parse::<u32>().ok());
2097    let (floor_major, floor_minor) = IN_GUEST_LITEINST_KERNEL_FLOOR;
2098    match (numbers.next().flatten(), numbers.next().flatten()) {
2099        (Some(major), Some(minor)) if (major, minor) >= IN_GUEST_LITEINST_KERNEL_FLOOR => None,
2100        (Some(_), Some(_)) => Some(format!(
2101            "it needs Linux {floor_major}.{floor_minor} or later (SO_PEERPIDFD, which it uses to \
2102             admit each guest process and observe its exit); this kernel is {release}"
2103        )),
2104        _ => Some(format!(
2105            "it needs Linux {floor_major}.{floor_minor} or later (SO_PEERPIDFD), and the kernel \
2106             release {release:?} does not say which version this is"
2107        )),
2108    }
2109}
2110
2111/// How many `#!` interpreters [`in_guest_liteinst_program_gap`] follows before
2112/// it refuses the program. The kernel has its own small limit (`exec_binprm` in
2113/// `fs/exec.c`); a chain this check stops following is refused, not run, so the
2114/// check fails closed whichever limit is lower.
2115#[cfg(feature = "liteinst")]
2116const MAX_INTERPRETER_DEPTH: usize = 4;
2117
2118/// Why the guest program would run code before, or without, the in-guest
2119/// runtime, or `None` when it would not.
2120///
2121/// The runtime is an `LD_PRELOAD` library whose constructor installs Detcore.
2122/// The dynamic loader never loads it into a statically linked image or an image
2123/// of another architecture, and ignores `LD_PRELOAD` in secure-execution mode
2124/// (set-user-ID, set-group-ID, or file capabilities). An executable's
2125/// `DT_PREINIT_ARRAY` runs before every library constructor. Each of those
2126/// would run guest code unmonitored, so each is refused. `#!` scripts are
2127/// checked through their interpreter, read as the kernel's script handler
2128/// reads it ([`script::KernelScript`]). A program or interpreter that is not a
2129/// regular file is refused before it is opened: the kernel executes only
2130/// regular files, opening a device can act on it, opening a socket fails, and
2131/// reading a FIFO that has no writer would block the check.
2132///
2133/// Every fact about one file (its type, header, ELF headers, mode bits and
2134/// capabilities) is read through one open descriptor, so they all describe
2135/// the same file. The check is still advisory, not containment: the `execve`
2136/// opens the path again, and a file replaced at that path in between is not
2137/// checked again.
2138///
2139/// Not covered, and recorded on
2140/// <https://github.com/rrnewton/hermit/issues/3520>:
2141/// - The loader itself, the C library's initialization, the executable's
2142///   `IFUNC` resolvers and the constructors of the executable's own shared
2143///   libraries all run before the runtime's constructor in every guest.
2144/// - Secure-execution mode that a security module or a parent with differing
2145///   real and effective IDs imposes; only the file's own mode bits and
2146///   capabilities are checked.
2147/// - Whether the `PT_INTERP` loader honours `LD_PRELOAD` at all.
2148/// - A `binfmt_misc` rule, which the kernel consults before its ELF and script
2149///   handlers, and which can hand a file this check accepted to another
2150///   interpreter (for example an emulator).
2151/// - A runtime library the loader fails to load: the loader continues without
2152///   it, and only the launch's check that the guest connected before it exited
2153///   reports the failure, after the guest ran.
2154/// - A `pre_exec` callback on `command` that changes the working directory or
2155///   the root directory: the check resolves paths against the working
2156///   directory `command` is configured with, in Hermit's root, before any
2157///   such callback runs.
2158#[cfg(feature = "liteinst")]
2159fn in_guest_liteinst_program_gap(command: &mut Command) -> Result<Option<String>, Error> {
2160    use std::io::Read;
2161    use std::os::unix::fs::OpenOptionsExt;
2162
2163    use script::KernelScript;
2164
2165    let mut path = pin_spawn_program(command)?;
2166    for _ in 0..=MAX_INTERPRETER_DEPTH {
2167        let shown = path.display().to_string();
2168        let not_regular = || {
2169            Ok(Some(format!(
2170                "{shown} is not a regular file, and the kernel executes only regular files"
2171            )))
2172        };
2173        if !fs::metadata(&path)
2174            .with_context(|| format!("cannot read guest program {shown}"))?
2175            .is_file()
2176        {
2177            return not_regular();
2178        }
2179        // The path can change type after the check above. O_NONBLOCK then keeps
2180        // the open of a FIFO that has no writer from blocking, and O_NOCTTY
2181        // keeps a terminal from becoming Hermit's controlling terminal; neither
2182        // changes how a regular file is read. The descriptor's own type is
2183        // checked again below.
2184        let mut file = fs::OpenOptions::new()
2185            .read(true)
2186            .custom_flags(libc::O_NONBLOCK | libc::O_NOCTTY)
2187            .open(&path)
2188            .with_context(|| format!("cannot read guest program {shown}"))?;
2189        if !file
2190            .metadata()
2191            .with_context(|| format!("cannot read guest program {shown}"))?
2192            .is_file()
2193        {
2194            return not_regular();
2195        }
2196        let mut header = Vec::new();
2197        (&file)
2198            .take(256)
2199            .read_to_end(&mut header)
2200            .with_context(|| format!("cannot read guest program {shown}"))?;
2201        match KernelScript::from_buf(&header) {
2202            KernelScript::Interpreter(interpreter) => {
2203                // The kernel opens a relative interpreter against the working
2204                // directory of the process calling execve.
2205                path = match command.get_current_dir() {
2206                    Some(directory) => directory.join(interpreter),
2207                    None => interpreter,
2208                };
2209                continue;
2210            }
2211            KernelScript::Declined => {
2212                return Ok(Some(format!(
2213                    "{shown} starts with #! but the kernel's script handler declines it (it \
2214                     names no interpreter, or the interpreter path fills the first 256 bytes), \
2215                     so the kernel would fail the execve or hand the file to another handler \
2216                     that need not load the runtime"
2217                )));
2218            }
2219            KernelScript::NotScript => {}
2220        }
2221        let Some(startup) = interp::elf_startup(&mut file)
2222            .with_context(|| format!("cannot read the ELF headers of guest program {shown}"))?
2223        else {
2224            return Ok(Some(format!(
2225                "{shown} is neither an ELF executable nor a #! script, so the kernel would hand \
2226                 it to a handler that need not load the runtime"
2227            )));
2228        };
2229        let reason = if !startup.x86_64 {
2230            "is not a 64-bit x86-64 ELF image, so the dynamic loader cannot load the 64-bit \
2231             runtime into it"
2232        } else if !startup.has_interp {
2233            "is statically linked (it has no PT_INTERP), so no dynamic loader runs to load the \
2234             runtime"
2235        } else if is_secure_execution_image(&file, &shown)? {
2236            "is set-user-ID, set-group-ID or has file capabilities, so the kernel runs it in \
2237             secure-execution mode, where the dynamic loader ignores LD_PRELOAD"
2238        } else if startup.has_preinit_array {
2239            "has a DT_PREINIT_ARRAY, whose functions run before the runtime's constructor"
2240        } else {
2241            return Ok(None);
2242        };
2243        return Ok(Some(format!(
2244            "guest program {shown} {reason}, so it would run unmonitored if Hermit launched it"
2245        )));
2246    }
2247    Ok(Some(format!(
2248        "the #! interpreters of guest program {} nest more than {MAX_INTERPRETER_DEPTH} deep",
2249        Path::new(command.get_program()).display()
2250    )))
2251}
2252
2253/// Resolves the program `command` executes with Reverie's
2254/// [`Command::find_program`], the call the in-guest launch makes, and pins the
2255/// result on `command`, keeping `argv[0]`.
2256///
2257/// The result is absolute, so the launch's own `find_program` returns it
2258/// unchanged and the launch executes the same path the check inspected,
2259/// whatever `PATH` or working directory either side would otherwise use. The
2260/// path, not its contents: a file replaced at that path in between is not
2261/// checked again (see [`in_guest_liteinst_program_gap`]). A program the launch
2262/// could not find is an error here, as it would be at launch.
2263#[cfg(feature = "liteinst")]
2264fn pin_spawn_program(command: &mut Command) -> Result<PathBuf, Error> {
2265    let arg0 = command.get_arg0().to_owned();
2266    let program = command.find_program().with_context(|| {
2267        format!(
2268            "cannot find guest program {}",
2269            Path::new(command.get_program()).display()
2270        )
2271    })?;
2272    command.program(&program).arg0(arg0);
2273    Ok(program)
2274}
2275
2276/// Whether executing `file` (shown as `shown`) puts the kernel in
2277/// secure-execution mode (`AT_SECURE`): set-user-ID, set-group-ID (with group
2278/// execute, as the kernel requires), or a `security.capability` attribute.
2279#[cfg(feature = "liteinst")]
2280fn is_secure_execution_image(file: &fs::File, shown: &str) -> Result<bool, Error> {
2281    let mode = file
2282        .metadata()
2283        .with_context(|| format!("cannot stat guest program {shown}"))?
2284        .permissions()
2285        .mode();
2286    if mode & 0o4000 != 0 || mode & 0o2010 == 0o2010 {
2287        return Ok(true);
2288    }
2289    // SAFETY: the descriptor is open for the call, the name is NUL-terminated,
2290    // and a zero-sized query writes no value.
2291    let size = unsafe {
2292        libc::fgetxattr(
2293            file.as_raw_fd(),
2294            c"security.capability".as_ptr(),
2295            std::ptr::null_mut(),
2296            0,
2297        )
2298    };
2299    if size >= 0 {
2300        return Ok(true);
2301    }
2302    match io::Error::last_os_error().raw_os_error() {
2303        Some(libc::ENODATA) | Some(libc::ENOTSUP) => Ok(false),
2304        _ => Err(Error::new(io::Error::last_os_error()).context(format!(
2305            "cannot read the file capabilities of guest program {shown}"
2306        ))),
2307    }
2308}
2309
2310/// Refuses a wall-clock bound on an in-guest LiteInst run. The bound drops the
2311/// run's future, and dropping it neither kills the guest nor stops the
2312/// blocking task that waits for it.
2313#[cfg(feature = "liteinst")]
2314fn refuse_in_guest_liteinst_timeout(
2315    backend: Backend,
2316    timeout: Option<Duration>,
2317) -> Result<(), Error> {
2318    if backend.is_in_guest_liteinst() && timeout.is_some() {
2319        return Err(Error::new(LiteinstInGuestRefusal::for_backend(
2320            backend,
2321            "a wall-clock timeout has not been qualified for the in-guest runtime, whose run \
2322             future does not kill the guest when it is dropped",
2323        )));
2324    }
2325    Ok(())
2326}
2327
2328#[cfg(not(feature = "liteinst"))]
2329fn refuse_in_guest_liteinst_timeout(
2330    _backend: Backend,
2331    _timeout: Option<Duration>,
2332) -> Result<(), Error> {
2333    Ok(())
2334}
2335
2336#[cfg(feature = "liteinst")]
2337fn liteinst_unavailable_reason() -> Option<String> {
2338    liteinst_runtime_unavailable_reason()
2339}
2340
2341// LiteInst is an experimental backend whose architecture is not settled, so the
2342// published crate leaves it out: `reverie-liteinst` (and through it `liteinst2`)
2343// is an optional dependency behind the `liteinst` feature, which
2344// `third-party-backends` enables for every validation build.
2345#[cfg(not(feature = "liteinst"))]
2346fn liteinst_unavailable_reason() -> Option<String> {
2347    Some("this build was compiled without the liteinst backend: the `liteinst` feature is not enabled; rebuild with `--features liteinst` (or `--features third-party-backends`). This says nothing about whether LiteInst works on this machine -- it has not been checked".to_owned())
2348}
2349
2350#[cfg(feature = "liteinst")]
2351fn liteinst_runtime_unavailable_reason() -> Option<String> {
2352    liteinst_tool_runtime_library_path().err().map(|error| {
2353        format!(
2354            "the in-guest Detcore runtime is unavailable: {error}; build it with `cargo build -p detcore-liteinst` in the target directory of this hermit binary, install it as rsrcs/libdetcore_liteinst.so, or name it with {LITEINST_TOOL_RUNTIME_ENV}"
2355        )
2356    })
2357}
2358
2359fn kvm_device_unavailable_reason(path: &Path) -> Option<String> {
2360    fs::OpenOptions::new()
2361        .read(true)
2362        .write(true)
2363        .open(path)
2364        .err()
2365        .map(|error| {
2366            format!(
2367                "cannot open {} read-write: {error}; grant access through the device owner/group \
2368                 or root",
2369                path.display()
2370            )
2371        })
2372}
2373
2374/// Process instrumentation backend used to run a Hermit guest.
2375#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, ValueEnum)]
2376pub enum Backend {
2377    /// Use Reverie's ptrace backend.
2378    #[default]
2379    Ptrace,
2380    /// Use LiteInst, with Detcore's Tool running inside the guest.
2381    Liteinst,
2382    /// Use in-guest LiteInst with syscall site patching off: the same
2383    /// in-guest Detcore host as `liteinst`, but every trapped syscall reaches
2384    /// the Tool through the seccomp `SIGSYS` fallback and no syscall site is
2385    /// rewritten. Hermit sets `REVERIE_LITEINST_SITE_PATCHING=0` in the
2386    /// guest's environment and refuses a run whose environment asks for any
2387    /// other value.
2388    InGuestTrap,
2389    /// Use the KVM backend.
2390    Kvm,
2391}
2392
2393/// A requested backend could not start because its required runtime support is absent.
2394///
2395/// Keep this as a typed error until the top-level command records its failure class.
2396/// The human-readable message is for the operator; consumers must use the class line
2397/// emitted by the command rather than matching this prose.
2398#[derive(Debug, Clone, Eq, PartialEq)]
2399pub struct BackendUnavailable {
2400    backend: Backend,
2401    reason: String,
2402}
2403
2404impl BackendUnavailable {
2405    pub fn new(backend: Backend, reason: impl Into<String>) -> Self {
2406        Self {
2407            backend,
2408            reason: reason.into(),
2409        }
2410    }
2411
2412    pub fn backend(&self) -> Backend {
2413        self.backend
2414    }
2415
2416    pub fn reason(&self) -> &str {
2417        &self.reason
2418    }
2419}
2420
2421impl std::fmt::Display for BackendUnavailable {
2422    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2423        write!(
2424            formatter,
2425            "backend `{}` is unavailable: {}",
2426            self.backend.as_str(),
2427            self.reason
2428        )
2429    }
2430}
2431
2432impl std::error::Error for BackendUnavailable {}
2433
2434impl Backend {
2435    const ALL: [Self; 4] = [Self::Ptrace, Self::Liteinst, Self::InGuestTrap, Self::Kvm];
2436
2437    /// Returns the command-line spelling for this backend.
2438    pub const fn as_str(self) -> &'static str {
2439        match self {
2440            Self::Ptrace => "ptrace",
2441            Self::Liteinst => "liteinst",
2442            Self::InGuestTrap => "in-guest-trap",
2443            Self::Kvm => "kvm",
2444        }
2445    }
2446
2447    /// Whether this backend hosts Detcore's Tool inside each guest process
2448    /// through the in-guest LiteInst preload runtime: `liteinst` and
2449    /// `in-guest-trap`, which differ only in whether syscall sites are patched.
2450    /// Every refusal, capability and launch step of in-guest LiteInst applies to
2451    /// both.
2452    pub const fn is_in_guest_liteinst(self) -> bool {
2453        matches!(self, Self::Liteinst | Self::InGuestTrap)
2454    }
2455
2456    /// The name of the backend runtime that executes this backend's guest, as
2457    /// its statistics source names itself (`BackendStatsSnapshot::BACKEND_NAME`).
2458    /// `in-guest-trap` runs the in-guest LiteInst runtime, so its statistics come
2459    /// from that runtime's source, which is named `liteinst`.
2460    pub const fn runtime_name(self) -> &'static str {
2461        match self {
2462            Self::InGuestTrap => Self::Liteinst.as_str(),
2463            other => other.as_str(),
2464        }
2465    }
2466
2467    /// Whether a run on this backend can deliver a precise timer event late
2468    /// (Reverie's skid-overshoot witness), which then refuses the run: exactly
2469    /// the backends that deliver preemption timer events, as their capability
2470    /// states ([`backend_delivers_preemption_timer`]). A backend that delivers
2471    /// none cannot produce the witness, and must not consume another run's.
2472    fn consumes_skid_overshoot_reports(self) -> bool {
2473        backend_delivers_preemption_timer(self)
2474    }
2475
2476    /// Returns backends whose Hermit integration prerequisites are met.
2477    ///
2478    /// Some integrations use CLI launch adapters rather than direct
2479    /// [`run_with_backend`] dispatch.
2480    pub fn available() -> impl Iterator<Item = Self> {
2481        Self::ALL
2482            .into_iter()
2483            .filter(|backend| backend.is_available())
2484    }
2485
2486    /// Returns whether this backend's integration prerequisites are met.
2487    pub fn is_available(self) -> bool {
2488        self.unavailable_reason().is_none()
2489    }
2490
2491    /// Returns an actionable error when this backend's prerequisites are not met.
2492    pub fn ensure_available(self) -> Result<(), Error> {
2493        self.ensure_available_for(NamespaceUse::Creates)
2494    }
2495
2496    /// [`Backend::ensure_available`] for a run that creates `namespaces`: a
2497    /// `--no-namespace` ptrace run is not refused where only `unshare` of a
2498    /// user namespace is denied, since it never makes that call; the ptrace
2499    /// and seccomp probes still apply.
2500    pub fn ensure_available_for(self, namespaces: NamespaceUse) -> Result<(), Error> {
2501        if let Some(reason) = self.unavailable_reason_for(namespaces) {
2502            Err(Error::new(BackendUnavailable::new(self, reason)))
2503        } else {
2504            Ok(())
2505        }
2506    }
2507
2508    fn unavailable_reason(self) -> Option<String> {
2509        self.unavailable_reason_for(NamespaceUse::Creates)
2510    }
2511
2512    fn unavailable_reason_for(self, namespaces: NamespaceUse) -> Option<String> {
2513        match self {
2514            Self::Ptrace => validate_tracing_environment(namespaces)
2515                .err()
2516                .map(|error| error.to_string()),
2517            // The same runtime, so the same prerequisites and the same reason.
2518            Self::Liteinst | Self::InGuestTrap => liteinst_unavailable_reason(),
2519            Self::Kvm => kvm_device_unavailable_reason(Path::new("/dev/kvm")),
2520        }
2521    }
2522}
2523
2524struct SkidOvershootReport {
2525    enabled: bool,
2526}
2527
2528/// The backend failed while running the guest, after Hermit launched it: the
2529/// DBT evidence check, a KVM execution failure, a SaBRe guest that never
2530/// reached Detcore. `run --verify` records it as `run_failed` with
2531/// `backend_failure` instead of leaving the `not_run` stamp.
2532///
2533/// It sits in the chain below the message that names the failure, so the
2534/// first `Error:` line is unchanged; it crosses the container error boundary
2535/// as [`error::FailureKind::BackendRunFailure`].
2536#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2537pub struct BackendRunFailure;
2538
2539impl std::fmt::Display for BackendRunFailure {
2540    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2541        f.write_str("the backend failed while running the guest")
2542    }
2543}
2544
2545impl std::error::Error for BackendRunFailure {}
2546
2547/// A run on a backend that delivers precise timer events (ptrace, e9patch
2548/// preprocessing on it, or KVM) observed late precise-timer delivery, so Hermit
2549/// refuses to treat the completed execution as deterministic evidence.
2550///
2551/// This is a type rather than a message match because it crosses the container
2552/// error boundary as [`error::FailureKind::PolicyRefusal`].
2553#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2554pub struct SkidOvershootError {
2555    count: u64,
2556}
2557
2558impl SkidOvershootError {
2559    pub fn new(count: u64) -> Self {
2560        assert!(
2561            count > 0,
2562            "a skid-overshoot refusal requires a positive count"
2563        );
2564        Self { count }
2565    }
2566
2567    pub fn count(&self) -> u64 {
2568        self.count
2569    }
2570}
2571
2572impl std::fmt::Display for SkidOvershootError {
2573    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2574        write!(
2575            f,
2576            "observed {} {} report(s); refusing the result because precise PMU timer \
2577             delivery passed its target and deterministic execution was not established",
2578            self.count,
2579            reverie::SKID_OVERSHOOT_MARKER
2580        )
2581    }
2582}
2583
2584impl std::error::Error for SkidOvershootError {}
2585
2586fn take_skid_overshoot_error() -> Option<SkidOvershootError> {
2587    let count = reverie::take_skid_overshoot_count();
2588    (count > 0).then(|| SkidOvershootError::new(count))
2589}
2590
2591impl SkidOvershootReport {
2592    fn begin(enabled: bool) -> Self {
2593        if enabled {
2594            // A previous invocation in this process must not qualify this one.
2595            let _ = reverie::take_skid_overshoot_count();
2596        }
2597        Self { enabled }
2598    }
2599
2600    fn finish<T>(self, result: Result<T, Error>) -> Result<T, Error> {
2601        if !self.enabled {
2602            return result;
2603        }
2604
2605        let Some(overshoot) = take_skid_overshoot_error() else {
2606            return result;
2607        };
2608        match result {
2609            Ok(_) => Err(Error::new(overshoot)),
2610            Err(error) => Err(error.context(overshoot)),
2611        }
2612    }
2613
2614    fn finish_with_count<T>(self, result: Result<T, Error>) -> Result<(T, u64), Error> {
2615        if !self.enabled {
2616            return result.map(|value| (value, 0));
2617        }
2618
2619        let count = reverie::take_skid_overshoot_count();
2620        match result {
2621            Ok(value) => Ok((value, count)),
2622            Err(error) if count > 0 => Err(error.context(SkidOvershootError::new(count))),
2623            Err(error) => Err(error),
2624        }
2625    }
2626}
2627
2628// SaBRe's statistics and RPC support are selected only by `sabre`; the external
2629// loader and Detcore plugin must also be supplied. e9patch preprocessing requires
2630// external `e9tool`/`e9patch` artifacts. Neither path is enabled by default.
2631
2632// TODO-HUMAN-REVIEW(PR-739): Review SaBRe loader discovery and executable validation.
2633
2634/// `detcore_liteinst::DETLOG_FORWARD_ENV`; that crate is a preload `cdylib`, so
2635/// Hermit cannot import the constant.
2636#[cfg(feature = "liteinst")]
2637const LITEINST_DETLOG_FORWARD_ENV: &str = "HERMIT_LITEINST_FORWARD_DETLOG";
2638/// `detcore_liteinst::DETLOG_FORWARD_POLICY_ENV`, for the same reason.
2639#[cfg(feature = "liteinst")]
2640const LITEINST_DETLOG_FORWARD_POLICY_ENV: &str = "HERMIT_LITEINST_FORWARD_DETLOG_POLICY";
2641
2642/// Where in-guest LiteInst and SaBRe runs in this process forward Detcore's
2643/// DETLOG records; see [`forward_in_guest_detlogs_to`].
2644static IN_GUEST_DETLOG_SINK: Mutex<Option<std::os::fd::OwnedFd>> = Mutex::new(None);
2645
2646/// Makes the in-guest LiteInst and SaBRe runs this process starts forward the
2647/// DETLOG records Detcore logs inside the guest to `socket`, the sending end of a Unix
2648/// `SOCK_SEQPACKET` pair. `hermit run --verify` gives each run its own pair and
2649/// reads the records from the other end into that run's log. Each record is one
2650/// message, one line (`detcore::detlog::forwarded_line`).
2651/// A record is forwarded when the CLI's filter logs INFO for its module (see
2652/// [`in_guest_detlog_forward_policy`]); without a socket none are forwarded.
2653#[doc(hidden)]
2654pub fn forward_in_guest_detlogs_to(socket: std::os::fd::OwnedFd) {
2655    *IN_GUEST_DETLOG_SINK.lock().unwrap() = Some(socket);
2656}
2657
2658/// Release the coordinator's source endpoint after actual physical teardown.
2659#[doc(hidden)]
2660pub fn clear_in_guest_detlog_forwarding() {
2661    IN_GUEST_DETLOG_SINK.lock().unwrap().take();
2662}
2663
2664/// What Hermit tells the in-guest LiteInst runtime, which travels in the
2665/// sealed preload bootstrap rather than the guest's environment
2666/// ([`detcore::in_guest_launch`]), so none of it lands on the guest's initial
2667/// stack or in `/proc/self/environ`:
2668/// - this build's [`detcore::config_wire_fingerprint`], which the runtime
2669///   compares with its own before it decodes the configuration the
2670///   coordinator sends. A runtime built from another tree then fails naming
2671///   both fingerprints and its own path, not with a bare decoding error
2672///   (<https://github.com/rrnewton/hermit/issues/3986>);
2673/// - when `policy` forwards any record, the socket chosen with
2674///   [`forward_in_guest_detlogs_to`] (an inheritable duplicate, which the
2675///   runtime moves to a descriptor Reverie reserves and protects from the
2676///   guest) and the encoded policy, which the guest's Detcore applies to each
2677///   record by its module as each in-process `detlog!` callsite asks tracing.
2678///
2679/// The caller keeps the returned duplicate open until the guest has started.
2680/// Hermit's own variable names are removed from `command`, so a stale value
2681/// cannot reach a runtime that falls back to the environment.
2682#[cfg(feature = "liteinst")]
2683fn in_guest_liteinst_launch(
2684    command: &mut Command,
2685    policy: &detcore::detlog::ForwardPolicy,
2686) -> Result<
2687    (
2688        detcore::in_guest_launch::InGuestLaunch,
2689        Option<std::os::fd::OwnedFd>,
2690    ),
2691    Error,
2692> {
2693    command.env_remove(LITEINST_DETLOG_FORWARD_ENV);
2694    command.env_remove(LITEINST_DETLOG_FORWARD_POLICY_ENV);
2695    command.env_remove(detcore::LITEINST_CONFIG_FINGERPRINT_ENV);
2696    let mut launch = detcore::in_guest_launch::InGuestLaunch {
2697        coordinator_fingerprint: Some(detcore::config_wire_fingerprint()),
2698        ..Default::default()
2699    };
2700    // Asking only whether INFO is on for `detcore`, and then forwarding
2701    // everything, kept records a target-scoped filter drops under ptrace
2702    // (info,detcore::random=warn) and could forward nothing where it keeps
2703    // some (warn,detcore::random=info).
2704    if !policy.forwards_any() {
2705        return Ok((launch, None));
2706    }
2707    let Some(duplicate) = inheritable_in_guest_detlog_socket()? else {
2708        return Ok((launch, None));
2709    };
2710    launch.forward_request = Some(detcore::detlog::tool_output_env_value(
2711        duplicate.as_raw_fd(),
2712    )?);
2713    launch.forward_policy = Some(policy.encode());
2714    Ok((launch, Some(duplicate)))
2715}
2716
2717/// An inheritable duplicate of the socket chosen with
2718/// [`forward_in_guest_detlogs_to`], or `None` when none was chosen. The
2719/// caller passes its number to the guest and keeps it open until the guest
2720/// has started.
2721#[doc(hidden)]
2722pub fn inheritable_in_guest_detlog_socket() -> Result<Option<std::os::fd::OwnedFd>, Error> {
2723    let sink = IN_GUEST_DETLOG_SINK.lock().unwrap();
2724    let Some(socket) = sink.as_ref() else {
2725        return Ok(None);
2726    };
2727    // SAFETY: F_DUPFD on an open descriptor returns a new descriptor or -1.
2728    let duplicate = unsafe { libc::fcntl(socket.as_raw_fd(), libc::F_DUPFD, 3) };
2729    if duplicate < 0 {
2730        return Err(Error::new(std::io::Error::last_os_error())
2731            .context("duplicating the in-guest DETLOG forwarding socket"));
2732    }
2733    // SAFETY: fcntl returned a new descriptor this function now owns.
2734    Ok(Some(unsafe {
2735        std::os::fd::OwnedFd::from_raw_fd(duplicate)
2736    }))
2737}
2738
2739/// The INFO policy for DETLOG records an in-guest Detcore (SaBRe's plugin,
2740/// in-guest LiteInst) forwards: the current subscriber's answer for every
2741/// target the CLI's `RUST_LOG` names (the text
2742/// `EffectiveFilter::from_default_env` builds every CLI subscriber from) and
2743/// for any other target.
2744#[doc(hidden)]
2745pub fn in_guest_detlog_forward_policy() -> detcore::detlog::ForwardPolicy {
2746    let directives = std::env::var(tracing_subscriber::EnvFilter::DEFAULT_ENV).unwrap_or_default();
2747    detcore::detlog::ForwardPolicy::from_current_subscriber(&directives)
2748}
2749
2750// TODO-HUMAN-REVIEW(PR-845): Review the neutral-name workaround for SaBRe's
2751// dynamic-loader prefix collision.
2752
2753// TODO-HUMAN-REVIEW(PR-738): Review controller/plugin artifact separation.
2754
2755// AUTONOMOUS-BOT-IMPLEMENTED
2756// TODO-HUMAN-REVIEW(PR-774): Review the bounded SaBRe RPC disconnect drain.
2757
2758// AUTONOMOUS-BOT-IMPLEMENTED
2759// TODO-HUMAN-REVIEW(PR-782): Review SaBRe RPC server shutdown errors.
2760
2761// AUTONOMOUS-BOT-IMPLEMENTED
2762// TODO-HUMAN-REVIEW(PR-789): Review complete SaBRe RPC shutdown ordering.
2763
2764fn ensure_backend_dispatch(backend: Backend) -> Result<(), Error> {
2765    // The CLI probes ptrace readiness before entering its container; repeating
2766    // the namespace probe here would test nested namespaces instead of the host.
2767    if backend == Backend::Ptrace {
2768        return Ok(());
2769    }
2770
2771    // KVM and DBT have dedicated dispatches (`run_kvm` and `run_dbt`); neither
2772    // must reach this generic rejection path.
2773    backend.ensure_available()?;
2774    Err(anyhow!(
2775        "backend `{}` has no Hermit dispatch implementation",
2776        backend.as_str()
2777    ))
2778}
2779
2780// AUTONOMOUS-BOT-IMPLEMENTED
2781// TODO-HUMAN-REVIEW(PR-738): Review SaBRe coordinator lifetime and artifact loading.
2782
2783/// Guest-physical memory available to the single-process KVM personality.
2784// The KVM personality is a sparse MAP_NORESERVE address space. QEMU needs room
2785// for its own ELF mappings in addition to the nested machine's RAM mapping.
2786// reverie-kvm places every guest mapping in this window, even a PROT_NONE
2787// reservation, so a runtime that reserves address space up front needs room
2788// here: node's V8 reserves 256, 128 and then 512 MiB twice, which 1 GiB could
2789// not hold (https://github.com/rrnewton/hermit/issues/4366). 3 GiB is the
2790// largest window reverie-kvm's bootstrap identity-maps. The window costs the
2791// host only the pages a guest touches, and mmap and munmap discard ranges of
2792// 2 MiB or more rather than writing zeros over them
2793// (https://github.com/rrnewton/reverie/pull/1059). madvise(MADV_DONTNEED)
2794// still writes zeros over every page, and node calls it over its 512 MiB
2795// reservation, so a node run peaks near 880 MB of host memory until that is
2796// made lazy too (https://github.com/rrnewton/hermit/issues/4366).
2797#[cfg(target_arch = "x86_64")]
2798const KVM_GUEST_MEMORY_BYTES: usize = 3 * 1024 * 1024 * 1024;
2799
2800/// Maximum `#!` interpreter indirection levels, matching the Linux kernel's
2801/// `BINPRM_MAX_RECURSION` limit for chained script interpreters.
2802#[cfg(target_arch = "x86_64")]
2803const MAX_SHEBANG_DEPTH: usize = 4;
2804
2805/// Resolve `#!` interpreter scripts before the reverie-kvm ELF loader runs.
2806///
2807/// The KVM ELF loader can only map ELF images, so a guest program that is
2808/// actually a `#!`-script (for example `/usr/local/bin/file` -> `#!/bin/bash`,
2809/// or `/usr/bin/pkg-config` -> `#!/usr/bin/sh`) must be rewritten to launch its
2810/// interpreter, exactly as the kernel's `execve(2)` `binfmt_script` handler
2811/// does. On success the returned file is the ELF image to load and `argv` has
2812/// the interpreter, its shebang arguments, and the script path prepended in
2813/// kernel order: `[interp, shebang_args.., script_path, <original argv[1..]>]`.
2814/// Each file is opened once and its `#!` line is read from that open file, so
2815/// the file returned is the one whose bytes were examined, as the kernel execs
2816/// the file it read.
2817///
2818/// The interpreter line is parsed with hermit's shared [`Shebang`] so the KVM
2819/// backend matches how the ptrace backend and recorder treat `#!`-scripts.
2820#[cfg(target_arch = "x86_64")]
2821fn resolve_kvm_shebang(
2822    resolved_program: &Path,
2823    mut argv: Vec<String>,
2824) -> Result<(PathBuf, Vec<String>, fs::File), Error> {
2825    let mut load_path = resolved_program.to_path_buf();
2826    let (mut file, mut image) = read_kvm_image(&load_path)
2827        .map_err(|error| anyhow!("failed to read KVM guest executable {load_path:?}: {error}"))?;
2828
2829    let mut depth = 0;
2830    while image.starts_with(b"#!") {
2831        depth += 1;
2832        if depth > MAX_SHEBANG_DEPTH {
2833            return Err(anyhow!(
2834                "too many levels of `#!` interpreter indirection loading {resolved_program:?}"
2835            ));
2836        }
2837        let (interpreter, shebang_args) = Shebang::from_buf(&image)
2838            .ok_or_else(|| anyhow!("malformed `#!` interpreter line in {load_path:?}"))?
2839            .into_parts();
2840        let interpreter_str = interpreter
2841            .to_str()
2842            .ok_or_else(|| anyhow!("non-UTF-8 `#!` interpreter path in {load_path:?}"))?
2843            .to_owned();
2844
2845        // Rewrite argv in kernel order. The prior argv[0] (the script's own
2846        // name) is dropped on the first level; on deeper levels the previous
2847        // interpreter path is preserved as a positional argument, matching
2848        // `binfmt_script`.
2849        let mut rewritten = Vec::with_capacity(argv.len() + shebang_args.len() + 2);
2850        rewritten.push(interpreter_str);
2851        for arg in &shebang_args {
2852            rewritten.push(
2853                arg.to_str()
2854                    .ok_or_else(|| anyhow!("non-UTF-8 `#!` interpreter argument in {load_path:?}"))?
2855                    .to_owned(),
2856            );
2857        }
2858        rewritten.push(load_path.to_string_lossy().into_owned());
2859        rewritten.extend_from_slice(&argv[1..]);
2860        argv = rewritten;
2861
2862        load_path = interpreter;
2863        (file, image) = read_kvm_image(&load_path).map_err(|error| {
2864            anyhow!(
2865                "failed to read `#!` interpreter {load_path:?} for {resolved_program:?}: {error}"
2866            )
2867        })?;
2868    }
2869
2870    Ok((load_path, argv, file))
2871}
2872
2873/// Opens `path` and reads its whole contents from that open file.
2874#[cfg(target_arch = "x86_64")]
2875fn read_kvm_image(path: &Path) -> io::Result<(fs::File, Vec<u8>)> {
2876    let mut file = fs::File::open(path)?;
2877    let mut image = Vec::new();
2878    io::Read::read_to_end(&mut file, &mut image)?;
2879    Ok((file, image))
2880}
2881
2882/// The container replaces this host directory with a private, empty tmpfs, so
2883/// nothing beneath it is visible to the guest. Kept next to the diagnostic that
2884/// depends on it; `run.rs` has its own `TMP_DIR` for the mount itself.
2885#[cfg(target_arch = "x86_64")]
2886const CONTAINER_REPLACED_TMP: &str = "/tmp";
2887
2888/// Explain why a working directory that plainly exists on the host could not be
2889/// resolved for the guest.
2890///
2891/// ⚠️ THIS FUNCTION EXISTS BECAUSE THE BARE ERROR CAUSED A MISDIAGNOSIS, not to
2892/// be tidy. `fs::canonicalize` runs after the container's mounts are applied, and
2893/// the container bind-mounts a fresh empty tmpfs over `/tmp`
2894/// (`bin/hermit/container.rs`). So a cwd beneath `/tmp` is genuinely absent from
2895/// the guest's mount namespace and `canonicalize` returns a perfectly correct
2896/// `NotFound` -- for a directory the user can `ls`.
2897///
2898/// Reported 2026-08-25 as "run_kvm_ cli tests fail from a git worktree". They do
2899/// not. Measured across five working directories with one binary: a PLAIN
2900/// directory under `/tmp` with no git in it FAILS, and a linked git worktree
2901/// under `/home` WORKS. The only property that tracks the failure is "the cwd is
2902/// under /tmp"; `/tmp` itself resolves because the mount POINT exists. The git
2903/// hypothesis cost real investigation time, and it came from this message saying
2904/// only "No such file or directory".
2905///
2906/// It mattered more than a stray report because the historical landing recipe
2907/// placed detached worktrees below `/tmp`, exactly where this fires. Current
2908/// dev-hermit worktrees are created through wrkslots below `worktrees/`, but the
2909/// diagnostic remains necessary for arbitrary callers and older checkouts.
2910///
2911/// The message names the mechanism and both ways out. It deliberately does NOT
2912/// silently fall back to `/`: changing the guest's working directory out from
2913/// under a caller who asked for a specific one would trade a loud, correct
2914/// failure for a quiet, wrong success.
2915#[cfg(target_arch = "x86_64")]
2916fn kvm_cwd_resolution_error(requested_cwd: &Path, error: &std::io::Error) -> Error {
2917    let under_replaced_tmp = requested_cwd.starts_with(CONTAINER_REPLACED_TMP)
2918        && requested_cwd != Path::new(CONTAINER_REPLACED_TMP);
2919    if error.kind() == std::io::ErrorKind::NotFound && under_replaced_tmp {
2920        anyhow!(
2921            "failed to resolve KVM guest working directory {:?}: {error}\n\
2922             \n\
2923             This directory may well exist on the host. The container replaces \
2924             {CONTAINER_REPLACED_TMP} with a private, empty tmpfs, so nothing beneath \
2925             {CONTAINER_REPLACED_TMP} is visible to the guest, and resolving the working \
2926             directory happens inside that mount namespace.\n\
2927             Either run from a directory outside {CONTAINER_REPLACED_TMP}, or pass \
2928             --workdir with a path that exists inside the guest.",
2929            requested_cwd
2930        )
2931    } else {
2932        anyhow!(
2933            "failed to resolve KVM guest working directory {:?}: {error}",
2934            requested_cwd
2935        )
2936    }
2937}
2938
2939/// Bind KVM's raw mountinfo and fdinfo to the same filesystem namespace.
2940///
2941/// The CLI supplies IdentityGuard provenance from the completed container: the
2942/// namespace's row/parent order and the namespace it was captured in
2943/// ([`detcore_model::config::MountNamespaceIdentity`], whose kernel ID Linux
2944/// never reuses during a boot). The backend requires its own mount namespace
2945/// to be that one and keeps the producer's order, the same snapshot the
2946/// container mount table was built from; it does not take a second one, and
2947/// it refuses provenance that is not well formed (a duplicate ID, an empty
2948/// order, an assignment Detcore's pool would refuse, a root rewrite or table
2949/// row outside the order).
2950///
2951/// Two captures of one private namespace need not agree. When a host process
2952/// removes the directory a host mount was on, Linux detaches the mounts on that
2953/// dentry in every mount namespace (`vfs_rmdir`, `vfs_unlink` and rename call
2954/// `detach_mounts`, and `__detach_mounts` in fs/namespace.c unmounts each
2955/// mount on the dentry with `umount_tree`), so a copied host row can leave the
2956/// container between two reads although nothing propagates into it. The
2957/// snapshot keeps naming that row. A detached mount stays usable through what
2958/// still holds it (an open descriptor, a working directory, a directory
2959/// descriptor), and fdinfo `mnt_id` and STATX_MNT_ID still number it through
2960/// the order; later mountinfo views lack it and stay ordered subsets of the
2961/// snapshot, which Detcore's validation accepts. A kernel automount (an autofs
2962/// trigger reached by an ordinary path lookup) can add a row after the
2963/// snapshot despite the private propagation: fdinfo and statx number its new
2964/// ID on first observation, and a later kernel-view mountinfo that shows it is
2965/// not an ordered subset and is refused. The container mount table never shows
2966/// it.
2967/// A library caller that built no container and supplies no provenance gets
2968/// this capture; `--no-namespace` runs supply it from Hermit's own namespace
2969/// (`complete_container_config`). Either way Detcore renders the guest's mount
2970/// files from the container mount table. KVM does not permit guest namespace
2971/// changes.
2972#[cfg(target_arch = "x86_64")]
2973fn prepare_kvm_mountinfo_config(
2974    config: &mut DetConfig,
2975    namespace: impl FnOnce() -> Result<detcore_model::config::MountNamespaceIdentity, Error>,
2976    capture: impl FnOnce() -> Result<Vec<u64>, Error>,
2977) -> Result<(), Error> {
2978    if !config.mountinfo_mount_ids_captured
2979        && (!config.mountinfo_mount_ids.is_empty()
2980            || !config.mount_id_assignment_order.is_empty()
2981            || config.mountinfo_mount_namespace.is_some())
2982    {
2983        anyhow::bail!("KVM mount identities have no producer-captured namespace");
2984    }
2985    if config.mountinfo_mount_ids_captured {
2986        let Some(producer) = config.mountinfo_mount_namespace else {
2987            anyhow::bail!(
2988                "KVM producer-captured mount identities do not name their mount namespace"
2989            );
2990        };
2991        // Every mount namespace has its root, so a real one never yields an
2992        // empty order; and Detcore's pool refuses a malformed one.
2993        if config.mountinfo_mount_ids.is_empty()
2994            || !detcore_model::procfs::mount_identity_provenance_is_well_formed(
2995                &config.mountinfo_mount_ids,
2996                &config.mount_id_assignment_order,
2997            )
2998        {
2999            anyhow::bail!(
3000                "KVM producer-captured mount identities are malformed ({} listed, {} assigned)",
3001                config.mountinfo_mount_ids.len(),
3002                config.mount_id_assignment_order.len()
3003            );
3004        }
3005        if let Some(missing) = config
3006            .mountinfo_root_rewrites
3007            .iter()
3008            .map(|rewrite| rewrite.raw_mount_id)
3009            .find(|id| !config.mountinfo_mount_ids.contains(id))
3010        {
3011            anyhow::bail!(
3012                "KVM mount root rewrite names mount {missing}, which the producer's snapshot does \
3013                 not have"
3014            );
3015        }
3016        let backend = namespace().context("failed to identify the KVM backend mount namespace")?;
3017        if producer != backend {
3018            anyhow::bail!(
3019                "KVM producer-captured mount identities name another mount namespace \
3020                 (captured in {producer:?}, backend in {backend:?})"
3021            );
3022        }
3023    } else {
3024        let order = capture().context("failed to capture the KVM backend mount namespace")?;
3025        config.mountinfo_mount_ids = order;
3026        config.mountinfo_mount_ids_captured = true;
3027    }
3028    // The container mount table Detcore renders on KVM too names mounts of
3029    // this same namespace: refuse one built in another.
3030    if let Some(table) = &config.container_mount_table
3031        && let Some(missing) = table
3032            .mounts
3033            .iter()
3034            .map(|row| row.raw_mount_id)
3035            .find(|id| !config.mountinfo_mount_ids.contains(id))
3036    {
3037        anyhow::bail!(
3038            "KVM container mount table names mount {missing}, which the backend namespace does \
3039             not have"
3040        );
3041    }
3042    // The backend now exposes the actual namespace's devices. There is no
3043    // synthetic 0:1 root device to translate; stat and mountinfo use the same
3044    // raw identities before entering Detcore's shared DevicePool.
3045    config.mountinfo_device_rewrites.clear();
3046    Ok(())
3047}
3048
3049// AUTONOMOUS-BOT-IMPLEMENTED
3050// TODO-HUMAN-REVIEW(https://github.com/rrnewton/hermit/issues/3695): Review
3051// host metadata timestamps for KVM runs.
3052/// Whether a KVM run declares to the backend that Detcore rewrites every
3053/// metadata timestamp (`set_host_metadata_timestamps`).
3054///
3055/// The backend reports the kernel's timestamps for host files in every run;
3056/// Detcore decides what the guest sees, and with virtual metadata it rewrites
3057/// every timestamp that a stat call returns and reads the stored host values
3058/// for its own checks (`set_file_times` confirms that `utimensat` stored the
3059/// requested mtime). The setting additionally makes the backend classify the
3060/// carriers it makes for its own data (virtual files, `/proc` snapshots, the
3061/// virtual signalfd's eventfd, the self-pidfd proxy) so that they keep fixed
3062/// timestamps through every alias; without it they report the carrier's
3063/// kernel timestamps. The backend refuses the setting unless Detcore owns
3064/// every guest thread, which `run_kvm` never opts out of. It is enabled only
3065/// with virtual metadata, so that something rewrites the timestamps, and with
3066/// sequentialized threads. The setting no longer decides whether another
3067/// guest thread can read a host file's kernel timestamps before Detcore
3068/// rewrites them: the backend reports them either way, and sequentialization
3069/// alone closes that window.
3070#[cfg(target_arch = "x86_64")]
3071fn kvm_reports_stored_metadata_timestamps(config: &DetConfig) -> bool {
3072    config.virtualize_metadata && config.sequentialize_threads
3073}
3074
3075/// Refuses the KVM backend: reverie-kvm is an x86-64 VMM.
3076#[cfg(not(target_arch = "x86_64"))]
3077async fn run_kvm(
3078    _command: &Command,
3079    _config: DetConfig,
3080    _print_summary: bool,
3081    _print_summary_to_json_file: &Option<PathBuf>,
3082    _capture_output: bool,
3083) -> Result<Output, Error> {
3084    anyhow::bail!(
3085        "the KVM backend runs only on x86-64 hosts, and this host is {}",
3086        std::env::consts::ARCH
3087    )
3088}
3089
3090/// Dispatch a command onto the real reverie-kvm Tool runtime.
3091#[cfg(target_arch = "x86_64")]
3092async fn run_kvm(
3093    command: &Command,
3094    mut config: DetConfig,
3095    print_summary: bool,
3096    print_summary_to_json_file: &Option<PathBuf>,
3097    capture_output: bool,
3098) -> Result<Output, Error> {
3099    let dispatch_started = Instant::now();
3100    // Before anything is reserved: under a finite RLIMIT_AS the run would
3101    // stop wherever the guest's process tree put the backend's peak
3102    // reservation (kvm_address_space), so it is refused here, every time.
3103    kvm_address_space::admit_kvm_address_space(KVM_GUEST_MEMORY_BYTES)?;
3104    // A caller that built no container (a library caller): the KVM guest's
3105    // host file operations run in this process's namespace, whatever its
3106    // command asks (the backend applies no chroot or mount of the command),
3107    // so its table is this namespace's. A caller with no provenance at all
3108    // takes the order from the same read; malformed provenance is still
3109    // refused below.
3110    if config.container_mount_table.is_none() {
3111        let launch_cwd = command
3112            .get_current_dir()
3113            .map_or_else(|| Path::new(".").to_owned(), Path::to_owned);
3114        let (table, order) = unplanned_container_mount_table(&launch_cwd)?;
3115        if !config.mountinfo_mount_ids_captured
3116            && config.mountinfo_mount_ids.is_empty()
3117            && config.mount_id_assignment_order.is_empty()
3118            && config.mountinfo_mount_namespace.is_none()
3119        {
3120            config.mountinfo_mount_namespace = Some(current_mount_namespace()?);
3121            config.mountinfo_mount_ids = order;
3122            config.mountinfo_mount_ids_captured = true;
3123        }
3124        config.container_mount_table = Some(table);
3125    }
3126    let keep = config
3127        .container_mount_table
3128        .as_ref()
3129        .map(detcore_model::container_mounts::ContainerMountTable::shown_mount_ids)
3130        .unwrap_or_default();
3131    prepare_kvm_mountinfo_config(&mut config, current_mount_namespace, || {
3132        capture_mountinfo_identity_order_keeping(&keep)
3133    })?;
3134    let stdin = if capture_output {
3135        let (snapshot_reserved, snapshot) = output_backend_stdin_reservation()?;
3136        if snapshot_reserved {
3137            snapshot
3138        } else {
3139            // Public KVM output-capture callers do not pass through the CLI's
3140            // verify setup. Preserve their existing stdin behavior instead of
3141            // silently replacing it with /dev/null merely because output is
3142            // captured.
3143            reserved_kvm_stdin()?
3144        }
3145    } else {
3146        reserved_kvm_stdin()?
3147    };
3148    // Which of the guest's standard streams are one open file description:
3149    // the stdin this launch hands the guest, and this process's own standard
3150    // output and error, which the executor passes through unless it captures
3151    // them. A captured run's output streams are the executor's own carriers,
3152    // so nothing is known there.
3153    config.standard_stream_aliases = if capture_output {
3154        None
3155    } else {
3156        standard_stream_aliases([stdin.as_ref().map(AsRawFd::as_raw_fd), Some(1), Some(2)])
3157    };
3158    // Lazy fallback: `unwrap_or(std::env::current_dir()?)` would evaluate
3159    // the host cwd even when the command carries one, and inside an
3160    // --image chroot that cwd is unreachable, so getcwd fails ENOENT.
3161    let requested_cwd = match command.get_current_dir() {
3162        Some(cwd) => cwd.to_owned(),
3163        None => std::env::current_dir()?,
3164    };
3165    let cwd = fs::canonicalize(&requested_cwd)
3166        .map_err(|error| kvm_cwd_resolution_error(&requested_cwd, &error))?;
3167    let program = command
3168        .get_program()
3169        .to_str()
3170        .ok_or_else(|| anyhow!("KVM guest executable path is not valid UTF-8"))?
3171        .to_owned();
3172    if !cwd.is_dir() {
3173        return Err(anyhow!(
3174            "KVM guest working directory is not a directory: {:?}",
3175            cwd
3176        ));
3177    }
3178    let resolved_program = command.find_program().map_err(|error| {
3179        anyhow!("failed to resolve KVM guest executable {program:?} in the guest PATH: {error}")
3180    })?;
3181    let mut argv = Vec::with_capacity(1 + command.get_args().count());
3182    argv.push(program.clone());
3183    for argument in command.get_args() {
3184        argv.push(
3185            argument
3186                .to_str()
3187                .ok_or_else(|| anyhow!("KVM guest argument is not valid UTF-8"))?
3188                .to_owned(),
3189        );
3190    }
3191
3192    // Rewrite `#!`-scripts to their interpreter before the ELF loader sees them.
3193    let (_interpreter_path, argv, executable) = resolve_kvm_shebang(&resolved_program, argv)?;
3194    // After shebang resolution the executable is the interpreter (argv[0]).
3195    let program = argv.first().cloned().unwrap_or(program);
3196    let envp = command
3197        .get_captured_envs()
3198        .into_iter()
3199        .map(|(key, value)| {
3200            let key = key
3201                .to_str()
3202                .ok_or_else(|| anyhow!("KVM guest environment key is not valid UTF-8"))?;
3203            let value = value
3204                .to_str()
3205                .ok_or_else(|| anyhow!("KVM guest environment value is not valid UTF-8"))?;
3206            Ok(format!("{key}={value}"))
3207        })
3208        .collect::<Result<Vec<_>, Error>>()?;
3209    tracing::info!(
3210        target: "hermit::kvm",
3211        program = %program,
3212        argv = ?argv,
3213        cwd = %cwd.display(),
3214        env_count = envp.len(),
3215        "launching guest through reverie-kvm",
3216    );
3217    let argv = argv.iter().map(String::as_str).collect::<Vec<_>>();
3218    let envp = envp.iter().map(String::as_str).collect::<Vec<_>>();
3219
3220    let setup_started = Instant::now();
3221    // `prepare_backend_config` already copied `KvmBackend::capabilities()`.
3222    // KVM does not enter Hermit's UTS namespace, so Detcore must provide the
3223    // same synthetic identity that the namespace-backed ptrace path exposes.
3224    // TODO-HUMAN-REVIEW(PR-998): Review KVM UTS namespace parity.
3225    config.has_uts_namespace = false;
3226    let mut backend = reverie_kvm::KvmBackend::new_with_stdin(KVM_GUEST_MEMORY_BYTES, stdin)
3227        .map_err(|error| anyhow!("failed to initialize reverie-kvm: {error}"))?;
3228    // AUTONOMOUS-BOT-IMPLEMENTED
3229    // TODO-HUMAN-REVIEW(PR-1120): Review KVM's canonical Detcore root identity.
3230    backend
3231        .set_root_pid(detcore::ROOT_DETPID.as_raw())
3232        .map_err(|error| anyhow!("failed to configure KVM root PID: {error}"))?;
3233    // Guests see their task IDs, so number them as the ptrace reference does
3234    // (one extra ID per task for its tracer thread; see reverie::task_ids).
3235    backend
3236        .set_ids_per_task(reverie::task_ids::HERMIT_PTRACE_IDS_PER_TASK)
3237        .map_err(|error| anyhow!("failed to configure KVM task IDs: {error}"))?;
3238    // The ptrace launcher execs the path `find_program` resolved, so that is
3239    // the filename Linux records in AT_EXECFN and copies to the top of the
3240    // stack, including for a `#!` script whose argv now starts with the
3241    // interpreter. The guest's executable is the file itself, the
3242    // interpreter's for a script, as Linux keeps the file it execs: an exec
3243    // or stat of `/proc/self/exe` reaches it from the first image, as it does
3244    // under ptrace. Installing bytes left the first image with no file, so an
3245    // exec of `/proc/self/exe` failed with ENOENT there. The file is read in
3246    // full twice: by `resolve_kvm_shebang`, which only looks for a `#!` line,
3247    // and again by the loader, which keeps the one copy of the image that
3248    // the process holds for the run (LinuxEmu's `executable_image`, which a
3249    // self-exec runs again). `run_kvm` keeps no copy of its own; holding one
3250    // for the whole run cost 75 MB for node
3251    // (https://github.com/rrnewton/reverie/issues/1063).
3252    backend
3253        .install_static_elf_file_with_exec_context(
3254            executable,
3255            resolved_program.as_os_str(),
3256            &argv,
3257            &envp,
3258            &cwd,
3259        )
3260        .map_err(|error| anyhow!("failed to load KVM guest executable {program:?}: {error}"))?;
3261    // The placeholder directory Detcore serves `/proc` and the descriptor
3262    // directories on (`DetConfig::directory_placeholder`, a container's
3263    // empty read-only tmpfs at `/proc/fs/nfsd`): the executor reaches it only
3264    // by Detcore's own open and stat of that exact path, and hides it from
3265    // every other route a guest could take to it (a symbolic link it makes, a
3266    // path relative to its working directory) with ENOENT, as Detcore hides
3267    // `/proc/fs`, so that no answer of the tmpfs reaches the guest
3268    // (https://github.com/rrnewton/hermit/issues/4781).
3269    if let Some(placeholder) = config.directory_placeholder.as_deref() {
3270        backend
3271            .reserve_tool_directory(placeholder)
3272            .map_err(|error| {
3273                anyhow!("failed to reserve the KVM placeholder directory {placeholder:?}: {error}")
3274            })?;
3275    }
3276    // The KVM backend defaults to Tool-owned guest threads: CLONE_THREAD
3277    // workers are driven through the Detcore tool loop and their futex/CLEARTID
3278    // synchronization routes to Detcore, matching the golden ptrace model
3279    // ("follow children"). Detcore registers every cloned thread as a tool
3280    // thread (detcore/src/syscalls/threads.rs), so the backend must stay in
3281    // that mode: host-owned threads would split worker execution and futex
3282    // ownership across two synchronization domains, which deadlocks
3283    // pthread_join.
3284    if kvm_reports_stored_metadata_timestamps(&config) {
3285        backend
3286            .set_host_metadata_timestamps(true)
3287            .map_err(|error| anyhow!("failed to configure KVM metadata timestamps: {error}"))?;
3288    }
3289
3290    let execution_started = Instant::now();
3291    let completion = hermit_run_kvm::run_detcore(&mut backend, config, capture_output)
3292        .await
3293        .map_err(|error| kvm_execution_error(error, None))?;
3294    let cleanup_started = Instant::now();
3295    let result =
3296        finish_kvm_tool_completion(completion, print_summary, print_summary_to_json_file).await;
3297    let teardown_started = Instant::now();
3298    // Drop explicitly so the host's KVM VM teardown cost remains observable.
3299    drop(backend);
3300    let teardown_finished = Instant::now();
3301    let (status, stdout, stderr) = result?;
3302
3303    // Every field below is a host wall-clock duration. Keep this diagnostic at
3304    // DEBUG so it remains available for backend profiling without entering the
3305    // INFO stream exposed to canonical comparison. KVM's built-in verification
3306    // is still output/status-only; this classification does not claim KVM L2.
3307    tracing::debug!(
3308        target: "hermit::kvm",
3309        prepare_us = setup_started.duration_since(dispatch_started).as_micros() as u64,
3310        setup_us = execution_started.duration_since(setup_started).as_micros() as u64,
3311        execution_us = cleanup_started.duration_since(execution_started).as_micros() as u64,
3312        cleanup_us = teardown_started.duration_since(cleanup_started).as_micros() as u64,
3313        teardown_us = teardown_finished.duration_since(teardown_started).as_micros() as u64,
3314        lifecycle_us = teardown_finished.duration_since(dispatch_started).as_micros() as u64,
3315        "reverie-kvm lifecycle phase timings",
3316    );
3317
3318    if !capture_output {
3319        std::io::stdout().write_all(&stdout)?;
3320        std::io::stderr().write_all(&stderr)?;
3321    }
3322
3323    Ok(Output {
3324        status,
3325        stdout,
3326        stderr,
3327    })
3328}
3329
3330// A runtime error still owns GlobalState. Finish its terminal daemon before
3331// returning the typed cause to the CLI's error classifier.
3332#[cfg(target_arch = "x86_64")]
3333async fn finish_kvm_tool_completion(
3334    completion: reverie_kvm::ToolRunCompletion<detcore::GlobalState>,
3335    print_summary: bool,
3336    print_summary_to_json_file: &Option<PathBuf>,
3337) -> Result<(ExitStatus, Vec<u8>, Vec<u8>), Error> {
3338    match completion.result {
3339        Ok(output) => {
3340            completion
3341                .global_state
3342                .clean_up(print_summary, print_summary_to_json_file)
3343                .await?;
3344            Ok(output)
3345        }
3346        Err(error) => {
3347            let cleanup = completion
3348                .global_state
3349                .clean_up_after_backend_failure()
3350                .await;
3351            Err(kvm_execution_error(error, Some(cleanup)))
3352        }
3353    }
3354}
3355
3356#[cfg(target_arch = "x86_64")]
3357fn kvm_execution_error(
3358    primary: reverie_kvm::Error,
3359    cleanup: Option<detcore::BackendFailureCleanup>,
3360) -> Error {
3361    // The manifest runner retains the first Error line. Keep the cause there,
3362    // as well as the typed backend and optional scheduler errors in the chain.
3363    let message = format!("KVM guest execution failed: {primary}");
3364    let mut error = Error::new(primary).context(BackendRunFailure);
3365    if let Some(cleanup) = cleanup {
3366        if let Err(recording) = cleanup.preemption_recording {
3367            error = error.context(format!("partial preemption recording failed: {recording}"));
3368        }
3369        if let Err(scheduler) = cleanup.scheduler {
3370            error = error.context(scheduler);
3371        }
3372    }
3373    error.context(message)
3374}
3375
3376#[cfg(all(test, target_arch = "x86_64"))]
3377mod kvm_failure_tests;
3378
3379#[cfg(all(test, feature = "kvm-execution-tests"))]
3380mod kvm_execution_tests;
3381
3382// TODO-HUMAN-REVIEW(PR-743): Review bounded relaunch before DBT guest execution.
3383
3384// AUTONOMOUS-BOT-IMPLEMENTED
3385// TODO-HUMAN-REVIEW(PR-737): Review public DBT dispatch and child environment ownership.
3386// NOTE: A single-threaded executor is used here so that the tokio threads
3387// themselves wouldn't contribute non-determinism to the PID namespace. This
3388// could also be changed to a specific number of threads and that would be
3389// deterministic, but it shouldn't be based on the number of cores. When the
3390// thread count is based off of the number of cores in the machine, then two
3391// runs on different machines with a different number of cores will not be the
3392// same.
3393/// Run the given command as deterministically as possible.
3394///
3395/// # Stack size
3396///
3397/// On the ptrace backend this and the other launch functions (`run*`,
3398/// `record*`, `replay*`, [`HermitData::record`]) run the whole ptrace session
3399/// on a tracer thread of their own, with an 8 MiB stack (the main thread's
3400/// usual size, the default `ulimit -s`), so the calling thread needs no
3401/// particular stack: a recording through the library returned on a 95 KiB
3402/// caller thread. One tracer thread serves each calling thread for the
3403/// caller's lifetime, so an operation kept at a [`HermitCleanupUnconfirmed`]
3404/// checkpoint is resumed from the same caller. That thread stays, idle, until
3405/// the caller exits, and a tracing subscriber set for the calling thread only
3406/// does not see the session's events (a global subscriber does). The session itself, in a debug
3407/// build, needs about 2 MiB, mostly Detcore's syscall handler, which at
3408/// opt-level 0 keeps every syscall arm's temporaries in one frame
3409/// (<https://github.com/rrnewton/hermit/issues/4389>).
3410///
3411/// The other backends still run on the calling thread, which then needs that
3412/// stack itself: launching from a thread spawned with 4 MiB
3413/// (`std::thread::Builder::stack_size`) is recommended for them, and Rust's
3414/// default 2 MiB for a spawned thread or a test is not enough in a debug build
3415/// with the `third-party-backends` feature. Optimised builds need much less:
3416/// in the `validate` profile a 512 KiB thread was enough.
3417pub fn run(
3418    command: Command,
3419    config: DetConfig,
3420    print_summary: bool,
3421    print_summary_to_json_file: &Option<PathBuf>,
3422) -> Result<ExitStatus, Error> {
3423    run_with_backend(
3424        command,
3425        config,
3426        print_summary,
3427        print_summary_to_json_file,
3428        Backend::Ptrace,
3429    )
3430}
3431
3432/// Run the given command using the selected instrumentation backend.
3433///
3434/// On ptrace it needs no particular stack from the calling thread; see [`run`].
3435pub fn run_with_backend(
3436    command: Command,
3437    config: DetConfig,
3438    print_summary: bool,
3439    print_summary_to_json_file: &Option<PathBuf>,
3440    backend: Backend,
3441) -> Result<ExitStatus, Error> {
3442    run_with_backend_timeout(
3443        command,
3444        config,
3445        print_summary,
3446        print_summary_to_json_file,
3447        backend,
3448        None,
3449    )
3450}
3451
3452/// [`run_with_backend`] with a hermit-enforced wall-clock bound on the guest.
3453///
3454/// On ptrace it needs no particular stack from the calling thread; see [`run`].
3455///
3456/// This library never installs a process alarm. Ordinary ptrace keeps the
3457/// original runtime, operation, failed GlobalState and backing guards when a
3458/// finite cleanup attempt is unconfirmed; downcast [`HermitCleanupUnconfirmed`]
3459/// and recover it on the original process/thread. A timeout is measured before
3460/// startup and is not a certificate of cleanup. `None` leaves execution
3461/// unbounded, but fatal cleanup attempts remain finite. Nonordinary backends
3462/// retain their existing cooperative timeout/drop semantics. Synchronous stalls
3463/// require caller-owned supervision; only the CLI owns its hard alarm fallback.
3464///
3465/// ⚠️ NOT PART OF `DetConfig`, DELIBERATELY. A wall-clock bound is host state,
3466/// and `DetConfig` is the determinism configuration that is serialized to disk
3467/// and reasoned about as guest-visible. Threading a real-time deadline through
3468/// it would put a nondeterministic quantity inside the structure whose entire
3469/// job is to exclude them.
3470pub fn run_with_backend_timeout(
3471    command: Command,
3472    config: DetConfig,
3473    print_summary: bool,
3474    print_summary_to_json_file: &Option<PathBuf>,
3475    backend: Backend,
3476    timeout: Option<Duration>,
3477) -> Result<ExitStatus, Error> {
3478    if backend == Backend::Ptrace {
3479        let summary_path = print_summary_to_json_file.clone();
3480        let command = tracer_thread::SendCommand::new(command);
3481        return ptrace_completion::run_on_tracer_thread(timeout, move |control| async move {
3482            let command = command.into_inner();
3483            let report = SkidOvershootReport::begin(true);
3484            let config = prepare_backend_config(config, backend);
3485            let result = dispatch_backend(
3486                command,
3487                config,
3488                print_summary,
3489                &summary_path,
3490                backend,
3491                Some(control),
3492            )
3493            .await;
3494            report.finish(result)
3495        });
3496    }
3497    let skid_overshoot_report =
3498        SkidOvershootReport::begin(backend.consumes_skid_overshoot_reports());
3499    if backend == Backend::Kvm {
3500        ensure_kvm_stdin_reserved()?;
3501    }
3502    let config = prepare_backend_config(config, backend);
3503    let result = run_with_backend_inner(
3504        command,
3505        config,
3506        print_summary,
3507        print_summary_to_json_file,
3508        backend,
3509        timeout,
3510    );
3511    skid_overshoot_report.finish(result)
3512}
3513
3514// TODO-HUMAN-REVIEW(PR-749): Review LiteInst backend configuration normalization.
3515#[doc(hidden)]
3516pub fn prepare_backend_config(config: DetConfig, backend: Backend) -> DetConfig {
3517    prepare_backend_config_with_capabilities(config, backend_capabilities(backend))
3518}
3519
3520/// Prepare the common Detcore configuration from the physical runtime's
3521/// declared facts. This is the same normalization used by builtin backends.
3522#[doc(hidden)]
3523pub fn prepare_backend_config_with_capabilities(
3524    mut config: DetConfig,
3525    capabilities: reverie::BackendCapabilities,
3526) -> DetConfig {
3527    // Detcore reads what the selected backend can do, never which backend it is.
3528    // Each backend states its own facts in `Backend::capabilities()`.
3529    //
3530    // Three properties hold on every backend, so they are not capabilities:
3531    // - Guest-visible wall and monotonic clocks stay in the same global
3532    //   virtual-time domain as timers, sleeps, and timeout deadlines. SaBRe's
3533    //   per-thread execution clock does not advance when the scheduler skips
3534    //   global time while a thread is blocked.
3535    // - A forked process child runs concurrently with its parent. Detcore
3536    //   blocks the parent only for CLONE_VFORK.
3537    // - Detcore dispatches every cloned thread's syscalls through its tool.
3538    // AUTONOMOUS-BOT-IMPLEMENTED
3539    // TODO-HUMAN-REVIEW(PR-845): Review backend-local guest clock observations.
3540    // TODO-HUMAN-REVIEW(PR-1013): Review backend child process execution ordering.
3541    // TODO-HUMAN-REVIEW(PR-1013): Review backend thread callback coverage.
3542    // TODO-HUMAN-REVIEW(PR-1122): Review concurrent KVM process-child scheduling.
3543    // AUTONOMOUS-BOT-IMPLEMENTED
3544    // TODO-HUMAN-REVIEW(PR-3635): Review in-guest LiteInst killed-thread RPC cancellation.
3545    // AUTONOMOUS-BOT-IMPLEMENTED
3546    // TODO-HUMAN-REVIEW(PR-1152): KVM defers the vfork child spawn, so the child
3547    // registers its vfork barrier only after the parent posts BlockedExternalContinue. ptrace keeps
3548    // the parent kernel-blocked until the child registers.
3549    config.backend = capabilities;
3550    // Whether the run's process signals are tool-controlled is not decided
3551    // here: Detcore installs the backend's process signal control when the
3552    // backend offers it and threads are sequentialized, and reads the installed
3553    // control from then on.
3554    // Whether blocked waits decide signal interruption from the kernel's
3555    // signal state (https://github.com/rrnewton/hermit/issues/3146), whether a
3556    // `--max-timeslice` really preempts (https://github.com/rrnewton/hermit/issues/4068)
3557    // and whether guest code can run between two injected calls are the
3558    // backend's own stated facts in `config.backend`
3559    // (`supports_blocked_wait_signal_interruption`,
3560    // `delivers_preemption_timer_events`, `runs_guest_code_between_injections`),
3561    // not derived from its name here (audit step A9 of
3562    // https://github.com/rrnewton/hermit/issues/4485).
3563    // Only backends where Detcore treats a terminal's signals as reaching the
3564    // guest from the host read it (`Config::guest_may_inherit_a_terminal`, and
3565    // `BackendCapabilities::host_timed_signals_are_physical`), so the host is
3566    // asked only for them.
3567    config.guest_may_inherit_a_terminal =
3568        config.backend.host_timed_signals_are_physical && launcher_may_pass_a_terminal();
3569    // The in-guest runtime's reserved signals are set only for the default
3570    // in-guest route, after this preparation (`in_guest_reserved_signals`),
3571    // so whatever a caller's configuration carried, every backend and every
3572    // other route starts from the empty set (A5 step 3c, D13(a)).
3573    config.in_guest_reserved_signals = 0;
3574    // So is the refusal of host-timed signal producers
3575    // (`in_guest_refuses_host_timed_producers`, A5 step 3c slice 0b).
3576    config.in_guest_refuses_host_timed_producers = false;
3577    // And the in-guest delivery gate, which only the default route's launch
3578    // sets, from the final configuration and the process's PID-namespace
3579    // ownership (`in_guest_signal_delivery_gate`, A5 step 3c.1, D35 and D52).
3580    config.in_guest_signal_delivery = false;
3581    config
3582}
3583
3584/// The configuration of an in-guest run on the loader route (the exec-stop,
3585/// native-exec and mapped-first-admission launchers of `hermit run`): the
3586/// `in-guest-trap` backend's preparation with site patching off. It keeps
3587/// today's behaviour on that route (D13(a), and D26 of the A5 step 3c design:
3588/// the route's own comparison gates any change), so the in-guest runtime's
3589/// reserved-signal set stays empty here whatever `config` carried; only the
3590/// default route sets it (`in_guest_reserved_signals`).
3591pub fn prepare_in_guest_loader_route_config(config: DetConfig) -> DetConfig {
3592    let mut config = prepare_backend_config(config, Backend::InGuestTrap);
3593    config.in_guest_site_patching_off = true;
3594    config
3595}
3596
3597/// Whether the guest may start with a terminal, which can make Linux send the
3598/// guest signals at moments the host sets, such as SIGHUP when the terminal
3599/// hangs up and SIGWINCH when it is resized
3600/// (`Config::guest_may_inherit_a_terminal`): one of this process's standard
3601/// descriptors, which the guest inherits, is a terminal, or this process has a
3602/// controlling terminal, which the guest inherits with its session because
3603/// neither Hermit nor Reverie starts a new session for it. When the
3604/// controlling terminal cannot be read, the answer is yes.
3605fn launcher_may_pass_a_terminal() -> bool {
3606    // SAFETY: isatty only inspects the descriptor.
3607    let standard_terminal = (0..=2).any(|fd| unsafe { libc::isatty(fd) } == 1);
3608    may_pass_a_terminal(standard_terminal, controlling_terminal_device())
3609}
3610
3611/// The rule of `launcher_may_pass_a_terminal`: a terminal standard descriptor,
3612/// a controlling terminal (a nonzero device number), or an unknown one.
3613fn may_pass_a_terminal(standard_terminal: bool, controlling_terminal: Option<i64>) -> bool {
3614    standard_terminal || controlling_terminal != Some(0)
3615}
3616
3617/// This process's controlling terminal as `/proc/self/stat` reports it: field
3618/// 7, `tty_nr`, the terminal's device number, or 0 when there is none. `None`
3619/// when it cannot be read.
3620fn controlling_terminal_device() -> Option<i64> {
3621    parse_controlling_terminal_device(&std::fs::read_to_string("/proc/self/stat").ok()?)
3622}
3623
3624/// `tty_nr` from the text of a `/proc/<pid>/stat` file. The command name in
3625/// field 2 is parenthesized and may hold spaces and parentheses, so the fields
3626/// after it are counted from its last `)`: state, ppid, pgrp, session, tty_nr.
3627fn parse_controlling_terminal_device(stat: &str) -> Option<i64> {
3628    let after_command = stat.get(stat.rfind(')')? + 1..)?;
3629    after_command.split_ascii_whitespace().nth(4)?.parse().ok()
3630}
3631
3632/// What the selected backend can do, as Detcore consumes it.
3633///
3634/// Backends whose crate is always linked are asked directly. The LiteInst and
3635/// DBT constants are checked against their crates by tests built with those
3636/// features; the SaBRe value has no such check. e9patch takes ptrace's.
3637#[doc(hidden)]
3638pub fn backend_capabilities(backend: Backend) -> reverie::BackendCapabilities {
3639    match backend {
3640        // e9patch is binary-rewriting preprocessing whose guest runs on the
3641        // ptrace backend (`hermit run --backend=e9patch` configures its run as
3642        // `Backend::Ptrace`), so it has ptrace's capabilities. A direct caller
3643        // used to get two of them turned off by this name.
3644        Backend::Ptrace => <reverie_ptrace::PtraceBackend as reverie::Backend>::capabilities(),
3645        #[cfg(target_arch = "x86_64")]
3646        Backend::Kvm => reverie_kvm::KvmBackend::capabilities(),
3647        // reverie-kvm is an x86-64 VMM; elsewhere the backend's declared
3648        // capabilities stand in, and `run_kvm` refuses the run.
3649        #[cfg(not(target_arch = "x86_64"))]
3650        Backend::Kvm => reverie::BackendCapabilities::KVM,
3651        // Site patching changes how a syscall reaches the Tool, not what the
3652        // Tool host can do, so in-guest-trap has in-guest LiteInst's capabilities.
3653        Backend::Liteinst | Backend::InGuestTrap => reverie::BackendCapabilities::LITEINST_IN_GUEST,
3654    }
3655}
3656
3657/// Whether `backend`'s runtime arms a PMU preemption timer and delivers its
3658/// events, so `--max-timeslice` really preempts a thread that makes no syscall:
3659/// the backend's stated `delivers_preemption_timer_events`. ptrace (and e9patch
3660/// preprocessing, which runs on it) and KVM do. SaBRe accepts a timer request
3661/// and delivers nothing; DBT and in-guest LiteInst refuse it.
3662pub fn backend_delivers_preemption_timer(backend: Backend) -> bool {
3663    backend_capabilities(backend).delivers_preemption_timer_events
3664}
3665
3666/// After an in-guest LiteInst run, waits (bounded) until the watcher has
3667/// reported every admitted process's physical exit, then releases any exit
3668/// barrier still recorded. Release comes only after that drain: an exit still
3669/// in flight is reported first.
3670///
3671/// Every report is applied again to the run's returned state. The backend
3672/// lets go of its own reference to that state when the run ends, and a report
3673/// that arrived afterwards reached nothing; applying a report twice consumes
3674/// nothing the second time.
3675///
3676/// A failed drain, a fired watchdog or a refused admission fails the run:
3677/// it records a determinism loss, so verification refuses to compare it, and
3678/// returns the failure, after the barriers are released.
3679#[cfg(feature = "liteinst")]
3680async fn settle_in_guest_exits(
3681    exits: &in_guest_exits::InGuestExitAdmission,
3682    global_state: &detcore::GlobalState,
3683) -> Result<(), Error> {
3684    let drained = exits.drain(std::time::Duration::from_secs(10)).await;
3685    for pid in exits.reported_exits() {
3686        global_state.complete_physical_process_exit(pid);
3687    }
3688    global_state.release_all_physical_process_exits();
3689    let failure = exits.failure().or_else(|| {
3690        drained
3691            .err()
3692            .map(|error| format!("not every in-guest LiteInst guest exit was reported: {error:?}"))
3693    });
3694    match failure {
3695        Some(reason) => {
3696            detcore::detlog::record_determinism_loss(&reason);
3697            Err(Error::msg(reason))
3698        }
3699        None => Ok(()),
3700    }
3701}
3702
3703// TODO-HUMAN-REVIEW(PR-736): Review reserved LiteInst runtime failure statuses.
3704//
3705// ⚠️ THIS PREDICATE AND THE CLASSIFIER MUST AGREE ABOUT THE SIGNAL BAND, AND A
3706// HARD-CODED RANGE CANNOT. They do NOT cover the same statuses and are not meant
3707// to -- see the table below for what each covers. It is the SIGNAL half that has
3708// to move together, because both readers derive it from the same fact. When the reserved set grew to include the signal
3709// band, `Exited(130)` fell outside `122..=127` and is NOT `Signaled(_, _)` --
3710// it is a status hermit CHOSE, not an actual signal death -- so LiteInst
3711// skipped the forced shutdown and `clean_up` could wait forever on
3712// `handle.await` (detcore/src/tool_global.rs). The reserved set had grown past
3713// the range this hard-coded, and nothing connected the two.
3714//
3715// Keying the SIGNAL HALF on `signal_from_exit_status` -- the same predicate
3716// `classify_container_result` uses -- is what stops that half drifting again: a
3717// change to the band moves both readers or neither.
3718//
3719// ⚠️ THE TWO PREDICATES ARE NOT THE SAME SET, AND AN EARLIER VERSION OF THIS
3720// COMMENT SAID THEY WERE. agent(hermit-007)'s codex lane caught the overclaim.
3721// They deliberately differ:
3722//
3723//   this predicate            122..=127        +  signal band  +  real Signaled
3724//   classify_container_result 122 + 123 + 124  +  signal band
3725//
3726// 123..=127 force a LiteInst shutdown and are classified as NEITHER a refusal
3727// nor a signal death. The classifier gives 123 and 124 arms of their own -- 123
3728// is the log byte cap (hermit's `--max-log-bytes`, and the value reserved for
3729// safehermit's log-cap kill, which still exits 125), 124 a deadline hermit
3730// enforced -- while 125 (hermit itself) and 126/127 (exec-level) have none, and
3731// each has a different producer. Only the signal half is shared, and only the
3732// signal half is protected from drift. Do not "simplify" this by assuming the
3733// two agree; the `122..=127` bound here is still hard-coded and still needs a
3734// human to widen it if the reserved set grows again.
3735#[cfg(feature = "liteinst")]
3736fn liteinst_requires_forced_shutdown(status: ExitStatus) -> bool {
3737    match status {
3738        // A real signal death: the process never chose a status at all.
3739        ExitStatus::Signaled(_, _) | ExitStatus::SignaledRaw(_, _) => true,
3740        ExitStatus::Exited(code) => {
3741            // The 122..=127 reserved band, plus any status hermit emits to mean
3742            // "killed by signal N".
3743            (122..=127).contains(&code) || detcore_model::signal_from_exit_status(code).is_some()
3744        }
3745    }
3746}
3747
3748/// The guest outlived the wall-clock bound the caller asked hermit to enforce.
3749///
3750/// A DISTINCT TYPE, NOT A STRING, because `failure_exit_code` and
3751/// `classify_failure` both have to recognise it and a prose match would break
3752/// silently the first time the wording changed.
3753#[derive(Debug)]
3754pub struct GuestTimedOut {
3755    /// The bound that was exceeded, as the caller spelled it.
3756    pub limit: Duration,
3757    /// The guest processes still running when the bound fired, in one line
3758    /// (`timeout_survivors::traced_processes_now`), where the backend could
3759    /// read them. A run lasts until every guest process exits, so this is what
3760    /// tells a stuck root apart from a root that exited and left a blocked
3761    /// child behind.
3762    pub survivors: Option<String>,
3763}
3764
3765impl std::fmt::Display for GuestTimedOut {
3766    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3767        write!(
3768            f,
3769            "Guest exceeded the --timeout bound of {} seconds",
3770            self.limit.as_secs()
3771        )?;
3772        if let Some(survivors) = &self.survivors {
3773            write!(f, "; {survivors}")?;
3774        }
3775        Ok(())
3776    }
3777}
3778
3779impl std::error::Error for GuestTimedOut {}
3780
3781/// The run ended with a scheduler terminal outcome
3782/// ([`detcore::TerminalOutcome`]): a deadlock under Hermit's model (Detcore's
3783/// scheduler found every remaining thread blocked with no wake it models), or
3784/// the run's virtual clock passed `--virtual-timeout`. Neither is a guest
3785/// result. The scheduler printed its report.
3786///
3787/// The ptrace runtime publishes it as the termination that tears the parked
3788/// guest down, as `--timeout` publishes [`GuestTimedOut`]. Once the run is
3789/// finished (summary printed, recording metadata written) the run functions
3790/// return it as their error, carrying the output the guest wrote before the
3791/// outcome when the run captured it; that output's status is the outcome's
3792/// ([`detcore::TerminalOutcome::exit_status`]). A run whose teardown failed is
3793/// not this but a ptrace failure, reported as Hermit's. The CLI reports it by
3794/// the outcome's class line rather than as a guest exit status or an internal
3795/// failure. Backends whose scheduler cannot hand the outcome back to the run
3796/// exit the process with the outcome's status instead.
3797#[non_exhaustive]
3798pub struct SchedulerEndedRun {
3799    /// How the scheduler ended the run.
3800    pub outcome: detcore::TerminalOutcome,
3801    /// The captured output, for a run that captured it.
3802    pub output: Option<Output>,
3803}
3804
3805impl SchedulerEndedRun {
3806    /// A finished run that ended with `outcome`, with its output when it
3807    /// captured one.
3808    pub fn new(outcome: detcore::TerminalOutcome, output: Option<Output>) -> Self {
3809        Self { outcome, output }
3810    }
3811
3812    /// Separate a finished run's terminal outcome from its other errors. An
3813    /// outcome beside a skid overshoot stays the overshoot's error, which
3814    /// refuses the run.
3815    pub fn split<T>(result: Result<T, Error>) -> Result<Result<T, SchedulerEndedRun>, Error> {
3816        match result {
3817            Ok(value) => Ok(Ok(value)),
3818            Err(error)
3819                if error.downcast_ref::<SkidOvershootError>().is_some()
3820                    || detcore::ReplayDepartureError::in_error(&error).is_some() =>
3821            {
3822                Err(error)
3823            }
3824            Err(error) => error.downcast::<SchedulerEndedRun>().map(Err),
3825        }
3826    }
3827
3828    /// The output of a run that ended with an outcome and captured it.
3829    pub fn into_output(self) -> Result<Output, Error> {
3830        self.output.ok_or_else(|| {
3831            Error::msg("a run that captured output ended with a terminal outcome and returned none")
3832        })
3833    }
3834
3835    /// A finished run's status, and the outcome it ended with, if any, in
3836    /// which case the status is the outcome's.
3837    pub fn status_of(
3838        result: Result<ExitStatus, Error>,
3839    ) -> Result<(ExitStatus, Option<detcore::TerminalOutcome>), Error> {
3840        Ok(match Self::split(result)? {
3841            Ok(status) => (status, None),
3842            Err(ended) => (
3843                ExitStatus::Exited(ended.outcome.exit_status()),
3844                Some(ended.outcome),
3845            ),
3846        })
3847    }
3848
3849    /// A finished run's output, and the outcome it ended with, if any, in
3850    /// which case the output's status is the outcome's.
3851    pub fn output_of(
3852        result: Result<Output, Error>,
3853    ) -> Result<(Output, Option<detcore::TerminalOutcome>), Error> {
3854        Ok(match Self::split(result)? {
3855            Ok(output) => (output, None),
3856            Err(ended) => {
3857                let outcome = ended.outcome;
3858                (ended.into_output()?, Some(outcome))
3859            }
3860        })
3861    }
3862}
3863
3864impl std::fmt::Debug for SchedulerEndedRun {
3865    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3866        let mut debug = f.debug_struct("SchedulerEndedRun");
3867        debug.field("outcome", &self.outcome);
3868        if let Some(output) = &self.output {
3869            debug
3870                .field("stdout_bytes", &output.stdout.len())
3871                .field("stderr_bytes", &output.stderr.len());
3872        }
3873        debug.finish()
3874    }
3875}
3876
3877impl std::fmt::Display for SchedulerEndedRun {
3878    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3879        match self.outcome {
3880            detcore::TerminalOutcome::GuestDeadlock => write!(
3881                f,
3882                "Deadlock under Hermit's model: no remaining thread can be woken by anything Hermit models"
3883            ),
3884            detcore::TerminalOutcome::VirtualTimeout { bound_ns } => write!(
3885                f,
3886                "The run's virtual clock passed --virtual-timeout={bound_ns} (virtual \
3887                 nanoseconds): a bound Hermit imposed, not a guest result"
3888            ),
3889        }
3890    }
3891}
3892
3893impl std::error::Error for SchedulerEndedRun {}
3894
3895/// Nonordinary backends retain their existing cooperative timeout behavior.
3896/// There is no process-wide alarm in this library. A caller needing a bound on
3897/// synchronous stalls must own external supervision; the CLI arms its own
3898/// separate init-process fallback before invoking the library. Ordinary ptrace
3899/// uses the retained whole-operation driver instead of cancelling this future.
3900async fn with_run_deadline<F>(timeout: Option<Duration>, guest: F) -> Result<ExitStatus, Error>
3901where
3902    F: std::future::Future<Output = Result<ExitStatus, Error>>,
3903{
3904    match timeout {
3905        None => guest.await,
3906        Some(limit) => match tokio::time::timeout(limit, guest).await {
3907            Ok(result) => result,
3908            Err(_) => Err(Error::new(GuestTimedOut {
3909                limit,
3910                survivors: None,
3911            })),
3912        },
3913    }
3914}
3915
3916#[tokio::main(flavor = "current_thread")]
3917async fn run_with_backend_inner(
3918    command: Command,
3919    config: DetConfig,
3920    print_summary: bool,
3921    print_summary_to_json_file: &Option<PathBuf>,
3922    backend: Backend,
3923    timeout: Option<Duration>,
3924) -> Result<ExitStatus, Error> {
3925    refuse_in_guest_liteinst_timeout(backend, timeout)?;
3926    // Keep the large backend future off the container supervisor's stack
3927    // before the deadline and Tokio wrappers capture it. Polling and dropping
3928    // stay inline, including the backend teardown on timeout.
3929    let guest = Box::pin(dispatch_backend(
3930        command,
3931        config,
3932        print_summary,
3933        print_summary_to_json_file,
3934        backend,
3935        None,
3936    ));
3937    with_run_deadline(timeout, guest).await
3938}
3939
3940/// The guest's first image, as its `/proc/self/exe` link names it
3941/// (`detcore::Config::launch_image`): the program the launch executes, found
3942/// as the launch finds it, then resolved as the kernel resolves an `execve`
3943/// (`detcore::exec_record::ExecRecord::resolve`). Read in the container,
3944/// whose root is the guest's. A launch through a launcher names it itself
3945/// (`run_sabre`). `None` when it cannot be found, which leaves the link to the
3946/// kernel, and the launch reports the missing program itself.
3947pub fn launched_image(command: &Command) -> Option<detcore::LaunchImage> {
3948    let program = command.find_program().ok()?;
3949    let cwd = match command.get_current_dir() {
3950        Some(cwd) => cwd.to_owned(),
3951        None => std::env::current_dir().ok()?,
3952    };
3953    let record = detcore::exec_record::ExecRecord::resolve(Path::new("/"), &cwd, &program)?;
3954    Some(detcore::LaunchImage {
3955        exe: Some(record.exe),
3956        launcher: None,
3957    })
3958}
3959
3960async fn dispatch_backend(
3961    command: Command,
3962    mut config: DetConfig,
3963    print_summary: bool,
3964    print_summary_to_json_file: &Option<PathBuf>,
3965    backend: Backend,
3966    control: Option<std::rc::Rc<ptrace_completion::Control>>,
3967) -> Result<ExitStatus, Error> {
3968    // Every launch path, ptrace's included, reaches the guest through here.
3969    crate::cpu_profile_admission::refuse_unadmitted_launch(&config, backend == Backend::Ptrace)?;
3970    detcore_model::virtual_timeout::refuse_debugger(config.virtual_timeout, config.gdbserver)?;
3971    crate::preemption_admission::refuse_unqualified_annotated_launch(&config)?;
3972    detcore::preemptions::validate_config_replay_inputs(&config)?;
3973    // Do this while the actual Tool type is available, before a DBT/SaBRe
3974    // launcher erases it to GlobalTool or constructs an RPC server.
3975    <detcore::Detcore as reverie::Tool>::facility_admission(&config)?;
3976    config.launch_image = launched_image(&command);
3977    if backend == Backend::Kvm {
3978        return Ok(run_kvm(
3979            &command,
3980            config,
3981            print_summary,
3982            print_summary_to_json_file,
3983            false,
3984        )
3985        .await?
3986        .status);
3987    }
3988
3989    if backend.is_in_guest_liteinst() {
3990        #[cfg(feature = "liteinst")]
3991        {
3992            let stats_request = in_guest_stats_request(backend, print_summary_to_json_file);
3993            let mut command = command;
3994            refuse_in_guest_liteinst_run(&mut command, &config, backend)?;
3995            let (launch, _detlog_descriptor) =
3996                in_guest_liteinst_launch(&mut command, &in_guest_detlog_forward_policy())?;
3997            // After Hermit's other callbacks, so the guest's own check is the
3998            // last one that can change what it checks (`LaunchAudit::install`).
3999            let audit = neutralize_inherited_signal_producers(&mut command, backend)?;
4000            let tool_data = launch.encode();
4001            // Every Tool image must forward on that socket, by that policy;
4002            // see detcore::Config::in_guest_detlog_forward_policy.
4003            let mut config = config;
4004            config.in_guest_detlog_forward_policy = _detlog_descriptor
4005                .is_some()
4006                .then(|| in_guest_detlog_forward_policy().encode());
4007            config.in_guest_site_patching_off = in_guest_site_patching_off(backend);
4008            config.in_guest_refuses_host_timed_producers =
4009                in_guest_refuses_host_timed_producers(backend);
4010            // Last, from the configuration every Tool image receives.
4011            config.in_guest_reserved_signals = in_guest_reserved_signals(&config, backend);
4012            // The delivery gate, from the final configuration and whether
4013            // this process owns the guests' PID namespace; off until the
4014            // runtime takes it (A5 step 3c.1, D35, D52).
4015            config.in_guest_signal_delivery = record_delivery_gate(
4016                runtime_delivers_signals()
4017                    && in_guest_signal_delivery_gate(
4018                        &config,
4019                        backend,
4020                        scheduler_owns_guest_pid_namespace(),
4021                    ),
4022            );
4023            let preload = liteinst_tool_runtime_library_path()?;
4024            let exits = std::sync::Arc::new(in_guest_exits::InGuestExitAdmission::default());
4025            let (exit_status, mut global_state, dispatch_stats, process_reports) =
4026                reverie_liteinst::LiteinstBackend::with_connection_admission(
4027                    exits.clone(),
4028                    async {
4029                        if stats_request.is_enabled() {
4030                            let (exit_status, global_state, source) =
4031                                reverie_liteinst::LiteinstBackend::run_with_preload_data_and_stats::<
4032                                    Detcore,
4033                                >(command, config, preload, tool_data)
4034                                .await?;
4035                            let dispatch_stats =
4036                                backend_stats::report(backend, stats_request, &source);
4037                            let process_reports = source.snapshot().process_reports();
4038                            Ok::<_, reverie::Error>((
4039                                exit_status,
4040                                global_state,
4041                                dispatch_stats,
4042                                process_reports,
4043                            ))
4044                        } else {
4045                            let (exit_status, global_state) =
4046                                reverie_liteinst::LiteinstBackend::run_with_preload_data::<Detcore>(
4047                                    command, config, preload, tool_data,
4048                                )
4049                                .await?;
4050                            Ok((exit_status, global_state, None, 0))
4051                        }
4052                    },
4053                )
4054                .await
4055                .inspect_err(|error| exits.fail_launch(error))
4056                // A standard descriptor the guest's own check refused
4057                // (D25) fails the launch; name it.
4058                .map_err(|error| match &audit {
4059                    Some(audit) => audit.launch_error(backend, error),
4060                    None => Error::from(error),
4061                })?;
4062            let patched =
4063                in_guest_trap_patching_evidence(backend, dispatch_stats.as_ref(), process_reports);
4064            let settled = settle_in_guest_exits(&exits, &global_state).await;
4065            let policy_refusal = global_state.policy_refusal();
4066            if settled.is_err() || liteinst_requires_forced_shutdown(exit_status) {
4067                global_state.force_shutdown_with_error();
4068                global_state.cancel_internal_scheduler().await;
4069            }
4070            // The in-guest runtime starts after the guest's dynamic loader,
4071            // whose syscalls the histogram therefore misses.
4072            global_state.note_syscall_observation_gap(
4073                detcore::SyscallObservationGap::InProcessRuntimeStart,
4074            );
4075            let finalized = global_state
4076                .clean_up_with_dispatch_stats(
4077                    print_summary,
4078                    print_summary_to_json_file,
4079                    dispatch_stats,
4080                )
4081                .await;
4082            // After the scheduler has shut down, so the line's place among
4083            // the run's INFO lines is fixed (D25); a stopping refusal logs it
4084            // just before it exits instead.
4085            detcore::host_timed_producers::emit_run_end_log();
4086            let result = (|| {
4087                refuse_in_guest_policy(backend, policy_refusal)?;
4088                settled?;
4089                refuse_in_guest_trap_patching(backend, patched)
4090            })();
4091            detcore::combine_finalization(result, finalized)?;
4092            return Ok(exit_status);
4093        }
4094        #[cfg(not(feature = "liteinst"))]
4095        {
4096            backend.ensure_available()?;
4097            unreachable!("LiteInst availability must fail when the feature is disabled");
4098        }
4099    }
4100    ensure_backend_dispatch(backend)?;
4101
4102    let stats_request = backend_stats::request(print_summary_to_json_file);
4103    let control = control
4104        .ok_or_else(|| Error::msg("ordinary ptrace requires its retained operation owner"))?;
4105    // A sequentialized guest tells gdbserver not to serialize its threads;
4106    // see hermit_run_ptrace::spawn_tracer.
4107    let tracer = hermit_run_ptrace::spawn_tracer(
4108        command,
4109        config.clone(),
4110        config
4111            .gdbserver
4112            .then(|| gdbserver::connection(config.gdbserver_port))
4113            .transpose()?,
4114        config.sequentialize_threads,
4115        stats_request,
4116    )
4117    .await?;
4118    let stats_source = tracer.backend_stats();
4119    let (exit_status, global_state) = ptrace_completion::wait(tracer, control).await?;
4120    let terminal_outcome = global_state.terminal_outcome();
4121    let dispatch_stats =
4122        backend_stats::report_if_collected(backend, stats_request, stats_source.as_ref());
4123    let cleanup = global_state
4124        .clean_up_with_dispatch_stats(print_summary, print_summary_to_json_file, dispatch_stats)
4125        .await;
4126    let completed = match terminal_outcome {
4127        Some(outcome) => Err(SchedulerEndedRun::new(outcome, None)),
4128        None => Ok(exit_status),
4129    };
4130    complete_ptrace_run(completed, cleanup, exit_status)
4131}
4132
4133// AUTONOMOUS-BOT-IMPLEMENTED
4134// TODO-HUMAN-REVIEW(https://github.com/rrnewton/hermit/pull/4860)
4135// Both ptrace completion forms use this adapter. A pure departure retains the
4136// owned terminal/output value under typed context, matching skid's refusal
4137// priority. Composite cleanup/I/O failures keep their existing primary cause.
4138fn complete_ptrace_run<T>(
4139    completed: Result<T, SchedulerEndedRun>,
4140    cleanup: Result<(), Error>,
4141    status: ExitStatus,
4142) -> Result<T, Error> {
4143    match cleanup {
4144        Ok(()) => completed.map_err(Error::new),
4145        Err(error) => {
4146            if error
4147                .downcast_ref::<detcore::FinalizationFailure>()
4148                .is_none()
4149                && error.is::<detcore::ReplayDepartureError>()
4150                && let Err(terminal) = completed
4151            {
4152                return Err(Error::new(terminal).context(
4153                    error
4154                        .downcast::<detcore::ReplayDepartureError>()
4155                        .expect("type checked"),
4156                ));
4157            }
4158            Err(error.context(format!(
4159                "ptrace cleanup after observed guest status {status:?}"
4160            )))
4161        }
4162    }
4163}
4164
4165/// Variant of `run` that also captures stdout/stderr.
4166///
4167/// On ptrace it needs no particular stack from the calling thread; see [`run`].
4168pub fn run_with_output(
4169    command: Command,
4170    config: DetConfig,
4171    print_summary: bool,
4172    print_summary_to_json_file: &Option<PathBuf>,
4173) -> Result<Output, Error> {
4174    run_with_output_backend(
4175        command,
4176        config,
4177        print_summary,
4178        print_summary_to_json_file,
4179        Backend::Ptrace,
4180    )
4181}
4182
4183/// Variant of [`run_with_backend`] that also captures stdout/stderr.
4184///
4185/// On ptrace it needs no particular stack from the calling thread; see [`run`].
4186pub fn run_with_output_backend(
4187    command: Command,
4188    config: DetConfig,
4189    print_summary: bool,
4190    print_summary_to_json_file: &Option<PathBuf>,
4191    backend: Backend,
4192) -> Result<Output, Error> {
4193    if backend == Backend::Kvm {
4194        ensure_kvm_stdin_reserved()?;
4195    }
4196    run_with_output_backend_timeout(
4197        command,
4198        config,
4199        print_summary,
4200        print_summary_to_json_file,
4201        backend,
4202        None,
4203    )
4204}
4205
4206/// [`run_with_output_backend`] with a hermit-enforced wall-clock bound.
4207///
4208/// See [`run_with_backend_timeout`]; the teardown semantics are identical. On
4209/// ptrace it needs no particular stack from the calling thread; see [`run`].
4210pub fn run_with_output_backend_timeout(
4211    command: Command,
4212    config: DetConfig,
4213    print_summary: bool,
4214    print_summary_to_json_file: &Option<PathBuf>,
4215    backend: Backend,
4216    timeout: Option<Duration>,
4217) -> Result<Output, Error> {
4218    if backend == Backend::Ptrace {
4219        let summary_path = print_summary_to_json_file.clone();
4220        let command = tracer_thread::SendCommand::new(command);
4221        return ptrace_completion::run_on_tracer_thread(timeout, move |control| async move {
4222            let command = command.into_inner();
4223            let report = SkidOvershootReport::begin(true);
4224            // Box before the retained operation captures the backend state;
4225            // constructing and polling it still happen after admission here.
4226            let result = Box::pin(dispatch_output_backend(
4227                command,
4228                prepare_backend_config(config, backend),
4229                print_summary,
4230                &summary_path,
4231                backend,
4232                Some(control),
4233            ))
4234            .await;
4235            report.finish(result)
4236        });
4237    }
4238    let (output, skid_overshoots) = run_with_output_backend_timeout_and_skid_overshoots(
4239        command,
4240        config,
4241        print_summary,
4242        print_summary_to_json_file,
4243        backend,
4244        timeout,
4245    )?;
4246    if skid_overshoots > 0 {
4247        return Err(Error::new(SkidOvershootError::new(skid_overshoots)));
4248    }
4249    Ok(output)
4250}
4251
4252/// Run with captured output and return the number of precise PMU timer
4253/// overshoots alongside it instead of refusing before a caller can inspect the
4254/// completed output. The `--verify` path uses this to finish and publish its
4255/// comparison before it refuses the overshoot-tainted result.
4256#[doc(hidden)]
4257pub fn run_with_output_backend_timeout_and_skid_overshoots(
4258    command: Command,
4259    config: DetConfig,
4260    print_summary: bool,
4261    print_summary_to_json_file: &Option<PathBuf>,
4262    backend: Backend,
4263    timeout: Option<Duration>,
4264) -> Result<(Output, u64), Error> {
4265    if backend == Backend::Ptrace {
4266        let summary_path = print_summary_to_json_file.clone();
4267        let command = tracer_thread::SendCommand::new(command);
4268        return ptrace_completion::run_on_tracer_thread(timeout, move |control| async move {
4269            let command = command.into_inner();
4270            let report = SkidOvershootReport::begin(true);
4271            let result = Box::pin(dispatch_output_backend(
4272                command,
4273                prepare_backend_config(config, backend),
4274                print_summary,
4275                &summary_path,
4276                backend,
4277                Some(control),
4278            ))
4279            .await;
4280            report.finish_with_count(result)
4281        });
4282    }
4283    let skid_overshoot_report =
4284        SkidOvershootReport::begin(backend.consumes_skid_overshoot_reports());
4285    if backend == Backend::Kvm {
4286        // Reserve before the Tokio runtime can reuse a closed fd 0. KVM
4287        // verification consumes the explicit output snapshot in `run_kvm`;
4288        // public output-capture callers without one retain this reservation.
4289        ensure_kvm_stdin_reserved()?;
4290    }
4291    let config = prepare_backend_config(config, backend);
4292    let result = run_with_output_backend_inner(
4293        command,
4294        config,
4295        print_summary,
4296        print_summary_to_json_file,
4297        backend,
4298        timeout,
4299    );
4300    skid_overshoot_report.finish_with_count(result)
4301}
4302
4303#[tokio::main(flavor = "current_thread")]
4304async fn run_with_output_backend_inner(
4305    command: Command,
4306    config: DetConfig,
4307    print_summary: bool,
4308    print_summary_to_json_file: &Option<PathBuf>,
4309    backend: Backend,
4310    timeout: Option<Duration>,
4311) -> Result<Output, Error> {
4312    refuse_in_guest_liteinst_timeout(backend, timeout)?;
4313    // Keep the backend state off the caller's stack before Tokio and the
4314    // deadline future capture it. Polling and teardown stay on this thread.
4315    let guest = Box::pin(dispatch_output_backend(
4316        command,
4317        config,
4318        print_summary,
4319        print_summary_to_json_file,
4320        backend,
4321        None,
4322    ));
4323    match timeout {
4324        None => guest.await,
4325        Some(limit) => match tokio::time::timeout(limit, guest).await {
4326            Ok(result) => result,
4327            Err(_elapsed) => Err(Error::new(GuestTimedOut {
4328                limit,
4329                survivors: None,
4330            })),
4331        },
4332    }
4333}
4334
4335async fn dispatch_output_backend(
4336    mut command: Command,
4337    mut config: DetConfig,
4338    print_summary: bool,
4339    print_summary_to_json_file: &Option<PathBuf>,
4340    backend: Backend,
4341    control: Option<std::rc::Rc<ptrace_completion::Control>>,
4342) -> Result<Output, Error> {
4343    // Every launch path, ptrace's included, reaches the guest through here.
4344    crate::cpu_profile_admission::refuse_unadmitted_launch(&config, backend == Backend::Ptrace)?;
4345    detcore_model::virtual_timeout::refuse_debugger(config.virtual_timeout, config.gdbserver)?;
4346    crate::preemption_admission::refuse_unqualified_annotated_launch(&config)?;
4347    detcore::preemptions::validate_config_replay_inputs(&config)?;
4348    <detcore::Detcore as reverie::Tool>::facility_admission(&config)?;
4349    config.launch_image = launched_image(&command);
4350    if backend == Backend::Kvm {
4351        return run_kvm(
4352            &command,
4353            config,
4354            print_summary,
4355            print_summary_to_json_file,
4356            true,
4357        )
4358        .await;
4359    }
4360
4361    if backend.is_in_guest_liteinst() {
4362        #[cfg(feature = "liteinst")]
4363        {
4364            command.stdin(output_backend_stdin()?);
4365            let stats_request = in_guest_stats_request(backend, print_summary_to_json_file);
4366            refuse_in_guest_liteinst_run(&mut command, &config, backend)?;
4367            let (launch, _detlog_descriptor) =
4368                in_guest_liteinst_launch(&mut command, &in_guest_detlog_forward_policy())?;
4369            // After Hermit's other callbacks, so the guest's own check is the
4370            // last one that can change what it checks (`LaunchAudit::install`).
4371            let audit = neutralize_inherited_signal_producers(&mut command, backend)?;
4372            let tool_data = launch.encode();
4373            // Every Tool image must forward on that socket, by that policy;
4374            // see detcore::Config::in_guest_detlog_forward_policy.
4375            let mut config = config;
4376            config.in_guest_detlog_forward_policy = _detlog_descriptor
4377                .is_some()
4378                .then(|| in_guest_detlog_forward_policy().encode());
4379            config.in_guest_site_patching_off = in_guest_site_patching_off(backend);
4380            config.in_guest_refuses_host_timed_producers =
4381                in_guest_refuses_host_timed_producers(backend);
4382            // Last, from the configuration every Tool image receives.
4383            config.in_guest_reserved_signals = in_guest_reserved_signals(&config, backend);
4384            // The delivery gate, from the final configuration and whether
4385            // this process owns the guests' PID namespace; off until the
4386            // runtime takes it (A5 step 3c.1, D35, D52).
4387            config.in_guest_signal_delivery = record_delivery_gate(
4388                runtime_delivers_signals()
4389                    && in_guest_signal_delivery_gate(
4390                        &config,
4391                        backend,
4392                        scheduler_owns_guest_pid_namespace(),
4393                    ),
4394            );
4395            let preload = liteinst_tool_runtime_library_path()?;
4396            let exits = std::sync::Arc::new(in_guest_exits::InGuestExitAdmission::default());
4397            let (output, mut global_state, dispatch_stats, process_reports) =
4398                reverie_liteinst::LiteinstBackend::with_connection_admission(exits.clone(), async {
4399                    if stats_request.is_enabled() {
4400                        let (output, global_state, source) =
4401                            reverie_liteinst::LiteinstBackend::run_with_output_and_preload_data_and_stats::<
4402                                Detcore,
4403                            >(command, config, preload, tool_data)
4404                            .await?;
4405                        let dispatch_stats = backend_stats::report(backend, stats_request, &source);
4406                        let process_reports = source.snapshot().process_reports();
4407                        Ok::<_, reverie::Error>((output, global_state, dispatch_stats, process_reports))
4408                    } else {
4409                        let (output, global_state) =
4410                            reverie_liteinst::LiteinstBackend::run_with_output_and_preload_data::<
4411                                Detcore,
4412                            >(command, config, preload, tool_data)
4413                            .await?;
4414                        Ok((output, global_state, None, 0))
4415                    }
4416                })
4417                .await
4418                .inspect_err(|error| exits.fail_launch(error))
4419                // A standard descriptor the guest's own check refused
4420                // (D25) fails the launch; name it.
4421                .map_err(|error| match &audit {
4422                    Some(audit) => audit.launch_error(backend, error),
4423                    None => Error::from(error),
4424                })?;
4425            let patched =
4426                in_guest_trap_patching_evidence(backend, dispatch_stats.as_ref(), process_reports);
4427            let settled = settle_in_guest_exits(&exits, &global_state).await;
4428            let policy_refusal = global_state.policy_refusal();
4429            let output = Output {
4430                status: output.status.into(),
4431                stdout: output.stdout,
4432                stderr: output.stderr,
4433            };
4434            let status = output.status;
4435            if settled.is_err() || liteinst_requires_forced_shutdown(status) {
4436                global_state.force_shutdown_with_error();
4437                global_state.cancel_internal_scheduler().await;
4438            }
4439            // The in-guest runtime starts after the guest's dynamic loader,
4440            // whose syscalls the histogram therefore misses.
4441            global_state.note_syscall_observation_gap(
4442                detcore::SyscallObservationGap::InProcessRuntimeStart,
4443            );
4444            let finalized = global_state
4445                .clean_up_with_dispatch_stats(
4446                    print_summary,
4447                    print_summary_to_json_file,
4448                    dispatch_stats,
4449                )
4450                .await;
4451            // After the scheduler has shut down, so the line's place among
4452            // the run's INFO lines is fixed (D25); a stopping refusal logs it
4453            // just before it exits instead.
4454            detcore::host_timed_producers::emit_run_end_log();
4455            let result = (|| {
4456                refuse_in_guest_policy(backend, policy_refusal)?;
4457                settled?;
4458                refuse_in_guest_trap_patching(backend, patched)
4459            })();
4460            detcore::combine_finalization(result, finalized)?;
4461            return Ok(Output {
4462                status,
4463                stdout: output.stdout,
4464                stderr: output.stderr,
4465            });
4466        }
4467        #[cfg(not(feature = "liteinst"))]
4468        {
4469            backend.ensure_available()?;
4470            unreachable!("LiteInst availability must fail when the feature is disabled");
4471        }
4472    }
4473    ensure_backend_dispatch(backend)?;
4474
4475    let stats_request = backend_stats::request(print_summary_to_json_file);
4476    command.stdin(output_backend_stdin()?);
4477    command.stdout(Stdio::piped());
4478    command.stderr(Stdio::piped());
4479    let control = control
4480        .ok_or_else(|| Error::msg("ordinary ptrace requires its retained operation owner"))?;
4481    // A sequentialized guest tells gdbserver not to serialize its threads;
4482    // see hermit_run_ptrace::spawn_tracer.
4483    let tracer = hermit_run_ptrace::spawn_tracer(
4484        command,
4485        config.clone(),
4486        config
4487            .gdbserver
4488            .then(|| gdbserver::connection(config.gdbserver_port))
4489            .transpose()?,
4490        config.sequentialize_threads,
4491        stats_request,
4492    )
4493    .await?;
4494    let stats_source = tracer.backend_stats();
4495    let (output, global_state) = ptrace_completion::wait_with_output(tracer, control).await?;
4496    let terminal_outcome = global_state.terminal_outcome();
4497    let dispatch_stats =
4498        backend_stats::report_if_collected(backend, stats_request, stats_source.as_ref());
4499    let cleanup = global_state
4500        .clean_up_with_dispatch_stats(print_summary, print_summary_to_json_file, dispatch_stats)
4501        .await;
4502    let status = output.status;
4503    let completed = match terminal_outcome {
4504        Some(outcome) => Err(SchedulerEndedRun::new(outcome, Some(output))),
4505        None => Ok(output),
4506    };
4507    complete_ptrace_run(completed, cleanup, status)
4508}
4509
4510/// Holds the context necessary to run high-level hermit functions.
4511pub struct HermitData {
4512    // The data directory. Defaults to `~/.cache/hermit`. Note that we shouldn't
4513    // expect this to exist in any of the functions that are called.
4514    data_dir: PathBuf,
4515}
4516
4517fn collect_recording_ids(
4518    entries: impl IntoIterator<Item = io::Result<fs::DirEntry>>,
4519    data_dir: &Path,
4520    mut file_type: impl FnMut(&fs::DirEntry) -> io::Result<fs::FileType>,
4521) -> Result<Vec<Id>, Error> {
4522    let mut recordings = Vec::new();
4523    for entry in entries {
4524        let entry = entry.with_context(|| {
4525            format!(
4526                "Failed to read an entry in recording inventory {}",
4527                data_dir.display()
4528            )
4529        })?;
4530        let path = entry.path();
4531        if file_type(&entry)
4532            .with_context(|| {
4533                format!(
4534                    "Failed to inspect recording inventory entry {}",
4535                    path.display()
4536                )
4537            })?
4538            .is_dir()
4539            && let Some(id) = entry
4540                .file_name()
4541                .to_str()
4542                .and_then(|name| name.parse::<Id>().ok())
4543        {
4544            recordings.push(id);
4545        }
4546    }
4547    Ok(recordings)
4548}
4549
4550impl Default for HermitData {
4551    fn default() -> Self {
4552        Self::new()
4553    }
4554}
4555
4556impl HermitData {
4557    /// Creates an instance of `HermitData` using `~/.cache/hermit` as the data
4558    /// directory.
4559    pub fn new() -> Self {
4560        Self::with_dir(
4561            dirs::cache_dir()
4562                .map_or_else(|| PathBuf::from("/tmp/hermit"), |dir| dir.join("hermit")),
4563        )
4564    }
4565
4566    /// Creates a `HermitData` using the given directory as the base path for
4567    /// storing recording data.
4568    pub fn with_dir<P>(data_dir: P) -> Self
4569    where
4570        P: Into<PathBuf>,
4571    {
4572        Self {
4573            data_dir: data_dir.into(),
4574        }
4575    }
4576
4577    /// Returns the path to the data directory where recordings are stored.
4578    pub fn data_dir(&self) -> &Path {
4579        &self.data_dir
4580    }
4581
4582    /// Records the execution of the given command, returning its `Recording`.
4583    ///
4584    /// If recording failed, then an error is returned. Note that if the command
4585    /// itself failed, then we still return a successful recording, but its exit
4586    /// status will be non-zero.
4587    ///
4588    /// This low-level API does not create a Hermit container, so it has no
4589    /// authenticated private-mount provenance to supply. Detcore derives mount
4590    /// identities from the completed guest namespace. CLI container callers
4591    /// with producer-owned root provenance must use
4592    /// [`Self::record_with_mountinfo`].
4593    ///
4594    /// The ptrace session runs on a tracer thread, so the calling thread needs no
4595    /// particular stack; see [`run`].
4596    pub fn record(&self, command: Command) -> Result<Recording, Error> {
4597        let store = Self {
4598            data_dir: self.data_dir.clone(),
4599        };
4600        let command = tracer_thread::SendCommand::new(command);
4601        ptrace_completion::run_on_tracer_thread(None, move |control| async move {
4602            let command = command.into_inner();
4603            let data = store.create_recording_dir()?;
4604            let exit_status = record_to_async(
4605                command,
4606                data.path(),
4607                MountProvenance::default(),
4608                None,
4609                None,
4610                control,
4611            )
4612            .await?;
4613            store.commit_recording(data, exit_status)
4614        })
4615    }
4616
4617    /// Records with mountinfo provenance captured from the active container.
4618    ///
4619    /// The ptrace session runs on a tracer thread, so the calling thread needs no
4620    /// particular stack; see [`run`].
4621    pub fn record_with_mountinfo(
4622        &self,
4623        command: Command,
4624        mountinfo_root_rewrites: Vec<detcore_model::config::MountInfoRootRewrite>,
4625    ) -> Result<Recording, Error> {
4626        let store = Self {
4627            data_dir: self.data_dir.clone(),
4628        };
4629        let command = tracer_thread::SendCommand::new(command);
4630        ptrace_completion::run_on_tracer_thread(None, move |control| async move {
4631            let command = command.into_inner();
4632            let data = store.create_recording_dir()?;
4633            let mount_ids = capture_mountinfo_identity_order()?;
4634            let exit_status = record_to_async(
4635                command,
4636                data.path(),
4637                MountProvenance {
4638                    root_rewrites: mountinfo_root_rewrites,
4639                    container_mount_table: None,
4640                    mount_ids: None,
4641                },
4642                Some(mount_ids),
4643                None,
4644                control,
4645            )
4646            .await?;
4647            store.commit_recording(data, exit_status)
4648        })
4649    }
4650
4651    /// Creates a temporary directory for a recording that has not been committed yet.
4652    pub fn create_recording_dir(&self) -> Result<tempfile::TempDir, Error> {
4653        let tmp_data_dir = self.data_dir.join("tmp");
4654
4655        fs::create_dir_all(&tmp_data_dir).with_context(|| {
4656            format!(
4657                "Failed to create recording directory: {}",
4658                self.data_dir.display()
4659            )
4660        })?;
4661
4662        Ok(tempfile::TempDir::new_in(tmp_data_dir)?)
4663    }
4664
4665    /// Commits a completed temporary recording to the recording store.
4666    pub fn commit_recording(
4667        &self,
4668        data: tempfile::TempDir,
4669        exit_status: ExitStatus,
4670    ) -> Result<Recording, Error> {
4671        let id = Id::unique();
4672
4673        // Atomically move the temporary recording to its final location.
4674        fs::rename(data.keep(), self.data_dir.join(id.to_string()))?;
4675
4676        self.note_latest(&id, true)
4677            .with_context(|| format!("Failed to update {:?}", self.data_dir.join(LAST)))?;
4678
4679        Ok(Recording { id, exit_status })
4680    }
4681
4682    /// Replays the given recording ID.
4683    ///
4684    /// The ptrace session runs on a tracer thread, so the calling thread needs no
4685    /// particular stack; see [`run`].
4686    pub fn replay(&self, id: Id) -> Result<ExitStatus, Error> {
4687        let data = self.data_dir.join(id.to_string());
4688        replay_from(&data)
4689    }
4690
4691    /// Replays the given recording ID with a gdbserver available to attach to.
4692    ///
4693    /// The ptrace session runs on a tracer thread, so the calling thread needs no
4694    /// particular stack; see [`run`].
4695    pub fn replay_with_gdbserver(&self, id: Id, port: u16) -> Result<ExitStatus, Error> {
4696        let data = self.data_dir.join(id.to_string());
4697        replay_with_gdbserver(&data, port)
4698    }
4699
4700    /// Returns an iterator over the recordings.
4701    ///
4702    /// Use [`Self::recording_metadata`] to get more information about a recording.
4703    pub fn recordings(&self) -> impl Iterator<Item = Id> + use<> {
4704        fs::read_dir(&self.data_dir)
4705            .ok()
4706            .into_iter()
4707            .flatten()
4708            .filter_map(|entry| {
4709                let entry = entry.ok()?;
4710
4711                if entry.file_type().ok()?.is_dir() {
4712                    Some(entry.file_name().to_str()?.parse::<Id>().ok()?)
4713                } else {
4714                    None
4715                }
4716            })
4717    }
4718
4719    /// Returns all recordings, or an error if the data directory cannot be
4720    /// enumerated completely.
4721    ///
4722    /// A missing data directory has no recordings. Other filesystem errors are
4723    /// returned rather than being mistaken for an empty or partial inventory.
4724    pub fn try_recordings(&self) -> Result<Vec<Id>, Error> {
4725        let entries = match fs::read_dir(&self.data_dir) {
4726            Ok(entries) => entries,
4727            Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
4728            Err(error) => {
4729                return Err(error).with_context(|| {
4730                    format!(
4731                        "Failed to read recording inventory in {}",
4732                        self.data_dir.display()
4733                    )
4734                });
4735            }
4736        };
4737
4738        collect_recording_ids(entries, &self.data_dir, fs::DirEntry::file_type)
4739    }
4740
4741    /// Returns the metadata of a recording.
4742    pub fn recording_metadata(&self, id: Id) -> Result<Metadata, Error> {
4743        Metadata::load(&self.data_dir.join(id.to_string()))
4744    }
4745
4746    /// Deletes a recording.
4747    pub fn remove(&self, id: Id) -> Result<(), Error> {
4748        let path = self.data_dir.join(id.to_string());
4749
4750        // Before deleting anything, make sure this file exists. This may not be a
4751        // recording if this file does not exist. A run manifest marks a
4752        // recording too: a run that drew an input from the host leaves only
4753        // that (https://github.com/rrnewton/hermit/issues/4271).
4754        let metadata_path = [METADATA_NAME, RUN_MANIFEST_NAME]
4755            .into_iter()
4756            .map(|name| path.join(name))
4757            .find(|marker| marker.exists())
4758            .unwrap_or_else(|| path.join(METADATA_NAME));
4759        let metadata = fs::metadata(&metadata_path)
4760            .with_context(|| format!("Failed to find {:?}", metadata_path))?;
4761
4762        if !metadata.is_file() {
4763            return Err(anyhow!("{:?} is not a file", metadata_path));
4764        }
4765
4766        // Do a recursive delete on the directory. Note that this does not follow
4767        // symlinks.
4768        fs::remove_dir_all(path)?;
4769
4770        Ok(())
4771    }
4772
4773    /// Returns the last recorded ID.
4774    pub fn last_id(&self) -> Result<Id, Error> {
4775        Ok(fs::read_to_string(self.data_dir.join(LAST))?.parse()?)
4776    }
4777
4778    /// Atomically names `id` as the newest recording: `last-run` always,
4779    /// and `last`, which a bare `hermit replay` replays, when the user
4780    /// `asked` for the recording (`hermit record`, `hermit run
4781    /// --record-networking`) rather than a run leaving its manifest; see
4782    /// <https://github.com/rrnewton/hermit/issues/4271>.
4783    pub fn note_latest(&self, id: &Id, asked: bool) -> Result<(), Error> {
4784        let tmp = self.data_dir.join("tmp");
4785        fs::create_dir_all(&tmp)?;
4786        let names = if asked {
4787            &[LAST_RUN, LAST][..]
4788        } else {
4789            &[LAST_RUN][..]
4790        };
4791        for name in names {
4792            let mut file = tempfile::NamedTempFile::new_in(&tmp)?;
4793            write!(file, "{}", id)?;
4794            file.persist(self.data_dir.join(name))?;
4795        }
4796        Ok(())
4797    }
4798}
4799
4800/// What a recording takes from the completed recording container's mount
4801/// namespace, which a caller without a Hermit container leaves empty.
4802#[derive(Clone, Debug, Default)]
4803pub struct MountProvenance {
4804    /// The proven roots of Hermit's own mounts
4805    /// (`detcore::Config::mountinfo_root_rewrites`).
4806    pub root_rewrites: Vec<detcore_model::config::MountInfoRootRewrite>,
4807    /// The guest's container mount table
4808    /// (`detcore::Config::container_mount_table`), which the recording carries
4809    /// for its replay.
4810    pub container_mount_table: Option<detcore_model::container_mounts::ContainerMountTable>,
4811    /// The namespace's identity order, read with the table
4812    /// (`detcore::Config::mountinfo_mount_ids`). `None` asks the recorder to
4813    /// capture it. A table and an order from two reads need not agree: a host
4814    /// process that removes a host mount's directory detaches the copy of that
4815    /// mount from the container too, so a row the table shows can be gone
4816    /// from a later read.
4817    pub mount_ids: Option<Vec<u64>>,
4818}
4819
4820/// `NS_GET_MNTNS_ID`, `_IOR(NSIO, 5, __u64)` in Linux's uapi/linux/nsfs.h.
4821const NS_GET_MNTNS_ID: libc::c_ulong = (2 << 30) | (8 << 16) | (0xb7 << 8) | 5;
4822
4823/// The calling thread's mount namespace (`DetConfig::mountinfo_mount_namespace`):
4824/// the kernel's namespace ID from `ioctl(NS_GET_MNTNS_ID)` on
4825/// `/proc/thread-self/ns/mnt`, and the descriptor's device and inode numbers.
4826/// The calling thread's, not the thread group leader's: a library caller's
4827/// thread can have a mount namespace of its own, and every capture here
4828/// (`/proc/thread-self/mountinfo` and `fdinfo`) reads that same thread's. A
4829/// kernel without the ioctl (ENOTTY or EINVAL) yields no ID; any other failure
4830/// is an error.
4831pub fn current_mount_namespace() -> Result<detcore_model::config::MountNamespaceIdentity, Error> {
4832    use std::os::fd::AsRawFd;
4833    use std::os::unix::fs::MetadataExt;
4834    let file = fs::File::open("/proc/thread-self/ns/mnt")
4835        .context("failed to open this thread's mount namespace")?;
4836    let metadata = file
4837        .metadata()
4838        .context("failed to read this process's mount namespace")?;
4839    let mut id: u64 = 0;
4840    // SAFETY: NS_GET_MNTNS_ID writes one u64 through the pointer, which
4841    // points at `id` for the duration of the call.
4842    let id =
4843        if unsafe { libc::ioctl(file.as_raw_fd(), NS_GET_MNTNS_ID, &mut id as *mut u64) } == 0 {
4844            Some(id)
4845        } else {
4846            let error = std::io::Error::last_os_error();
4847            match error.raw_os_error() {
4848                Some(libc::ENOTTY | libc::EINVAL) => None,
4849                _ => {
4850                    return Err(Error::new(error)
4851                        .context("failed to read this process's mount namespace ID"));
4852                }
4853            }
4854        };
4855    Ok(detcore_model::config::MountNamespaceIdentity {
4856        id,
4857        device: metadata.dev(),
4858        inode: metadata.ino(),
4859    })
4860}
4861
4862/// The calling thread's mount table (`/proc/thread-self/mountinfo`): the rows
4863/// of the namespace [`current_mount_namespace`] names.
4864pub fn thread_mountinfo() -> Result<Vec<u8>, Error> {
4865    fs::read("/proc/thread-self/mountinfo")
4866        .context("Failed to read this thread's mount table (/proc/thread-self/mountinfo)")
4867}
4868
4869/// The raw mount ID of the mount `path` reaches in the calling thread's
4870/// namespace, from the `fdinfo` of an `O_PATH` descriptor held on it; `None`
4871/// where it cannot be opened. O_PATH pins the object without read
4872/// permission.
4873fn thread_mount_id_of(path: &Path) -> Option<u64> {
4874    held_mount_id(&hold_path(path, false).ok()?)
4875}
4876
4877/// An `O_PATH` descriptor held on `path` (a directory, with `directory`),
4878/// resolved as `chdir` and `open` resolve it: following symbolic links, and
4879/// a relative path from the calling thread's working directory, whose name is
4880/// never asked for. Holding it pins the mount it reaches, and the lookup has
4881/// already triggered any automount on the way.
4882fn hold_path(path: &Path, directory: bool) -> nix::Result<std::os::fd::OwnedFd> {
4883    use nix::fcntl::OFlag;
4884    let flags = OFlag::O_PATH
4885        | OFlag::O_CLOEXEC
4886        | if directory {
4887            OFlag::O_DIRECTORY
4888        } else {
4889            OFlag::empty()
4890        };
4891    nix::fcntl::open(path, flags, nix::sys::stat::Mode::empty())
4892}
4893
4894/// The raw mount ID of the mount `held` reaches, from its
4895/// `/proc/thread-self/fdinfo`.
4896fn held_mount_id(held: &std::os::fd::OwnedFd) -> Option<u64> {
4897    let fdinfo = fs::read(format!("/proc/thread-self/fdinfo/{}", held.as_raw_fd())).ok()?;
4898    detcore_model::procfs::parse_fdinfo_mount_id(&fdinfo)
4899}
4900
4901/// Capture the calling thread's mount namespace's raw IDs in the exact
4902/// row/parent order used by Detcore's canonical mountinfo mapping.
4903pub fn capture_mountinfo_identity_order() -> Result<Vec<u64>, Error> {
4904    mountinfo_identity_order_from_raw(&thread_mountinfo()?)
4905}
4906
4907/// [`capture_mountinfo_identity_order`], keeping every row whose mount ID is
4908/// in `keep` (the rows a container mount table shows) whatever its name: a
4909/// mount Hermit installed is never dropped by the seed filter, so a table
4910/// showing it validates against the captured order.
4911pub fn capture_mountinfo_identity_order_keeping(
4912    keep: &std::collections::BTreeSet<u64>,
4913) -> Result<Vec<u64>, Error> {
4914    mountinfo_identity_order_keeping_from_raw(&thread_mountinfo()?, keep)
4915}
4916
4917/// Each of [`BASE_MOUNTPOINTS`](detcore_model::container_mounts::BASE_MOUNTPOINTS)
4918/// that the calling thread can open, as the kernel escapes it in `mountinfo`,
4919/// with the mount ID of a descriptor held on it: rule 2's proof of
4920/// `ContainerMountTable::select`. The base mountpoints hold no byte the kernel
4921/// escapes (space, tab, newline or backslash), so each is its own escaped form.
4922pub fn base_mountpoint_mount_ids() -> Vec<(Vec<u8>, u64)> {
4923    detcore_model::container_mounts::BASE_MOUNTPOINTS
4924        .iter()
4925        .filter_map(|mountpoint| {
4926            let id = thread_mount_id_of(Path::new(mountpoint))?;
4927            Some((mountpoint.as_bytes().to_vec(), id))
4928        })
4929        .collect()
4930}
4931
4932/// A container mount table and the raw identity order Detcore validates it
4933/// against ([`unplanned_container_mount_table`]).
4934pub type UnplannedMountTable = (
4935    detcore_model::container_mounts::ContainerMountTable,
4936    Vec<u64>,
4937);
4938
4939// TODO-HUMAN-REVIEW(https://github.com/rrnewton/hermit/issues/4213): Review
4940// the container mount table of a run in a namespace Hermit did not make: the
4941// membership rule of K3a with the launch mounts in place of Hermit's
4942// installations.
4943/// The container mount table of a run whose guest runs in the calling
4944/// thread's own mount namespace, in which Hermit installed no mount
4945/// (`--no-namespace`, or a library caller that built no container, on KVM),
4946/// and the raw identity order Detcore validates it against, both from one
4947/// read of the namespace's rows. Shown are:
4948/// - the launch mounts: the mount holding `launch_cwd`, the guest's working
4949///   directory at launch, and the mount holding the host's `/tmp`, which the
4950///   guest shares, with their ancestors (rules 1 and 3 of
4951///   `ContainerMountTable::select`, as for Hermit's installations), so that
4952///   `df .` and `findmnt -T .` from a checkout find their row;
4953/// - the base mountpoints mounted here (rule 2).
4954///
4955/// Both are facts of the launch, fixed before the guest starts, as the base
4956/// mountpoints are. No other host mount is shown; a file on one reports its
4957/// nearest shown ancestor. A shown row's parent is its nearest shown
4958/// ancestor, as for every table.
4959///
4960/// `launch_cwd` may be relative (`.` for the inherited working directory):
4961/// it is resolved from the calling thread's working directory as the guest's
4962/// `chdir` will resolve it, without asking that directory for its name, so a
4963/// removed working directory does not stop a launch with an absolute
4964/// `--workdir`.
4965pub fn unplanned_container_mount_table(launch_cwd: &Path) -> Result<UnplannedMountTable, Error> {
4966    unplanned_container_mount_table_with(launch_cwd, thread_mountinfo)
4967}
4968
4969/// [`unplanned_container_mount_table`], reading the mount table with `read`.
4970/// The launch directories and the base mountpoints are resolved and held
4971/// first, then the table is read, then the mount IDs are taken from the held
4972/// descriptors: a mount that resolving a launch directory creates (an
4973/// automount on its path) is already in the rows read, and one that leaves
4974/// afterwards is still named by them. A launch mount the read does not list
4975/// is refused, never dropped.
4976fn unplanned_container_mount_table_with(
4977    launch_cwd: &Path,
4978    read: impl FnOnce() -> Result<Vec<u8>, Error>,
4979) -> Result<UnplannedMountTable, Error> {
4980    let working = hold_path(launch_cwd, true).with_context(|| {
4981        format!(
4982            "the guest's working directory {} cannot be opened for its mount table",
4983            launch_cwd.display()
4984        )
4985    })?;
4986    // The host /tmp, which the guest shares, if there is one.
4987    let tmp = hold_path(Path::new("/tmp"), true).ok();
4988    let base = detcore_model::container_mounts::BASE_MOUNTPOINTS
4989        .iter()
4990        .filter_map(|mountpoint| Some((*mountpoint, hold_path(Path::new(mountpoint), false).ok()?)))
4991        .collect::<Vec<_>>();
4992    let raw = read()?;
4993    let launch = std::iter::once(&working)
4994        .chain(tmp.as_ref())
4995        .map(|held| {
4996            held_mount_id(held)
4997                .ok_or_else(|| anyhow!("a launch directory's mount ID cannot be read"))
4998        })
4999        .collect::<Result<std::collections::BTreeSet<_>, Error>>()?;
5000    let base = base
5001        .iter()
5002        .filter_map(|(mountpoint, held)| {
5003            Some((mountpoint.as_bytes().to_vec(), held_mount_id(held)?))
5004        })
5005        .collect::<Vec<_>>();
5006    unplanned_container_mount_table_from(&raw, &launch, &base)
5007}
5008
5009fn unplanned_container_mount_table_from(
5010    raw: &[u8],
5011    launch: &std::collections::BTreeSet<u64>,
5012    base: &[(Vec<u8>, u64)],
5013) -> Result<UnplannedMountTable, Error> {
5014    use detcore_model::container_mounts::ContainerMountTable;
5015    let rows = detcore_model::container_mounts::parse_container_mounts(raw)
5016        .ok_or_else(|| anyhow!("this thread's mount table has a malformed row"))?;
5017    if let Some(missing) = launch
5018        .iter()
5019        .find(|id| !rows.iter().any(|row| row.raw_mount_id == **id))
5020    {
5021        anyhow::bail!(
5022            "the launch mount {missing} (holding the guest's working directory or /tmp) is not in \
5023             this thread's mount table, read after it was resolved; refusing a table that would \
5024             omit the mount the guest starts on"
5025        );
5026    }
5027    let never = rows
5028        .iter()
5029        .filter(|row| row.is_ephemeral_host_seed())
5030        .map(|row| row.raw_mount_id)
5031        .collect();
5032    let table = ContainerMountTable::select(&rows, launch, base, &never);
5033    let order = mountinfo_identity_order_keeping_from_raw(raw, &table.shown_mount_ids())?;
5034    Ok((table, order))
5035}
5036
5037/// Producer provenance follows the guest mount model: ephemeral host seed rows
5038/// (other processes' squashfuse mounts) are not namespace members and must not
5039/// enter the captured identity order. Detcore drops the same rows with the same
5040/// filter when it captures the guest's mountinfo.
5041fn mountinfo_identity_order_from_raw(raw: &[u8]) -> Result<Vec<u64>, Error> {
5042    mountinfo_identity_order_keeping_from_raw(raw, &std::collections::BTreeSet::new())
5043}
5044
5045/// The identity order of the mountinfo contents `raw` (seed rows dropped, as
5046/// `mountinfo_identity_order_from_raw` drops them), keeping a seed row whose
5047/// mount ID is in `keep`.
5048pub fn mountinfo_identity_order_keeping_from_raw(
5049    raw: &[u8],
5050    keep: &std::collections::BTreeSet<u64>,
5051) -> Result<Vec<u64>, Error> {
5052    let mut kept = Vec::with_capacity(raw.len());
5053    for line in raw.split_inclusive(|byte| *byte == b'\n') {
5054        let body = line.strip_suffix(b"\n").unwrap_or(line);
5055        let id = body
5056            .split(|byte| *byte == b' ')
5057            .next()
5058            .and_then(|field| std::str::from_utf8(field).ok()?.parse::<u64>().ok());
5059        if !detcore_model::procfs::is_ephemeral_host_seed_mount(body)
5060            || id.is_some_and(|id| keep.contains(&id))
5061        {
5062            kept.extend_from_slice(line);
5063        }
5064    }
5065    mountinfo_identity_order_from(&kept)
5066}
5067
5068fn mountinfo_identity_order_from(contents: &[u8]) -> Result<Vec<u64>, Error> {
5069    use std::collections::BTreeSet;
5070
5071    let rows = detcore_model::procfs::parse_mountinfo(contents)
5072        .ok_or_else(|| anyhow!("malformed mountinfo"))?;
5073    let mut visible = Vec::new();
5074    let mut parents = Vec::new();
5075    let mut seen = BTreeSet::new();
5076    for row in rows {
5077        seen.insert(row.raw_mount_id);
5078        visible.push(row.raw_mount_id);
5079        parents.push(row.raw_parent_id);
5080    }
5081    for parent in parents {
5082        if seen.insert(parent) {
5083            visible.push(parent);
5084        }
5085    }
5086    Ok(visible)
5087}
5088
5089impl<'a> From<Option<&'a PathBuf>> for HermitData {
5090    fn from(data_dir: Option<&'a PathBuf>) -> Self {
5091        data_dir.map_or_else(Self::new, Self::with_dir)
5092    }
5093}
5094
5095/// Records to an existing directory. The caller must keep its backing resources
5096/// alive through any [`HermitCleanupUnconfirmed`] recovery. [`HermitData::record`]
5097/// owns its temporary recording directory inside the retained operation.
5098///
5099/// The ptrace session runs on a tracer thread, so the calling thread needs no
5100/// particular stack; see [`run`].
5101pub fn record_to(command: Command, dir: &Path) -> Result<ExitStatus, Error> {
5102    let dir = dir.to_owned();
5103    let command = tracer_thread::SendCommand::new(command);
5104    ptrace_completion::run_on_tracer_thread(None, move |control| async move {
5105        let command = command.into_inner();
5106        record_to_async(
5107            command,
5108            &dir,
5109            MountProvenance::default(),
5110            None,
5111            None,
5112            control,
5113        )
5114        .await
5115    })
5116}
5117
5118/// Records with producer-owned mountinfo provenance. The directory-lifetime
5119/// obligation is the same as [`record_to`]. `options` are stored in the
5120/// recording's `metadata.json`, and a replay uses the same network, `/proc`
5121/// level, CPU profile and schedule.
5122///
5123/// The ptrace session runs on a tracer thread, so the calling thread needs no
5124/// particular stack; see [`run`].
5125pub fn record_to_with_mountinfo(
5126    command: Command,
5127    dir: &Path,
5128    mountinfo: MountProvenance,
5129    options: RecordOptions,
5130) -> Result<ExitStatus, Error> {
5131    let dir = dir.to_owned();
5132    let command = tracer_thread::SendCommand::new(command);
5133    ptrace_completion::run_on_tracer_thread(None, move |control| async move {
5134        let command = command.into_inner();
5135        let mut mountinfo = mountinfo;
5136        let mount_ids = match mountinfo.mount_ids.take() {
5137            Some(mount_ids) => mount_ids,
5138            None => capture_mountinfo_identity_order_keeping(
5139                &mountinfo
5140                    .container_mount_table
5141                    .as_ref()
5142                    .map(detcore_model::container_mounts::ContainerMountTable::shown_mount_ids)
5143                    .unwrap_or_default(),
5144            )?,
5145        };
5146        record_to_async(
5147            command,
5148            &dir,
5149            mountinfo,
5150            Some(mount_ids),
5151            Some(options),
5152            control,
5153        )
5154        .await
5155    })
5156}
5157
5158// Private plumbing: Record::spawn_with_mountinfo's parameters plus the
5159// ptrace completion control.
5160async fn record_to_async(
5161    command: Command,
5162    dir: &Path,
5163    mountinfo: MountProvenance,
5164    mountinfo_mount_ids: Option<Vec<u64>>,
5165    options: Option<RecordOptions>,
5166    control: std::rc::Rc<ptrace_completion::Control>,
5167) -> Result<ExitStatus, Error> {
5168    let report = SkidOvershootReport::begin(true);
5169    let result = async {
5170        Record::spawn_with_mountinfo(command, dir, mountinfo, mountinfo_mount_ids, options)
5171            .await?
5172            .wait(control)
5173            .await
5174    }
5175    .await;
5176    report.finish(result)
5177}
5178
5179/// Records and captures output. Keep the directory's backing resources alive
5180/// through any cleanup checkpoint, as for [`record_to`].
5181///
5182/// The ptrace session runs on a tracer thread, so the calling thread needs no
5183/// particular stack; see [`run`].
5184pub fn record_with_output(command: Command, dir: &Path) -> Result<Output, Error> {
5185    let dir = dir.to_owned();
5186    let command = tracer_thread::SendCommand::new(command);
5187    ptrace_completion::run_on_tracer_thread(None, move |control| async move {
5188        let command = command.into_inner();
5189        record_with_output_async(
5190            command,
5191            &dir,
5192            MountProvenance::default(),
5193            None,
5194            None,
5195            control,
5196        )
5197        .await
5198    })
5199}
5200
5201/// Records with captured output and exact mountinfo provenance.
5202/// `options` are as for [`record_to_with_mountinfo`].
5203///
5204/// The ptrace session runs on a tracer thread, so the calling thread needs no
5205/// particular stack; see [`run`].
5206pub fn record_with_output_with_mountinfo(
5207    command: Command,
5208    dir: &Path,
5209    mountinfo: MountProvenance,
5210    options: RecordOptions,
5211) -> Result<Output, Error> {
5212    let dir = dir.to_owned();
5213    let command = tracer_thread::SendCommand::new(command);
5214    ptrace_completion::run_on_tracer_thread(None, move |control| async move {
5215        let command = command.into_inner();
5216        let mut mountinfo = mountinfo;
5217        let mount_ids = match mountinfo.mount_ids.take() {
5218            Some(mount_ids) => mount_ids,
5219            None => capture_mountinfo_identity_order_keeping(
5220                &mountinfo
5221                    .container_mount_table
5222                    .as_ref()
5223                    .map(detcore_model::container_mounts::ContainerMountTable::shown_mount_ids)
5224                    .unwrap_or_default(),
5225            )?,
5226        };
5227        record_with_output_async(
5228            command,
5229            &dir,
5230            mountinfo,
5231            Some(mount_ids),
5232            Some(options),
5233            control,
5234        )
5235        .await
5236    })
5237}
5238
5239// Private plumbing: Record::spawn_with_mountinfo's parameters plus the
5240// ptrace completion control.
5241async fn record_with_output_async(
5242    mut command: Command,
5243    dir: &Path,
5244    mountinfo: MountProvenance,
5245    mountinfo_mount_ids: Option<Vec<u64>>,
5246    options: Option<RecordOptions>,
5247    control: std::rc::Rc<ptrace_completion::Control>,
5248) -> Result<Output, Error> {
5249    let report = SkidOvershootReport::begin(true);
5250    command.stdin(Stdio::null());
5251    command.stdout(Stdio::piped());
5252    command.stderr(Stdio::piped());
5253    let result = async {
5254        Record::spawn_with_mountinfo(command, dir, mountinfo, mountinfo_mount_ids, options)
5255            .await?
5256            .wait_with_output(control)
5257            .await
5258    }
5259    .await;
5260    report.finish(result)
5261}
5262
5263/// Replays an existing recording. Materialization and chroot guards remain with
5264/// the original runtime through any cleanup checkpoint.
5265///
5266/// The ptrace session runs on a tracer thread, so the calling thread needs no
5267/// particular stack; see [`run`].
5268pub fn replay_from(dir: &Path) -> Result<ExitStatus, Error> {
5269    replay_plain(dir, None, &[])
5270}
5271
5272/// Replays with a gdb server.
5273///
5274/// The ptrace session runs on a tracer thread, so the calling thread needs no
5275/// particular stack; see [`run`].
5276pub fn replay_with_gdbserver(dir: &Path, port: u16) -> Result<ExitStatus, Error> {
5277    replay_plain(dir, Some(port), &[])
5278}
5279
5280/// Replays with a gdb server and mounts inside the replay chroot.
5281///
5282/// The ptrace session runs on a tracer thread, so the calling thread needs no
5283/// particular stack; see [`run`].
5284pub fn replay_with_gdbserver_and_mounts(
5285    dir: &Path,
5286    port: u16,
5287    mounts: &[Mount],
5288) -> Result<ExitStatus, Error> {
5289    replay_plain(dir, Some(port), mounts)
5290}
5291
5292fn replay_plain(dir: &Path, port: Option<u16>, mounts: &[Mount]) -> Result<ExitStatus, Error> {
5293    let dir = dir.to_owned();
5294    let mounts = mounts.to_vec();
5295    ptrace_completion::run_on_tracer_thread(None, move |control| async move {
5296        let report = SkidOvershootReport::begin(true);
5297        let result = async {
5298            Replay::spawn(&dir, false, port, &mounts)
5299                .await?
5300                .wait(control)
5301                .await
5302        }
5303        .await;
5304        report.finish(result)
5305    })
5306}
5307
5308/// Replays with captured output.
5309///
5310/// The ptrace session runs on a tracer thread, so the calling thread needs no
5311/// particular stack; see [`run`].
5312pub fn replay_with_output(dir: &Path) -> Result<Output, Error> {
5313    replay_with_output_and_mounts(dir, &[])
5314}
5315
5316/// Replays with captured output and mounts inside the replay chroot.
5317///
5318/// The ptrace session runs on a tracer thread, so the calling thread needs no
5319/// particular stack; see [`run`].
5320pub fn replay_with_output_and_mounts(dir: &Path, mounts: &[Mount]) -> Result<Output, Error> {
5321    let dir = dir.to_owned();
5322    let mounts = mounts.to_vec();
5323    ptrace_completion::run_on_tracer_thread(None, move |control| async move {
5324        let report = SkidOvershootReport::begin(true);
5325        let result = async {
5326            Replay::spawn(&dir, true, None, &mounts)
5327                .await?
5328                .wait_with_output(control)
5329                .await
5330        }
5331        .await;
5332        report.finish(result)
5333    })
5334}
5335
5336#[cfg(all(test, feature = "liteinst"))]
5337mod in_guest_delivery_harness;
5338
5339#[cfg(test)]
5340mod tests {
5341    use super::*;
5342
5343    // Historical capability rows are test data, not public backend selections.
5344    // Core rows still call the real builtin preparation path; external rows
5345    // exercise the same neutral normalizer with the actual declared contract.
5346    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
5347    enum CapabilityCase {
5348        Ptrace,
5349        Kvm,
5350        Liteinst,
5351        InGuestTrap,
5352        Dbt,
5353        Sabre,
5354        E9patch,
5355    }
5356
5357    fn prepare_capability_case(config: DetConfig, case: CapabilityCase) -> DetConfig {
5358        let core = match case {
5359            CapabilityCase::Ptrace => Some(Backend::Ptrace),
5360            CapabilityCase::Kvm => Some(Backend::Kvm),
5361            CapabilityCase::Liteinst => Some(Backend::Liteinst),
5362            CapabilityCase::InGuestTrap => Some(Backend::InGuestTrap),
5363            CapabilityCase::Dbt | CapabilityCase::Sabre | CapabilityCase::E9patch => None,
5364        };
5365        if let Some(core) = core {
5366            return prepare_backend_config(config, core);
5367        }
5368        let capabilities = match case {
5369            CapabilityCase::Dbt => reverie::BackendCapabilities::DBT,
5370            CapabilityCase::Sabre => reverie::BackendCapabilities::SABRE,
5371            // e9patch preprocesses then executes through the real ptrace Tool.
5372            CapabilityCase::E9patch => reverie::BackendCapabilities::PTRACE,
5373            _ => unreachable!("core capability cases return through their real owner"),
5374        };
5375        super::prepare_backend_config_with_capabilities(config, capabilities)
5376    }
5377
5378    static SKID_OVERSHOOT_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
5379
5380    /// The standard streams a launch hands a KVM guest are grouped by the
5381    /// descriptors themselves, not by this process's fds 0-2: a stdin that is
5382    /// a file of its own (a reserved stdin distinct from the output streams)
5383    /// stays a description of its own even where the output streams are one,
5384    /// and a stdin that is one description with the standard output joins it.
5385    #[test]
5386    fn standard_streams_are_grouped_by_the_descriptors_handed_over() {
5387        let directory = tempfile::tempdir().unwrap();
5388        let open = |name: &str| {
5389            fs::OpenOptions::new()
5390                .read(true)
5391                .write(true)
5392                .create(true)
5393                .truncate(false)
5394                .open(directory.path().join(name))
5395                .unwrap()
5396        };
5397        let reserved = open("reserved-stdin");
5398        let output = open("output");
5399        let output_copy = output.try_clone().unwrap();
5400        let error = open("error");
5401        let fd = |file: &fs::File| Some(file.as_raw_fd());
5402        assert_eq!(
5403            standard_stream_aliases([fd(&reserved), fd(&output), fd(&output_copy)]),
5404            Some([0, 1, 1])
5405        );
5406        let stdin_copy = output.try_clone().unwrap();
5407        assert_eq!(
5408            standard_stream_aliases([fd(&stdin_copy), fd(&output), fd(&error)]),
5409            Some([0, 0, 2])
5410        );
5411        assert_eq!(
5412            standard_stream_aliases([None, fd(&output), fd(&error)]),
5413            Some([0, 1, 2])
5414        );
5415    }
5416
5417    /// Runs `check_tracing_environment` against probes that report `denied`
5418    /// as unavailable and every other probe as available; returns the refusal,
5419    /// if any, and the probes in the order they ran.
5420    fn check_with_denied(
5421        namespaces: NamespaceUse,
5422        denied: &[CapabilityProbe],
5423    ) -> (Option<String>, Vec<CapabilityProbe>) {
5424        let mut ran = Vec::new();
5425        let result = check_tracing_environment(namespaces, |probe| {
5426            ran.push(probe);
5427            Ok(!denied.contains(&probe))
5428        });
5429        (result.err().map(|error| error.to_string()), ran)
5430    }
5431
5432    /// Where unprivileged user namespaces are denied, a run that creates them
5433    /// is refused at the namespace probe, and a `--no-namespace` run, which
5434    /// never calls unshare, is not: it still passes the ptrace and seccomp
5435    /// probes (https://github.com/rrnewton/hermit/issues/4515).
5436    #[test]
5437    fn denied_namespaces_refuse_only_a_run_that_creates_them() {
5438        let denied = [CapabilityProbe::Namespaces];
5439        let (refusal, ran) = check_with_denied(NamespaceUse::Creates, &denied);
5440        assert!(
5441            refusal.as_deref().is_some_and(|refusal| {
5442                refusal.contains("unshare(CLONE_NEWUSER | CLONE_NEWPID | CLONE_NEWIPC)")
5443            }),
5444            "{refusal:?}"
5445        );
5446        assert_eq!(ran, [CapabilityProbe::Namespaces]);
5447        let (refusal, ran) = check_with_denied(NamespaceUse::None, &denied);
5448        assert_eq!(refusal, None);
5449        assert_eq!(ran, [CapabilityProbe::Ptrace, CapabilityProbe::Seccomp]);
5450    }
5451
5452    /// A `--no-namespace` run is still refused where ptrace or seccomp is
5453    /// denied: skipping the namespace probe skips nothing else.
5454    #[test]
5455    fn a_run_without_namespaces_still_needs_ptrace_and_seccomp() {
5456        for denied in [CapabilityProbe::Ptrace, CapabilityProbe::Seccomp] {
5457            let (refusal, _) = check_with_denied(NamespaceUse::None, &[denied]);
5458            let expected = match denied {
5459                CapabilityProbe::Ptrace => "PTRACE_TRACEME",
5460                _ => "SECCOMP_SET_MODE_FILTER",
5461            };
5462            assert!(
5463                refusal
5464                    .as_deref()
5465                    .is_some_and(|refusal| refusal.contains(expected)),
5466                "{denied:?}: {refusal:?}"
5467            );
5468        }
5469        let (refusal, ran) = check_with_denied(NamespaceUse::Creates, &[]);
5470        assert_eq!(refusal, None);
5471        assert_eq!(
5472            ran,
5473            [
5474                CapabilityProbe::Namespaces,
5475                CapabilityProbe::Ptrace,
5476                CapabilityProbe::Seccomp
5477            ]
5478        );
5479    }
5480
5481    /// A run that drew an input from the host leaves a recording directory
5482    /// holding only `manifest.json`; `record rm` and `record clean` remove it
5483    /// as they remove a syscall recording, and still refuse a directory with
5484    /// neither marker (https://github.com/rrnewton/hermit/issues/4271).
5485    #[test]
5486    fn a_manifest_only_recording_can_be_removed() {
5487        let directory = tempfile::tempdir().unwrap();
5488        let data = HermitData::with_dir(directory.path());
5489        let id = Id::unique();
5490        let recording = directory.path().join(id.to_string());
5491        fs::create_dir_all(&recording).unwrap();
5492        assert!(data.remove(id).is_err());
5493        fs::write(recording.join(MANIFEST_NAME), "{}").unwrap();
5494        data.remove(id).unwrap();
5495        assert!(!recording.exists());
5496    }
5497
5498    /// The launch-time capture drops ephemeral host seed rows before ordering,
5499    /// so a seed's raw ID never enters the provenance Detcore checks guest
5500    /// views against, and every retained row keeps its place.
5501    #[test]
5502    fn identity_capture_excludes_host_seed_mounts_and_keeps_order() {
5503        let raw = b"10 1 8:1 / / rw - ext4 /dev/root rw\n\
5504                    76 10 0:50 / /mnt/xarfuse/uid-1/e62a203d-seed-nspid4026531836_cgpid16161-ns-4026531832 rw - fuse.squashfuse_ll squashfuse_ll rw\n\
5505                    20 10 0:7 / /test rw - tmpfs tmpfs rw\n\
5506                    30 10 0:51 / /mnt/xarfuse/stable-release rw - fuse.squashfuse_ll squashfuse_ll rw\n";
5507        let order = mountinfo_identity_order_from_raw(raw).unwrap();
5508        assert!(!order.contains(&76), "seed mount ID captured: {order:?}");
5509        assert_eq!(order, [10, 20, 30, 1]);
5510        assert_eq!(
5511            mountinfo_identity_order_from(raw).unwrap(),
5512            [10, 76, 20, 30, 1],
5513            "control: without the filter the seed ID is captured"
5514        );
5515    }
5516
5517    /// The namespace a test's captured provenance names.
5518    const NAMESPACE: detcore_model::config::MountNamespaceIdentity =
5519        detcore_model::config::MountNamespaceIdentity {
5520            id: Some(9001),
5521            device: 4,
5522            inode: 4026531840,
5523        };
5524
5525    #[test]
5526    fn kvm_mountinfo_config_preserves_captured_provenance() {
5527        let rewrite = detcore_model::config::MountInfoRootRewrite {
5528            raw_mount_id: 20,
5529            deterministic_root: b"/".to_vec(),
5530            raw_root_prefix: Some(b"/private-source".to_vec()),
5531            deterministic_root_prefix: Some(b"/tmp".to_vec()),
5532            raw_mountpoint_prefix: Some(b"/private-source".to_vec()),
5533            deterministic_mountpoint_prefix: Some(b"/tmp".to_vec()),
5534        };
5535        let mut config = DetConfig {
5536            mountinfo_mount_ids: vec![10, 20, 30],
5537            mountinfo_mount_ids_captured: true,
5538            mountinfo_mount_namespace: Some(NAMESPACE),
5539            mount_id_assignment_order: vec![700, 701],
5540            mountinfo_root_rewrites: vec![rewrite],
5541            mountinfo_device_rewrites: vec![(libc::makedev(0, 1), libc::makedev(8, 1))],
5542            ..DetConfig::default()
5543        };
5544        let mut expected = config.clone();
5545        expected.mountinfo_device_rewrites.clear();
5546        let (mut namespaces, mut captures) = (0, 0);
5547        prepare_kvm_mountinfo_config(
5548            &mut config,
5549            || {
5550                namespaces += 1;
5551                Ok(NAMESPACE)
5552            },
5553            || {
5554                captures += 1;
5555                Ok(vec![10, 30])
5556            },
5557        )
5558        .unwrap();
5559        assert_eq!(namespaces, 1, "the backend's namespace must be checked");
5560        assert_eq!(
5561            captures, 0,
5562            "the producer's snapshot is kept; the namespace is not read again"
5563        );
5564        assert_eq!(
5565            serde_json::to_value(&config).unwrap(),
5566            serde_json::to_value(&expected).unwrap(),
5567            "root rewrites, listed and assignment orders and every other setting must remain intact"
5568        );
5569    }
5570
5571    /// Provenance the backend keeps without reading the namespace again must
5572    /// be well formed by itself: these are the refusals a second, live capture
5573    /// used to supply (Codex post-facto review of dh-dt0n).
5574    #[test]
5575    fn kvm_mountinfo_config_refuses_malformed_captured_provenance() {
5576        let rewrite = |raw_mount_id| detcore_model::config::MountInfoRootRewrite {
5577            raw_mount_id,
5578            deterministic_root: b"/".to_vec(),
5579            raw_root_prefix: None,
5580            deterministic_root_prefix: None,
5581            raw_mountpoint_prefix: None,
5582            deterministic_mountpoint_prefix: None,
5583        };
5584        let valid = DetConfig {
5585            mountinfo_mount_ids: vec![10, 20, 1],
5586            mountinfo_mount_ids_captured: true,
5587            mountinfo_mount_namespace: Some(NAMESPACE),
5588            mount_id_assignment_order: vec![20],
5589            mountinfo_root_rewrites: vec![rewrite(20)],
5590            ..DetConfig::default()
5591        };
5592        prepare_kvm_mountinfo_config(
5593            &mut valid.clone(),
5594            || Ok(NAMESPACE),
5595            || panic!("captured provenance is not read again"),
5596        )
5597        .unwrap();
5598        let cases = [
5599            (
5600                "duplicate",
5601                DetConfig {
5602                    mountinfo_mount_ids: vec![10, 10],
5603                    ..valid.clone()
5604                },
5605                "malformed",
5606            ),
5607            (
5608                "empty",
5609                DetConfig {
5610                    mountinfo_mount_ids: vec![],
5611                    ..valid.clone()
5612                },
5613                "malformed",
5614            ),
5615            (
5616                "assigned twice",
5617                DetConfig {
5618                    mount_id_assignment_order: vec![20, 20],
5619                    ..valid.clone()
5620                },
5621                "malformed",
5622            ),
5623            (
5624                "zero assigned, unlisted",
5625                DetConfig {
5626                    mount_id_assignment_order: vec![0],
5627                    ..valid.clone()
5628                },
5629                "malformed",
5630            ),
5631            (
5632                "rewrite outside",
5633                DetConfig {
5634                    mountinfo_root_rewrites: vec![rewrite(30)],
5635                    ..valid.clone()
5636                },
5637                "root rewrite names mount 30",
5638            ),
5639        ];
5640        for (name, mut config, expected) in cases {
5641            let before = serde_json::to_value(&config).unwrap();
5642            let error = prepare_kvm_mountinfo_config(
5643                &mut config,
5644                || panic!("{name}: malformed provenance is refused before any read"),
5645                || panic!("{name}: captured provenance is not read again"),
5646            )
5647            .unwrap_err();
5648            assert!(error.to_string().contains(expected), "{name}: {error:#}");
5649            assert_eq!(serde_json::to_value(&config).unwrap(), before, "{name}");
5650        }
5651    }
5652
5653    /// Two reads of this process's namespace name it identically, and a
5654    /// kernel that reports a namespace ID reports a nonzero one.
5655    #[test]
5656    fn current_mount_namespace_is_stable_and_names_a_kernel_id_where_reported() {
5657        let first = current_mount_namespace().unwrap();
5658        assert_eq!(current_mount_namespace().unwrap(), first);
5659        assert_ne!(first.inode, 0);
5660        assert_ne!(first.id, Some(0));
5661    }
5662
5663    /// A host mount copied into the container can leave it between the
5664    /// producer's capture and the backend's start: Linux detaches the mounts on
5665    /// a dentry in every mount namespace when a host process removes that
5666    /// directory (`__detach_mounts`). The backend keeps the producer's order,
5667    /// which still names the row, and does not refuse; the order Detcore later
5668    /// validates guest views against is the producer's
5669    /// (minibeads dh-dt0n).
5670    #[test]
5671    fn kvm_mountinfo_config_keeps_a_snapshot_whose_host_row_left_the_namespace() {
5672        let producer = b"10 1 8:1 / / rw - ext4 /dev/root rw\n\
5673                         20 10 0:22 / /proc rw - proc proc rw\n\
5674                         30 10 0:50 / /data/a rw - tmpfs tmpfs rw\n";
5675        let now = b"10 1 8:1 / / rw - ext4 /dev/root rw\n\
5676                    20 10 0:22 / /proc rw - proc proc rw\n";
5677        let order = mountinfo_identity_order_from(producer).unwrap();
5678        let mut config = DetConfig {
5679            mountinfo_mount_ids: order.clone(),
5680            mountinfo_mount_ids_captured: true,
5681            mountinfo_mount_namespace: Some(NAMESPACE),
5682            ..DetConfig::default()
5683        };
5684        prepare_kvm_mountinfo_config(
5685            &mut config,
5686            || Ok(NAMESPACE),
5687            || mountinfo_identity_order_from(now),
5688        )
5689        .unwrap();
5690        assert_eq!(config.mountinfo_mount_ids, order);
5691        assert_eq!(config.mountinfo_mount_ids, [10, 20, 30, 1]);
5692        assert!(detcore_model::procfs::mount_ids_are_ordered_subset(
5693            &mountinfo_identity_order_from(now).unwrap(),
5694            &config.mountinfo_mount_ids
5695        ));
5696    }
5697
5698    #[test]
5699    fn kvm_mountinfo_config_refuses_provenance_of_another_namespace() {
5700        let config = DetConfig {
5701            mountinfo_mount_ids: vec![10, 20, 1],
5702            mountinfo_mount_ids_captured: true,
5703            mountinfo_mount_namespace: Some(NAMESPACE),
5704            mountinfo_device_rewrites: vec![(libc::makedev(0, 1), libc::makedev(8, 1))],
5705            ..DetConfig::default()
5706        };
5707        // The first is a recycled nsfs inode: device and inode match, the
5708        // kernel's never-reused namespace ID does not.
5709        for backend in [
5710            detcore_model::config::MountNamespaceIdentity {
5711                id: Some(9002),
5712                ..NAMESPACE
5713            },
5714            detcore_model::config::MountNamespaceIdentity {
5715                inode: NAMESPACE.inode + 1,
5716                ..NAMESPACE
5717            },
5718            detcore_model::config::MountNamespaceIdentity {
5719                device: NAMESPACE.device + 1,
5720                ..NAMESPACE
5721            },
5722            detcore_model::config::MountNamespaceIdentity {
5723                id: None,
5724                ..NAMESPACE
5725            },
5726        ] {
5727            let mut stale = config.clone();
5728            let before = serde_json::to_value(&stale).unwrap();
5729            let error = prepare_kvm_mountinfo_config(
5730                &mut stale,
5731                || Ok(backend),
5732                || panic!("captured provenance is not read again"),
5733            )
5734            .unwrap_err();
5735            assert!(
5736                error.to_string().contains("name another mount namespace"),
5737                "{error:#}"
5738            );
5739            assert_eq!(serde_json::to_value(&stale).unwrap(), before);
5740        }
5741        let mut unnamed = DetConfig {
5742            mountinfo_mount_namespace: None,
5743            ..config.clone()
5744        };
5745        let before = serde_json::to_value(&unnamed).unwrap();
5746        let error = prepare_kvm_mountinfo_config(
5747            &mut unnamed,
5748            || panic!("provenance without a namespace is refused before any read"),
5749            || panic!("provenance without a namespace is refused before any read"),
5750        )
5751        .unwrap_err();
5752        assert!(
5753            error
5754                .to_string()
5755                .contains("do not name their mount namespace"),
5756            "{error:#}"
5757        );
5758        assert_eq!(serde_json::to_value(&unnamed).unwrap(), before);
5759        let mut unreadable = config;
5760        let before = serde_json::to_value(&unreadable).unwrap();
5761        let error = prepare_kvm_mountinfo_config(
5762            &mut unreadable,
5763            || Err(std::io::Error::from(std::io::ErrorKind::PermissionDenied).into()),
5764            || panic!("captured provenance is not read again"),
5765        )
5766        .unwrap_err();
5767        assert!(
5768            error
5769                .downcast_ref::<std::io::Error>()
5770                .is_some_and(|error| error.kind() == std::io::ErrorKind::PermissionDenied)
5771        );
5772        assert_eq!(serde_json::to_value(&unreadable).unwrap(), before);
5773    }
5774
5775    #[test]
5776    fn kvm_mountinfo_config_captures_validated_ambient_namespace() {
5777        let contents = b"80 20 8:1 / / rw - ext4 /dev/root rw\n\
5778                         10 80 0:7 / /test rw - tmpfs tmpfs rw\n";
5779        let mut config = DetConfig::default();
5780        let mut expected = config.clone();
5781        expected.mountinfo_mount_ids = vec![80, 10, 20];
5782        expected.mountinfo_mount_ids_captured = true;
5783        prepare_kvm_mountinfo_config(
5784            &mut config,
5785            || panic!("an ambient capture has no producer namespace to compare"),
5786            || mountinfo_identity_order_from(contents),
5787        )
5788        .unwrap();
5789        assert_eq!(
5790            serde_json::to_value(&config).unwrap(),
5791            serde_json::to_value(&expected).unwrap(),
5792            "visible rows precede parent-only IDs and no guest setting changes"
5793        );
5794        assert!(detcore_model::procfs::mount_ids_are_ordered_subset(
5795            &[80, 10],
5796            &config.mountinfo_mount_ids
5797        ));
5798        assert!(!detcore_model::procfs::mount_ids_are_ordered_subset(
5799            &[10, 80],
5800            &config.mountinfo_mount_ids
5801        ));
5802        assert!(!detcore_model::procfs::mount_ids_are_ordered_subset(
5803            &[80, 99],
5804            &config.mountinfo_mount_ids
5805        ));
5806        assert!(!detcore_model::procfs::mount_ids_are_ordered_subset(
5807            &[80, 80],
5808            &config.mountinfo_mount_ids
5809        ));
5810
5811        let mut empty = DetConfig::default();
5812        prepare_kvm_mountinfo_config(
5813            &mut empty,
5814            || panic!("an ambient capture has no producer namespace to compare"),
5815            || mountinfo_identity_order_from(b""),
5816        )
5817        .unwrap();
5818        assert!(empty.mountinfo_mount_ids.is_empty());
5819        assert!(empty.mountinfo_mount_ids_captured);
5820    }
5821
5822    #[test]
5823    fn kvm_mountinfo_config_refuses_missing_or_malformed_capture() {
5824        let valid = b"10 20 8:1 / / rw - ext4 /dev/root rw\n";
5825        let config = DetConfig {
5826            mountinfo_device_rewrites: vec![(libc::makedev(0, 1), libc::makedev(8, 1))],
5827            ..DetConfig::default()
5828        };
5829        for contents in [
5830            b"malformed".as_slice(),
5831            b"10 20 8:1 / / rw - ext4 /dev/root rw\n10 20 8:1 / / rw - ext4 /dev/root rw\n",
5832            b"10 20 8:1 / / rw unknown:7 - ext4 /dev/root rw\n",
5833        ] {
5834            let mut candidate = config.clone();
5835            let before = serde_json::to_value(&candidate).unwrap();
5836            let error = prepare_kvm_mountinfo_config(
5837                &mut candidate,
5838                || panic!("an ambient capture has no producer namespace to compare"),
5839                || mountinfo_identity_order_from(contents),
5840            )
5841            .unwrap_err();
5842            assert!(format!("{error:#}").contains("malformed mountinfo"));
5843            assert_eq!(serde_json::to_value(&candidate).unwrap(), before);
5844        }
5845        let mut candidate = config;
5846        let before = serde_json::to_value(&candidate).unwrap();
5847        let error = prepare_kvm_mountinfo_config(
5848            &mut candidate,
5849            || panic!("an ambient capture has no producer namespace to compare"),
5850            || Err(std::io::Error::from(std::io::ErrorKind::PermissionDenied).into()),
5851        )
5852        .unwrap_err();
5853        assert!(
5854            error
5855                .downcast_ref::<std::io::Error>()
5856                .is_some_and(|error| error.kind() == std::io::ErrorKind::PermissionDenied)
5857        );
5858        assert_eq!(serde_json::to_value(&candidate).unwrap(), before);
5859        assert_eq!(mountinfo_identity_order_from(valid).unwrap(), [10, 20]);
5860    }
5861
5862    #[test]
5863    fn kvm_mountinfo_config_refuses_uncaptured_provenance_vectors() {
5864        for (listed, assigned, namespace) in [
5865            (vec![10], vec![], None),
5866            (vec![], vec![700], None),
5867            (vec![], vec![], Some(NAMESPACE)),
5868        ] {
5869            let mut config = DetConfig {
5870                mountinfo_mount_ids: listed,
5871                mount_id_assignment_order: assigned,
5872                mountinfo_mount_namespace: namespace,
5873                ..DetConfig::default()
5874            };
5875            let before = serde_json::to_value(&config).unwrap();
5876            let error = prepare_kvm_mountinfo_config(
5877                &mut config,
5878                || panic!("must not overwrite incomplete provenance"),
5879                || panic!("must not overwrite incomplete provenance"),
5880            )
5881            .unwrap_err();
5882            assert!(error.to_string().contains("no producer-captured namespace"));
5883            assert_eq!(serde_json::to_value(&config).unwrap(), before);
5884        }
5885    }
5886
5887    /// The container mount table Detcore renders on KVM must name mounts of
5888    /// the namespace the backend captured
5889    /// (https://github.com/rrnewton/hermit/issues/4213, K3a).
5890    #[test]
5891    fn kvm_mountinfo_config_refuses_a_mount_table_of_another_namespace() {
5892        let namespace = b"10 1 8:1 / / rw - ext4 /dev/root rw\n\
5893                          20 10 0:22 / /proc rw - proc proc rw\n\
5894                          40 10 0:23 / /elsewhere rw - tmpfs tmpfs rw\n";
5895        let rows = detcore_model::container_mounts::parse_container_mounts(namespace).unwrap();
5896        let table = |installed: &[u64]| {
5897            detcore_model::container_mounts::ContainerMountTable::select(
5898                &rows,
5899                &installed.iter().copied().collect(),
5900                &[],
5901                &std::collections::BTreeSet::new(),
5902            )
5903        };
5904        let config = |table| DetConfig {
5905            mountinfo_mount_ids: vec![10, 20, 1],
5906            mountinfo_mount_ids_captured: true,
5907            mountinfo_mount_namespace: Some(NAMESPACE),
5908            container_mount_table: Some(table),
5909            ..DetConfig::default()
5910        };
5911        let snapshot = || -> Result<Vec<u64>, Error> { panic!("the snapshot is kept") };
5912        let mut matching = config(table(&[20]));
5913        prepare_kvm_mountinfo_config(&mut matching, || Ok(NAMESPACE), snapshot).unwrap();
5914        let mut stale = config(table(&[40]));
5915        let error =
5916            prepare_kvm_mountinfo_config(&mut stale, || Ok(NAMESPACE), snapshot).unwrap_err();
5917        assert!(error.to_string().contains("names mount 40"), "{error:#}");
5918    }
5919
5920    /// A run in a namespace Hermit made no mount in shows only the base
5921    /// mountpoints mounted at their very paths, maps a file on any other
5922    /// mount to its nearest shown ancestor, and validates against an order
5923    /// read from the same bytes (https://github.com/rrnewton/hermit/issues/4213).
5924    #[test]
5925    fn an_unplanned_table_shows_only_the_base_mountpoints() {
5926        let namespace = b"10 1 8:1 / / rw - ext4 /dev/root rw\n\
5927                          20 10 0:22 / /proc rw - proc proc rw\n\
5928                          30 10 0:23 / /dev rw - devtmpfs dev rw\n\
5929                          40 10 0:24 / /data/eden rw - fuse.edenfs eden rw\n\
5930                          41 30 0:25 / /dev/shm rw - tmpfs tmpfs rw\n\
5931                          50 10 0:26 / /run/user/1000 rw - tmpfs tmpfs rw\n";
5932        // /run is no mount here: its descriptor reaches the root mount.
5933        let base = [
5934            (b"/".to_vec(), 10),
5935            (b"/proc".to_vec(), 20),
5936            (b"/dev".to_vec(), 30),
5937            (b"/dev/shm".to_vec(), 41),
5938            (b"/run".to_vec(), 10),
5939        ];
5940        for mountpoint in detcore_model::container_mounts::BASE_MOUNTPOINTS {
5941            assert!(
5942                !mountpoint.bytes().any(|byte| b" \t\n\\".contains(&byte)),
5943                "{mountpoint} needs mountinfo escaping"
5944            );
5945        }
5946        let no_launch = std::collections::BTreeSet::new();
5947        let (table, order) =
5948            unplanned_container_mount_table_from(namespace, &no_launch, &base).unwrap();
5949        assert_eq!(table.shown_mount_ids(), [10, 20, 30, 41].into());
5950        assert_eq!(table.shown_mount_id(40), 10);
5951        assert_eq!(table.shown_mount_id(50), 10);
5952        assert_eq!(order, [10, 20, 30, 40, 41, 50, 1]);
5953        let rendered = String::from_utf8(table.mountinfo()).unwrap();
5954        assert!(!rendered.contains("/data/eden") && !rendered.contains("/run/user"));
5955        // A launch from a checkout on /data/eden shows that mount too; a
5956        // file on the still hidden /run/user/1000 reports the root.
5957        let launch = [40].into();
5958        let (table, order) =
5959            unplanned_container_mount_table_from(namespace, &launch, &base).unwrap();
5960        assert_eq!(table.shown_mount_ids(), [10, 20, 30, 40, 41].into());
5961        assert_eq!(table.shown_mount_id(40), 40);
5962        assert_eq!(table.shown_mount_id(50), 10);
5963        assert_eq!(order, [10, 20, 30, 40, 41, 50, 1]);
5964    }
5965
5966    /// Runs test `name` again under `unshare --user --map-root-user
5967    /// --mount`, where it may mount, and returns true in this process, which
5968    /// then has nothing more to check; returns false in that child, which runs
5969    /// the test's body. `unshare(CLONE_NEWUSER)` refuses a multithreaded
5970    /// process, so the test cannot enter one itself. Where user namespaces
5971    /// are unavailable the test is skipped, saying why.
5972    fn rerun_in_user_namespace(name: &str) -> bool {
5973        const CHILD: &str = "HERMIT_TEST_USER_NAMESPACE_CHILD";
5974        if std::env::var_os(CHILD).is_some() {
5975            return false;
5976        }
5977        let probe = std::process::Command::new("unshare")
5978            .args(["--user", "--map-root-user", "--mount", "true"])
5979            .output();
5980        if !probe.as_ref().is_ok_and(|probe| probe.status.success()) {
5981            eprintln!("SKIPPED {name}: user namespaces are unavailable here: {probe:?}");
5982            return true;
5983        }
5984        let output = std::process::Command::new("unshare")
5985            .args(["--user", "--map-root-user", "--mount", "--"])
5986            .arg(std::env::current_exe().unwrap())
5987            .args(["--exact", name, "--nocapture", "--test-threads=1"])
5988            .env(CHILD, "1")
5989            .output()
5990            .expect("run the test under unshare");
5991        let stdout = String::from_utf8_lossy(&output.stdout);
5992        assert!(
5993            output.status.success() && stdout.contains("1 passed"),
5994            "the child failed or ran no test:\n{stdout}\n{}",
5995            String::from_utf8_lossy(&output.stderr)
5996        );
5997        true
5998    }
5999
6000    /// A library caller's thread with a mount namespace of its own captures
6001    /// its own namespace's rows, identity and mount IDs (all through
6002    /// `/proc/thread-self`), not the thread group leader's: otherwise the
6003    /// leader's rows and the thread's descriptor mount IDs never meet, and
6004    /// the table shows nothing, not even `/`. A thread may unshare its mount
6005    /// namespace only with CAP_SYS_ADMIN, which an unprivileged test gets in
6006    /// a user namespace of its own (`rerun_in_user_namespace`); there the test
6007    /// thread, not the leader, unshares the mount namespace.
6008    #[test]
6009    fn a_thread_with_its_own_mount_namespace_captures_its_own_view() {
6010        use std::os::unix::fs::MetadataExt;
6011        if rerun_in_user_namespace(
6012            "tests::a_thread_with_its_own_mount_namespace_captures_its_own_view",
6013        ) {
6014            return;
6015        }
6016        let checked = std::thread::spawn(|| {
6017            // SAFETY: unshare takes no pointer.
6018            assert_eq!(
6019                unsafe { libc::unshare(libc::CLONE_NEWNS) },
6020                0,
6021                "unshare(CLONE_NEWNS): {}",
6022                io::Error::last_os_error()
6023            );
6024            let own = current_mount_namespace().unwrap();
6025            let leader = fs::metadata("/proc/self/ns/mnt").unwrap();
6026            assert_ne!(
6027                own.inode,
6028                leader.ino(),
6029                "the thread kept the leader's namespace"
6030            );
6031            let (table, order) = unplanned_container_mount_table(Path::new(".")).unwrap();
6032            assert!(
6033                table
6034                    .mounts
6035                    .iter()
6036                    .any(|row| row.mountpoint.as_slice() == b"/"),
6037                "the thread's table shows no /: {table:?}"
6038            );
6039            let root = thread_mount_id_of(Path::new("/")).unwrap();
6040            assert!(table.shown_mount_ids().contains(&root));
6041            assert!(table.shown_mount_ids().iter().all(|id| order.contains(id)));
6042        })
6043        .join();
6044        assert!(checked.is_ok(), "the thread's checks failed");
6045    }
6046
6047    /// The launch directory is resolved and held before the mount table is
6048    /// read: a mount made on it between the two (here a tmpfs over it, in a
6049    /// user and mount namespace of the test's own) is not the launch mount,
6050    /// because the held directory is below it; the mount the directory was
6051    /// on is, and it is in the rows read. Had the directory been resolved
6052    /// after the read, it would have reached the new tmpfs. The same order
6053    /// puts a mount that resolving the directory triggers (an automount on
6054    /// its path) in the rows read.
6055    #[test]
6056    fn launch_directories_are_held_before_the_mount_table_is_read() {
6057        if rerun_in_user_namespace(
6058            "tests::launch_directories_are_held_before_the_mount_table_is_read",
6059        ) {
6060            return;
6061        }
6062        let directory = tempfile::tempdir().unwrap();
6063        let launch = directory.path().join("launch");
6064        fs::create_dir(&launch).unwrap();
6065        let before = thread_mount_id_of(&launch).unwrap();
6066        let target = launch.clone();
6067        let (table, _) = unplanned_container_mount_table_with(&launch, move || {
6068            let status = std::process::Command::new("mount")
6069                .args(["-t", "tmpfs", "none"])
6070                .arg(&target)
6071                .status()
6072                .expect("run mount");
6073            assert!(status.success(), "mount a tmpfs over the launch directory");
6074            thread_mountinfo()
6075        })
6076        .unwrap();
6077        let over = thread_mount_id_of(&launch).unwrap();
6078        assert_ne!(over, before, "the tmpfs did not cover the launch directory");
6079        assert!(table.shown_mount_ids().contains(&before));
6080        assert!(!table.shown_mount_ids().contains(&over));
6081        let _ = std::process::Command::new("umount").arg(&launch).status();
6082    }
6083
6084    /// A launch mount the mount table read does not list is refused, not
6085    /// dropped from the table.
6086    #[test]
6087    fn a_launch_mount_missing_from_the_read_is_refused() {
6088        let namespace = b"10 1 8:1 / / rw - ext4 /dev/root rw\n";
6089        let error = unplanned_container_mount_table_from(namespace, &[99].into(), &[])
6090            .unwrap_err()
6091            .to_string();
6092        assert!(error.contains("launch mount 99"), "{error}");
6093    }
6094
6095    #[test]
6096    fn skid_overshoot_report_covers_success_error_and_disabled_backends() {
6097        let _lock = SKID_OVERSHOOT_TEST_LOCK.lock().unwrap();
6098        let _ = reverie::take_skid_overshoot_count();
6099
6100        reverie::record_skid_overshoot();
6101        let empty_run = SkidOvershootReport::begin(true);
6102        assert_eq!(
6103            reverie::take_skid_overshoot_count(),
6104            0,
6105            "begin must clear evidence from a previous invocation"
6106        );
6107        empty_run.finish(Ok::<_, Error>(())).unwrap();
6108
6109        let success = SkidOvershootReport::begin(true);
6110        reverie::record_skid_overshoot();
6111        reverie::record_skid_overshoot();
6112        let error = success.finish(Ok::<_, Error>(7)).unwrap_err();
6113        assert_eq!(
6114            error
6115                .downcast_ref::<SkidOvershootError>()
6116                .expect("skid refusal must remain typed")
6117                .count(),
6118            2
6119        );
6120        assert!(
6121            error
6122                .to_string()
6123                .contains("observed 2 HERMIT_SKID_OVERSHOOT report(s)"),
6124            "{error:#}"
6125        );
6126        assert!(
6127            error
6128                .to_string()
6129                .contains("deterministic execution was not established"),
6130            "{error:#}"
6131        );
6132        assert_eq!(reverie::take_skid_overshoot_count(), 0);
6133
6134        let error = SkidOvershootReport::begin(true);
6135        reverie::record_skid_overshoot();
6136        let error = error
6137            .finish(Err::<(), _>(anyhow!("guest failed")))
6138            .unwrap_err();
6139        assert!(error.downcast_ref::<SkidOvershootError>().is_some());
6140        let error = format!("{error:#}");
6141        assert!(error.contains("guest failed"), "{error}");
6142        assert!(
6143            error.contains("observed 1 HERMIT_SKID_OVERSHOOT report(s)"),
6144            "{error}"
6145        );
6146        assert_eq!(reverie::take_skid_overshoot_count(), 0);
6147
6148        reverie::record_skid_overshoot();
6149        let disabled = SkidOvershootReport::begin(false);
6150        disabled.finish(Ok::<_, Error>(())).unwrap();
6151        assert_eq!(
6152            reverie::take_skid_overshoot_count(),
6153            1,
6154            "a backend that cannot produce PMU overshoots must not consume another run's evidence"
6155        );
6156    }
6157
6158    /// The backends whose runs can deliver a precise timer event late are the
6159    /// ones that deliver timer events at all: ptrace, e9patch preprocessing on
6160    /// it, and KVM. The others deliver none, so a late-delivery witness in the
6161    /// process cannot be theirs.
6162    #[test]
6163    fn timer_delivering_backends_consume_skid_overshoot_reports() {
6164        for backend in [Backend::Ptrace, Backend::Kvm] {
6165            assert!(backend.consumes_skid_overshoot_reports(), "{backend:?}");
6166        }
6167        for backend in [Backend::Liteinst, Backend::InGuestTrap] {
6168            assert!(!backend.consumes_skid_overshoot_reports(), "{backend:?}");
6169        }
6170    }
6171
6172    #[cfg(feature = "liteinst")]
6173    fn write_guest_program(directory: &Path, name: &str, bytes: &[u8], mode: u32) -> PathBuf {
6174        let path = directory.join(name);
6175        fs::write(&path, bytes).unwrap();
6176        fs::set_permissions(&path, fs::Permissions::from_mode(mode)).unwrap();
6177        // The kernel may clear a set-group-ID bit that this user cannot set;
6178        // a test that silently lost its bit would test nothing.
6179        assert_eq!(
6180            fs::metadata(&path).unwrap().permissions().mode() & 0o7777,
6181            mode,
6182            "{}",
6183            path.display()
6184        );
6185        path
6186    }
6187
6188    #[cfg(feature = "liteinst")]
6189    fn shebang_to(interpreter: &Path) -> Vec<u8> {
6190        let mut script = b"#!".to_vec();
6191        script.extend_from_slice(interpreter.as_os_str().as_bytes());
6192        script.extend_from_slice(b" -e\necho script\n");
6193        script
6194    }
6195
6196    #[test]
6197    #[cfg(feature = "liteinst")]
6198    fn in_guest_liteinst_runs_a_dynamically_linked_x86_64_program() {
6199        use interp::startup_fixtures::dynamic_executable;
6200
6201        let directory = tempfile::tempdir().unwrap();
6202        let program = write_guest_program(
6203            directory.path(),
6204            "dynamic",
6205            &dynamic_executable(false),
6206            0o755,
6207        );
6208        assert_eq!(
6209            in_guest_liteinst_program_gap(&mut Command::new(&program)).unwrap(),
6210            None
6211        );
6212    }
6213
6214    #[test]
6215    #[cfg(feature = "liteinst")]
6216    fn in_guest_liteinst_refuses_programs_that_start_before_the_runtime() {
6217        use interp::startup_fixtures::*;
6218
6219        let directory = tempfile::tempdir().unwrap();
6220        let cases: [(&str, Vec<u8>, &str); 4] = [
6221            (
6222                "static",
6223                elf(EM_X86_64, &[(PT_LOAD, b"code")]),
6224                "is statically linked",
6225            ),
6226            (
6227                "preinit",
6228                dynamic_executable(true),
6229                "has a DT_PREINIT_ARRAY",
6230            ),
6231            (
6232                "aarch64",
6233                elf(EM_AARCH64, &[(PT_INTERP, INTERP)]),
6234                "is not a 64-bit x86-64 ELF image",
6235            ),
6236            (
6237                "text",
6238                b"plain text, no #! line\n".to_vec(),
6239                "is neither an ELF executable nor a #! script",
6240            ),
6241        ];
6242        for (name, bytes, reason) in cases {
6243            let program = write_guest_program(directory.path(), name, &bytes, 0o755);
6244            let gap = in_guest_liteinst_program_gap(&mut Command::new(&program))
6245                .unwrap()
6246                .unwrap_or_else(|| panic!("{name} was not refused"));
6247            assert!(
6248                gap.contains(reason) && gap.contains(&program.display().to_string()),
6249                "{name}: {gap}"
6250            );
6251        }
6252    }
6253
6254    /// A failure recorded during an in-guest run fails the settle step and
6255    /// latches a determinism loss, so verification refuses the run (Codex
6256    /// implementation review, HIGH 3 and 4).
6257    #[tokio::test]
6258    #[cfg(feature = "liteinst")]
6259    async fn a_recorded_in_guest_failure_fails_the_run_and_latches_a_loss() {
6260        use std::os::fd::FromRawFd;
6261
6262        use reverie::GlobalTool;
6263        use reverie_rpc_transport::ConnectionAdmission;
6264
6265        let config = DetConfig::default().with_backend(|backend| {
6266            backend.process_exits_complete_asynchronously = true;
6267        });
6268        let global_state = detcore::GlobalState::init_global_state(&config).await;
6269        let exits = in_guest_exits::InGuestExitAdmission::default();
6270        // Admitting before any reporter is attached is refused and recorded.
6271        let fd = unsafe { libc::syscall(libc::SYS_pidfd_open, std::process::id() as i32, 0) };
6272        assert!(fd >= 0);
6273        let pidfd = unsafe { std::os::fd::OwnedFd::from_raw_fd(fd as i32) };
6274        assert!(exits.admit(pidfd).is_err());
6275        let error = settle_in_guest_exits(&exits, &global_state)
6276            .await
6277            .expect_err("a recorded failure must fail the run");
6278        assert!(
6279            error
6280                .to_string()
6281                .starts_with("in-guest LiteInst refused a guest connection:"),
6282            "{error}"
6283        );
6284        assert!(detcore::detlog::determinism_loss().is_some());
6285    }
6286
6287    #[test]
6288    fn in_guest_trap_is_in_guest_liteinst_and_forces_site_patching_off() {
6289        assert!(Backend::Liteinst.is_in_guest_liteinst());
6290        assert!(Backend::InGuestTrap.is_in_guest_liteinst());
6291        for backend in [Backend::Ptrace, Backend::Kvm] {
6292            assert!(!backend.is_in_guest_liteinst(), "{backend:?}");
6293            assert_eq!(backend.runtime_name(), backend.as_str(), "{backend:?}");
6294            assert_eq!(forced_site_patching_value(backend), None, "{backend:?}");
6295        }
6296        assert_eq!(Backend::InGuestTrap.as_str(), "in-guest-trap");
6297        assert_eq!(Backend::InGuestTrap.runtime_name(), "liteinst");
6298        assert_eq!(Backend::Liteinst.runtime_name(), "liteinst");
6299        // liteinst leaves the variable to the guest environment.
6300        assert_eq!(forced_site_patching_value(Backend::Liteinst), None);
6301        assert_eq!(forced_site_patching_value(Backend::InGuestTrap), Some("0"));
6302        // The CLI spelling parses to the variant and back.
6303        assert_eq!(
6304            <Backend as ValueEnum>::from_str("in-guest-trap", false),
6305            Ok(Backend::InGuestTrap)
6306        );
6307        assert!(Backend::ALL.contains(&Backend::InGuestTrap));
6308        for backend in Backend::ALL {
6309            let name = backend
6310                .to_possible_value()
6311                .expect("every backend is a visible CLI value");
6312            assert_eq!(name.get_name(), backend.as_str(), "{backend:?}");
6313        }
6314    }
6315
6316    /// in-guest-trap sets `REVERIE_LITEINST_SITE_PATCHING=0` on the guest
6317    /// command when the guest environment leaves it unset or already `0`, and
6318    /// refuses, before anything starts, an environment that sets any other
6319    /// value; the refusal names the value and both ways out. liteinst neither
6320    /// refuses nor changes the variable.
6321    #[test]
6322    #[cfg(feature = "liteinst")]
6323    fn in_guest_trap_refuses_a_guest_environment_that_turns_site_patching_on() {
6324        use std::ffi::OsStr;
6325
6326        let name = reverie_liteinst::SITE_PATCHING_ENV;
6327        let mut unset = Command::new("/bin/true");
6328        unset.env_remove(name);
6329        assert_eq!(
6330            in_guest_trap_site_patching_conflict(&unset, Backend::InGuestTrap),
6331            None
6332        );
6333        force_in_guest_site_patching(&mut unset, Backend::InGuestTrap);
6334        assert_eq!(unset.get_env(name).as_deref(), Some(OsStr::new("0")));
6335
6336        let mut zero = Command::new("/bin/true");
6337        zero.env(name, "0");
6338        assert_eq!(
6339            in_guest_trap_site_patching_conflict(&zero, Backend::InGuestTrap),
6340            None
6341        );
6342
6343        for value in ["1", "", "yes", " 0"] {
6344            let mut command = Command::new("/bin/true");
6345            command.env(name, value);
6346            let conflict = in_guest_trap_site_patching_conflict(&command, Backend::InGuestTrap)
6347                .unwrap_or_else(|| panic!("{value:?} was accepted"));
6348            assert!(conflict.contains(&format!("{name}={value},")), "{conflict}");
6349            assert!(conflict.contains("--backend=liteinst"), "{conflict}");
6350            assert!(conflict.contains("drop the variable"), "{conflict}");
6351            // The whole refusal path stops on it first, before the program,
6352            // kernel or timeslice checks, and leaves the value untouched.
6353            let error = refuse_in_guest_liteinst_run(
6354                &mut command,
6355                &DetConfig::default(),
6356                Backend::InGuestTrap,
6357            )
6358            .unwrap_err();
6359            let refusal = error
6360                .downcast_ref::<LiteinstInGuestRefusal>()
6361                .expect("a typed in-guest refusal, which the CLI reports as exit 122");
6362            assert!(
6363                refusal.to_string().starts_with(
6364                    "--backend=in-guest-trap (in-guest LiteInst, site patching off) refuses \
6365                     this run: the guest environment sets"
6366                ),
6367                "{refusal}"
6368            );
6369            assert_eq!(command.get_env(name).as_deref(), Some(OsStr::new(value)));
6370
6371            // liteinst takes the variable from the guest environment as it is.
6372            let mut liteinst = Command::new("/bin/true");
6373            liteinst.env(name, value);
6374            assert_eq!(
6375                in_guest_trap_site_patching_conflict(&liteinst, Backend::Liteinst),
6376                None
6377            );
6378            force_in_guest_site_patching(&mut liteinst, Backend::Liteinst);
6379            assert_eq!(liteinst.get_env(name).as_deref(), Some(OsStr::new(value)));
6380        }
6381    }
6382
6383    /// The coordinator's postcondition for an in-guest-trap run: a record
6384    /// with no patched and no candidate site passes; a patched site, a
6385    /// candidate, an unmeasured count or a missing record is refused, with a
6386    /// typed in-guest refusal and a determinism loss. Other backends are not
6387    /// held to it.
6388    #[test]
6389    #[cfg(feature = "liteinst")]
6390    fn an_in_guest_trap_run_that_patched_a_site_is_refused_after_it_ends() {
6391        let record = |candidates: Option<u64>, patched: Option<u64>| {
6392            reverie::DispatchStats::new(
6393                "in-guest-trap",
6394                reverie::DispatchCounters::ZERO,
6395                reverie::SiteCounters {
6396                    candidates,
6397                    patched,
6398                    fell_back: Some(0),
6399                },
6400            )
6401        };
6402        let clean = record(Some(0), Some(0));
6403        assert_eq!(
6404            in_guest_trap_patching_evidence(Backend::InGuestTrap, Some(&clean), 1),
6405            None
6406        );
6407        for (candidates, patched, shown) in [
6408            (Some(3), Some(3), "3 patched and 3 candidate"),
6409            (Some(1), Some(0), "0 patched and 1 candidate"),
6410            (Some(0), None, "unmeasured patched and 0 candidate"),
6411        ] {
6412            let reason = in_guest_trap_patching_evidence(
6413                Backend::InGuestTrap,
6414                Some(&record(candidates, patched)),
6415                1,
6416            )
6417            .unwrap_or_else(|| panic!("{candidates:?} {patched:?} was accepted"));
6418            assert!(reason.contains(shown), "{reason}");
6419        }
6420        assert!(
6421            in_guest_trap_patching_evidence(Backend::InGuestTrap, None, 1)
6422                .is_some_and(|reason| reason.contains("no LiteInst dispatch record"))
6423        );
6424        // An empty record from guests that collected nothing is not zero.
6425        assert!(
6426            in_guest_trap_patching_evidence(Backend::InGuestTrap, Some(&clean), 0)
6427                .is_some_and(|reason| reason.contains("no guest process reported"))
6428        );
6429        let patched = record(Some(3), Some(3));
6430        assert_eq!(
6431            in_guest_trap_patching_evidence(Backend::Liteinst, Some(&patched), 1),
6432            None
6433        );
6434        assert_eq!(
6435            in_guest_trap_patching_evidence(Backend::Liteinst, None, 0),
6436            None
6437        );
6438
6439        refuse_in_guest_trap_patching(Backend::InGuestTrap, None).unwrap();
6440        let error = refuse_in_guest_trap_patching(
6441            Backend::InGuestTrap,
6442            in_guest_trap_patching_evidence(Backend::InGuestTrap, Some(&patched), 1),
6443        )
6444        .unwrap_err();
6445        assert!(
6446            error.downcast_ref::<LiteinstInGuestRefusal>().is_some(),
6447            "{error:#}"
6448        );
6449        assert!(
6450            error
6451                .to_string()
6452                .starts_with("--backend=in-guest-trap (in-guest LiteInst, site patching off)"),
6453            "{error:#}"
6454        );
6455        assert!(
6456            detcore::detlog::determinism_loss().is_some_and(|loss| loss.contains("3 patched")),
6457            "the refusal must record a determinism loss"
6458        );
6459    }
6460
6461    /// Only an in-guest-trap run tells every Tool image to check the settings
6462    /// its runtime captured, and that run always collects the statistics the
6463    /// check requires.
6464    #[test]
6465    #[cfg(feature = "liteinst")]
6466    fn only_in_guest_trap_asks_tool_images_to_check_their_runtime() {
6467        for backend in Backend::ALL {
6468            let checks = in_guest_site_patching_off(backend);
6469            assert_eq!(checks, backend == Backend::InGuestTrap, "{backend:?}");
6470            if checks {
6471                assert!(in_guest_stats_request(backend, &None).is_enabled());
6472            }
6473        }
6474        assert!(!in_guest_stats_request(Backend::Liteinst, &None).is_enabled());
6475    }
6476
6477    /// A configuration that already carries a reserved-signal set (it is a
6478    /// public, serialized field) never keeps it through Hermit's preparation:
6479    /// every backend's preparation, the loader route's
6480    /// (`prepare_in_guest_loader_route_config`, which `hermit run`'s
6481    /// exec-stop, native-exec and mapped-first-admission launchers use) and
6482    /// the native-exec verify route's leave it empty. Only the default
6483    /// in-guest route sets it afterwards, from the backend and the
6484    /// subscription alone, never from the input.
6485    #[test]
6486    fn a_poisoned_reserved_signal_set_never_survives_preparation() {
6487        let poisoned = DetConfig {
6488            in_guest_reserved_signals: u64::MAX,
6489            ..DetConfig::default()
6490        };
6491        for backend in Backend::ALL {
6492            let prepared = prepare_backend_config(poisoned.clone(), backend);
6493            assert_eq!(prepared.in_guest_reserved_signals, 0, "{backend:?}");
6494            let expected = if backend.is_in_guest_liteinst() {
6495                in_guest_reserved_signals(
6496                    &prepare_backend_config(DetConfig::default(), backend),
6497                    backend,
6498                )
6499            } else {
6500                0
6501            };
6502            assert_eq!(
6503                in_guest_reserved_signals(&prepared, backend),
6504                expected,
6505                "{backend:?}"
6506            );
6507        }
6508        let loader = prepare_in_guest_loader_route_config(poisoned);
6509        assert_eq!(loader.in_guest_reserved_signals, 0);
6510        assert!(loader.in_guest_site_patching_off);
6511        assert_eq!(loader.backend, backend_capabilities(Backend::InGuestTrap));
6512    }
6513
6514    /// A configuration that already asks for the refusal of host-timed signal
6515    /// producers (a public, serialized field) never keeps it through Hermit's
6516    /// preparation: every backend's preparation and the loader route's (the
6517    /// exec-stop, native-exec and mapped-first-admission launchers, and the
6518    /// native-exec verify route) turn it off. Only the default in-guest route
6519    /// turns it on afterwards, from the backend alone: both in-guest backends
6520    /// and no other (A5 step 3c slice 0b).
6521    #[test]
6522    fn a_poisoned_producer_refusal_never_survives_preparation() {
6523        let poisoned = DetConfig {
6524            in_guest_refuses_host_timed_producers: true,
6525            ..DetConfig::default()
6526        };
6527        for backend in Backend::ALL {
6528            let prepared = prepare_backend_config(poisoned.clone(), backend);
6529            assert!(
6530                !prepared.in_guest_refuses_host_timed_producers,
6531                "{backend:?}"
6532            );
6533            assert_eq!(
6534                in_guest_refuses_host_timed_producers(backend),
6535                matches!(backend, Backend::Liteinst | Backend::InGuestTrap),
6536                "{backend:?}"
6537            );
6538        }
6539        let loader = prepare_in_guest_loader_route_config(poisoned);
6540        assert!(!loader.in_guest_refuses_host_timed_producers);
6541        assert!(loader.in_guest_site_patching_off);
6542    }
6543
6544    /// A configuration that every condition of the in-guest delivery gate
6545    /// holds for, on `backend`, as the default route's launch leaves it
6546    /// before it sets the gate: prepared, with site patching off.
6547    fn gate_ready(backend: Backend) -> DetConfig {
6548        let mut config = prepare_backend_config(
6549            DetConfig {
6550                sequentialize_threads: true,
6551                max_timeslice: None,
6552                ..DetConfig::default()
6553            },
6554            backend,
6555        );
6556        config.in_guest_site_patching_off = true;
6557        config
6558    }
6559
6560    /// A configuration that already turns the in-guest delivery gate on (a
6561    /// public, serialized field) never keeps it through Hermit's
6562    /// preparation: every backend's preparation and the loader route's (the
6563    /// exec-stop, native-exec and mapped-first-admission launchers, and the
6564    /// native-exec verify route) turn it off, also where every other
6565    /// condition holds (A5 step 3c.1, D35, D43, D52). Only the default
6566    /// route's launch sets it afterwards. Mutation it catches: the reset in
6567    /// `prepare_backend_config` removed.
6568    #[test]
6569    fn a_poisoned_signal_delivery_gate_never_survives_preparation() {
6570        let poisoned = DetConfig {
6571            in_guest_signal_delivery: true,
6572            sequentialize_threads: true,
6573            max_timeslice: None,
6574            in_guest_site_patching_off: true,
6575            ..DetConfig::default()
6576        };
6577        for backend in Backend::ALL {
6578            let prepared = prepare_backend_config(poisoned.clone(), backend);
6579            assert!(!prepared.in_guest_signal_delivery, "{backend:?}");
6580        }
6581        let loader = prepare_in_guest_loader_route_config(poisoned);
6582        assert!(!loader.in_guest_signal_delivery);
6583        assert!(loader.in_guest_site_patching_off);
6584        assert_eq!(loader.backend, backend_capabilities(Backend::InGuestTrap));
6585    }
6586
6587    /// D35's gate holds only for an in-guest backend with site patching off,
6588    /// sequentialized threads, none of the configurations in which Detcore
6589    /// cannot keep the two-phase protocol (D33: each one alone turns the gate
6590    /// off), and a launcher that owns the guest's PID namespace (D51).
6591    #[test]
6592    fn the_signal_delivery_gate_needs_every_condition() {
6593        let owned = true;
6594        for backend in Backend::ALL {
6595            let ready = gate_ready(backend);
6596            assert_eq!(
6597                in_guest_signal_delivery_gate(&ready, backend, owned),
6598                backend.is_in_guest_liteinst(),
6599                "{backend:?}"
6600            );
6601        }
6602        for backend in [Backend::Liteinst, Backend::InGuestTrap] {
6603            let ready = gate_ready(backend);
6604            assert!(detcore::two_phase_refusal(&ready).is_none(), "{backend:?}");
6605            let off = |change: &dyn Fn(&mut DetConfig)| {
6606                let mut config = ready.clone();
6607                change(&mut config);
6608                !in_guest_signal_delivery_gate(&config, backend, owned)
6609            };
6610            assert!(
6611                off(&|c| c.in_guest_site_patching_off = false),
6612                "{backend:?}"
6613            );
6614            assert!(off(&|c| c.sequentialize_threads = false), "{backend:?}");
6615            // D33's flags, each refused by `two_phase_refusal`.
6616            assert!(
6617                off(&|c| c.target_timeslice = std::num::NonZeroU64::new(1_000)),
6618                "{backend:?}"
6619            );
6620            assert!(
6621                off(&|c| c.max_timeslice = std::num::NonZeroU64::new(1_000)),
6622                "{backend:?}"
6623            );
6624            assert!(off(&|c| c.record_preemptions = true), "{backend:?}");
6625            assert!(
6626                off(&|c| c.replay_schedule_from = Some(PathBuf::from("schedule.json"))),
6627                "{backend:?}"
6628            );
6629            let mut pipe = ready.clone();
6630            pipe.pipe_wake_latency = Some(1_000);
6631            assert!(pipe.pipe_wake_latency_in_effect().is_some());
6632            assert!(
6633                !in_guest_signal_delivery_gate(&pipe, backend, owned),
6634                "{backend:?}"
6635            );
6636            // D51: no PID namespace the launcher owns.
6637            assert!(
6638                !in_guest_signal_delivery_gate(&ready, backend, false),
6639                "{backend:?}"
6640            );
6641        }
6642    }
6643
6644    /// `scheduler_owns_guest_pid_namespace` in a forked process, plain or
6645    /// the first process of a new user and PID namespace, as its exit status
6646    /// (1 owned, 0 not, 2 when a step failed).
6647    fn ownership_in_child(new_pid_namespace: bool) -> i32 {
6648        let observe = || i32::from(scheduler_owns_guest_pid_namespace());
6649        // SAFETY: plain process calls; each forked process calls only getpid,
6650        // unshare, fork, waitpid and _exit before it exits.
6651        unsafe {
6652            let launcher = libc::fork();
6653            assert!(launcher >= 0, "fork failed");
6654            if launcher == 0 {
6655                if !new_pid_namespace {
6656                    libc::_exit(observe());
6657                }
6658                if libc::unshare(libc::CLONE_NEWUSER | libc::CLONE_NEWPID) != 0 {
6659                    libc::_exit(2);
6660                }
6661                let init = libc::fork();
6662                if init == 0 {
6663                    libc::_exit(observe());
6664                }
6665                let mut status = 0;
6666                if init < 0
6667                    || libc::waitpid(init, &mut status, 0) != init
6668                    || !libc::WIFEXITED(status)
6669                {
6670                    libc::_exit(2);
6671                }
6672                libc::_exit(libc::WEXITSTATUS(status));
6673            }
6674            let mut status = 0;
6675            assert_eq!(libc::waitpid(launcher, &mut status, 0), launcher);
6676            assert!(libc::WIFEXITED(status), "wait status {status:#x}");
6677            libc::WEXITSTATUS(status)
6678        }
6679    }
6680
6681    /// D52: ownership of the guests' PID namespace is derived from the
6682    /// process, not supplied. The first process of a new user and PID
6683    /// namespace (PID 1 there, as `hermit run`'s container init is) owns it;
6684    /// a plain child, as under `--no-namespace`, does not, and neither does
6685    /// this test process. Mutation it catches: ownership forced true.
6686    #[test]
6687    fn only_a_pid_namespace_init_owns_the_guests_namespace() {
6688        assert!(!scheduler_owns_guest_pid_namespace());
6689        assert_eq!(
6690            ownership_in_child(false),
6691            0,
6692            "a plain child owns no namespace"
6693        );
6694        assert_eq!(
6695            ownership_in_child(true),
6696            1,
6697            "the init of a new PID namespace owns it (2: no user and PID namespace)"
6698        );
6699    }
6700
6701    /// D52: a public-API launch from a process that is not the init of a new
6702    /// PID namespace, like this test, never has the gate on, however ready
6703    /// the configuration; and until the in-guest runtime takes the gate
6704    /// (3c.1f), no launch sets it at all, owned or not.
6705    #[test]
6706    #[cfg(feature = "liteinst")]
6707    fn no_launch_outside_a_new_pid_namespace_and_no_3c1a_launch_turns_the_gate_on() {
6708        for backend in Backend::ALL {
6709            let ready = gate_ready(backend);
6710            assert!(
6711                !in_guest_signal_delivery_gate(
6712                    &ready,
6713                    backend,
6714                    scheduler_owns_guest_pid_namespace()
6715                ),
6716                "{backend:?}"
6717            );
6718            for owned in [false, true] {
6719                assert!(
6720                    !(IN_GUEST_RUNTIME_DELIVERS_SIGNALS
6721                        && in_guest_signal_delivery_gate(&ready, backend, owned)),
6722                    "{backend:?} {owned}"
6723                );
6724            }
6725        }
6726    }
6727
6728    /// D25: the guest's check logs the limit it found, unchanged when
6729    /// already unlimited and raised otherwise, and a limit it could not raise
6730    /// refuses the run, naming both values and the errno.
6731    #[test]
6732    #[cfg(feature = "liteinst")]
6733    fn an_in_guest_launch_logs_the_cpu_limit_it_found_and_refuses_one_it_cannot_raise() {
6734        let limit = |rlim_cur, rlim_max| libc::rlimit { rlim_cur, rlim_max };
6735        let unchanged = rlimit_cpu_report_line(limit(libc::RLIM_INFINITY, libc::RLIM_INFINITY));
6736        assert!(
6737            unchanged.contains(
6738                "was soft unlimited hard unlimited, the value Detcore reports; the guest starts \
6739                 with it unchanged"
6740            ),
6741            "{unchanged}"
6742        );
6743        let raised = rlimit_cpu_report_line(limit(1, libc::RLIM_INFINITY));
6744        assert!(
6745            raised.contains(
6746                "was soft 1s hard unlimited; Hermit sets it to soft unlimited hard unlimited"
6747            ),
6748            "{raised}"
6749        );
6750        let refused = rlimit_cpu_refusal(limit(3600, 7200), nix::errno::Errno::EPERM);
6751        assert!(
6752            refused.starts_with(
6753                "the guest would inherit a finite physical RLIMIT_CPU (soft 3600s, hard 7200s), \
6754                 and Hermit cannot raise it (EPERM: "
6755            ),
6756            "{refused}"
6757        );
6758        assert!(refused.contains("D25"), "{refused}");
6759    }
6760
6761    /// D25: what the guest's own check reports through its pipe becomes the
6762    /// refusal naming the descriptor and why (an unreadable one, or a table
6763    /// that cannot be listed, with its errno; an unraisable `RLIMIT_CPU` with
6764    /// its values), or, for a check that passed, the limit to log; an empty
6765    /// pipe (the check never ran) names nothing.
6766    #[test]
6767    #[cfg(feature = "liteinst")]
6768    fn the_launch_audit_reports_what_the_guest_check_found() {
6769        use std::os::fd::AsRawFd;
6770        let mut command = Command::new("/bin/true");
6771        let audit = LaunchAudit::install(&mut command).unwrap();
6772        let pipe = &audit.pipe;
6773        assert_eq!(pipe.read_report(), None);
6774        let write = |report: LaunchReport| {
6775            let bytes = report.encode();
6776            // SAFETY: a write of a local array.
6777            let written =
6778                unsafe { libc::write(pipe._write.as_raw_fd(), bytes.as_ptr().cast(), bytes.len()) };
6779            assert_eq!(written, bytes.len() as isize);
6780        };
6781        let report = |kind, bits, errno: i32, fd, rlimit| LaunchReport {
6782            kind,
6783            bits,
6784            errno: errno as u8,
6785            fd,
6786            rlimit,
6787        };
6788        for (sent, what) in [
6789            (
6790                report(
6791                    audit_bits::PRODUCER,
6792                    audit_bits::O_ASYNC | audit_bits::OWNER,
6793                    0,
6794                    0,
6795                    (0, 0),
6796                ),
6797                "the guest would inherit descriptor 0, which is already a signal producer: it has \
6798                 O_ASYNC and an owner",
6799            ),
6800            (
6801                report(
6802                    audit_bits::PRODUCER,
6803                    audit_bits::SOCKET | audit_bits::OWNER,
6804                    0,
6805                    1,
6806                    (0, 0),
6807                ),
6808                "descriptor 1, which is already a signal producer: it is a socket with an owner",
6809            ),
6810            (
6811                report(audit_bits::PRODUCER, audit_bits::LEASE, 0, 300, (0, 0)),
6812                "descriptor 300, which is already a signal producer: it holds a lease",
6813            ),
6814            (
6815                report(audit_bits::UNREADABLE, 0, libc::EACCES, 7, (0, 0)),
6816                "descriptor 7, whose signal owner and flags cannot be read (EACCES",
6817            ),
6818            (
6819                report(audit_bits::UNLISTED, 0, libc::EMFILE, -1, (0, 0)),
6820                "descriptor table could not be listed from /proc/self/fd (EMFILE",
6821            ),
6822            (
6823                report(audit_bits::RLIMIT_CPU, 0, libc::EPERM, -1, (3600, 3600)),
6824                "finite physical RLIMIT_CPU (soft 3600s, hard 3600s), and Hermit cannot raise it \
6825                 (EPERM",
6826            ),
6827        ] {
6828            write(sent);
6829            let received = pipe.read_report().unwrap();
6830            assert_eq!(received, sent);
6831            let reason = received.refusal().unwrap();
6832            assert!(reason.contains(what), "{reason}");
6833        }
6834        let passed = report(audit_bits::PASSED, 0, 0, -1, (1, libc::RLIM_INFINITY));
6835        write(passed);
6836        let received = pipe.read_report().unwrap();
6837        assert_eq!(received.refusal(), None);
6838        assert_eq!(
6839            rlimit_cpu_report_line(received.limit()),
6840            rlimit_cpu_report_line(libc::rlimit {
6841                rlim_cur: 1,
6842                rlim_max: libc::RLIM_INFINITY
6843            })
6844        );
6845        assert_eq!(pipe.read_report(), None);
6846    }
6847
6848    /// D25: the guest's check reads its whole descriptor table and skips
6849    /// close-on-exec descriptors and its own pipe: in this process, a pipe
6850    /// with O_ASYNC and an owner is reported only once its close-on-exec is
6851    /// cleared, by number and state, and the table scan reports exactly it.
6852    #[test]
6853    #[cfg(feature = "liteinst")]
6854    fn the_inherited_descriptor_scan_finds_a_producer_without_close_on_exec() {
6855        use std::os::fd::AsRawFd;
6856        let (read, write) = nix::unistd::pipe2(nix::fcntl::OFlag::O_CLOEXEC).unwrap();
6857        drop(write);
6858        let fd = read.as_raw_fd();
6859        // SAFETY: plain fcntl calls on a descriptor this test owns; no SIGIO
6860        // can be sent for a pipe with no writer.
6861        unsafe {
6862            assert_eq!(libc::fcntl(fd, libc::F_SETOWN, libc::getpid()), 0);
6863            let flags = libc::fcntl(fd, libc::F_GETFL);
6864            assert_eq!(libc::fcntl(fd, libc::F_SETFL, flags | libc::O_ASYNC), 0);
6865        }
6866        assert_eq!(check_inherited_descriptor(fd, -1), None);
6867        // SAFETY: as above.
6868        unsafe { assert_eq!(libc::fcntl(fd, libc::F_SETFD, 0), 0) };
6869        let expected = LaunchReport {
6870            kind: audit_bits::PRODUCER,
6871            bits: audit_bits::O_ASYNC | audit_bits::OWNER,
6872            errno: 0,
6873            fd,
6874            rlimit: (0, 0),
6875        };
6876        assert_eq!(check_inherited_descriptor(fd, -1), Some(expected));
6877        assert_eq!(
6878            check_inherited_descriptor(fd, fd),
6879            None,
6880            "the audit's own pipe"
6881        );
6882        // No other descriptor of this test process is a producer, so the
6883        // table scan reports exactly this one.
6884        assert_eq!(audit_inherited_descriptors(-1), Some(expected));
6885        // SAFETY: as above.
6886        unsafe { assert_eq!(libc::fcntl(fd, libc::F_SETFD, libc::FD_CLOEXEC), 0) };
6887    }
6888
6889    /// D25: an inherited descriptor that is already a producer (O_ASYNC with
6890    /// an owner, a socket with an owner, a lease) refuses the run, naming the
6891    /// descriptor, and so does one with only an owner (a directory
6892    /// notification, which cannot be read back, would signal it); one with
6893    /// only O_ASYNC, or an unowned socket, does not.
6894    #[test]
6895    #[cfg(feature = "liteinst")]
6896    fn an_inherited_signal_producer_is_refused_by_descriptor() {
6897        use detcore::host_timed_producers::ProducerState;
6898        let state = |o_async, owner, socket| ProducerState {
6899            o_async,
6900            owner,
6901            socket,
6902        };
6903        for (producer, lease, what) in [
6904            (state(true, true, false), false, "O_ASYNC and an owner"),
6905            (state(false, true, true), false, "a socket with an owner"),
6906            (state(false, false, false), true, "holds a lease"),
6907            (
6908                state(false, true, false),
6909                false,
6910                "it has an owner, which a directory",
6911            ),
6912        ] {
6913            let reason = inherited_producer_refusal(7, producer, lease).unwrap();
6914            assert!(
6915                reason.starts_with("the guest would inherit descriptor 7, "),
6916                "{reason}"
6917            );
6918            assert!(reason.contains(what), "{reason}");
6919        }
6920        for state in [
6921            state(true, false, false),
6922            state(false, false, true),
6923            state(true, false, true),
6924        ] {
6925            assert_eq!(
6926                inherited_producer_refusal(7, state, false),
6927                None,
6928                "{state:?}"
6929            );
6930        }
6931    }
6932
6933    /// Hermit's default in-guest route reserves SIGSYS always, SIGSEGV while
6934    /// Detcore's subscription traps an instruction, and SIGTRAP only while the
6935    /// guard router can be installed: with site patching on
6936    /// (`in_guest_site_patching_off` false), never with it off, on either
6937    /// in-guest backend (A5 step 3c slice 0c). Every other backend gets an
6938    /// empty set and no router whatever the setting, and so does the in-guest
6939    /// loader route, whose configuration is `prepare_backend_config`'s with no
6940    /// reserved set (A5 step 3c, design section 16.30).
6941    #[test]
6942    fn in_guest_reserved_signals_follow_the_backend_the_subscription_and_site_patching() {
6943        let bit = |signal: libc::c_int| 1_u64 << (signal - 1);
6944        for backend in Backend::ALL {
6945            for patching_off in [false, true] {
6946                let mut config = prepare_backend_config(DetConfig::default(), backend);
6947                assert_eq!(config.in_guest_reserved_signals, 0, "{backend:?}");
6948                config.in_guest_site_patching_off = patching_off;
6949                let router = in_guest_guard_router_installed(&config, backend);
6950                let reserved = in_guest_reserved_signals(&config, backend);
6951                let label = format!("{backend:?} patching_off={patching_off}");
6952                if !backend.is_in_guest_liteinst() {
6953                    assert!(!router, "{label}");
6954                    assert_eq!(reserved, 0, "{label}");
6955                    continue;
6956                }
6957                assert_eq!(router, !patching_off, "{label}");
6958                let trap = if patching_off { 0 } else { bit(libc::SIGTRAP) };
6959                // Without --passthru-opt Detcore always subscribes RDTSC.
6960                assert!(<Detcore as reverie::Tool>::subscriptions(&config).has_rdtsc());
6961                assert_eq!(
6962                    reserved,
6963                    bit(libc::SIGSYS) | bit(libc::SIGSEGV) | trap,
6964                    "{label}"
6965                );
6966
6967                // --passthru-opt with time and CPUID virtualization off traps
6968                // no instruction, so the runtime installs no SIGSEGV handler.
6969                let mut unsubscribed = config.clone();
6970                unsubscribed.passthru_opt = true;
6971                unsubscribed.virtualize_time = false;
6972                unsubscribed.virtualize_cpuid = false;
6973                let subscription = <Detcore as reverie::Tool>::subscriptions(&unsubscribed);
6974                assert!(!subscription.has_rdtsc() && !subscription.has_cpuid());
6975                assert_eq!(
6976                    in_guest_reserved_signals(&unsubscribed, backend),
6977                    bit(libc::SIGSYS) | trap,
6978                    "{label}"
6979                );
6980                for (time, cpuid) in [(true, false), (false, true)] {
6981                    let mut subscribed = unsubscribed.clone();
6982                    subscribed.virtualize_time = time;
6983                    subscribed.virtualize_cpuid = cpuid;
6984                    assert_eq!(
6985                        in_guest_reserved_signals(&subscribed, backend),
6986                        bit(libc::SIGSYS) | bit(libc::SIGSEGV) | trap,
6987                        "{label} time={time} cpuid={cpuid}"
6988                    );
6989                }
6990            }
6991        }
6992    }
6993
6994    /// The default in-guest route sets every Tool image's site-patching check
6995    /// from the backend and only then the reserved set, so SIGTRAP is
6996    /// reserved on `liteinst` (patching on, router installed) and not on
6997    /// `in-guest-trap` (patching off, no router): a SIGTRAP another process
6998    /// sends to an in-guest-trap guest is delivered as on Linux, while SIGSYS
6999    /// and SIGSEGV stay reserved on both (A5 step 3c slice 0c).
7000    #[test]
7001    #[cfg(feature = "liteinst")]
7002    fn the_default_route_reserves_sigtrap_only_with_site_patching_on() {
7003        let bit = |signal: libc::c_int| 1_u64 << (signal - 1);
7004        for backend in Backend::ALL {
7005            let mut config = prepare_backend_config(DetConfig::default(), backend);
7006            config.in_guest_site_patching_off = in_guest_site_patching_off(backend);
7007            let reserved = in_guest_reserved_signals(&config, backend);
7008            let expected = match backend {
7009                Backend::Liteinst => bit(libc::SIGSYS) | bit(libc::SIGSEGV) | bit(libc::SIGTRAP),
7010                Backend::InGuestTrap => bit(libc::SIGSYS) | bit(libc::SIGSEGV),
7011                _ => 0,
7012            };
7013            assert_eq!(reserved, expected, "{backend:?}");
7014            assert_eq!(
7015                in_guest_guard_router_installed(&config, backend),
7016                backend == Backend::Liteinst,
7017                "{backend:?}"
7018            );
7019        }
7020    }
7021
7022    #[test]
7023    #[cfg(feature = "liteinst")]
7024    fn in_guest_liteinst_refuses_a_kernel_older_than_6_5() {
7025        for release in [
7026            "6.5.0",
7027            "6.5",
7028            "6.10.3-200.fc40.x86_64",
7029            "7.1.3-0_fbk0_rc18",
7030            "10.0",
7031        ] {
7032            assert_eq!(in_guest_liteinst_kernel_gap(release), None, "{release}");
7033        }
7034        for release in ["6.4.16", "5.15.0-91-generic", "4.19"] {
7035            let gap = in_guest_liteinst_kernel_gap(release)
7036                .unwrap_or_else(|| panic!("{release} was not refused"));
7037            assert!(gap.contains("Linux 6.5 or later"), "{gap}");
7038            assert!(gap.contains(release), "{gap}");
7039        }
7040        for release in ["", "unknown", "6"] {
7041            assert!(
7042                in_guest_liteinst_kernel_gap(release).is_some(),
7043                "{release:?}"
7044            );
7045        }
7046        // No assertion about the kernel running the tests: that would make
7047        // the test's result depend on the host, and the releases above
7048        // already test both sides of the floor.
7049    }
7050
7051    #[test]
7052    #[cfg(feature = "liteinst")]
7053    fn in_guest_liteinst_refuses_secure_execution_images() {
7054        use interp::startup_fixtures::dynamic_executable;
7055
7056        let directory = tempfile::tempdir().unwrap();
7057        let bytes = dynamic_executable(false);
7058        let gap = |name: &str, mode: u32| {
7059            let program = write_guest_program(directory.path(), name, &bytes, mode);
7060            in_guest_liteinst_program_gap(&mut Command::new(&program)).unwrap()
7061        };
7062        for (name, mode) in [("setuid", 0o4755), ("setgid", 0o2755)] {
7063            let refusal = gap(name, mode).unwrap_or_else(|| panic!("{name} was not refused"));
7064            assert!(refusal.contains("secure-execution mode"), "{refusal}");
7065        }
7066        // Without group execute the kernel treats set-group-ID as mandatory
7067        // locking, not a credential change, so the loader honours LD_PRELOAD.
7068        assert_eq!(gap("setgid-no-group-exec", 0o2744), None);
7069    }
7070
7071    #[test]
7072    #[cfg(feature = "liteinst")]
7073    fn in_guest_liteinst_checks_scripts_through_their_interpreter() {
7074        use interp::startup_fixtures::*;
7075
7076        let directory = tempfile::tempdir().unwrap();
7077        let dynamic = write_guest_program(
7078            directory.path(),
7079            "dynamic",
7080            &dynamic_executable(false),
7081            0o755,
7082        );
7083        let static_elf = write_guest_program(
7084            directory.path(),
7085            "static",
7086            &elf(EM_X86_64, &[(PT_LOAD, b"code")]),
7087            0o755,
7088        );
7089
7090        let script = write_guest_program(
7091            directory.path(),
7092            "to-static",
7093            &shebang_to(&static_elf),
7094            0o755,
7095        );
7096        let gap = in_guest_liteinst_program_gap(&mut Command::new(&script))
7097            .unwrap()
7098            .expect("a script run by a static interpreter was not refused");
7099        assert!(
7100            gap.contains(&static_elf.display().to_string()) && gap.contains("statically linked"),
7101            "{gap}"
7102        );
7103
7104        // A relative interpreter resolves against the guest's working directory.
7105        let relative = write_guest_program(directory.path(), "relative", b"#!static\n", 0o755);
7106        let gap =
7107            in_guest_liteinst_program_gap(Command::new(&relative).current_dir(directory.path()))
7108                .unwrap()
7109                .expect("a relative static interpreter was not refused");
7110        assert!(gap.contains("statically linked"), "{gap}");
7111
7112        // The kernel keeps a carriage return in the interpreter path, so a
7113        // script with CRLF line endings runs `interp\r`, not `interp`.
7114        write_guest_program(
7115            directory.path(),
7116            "interp",
7117            &dynamic_executable(false),
7118            0o755,
7119        );
7120        let interp_cr = write_guest_program(
7121            directory.path(),
7122            "interp\r",
7123            &elf(EM_X86_64, &[(PT_LOAD, b"code")]),
7124            0o755,
7125        );
7126        let mut crlf = b"#!".to_vec();
7127        crlf.extend_from_slice(directory.path().join("interp").as_os_str().as_bytes());
7128        crlf.extend_from_slice(b"\r\necho script\r\n");
7129        let crlf = write_guest_program(directory.path(), "crlf", &crlf, 0o755);
7130        let gap = in_guest_liteinst_program_gap(&mut Command::new(&crlf))
7131            .unwrap()
7132            .expect("a CRLF script run by a static interp\\r was not refused");
7133        assert!(
7134            gap.contains(&interp_cr.display().to_string()) && gap.contains("statically linked"),
7135            "{gap}"
7136        );
7137
7138        // A #! line the kernel's script handler declines runs no interpreter:
7139        // the kernel fails the execve or tries another handler, so it is
7140        // refused.
7141        let mut truncated = b"#!/".to_vec();
7142        truncated.resize(256, b'a');
7143        for (name, bytes) in [
7144            ("empty", b"#! \t\nbody\n".to_vec()),
7145            ("truncated", truncated),
7146        ] {
7147            let program = write_guest_program(directory.path(), name, &bytes, 0o755);
7148            let gap = in_guest_liteinst_program_gap(&mut Command::new(&program))
7149                .unwrap()
7150                .unwrap_or_else(|| panic!("{name} was not refused"));
7151            assert!(
7152                gap.contains("the kernel's script handler declines it"),
7153                "{name}: {gap}"
7154            );
7155        }
7156
7157        // Four nested scripts are followed to the ELF image; a fifth is refused.
7158        let mut target = dynamic;
7159        for level in 1..=5 {
7160            target = write_guest_program(
7161                directory.path(),
7162                &format!("level-{level}"),
7163                &shebang_to(&target),
7164                0o755,
7165            );
7166            let gap = in_guest_liteinst_program_gap(&mut Command::new(&target)).unwrap();
7167            if level <= MAX_INTERPRETER_DEPTH {
7168                assert_eq!(gap, None, "level {level}");
7169            } else {
7170                let gap = gap.expect("five nested scripts were not refused");
7171                assert!(gap.contains("nest more than 4 deep"), "{gap}");
7172            }
7173        }
7174    }
7175
7176    #[test]
7177    #[cfg(feature = "liteinst")]
7178    fn in_guest_liteinst_refuses_a_fifo_without_blocking() {
7179        let directory = tempfile::tempdir().unwrap();
7180        let fifo = directory.path().join("fifo");
7181        let fifo_c = std::ffi::CString::new(fifo.as_os_str().as_bytes()).unwrap();
7182        assert_eq!(unsafe { libc::mkfifo(fifo_c.as_ptr(), 0o700) }, 0);
7183        let script = write_guest_program(directory.path(), "to-fifo", &shebang_to(&fifo), 0o755);
7184        // A socket cannot be opened at all (ENXIO), and opening a device can
7185        // act on it; both must be refused, not reported as unreadable.
7186        let socket = directory.path().join("socket");
7187        let _listener = std::os::unix::net::UnixListener::bind(&socket).unwrap();
7188        let to_socket =
7189            write_guest_program(directory.path(), "to-socket", &shebang_to(&socket), 0o755);
7190        let device = PathBuf::from("/dev/null");
7191        let to_device =
7192            write_guest_program(directory.path(), "to-device", &shebang_to(&device), 0o755);
7193        // `./fifo` reaches the check through find_program's working-directory
7194        // branch, which does not require a regular file; the scripts reach the
7195        // FIFO, the socket and the device as their interpreters. Nothing opens
7196        // the FIFO for writing, so a blocking open or read would never return:
7197        // each check runs on its own thread and must answer within 10 s.
7198        let cases = [
7199            (
7200                "program",
7201                PathBuf::from("./fifo"),
7202                Some(directory.path().to_owned()),
7203                fs::canonicalize(&fifo).unwrap(),
7204            ),
7205            ("interpreter", script, None, fifo.clone()),
7206            ("socket interpreter", to_socket, None, socket.clone()),
7207            ("device interpreter", to_device, None, device),
7208        ];
7209        for (name, program, current_dir, shown) in cases {
7210            let (sender, receiver) = std::sync::mpsc::channel();
7211            std::thread::spawn(move || {
7212                let mut command = Command::new(&program);
7213                if let Some(directory) = current_dir {
7214                    command.current_dir(directory);
7215                }
7216                let gap = in_guest_liteinst_program_gap(&mut command)
7217                    .map_err(|error| format!("{error:#}"));
7218                let _ = sender.send(gap);
7219            });
7220            let gap = receiver
7221                .recv_timeout(std::time::Duration::from_secs(10))
7222                .unwrap_or_else(|_| panic!("{name}: the check blocked on the FIFO"))
7223                .unwrap()
7224                .unwrap_or_else(|| panic!("{name}: the FIFO was not refused"));
7225            assert!(
7226                gap.contains(&format!("{} is not a regular file", shown.display())),
7227                "{name}: {gap}"
7228            );
7229        }
7230    }
7231
7232    #[test]
7233    #[cfg(feature = "liteinst")]
7234    fn in_guest_liteinst_pins_the_program_the_launch_resolves() {
7235        use interp::startup_fixtures::*;
7236
7237        let directory = tempfile::tempdir().unwrap();
7238        // A name that no host PATH directory holds.
7239        let name = "hermit-3520-static";
7240        let static_elf = write_guest_program(
7241            directory.path(),
7242            name,
7243            &elf(EM_X86_64, &[(PT_LOAD, b"code")]),
7244            0o755,
7245        );
7246        let search = format!("/nonexistent-directory:{}", directory.path().display());
7247        // The launch resolves the program with `find_program` on the command it
7248        // is given. Builds the command twice: once for the launch's own
7249        // resolution, once for the check, which must agree with it.
7250        let check = |build: &dyn Fn() -> Command| {
7251            let launch = build().find_program();
7252            let mut command = build();
7253            let arg0 = command.get_arg0().to_owned();
7254            let gap = in_guest_liteinst_program_gap(&mut command);
7255            match launch {
7256                Ok(program) => {
7257                    let gap = gap.unwrap().expect("the static image was not refused");
7258                    assert!(gap.contains("is statically linked"), "{gap}");
7259                    assert_eq!(Path::new(command.get_program()), program);
7260                    assert_eq!(command.get_arg0(), arg0);
7261                    // Pinned: the launch's own resolution returns it unchanged.
7262                    assert_eq!(command.find_program().unwrap(), program);
7263                    assert_eq!(
7264                        fs::canonicalize(&program).unwrap(),
7265                        fs::canonicalize(&static_elf).unwrap()
7266                    );
7267                    true
7268                }
7269                Err(_) => {
7270                    let error = gap.expect_err("the check found what the launch cannot");
7271                    assert!(
7272                        format!("{error:#}").contains("cannot find guest program"),
7273                        "{error:#}"
7274                    );
7275                    false
7276                }
7277            }
7278        };
7279
7280        // A path with a slash resolves against the guest's working directory.
7281        assert!(check(&|| {
7282            let mut command = Command::new(format!("./{name}"));
7283            command.current_dir(directory.path());
7284            command
7285        }));
7286        // A bare name is searched for in the command's PATH once the base
7287        // environment is cleared, skipping entries without such a file.
7288        assert!(check(&|| {
7289            let mut command = Command::new(name);
7290            command.env_clear().env("PATH", &search);
7291            command
7292        }));
7293        // Without a cleared environment the command's explicit PATH still
7294        // wins over the inherited one (Reverie cf745200 makes program lookup
7295        // read the environment the child receives), so both find the image.
7296        assert!(check(&|| {
7297            let mut command = Command::new(name);
7298            command.env("PATH", &search);
7299            command
7300        }));
7301        // A cleared environment without PATH (`--base-env=empty`) searches
7302        // Hermit's working directory, not the guest's and not /bin:/usr/bin.
7303        assert!(!check(&|| {
7304            let mut command = Command::new(name);
7305            command.env_clear().current_dir(directory.path());
7306            command
7307        }));
7308        // Not executable, or missing: the launch cannot run it, and neither
7309        // resolution finds it.
7310        fs::set_permissions(&static_elf, fs::Permissions::from_mode(0o644)).unwrap();
7311        assert!(!check(&|| {
7312            let mut command = Command::new(name);
7313            command.env_clear().env("PATH", &search);
7314            command
7315        }));
7316        assert!(!check(&|| Command::new(&static_elf)));
7317        assert!(!check(&|| Command::new("hermit-no-such-program-3520")));
7318    }
7319
7320    #[test]
7321    fn skid_overshoot_error_refuses_a_successful_result() {
7322        let _lock = SKID_OVERSHOOT_TEST_LOCK.lock().unwrap();
7323        let _ = reverie::take_skid_overshoot_count();
7324        assert!(take_skid_overshoot_error().is_none());
7325
7326        reverie::record_skid_overshoot();
7327        let error = take_skid_overshoot_error().expect("recorded overshoot must be reported");
7328        assert!(
7329            error
7330                .to_string()
7331                .contains("observed 1 HERMIT_SKID_OVERSHOOT report(s)")
7332        );
7333        assert!(
7334            error
7335                .to_string()
7336                .contains("deterministic execution was not established")
7337        );
7338        assert_eq!(reverie::take_skid_overshoot_count(), 0);
7339    }
7340
7341    #[test]
7342    fn skid_overshoot_report_preserves_success_without_an_overshoot() {
7343        let _lock = SKID_OVERSHOOT_TEST_LOCK.lock().unwrap();
7344        let _ = reverie::take_skid_overshoot_count();
7345
7346        let report = SkidOvershootReport::begin(true);
7347        assert_eq!(report.finish(Ok::<_, Error>(7)).unwrap(), 7);
7348        assert_eq!(reverie::take_skid_overshoot_count(), 0);
7349    }
7350
7351    #[test]
7352    fn skid_overshoot_count_can_cross_the_verify_boundary_with_the_output() {
7353        let _lock = SKID_OVERSHOOT_TEST_LOCK.lock().unwrap();
7354        let _ = reverie::take_skid_overshoot_count();
7355
7356        let report = SkidOvershootReport::begin(true);
7357        reverie::record_skid_overshoot();
7358        let (value, count) = report.finish_with_count(Ok::<_, Error>(7)).unwrap();
7359        assert_eq!(value, 7);
7360        assert_eq!(count, 1);
7361        assert_eq!(reverie::take_skid_overshoot_count(), 0);
7362    }
7363
7364    /// ⚠️ THE REGRESSION agent(hermit-007)'s CODEX LANE CAUGHT, PINNED FOR THE
7365    /// SIGNAL BAND. This test fails if the signal half stops being covered; it
7366    /// does NOT prove the predicate can never drift again, because the
7367    /// `122..=127` half is still a hard-coded literal that a future reserved
7368    /// value would fall outside exactly as 130 did.
7369    /// `liteinst_requires_forced_shutdown` hard-coded
7370    /// `Exited(122..=127)`. When the reserved set grew to include the signal
7371    /// band, `Exited(130)` fell outside it and is not `Signaled(_, _)` -- it is a
7372    /// status hermit CHOSE -- so LiteInst skipped the forced shutdown and
7373    /// `clean_up` could wait forever on `handle.await`. A reachable hang,
7374    /// introduced by widening the reserved set in one place and not the other.
7375    ///
7376    /// The 130 and 143 rows are the ones with teeth: revert the predicate to the
7377    /// bare range and they fail.
7378    #[test]
7379    #[cfg(feature = "liteinst")]
7380    fn liteinst_forced_shutdown_covers_every_status_hermit_reserves() {
7381        // The signal band. Not `Signaled`: hermit CHOSE these codes, because a
7382        // namespace init cannot produce a genuine signalled wait status.
7383        assert!(
7384            liteinst_requires_forced_shutdown(ExitStatus::Exited(130)),
7385            "128+SIGINT"
7386        );
7387        assert!(
7388            liteinst_requires_forced_shutdown(ExitStatus::Exited(143)),
7389            "128+SIGTERM"
7390        );
7391        assert!(
7392            liteinst_requires_forced_shutdown(ExitStatus::Exited(129)),
7393            "128+SIGHUP"
7394        );
7395
7396        // The pre-existing reserved band, unchanged.
7397        assert!(liteinst_requires_forced_shutdown(ExitStatus::Exited(122)));
7398        assert!(liteinst_requires_forced_shutdown(ExitStatus::Exited(125)));
7399        assert!(liteinst_requires_forced_shutdown(ExitStatus::Exited(127)));
7400
7401        // A real signal death still forces shutdown.
7402        assert!(liteinst_requires_forced_shutdown(ExitStatus::Signaled(
7403            nix::sys::signal::Signal::SIGKILL,
7404            false
7405        )));
7406
7407        // ⚠️ CONTROLS, so a predicate that simply returned `true` would fail
7408        // this test rather than pass it. An ordinary guest exit must NOT force a
7409        // shutdown; 128 is excluded because there is no signal 0, and 200 is
7410        // above SIGRTMAX so it is a guest's own number.
7411        assert!(!liteinst_requires_forced_shutdown(ExitStatus::Exited(0)));
7412        assert!(!liteinst_requires_forced_shutdown(ExitStatus::Exited(1)));
7413        assert!(!liteinst_requires_forced_shutdown(ExitStatus::Exited(121)));
7414        assert!(!liteinst_requires_forced_shutdown(ExitStatus::Exited(128)));
7415        assert!(!liteinst_requires_forced_shutdown(ExitStatus::Exited(200)));
7416        // ⚠️ THE BAND EDGES, WHICH THIS TEST STOPPED SHORT OF. It went up to 143
7417        // and then jumped to 200, so `MAX_SIGNO` could shrink from 64 to 15 with
7418        // every row still green -- proved vacuous by agent(hermit-005)'s codex
7419        // lane. 192 is `128 + SIGRTMAX`, the last status in the band; 193 is the
7420        // first outside it. Testing a range in the middle pins nothing at either end.
7421        assert!(
7422            liteinst_requires_forced_shutdown(ExitStatus::Exited(192)),
7423            "128 + SIGRTMAX is still a signal death and must force shutdown"
7424        );
7425        assert!(
7426            !liteinst_requires_forced_shutdown(ExitStatus::Exited(193)),
7427            "above SIGRTMAX the status is the guest's own and must NOT force shutdown"
7428        );
7429    }
7430
7431    /// ⚠️ The bare `NotFound` here was read as a git-worktree bug and cost real
7432    /// investigation time. The message must name the mount namespace, because
7433    /// the directory it is complaining about usually exists on the host.
7434    #[test]
7435    fn a_cwd_under_tmp_is_explained_by_the_container_tmpfs_not_by_a_missing_directory() {
7436        let error = std::io::Error::from(std::io::ErrorKind::NotFound);
7437        let rendered = format!(
7438            "{}",
7439            super::kvm_cwd_resolution_error(std::path::Path::new("/tmp/agent-checkout"), &error)
7440        );
7441        assert!(
7442            rendered.contains("private, empty tmpfs"),
7443            "must name the mechanism: {rendered}"
7444        );
7445        assert!(
7446            rendered.contains("--workdir"),
7447            "must name a way out: {rendered}"
7448        );
7449        assert!(
7450            rendered.contains("/tmp/agent-checkout"),
7451            "must still name the path asked for: {rendered}"
7452        );
7453    }
7454
7455    /// The explanation must NOT be attached to failures it does not explain --
7456    /// an error that volunteers the wrong cause is what produced the original
7457    /// misdiagnosis, in the other direction.
7458    #[test]
7459    fn the_tmpfs_explanation_is_withheld_where_it_does_not_apply() {
7460        let not_found = std::io::Error::from(std::io::ErrorKind::NotFound);
7461        // ⚠️ A CONSTANT ABSOLUTE PATH, NOT ONE READ FROM THE ENVIRONMENT. The only
7462        // property this arm needs is "outside /tmp", and a literal that is outside
7463        // /tmp by construction supplies it without asking the runner anything.
7464        //
7465        // The earlier form wrote a developer-specific home path, which made
7466        // `scripts/check-portable-paths.sh` red on main -- correctly: that gate
7467        // rejects such a path anywhere in a tracked build or run file, tests and
7468        // COMMENTS included, so spelling one out even to explain this would trip
7469        // it. Replacing it by reading $HOME then made the test depend on the
7470        // runner instead: measured by `agent(codex-rev-2641)`, HOME unset, empty,
7471        // relative and /tmp-rooted gave three different fixtures and one failure.
7472        // A unit test that consults the environment is answering a question about
7473        // the environment.
7474        //
7475        // ⚠️ AND NO GUARD IS NEEDED HERE, WHICH IS THE POINT OF USING A CONSTANT.
7476        // The $HOME form carried `assert!(!outside_path.starts_with("/tmp"))` to
7477        // stop a /tmp-rooted HOME making this vacuous. That assertion could never
7478        // fire: the `!outside.contains("tmpfs")` check below sits ABOVE it in
7479        // source order and panics first in exactly that case. It was decorative,
7480        // and an assertion over a compile-time constant would be too, so there is
7481        // none. The fixture cannot drift into /tmp because nothing computes it.
7482        let outside_path = std::path::Path::new("/nonexistent-home/wt");
7483        let outside = format!(
7484            "{}",
7485            super::kvm_cwd_resolution_error(outside_path, &not_found)
7486        );
7487        assert!(!outside.contains("tmpfs"), "not a /tmp path: {outside}");
7488
7489        // `/tmp` itself resolves in the guest -- the mount POINT exists -- so a
7490        // failure there is something else and must not be mislabelled.
7491        let tmp_itself = format!(
7492            "{}",
7493            super::kvm_cwd_resolution_error(std::path::Path::new("/tmp"), &not_found)
7494        );
7495        assert!(!tmp_itself.contains("tmpfs"), "/tmp itself: {tmp_itself}");
7496
7497        // A different errno under /tmp is a different fault (permissions, say).
7498        let denied = std::io::Error::from(std::io::ErrorKind::PermissionDenied);
7499        let other = format!(
7500            "{}",
7501            super::kvm_cwd_resolution_error(std::path::Path::new("/tmp/x"), &denied)
7502        );
7503        assert!(!other.contains("tmpfs"), "not NotFound: {other}");
7504    }
7505
7506    /// A build-flag message must not assert a machine condition it never tested.
7507    ///
7508    /// ⚠️ THIS IS THE FOURTH DIAGNOSTIC TONIGHT THAT NAMED AN UNESTABLISHED CAUSE,
7509    /// after the sigpipe checker, the manifest gate and the LiteInst pidfd error.
7510    /// "DBT support was not included in this build" reads as "this machine cannot
7511    /// run DBT", and cost four or five unnecessary abstentions across the fleet --
7512    /// agents declined to make DBT claims on a box where DynamoRIO is installed
7513    /// and `--features dbt` builds and runs. The absence is a build flag, and the
7514    /// message must say which flag and admit what it did not check.
7515    #[test]
7516    fn an_unbuilt_backend_names_the_flag_and_claims_nothing_about_the_machine() {
7517        let feature_disabled_backends: &[(Backend, &str)] = &[
7518            #[cfg(not(feature = "liteinst"))]
7519            (Backend::Liteinst, "liteinst"),
7520            // in-guest-trap is the same runtime behind the same feature.
7521            #[cfg(not(feature = "liteinst"))]
7522            (Backend::InGuestTrap, "liteinst"),
7523        ];
7524
7525        for &(backend, feature) in feature_disabled_backends {
7526            let reason = backend
7527                .unavailable_reason()
7528                .expect("a feature-disabled backend must report why it is unavailable");
7529            assert!(
7530                reason.contains(feature),
7531                "{backend:?}: a build-flag message must name the flag to rebuild with, got: {reason}"
7532            );
7533            assert!(
7534                reason.contains("has not been checked"),
7535                "{backend:?}: a build-flag message must not imply the machine was tested, got: {reason}"
7536            );
7537        }
7538    }
7539
7540    #[tokio::test]
7541    async fn virtual_timeout_with_debugger_is_refused_at_both_raw_library_dispatches() {
7542        for (gdbserver, bounded) in [(false, false), (true, false), (false, true), (true, true)] {
7543            let config = DetConfig {
7544                gdbserver,
7545                virtual_timeout: bounded.then(|| std::num::NonZeroU64::new(5_000_000_000).unwrap()),
7546                ..DetConfig::default()
7547            };
7548            // No retained operation owner: admitted cases stop at that later
7549            // boundary, without starting a guest or waiting for a debugger.
7550            // The unsupported combination must be refused before it is reached.
7551            let status = dispatch_backend(
7552                Command::new("/nonexistent/guest"),
7553                config.clone(),
7554                false,
7555                &None,
7556                Backend::Ptrace,
7557                None,
7558            )
7559            .await
7560            .unwrap_err();
7561            let output = dispatch_output_backend(
7562                Command::new("/nonexistent/guest"),
7563                config,
7564                false,
7565                &None,
7566                Backend::Ptrace,
7567                None,
7568            )
7569            .await
7570            .unwrap_err();
7571            for error in [status, output] {
7572                if gdbserver && bounded {
7573                    assert!(
7574                        error.is::<detcore_model::virtual_timeout::VirtualTimeoutWithDebugger>(),
7575                        "{error}"
7576                    );
7577                    let classified =
7578                        crate::error::SerializableError::from(error.context("raw library launch"));
7579                    assert_eq!(classified.kind(), crate::error::FailureKind::PolicyRefusal);
7580                } else {
7581                    assert!(
7582                        error
7583                            .to_string()
7584                            .contains("ordinary ptrace requires its retained operation owner"),
7585                        "{error}"
7586                    );
7587                }
7588            }
7589        }
7590    }
7591    /// A DBT runtime that does not export the fingerprint check's symbol, as
7592    /// one built before the check does not, is refused before DBT starts
7593    /// (this test's own executable exports no such symbol), and so is a file
7594    /// that is not ELF.
7595    use std::fs;
7596    use std::path::PathBuf;
7597
7598    use super::Backend;
7599    #[cfg(feature = "liteinst")]
7600    use super::ExitStatus;
7601    use super::HermitData;
7602    use super::Id;
7603    use super::collect_recording_ids;
7604    use super::kvm_device_unavailable_reason;
7605    #[cfg(feature = "liteinst")]
7606    use super::liteinst_requires_forced_shutdown;
7607    use super::liteinst_unavailable_reason;
7608    use super::output_backend_stdin_file;
7609    use super::prepare_backend_config;
7610    use super::reserve_output_stdin_snapshot;
7611    use super::resolve_kvm_shebang;
7612
7613    #[test]
7614    fn recording_inventory_preserves_missing_and_non_recording_cases() {
7615        let parent = tempfile::tempdir().expect("failed to create recording inventory parent");
7616        let missing = parent.path().join("missing");
7617        assert!(
7618            HermitData::with_dir(&missing)
7619                .try_recordings()
7620                .expect("a missing data directory should be empty")
7621                .is_empty()
7622        );
7623
7624        let inventory = parent.path().join("inventory");
7625        fs::create_dir(&inventory).expect("failed to create recording inventory");
7626        let recording: Id = "0123456789abcdef0123456789abcdef"
7627            .parse()
7628            .expect("valid recording ID");
7629        fs::create_dir(inventory.join(recording.to_string()))
7630            .expect("failed to create recording directory");
7631        fs::create_dir(inventory.join("not-a-recording"))
7632            .expect("failed to create invalid-name directory");
7633        fs::write(
7634            inventory.join("fedcba9876543210fedcba9876543210"),
7635            b"not a directory",
7636        )
7637        .expect("failed to create valid-looking non-directory");
7638
7639        assert_eq!(
7640            HermitData::with_dir(&inventory)
7641                .try_recordings()
7642                .expect("recording inventory should be readable"),
7643            vec![recording]
7644        );
7645        assert_eq!(
7646            HermitData::with_dir(&inventory)
7647                .recordings()
7648                .collect::<Vec<_>>(),
7649            vec![recording]
7650        );
7651    }
7652
7653    #[test]
7654    fn recording_inventory_rejects_an_error_after_a_valid_entry() {
7655        let inventory = tempfile::tempdir().expect("failed to create recording inventory");
7656        let recording: Id = "0123456789abcdef0123456789abcdef"
7657            .parse()
7658            .expect("valid recording ID");
7659        fs::create_dir(inventory.path().join(recording.to_string()))
7660            .expect("failed to create recording directory");
7661        let valid_entry = fs::read_dir(inventory.path())
7662            .expect("failed to open recording inventory")
7663            .next()
7664            .expect("missing recording entry")
7665            .expect("failed to read recording entry");
7666
7667        let entries = vec![
7668            Ok(valid_entry),
7669            Err(std::io::Error::other("injected entry failure")),
7670        ];
7671        let error = collect_recording_ids(entries, inventory.path(), fs::DirEntry::file_type)
7672            .expect_err("a partial inventory must not be returned");
7673        assert!(
7674            error
7675                .to_string()
7676                .contains("Failed to read an entry in recording inventory"),
7677            "unexpected error: {error:#}"
7678        );
7679    }
7680
7681    #[test]
7682    fn recording_inventory_rejects_an_entry_inspection_error() {
7683        let inventory = tempfile::tempdir().expect("failed to create recording inventory");
7684        let recording = inventory.path().join("0123456789abcdef0123456789abcdef");
7685        fs::create_dir(&recording).expect("failed to create recording directory");
7686        let entry = fs::read_dir(inventory.path())
7687            .expect("failed to open recording inventory")
7688            .next()
7689            .expect("missing recording entry")
7690            .expect("failed to read recording entry");
7691
7692        let error = collect_recording_ids([Ok(entry)], inventory.path(), |_| {
7693            Err(std::io::Error::other("injected inspection failure"))
7694        })
7695        .expect_err("an uninspectable entry must fail the inventory");
7696        assert!(
7697            error
7698                .to_string()
7699                .contains("Failed to inspect recording inventory entry"),
7700            "unexpected error: {error:#}"
7701        );
7702    }
7703
7704    /// Regression test for the `hermit run --verify` empty-stdin bug: a pipe
7705    /// (non-seekable) fed to hermit must be buffered and replayed *identically*
7706    /// to every run of `--verify`. Before the fix the output-capturing backend
7707    /// used `Stdio::null()`, so piped input was silently dropped and both runs
7708    /// saw empty input (a false "deterministic" pass, e.g. `gcc -x c -`
7709    /// compiling nothing). This asserts the reserved snapshot replays the exact
7710    /// bytes twice.
7711    #[test]
7712    fn output_stdin_snapshot_replays_pipe_to_repeated_runs() {
7713        use std::io::Read;
7714        use std::io::Write;
7715        use std::os::fd::FromRawFd;
7716
7717        // A pipe is non-seekable, exercising the buffer-into-tempfile path that
7718        // matches the real `echo prog | hermit run --verify` scenario.
7719        let mut fds = [0i32; 2];
7720        assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0);
7721        // SAFETY: pipe(2) just returned two fresh owned descriptors.
7722        let read_end = unsafe { fs::File::from_raw_fd(fds[0]) };
7723        let mut write_end = unsafe { fs::File::from_raw_fd(fds[1]) };
7724
7725        let payload = b"int main(){return 0;}\n";
7726        write_end.write_all(payload).unwrap();
7727        // Close the write end so draining the pipe hits EOF.
7728        drop(write_end);
7729
7730        reserve_output_stdin_snapshot(Some(read_end)).unwrap();
7731
7732        // Two successive runs (as `--verify` performs) must each read the full
7733        // payload from the start.
7734        for run in 0..2 {
7735            let mut file = output_backend_stdin_file()
7736                .unwrap()
7737                .unwrap_or_else(|| panic!("run {run}: expected a replayable stdin snapshot"));
7738            let mut got = Vec::new();
7739            file.read_to_end(&mut got).unwrap();
7740            assert_eq!(got, payload, "run {run} stdin replay mismatch");
7741        }
7742
7743        let mut file = output_backend_stdin_file().unwrap().unwrap();
7744        let error = file
7745            .write_all(b"guest-must-not-change-the-snapshot")
7746            .expect_err("the replayed stdin must be read-only");
7747        assert_eq!(error.raw_os_error(), Some(libc::EBADF));
7748    }
7749
7750    #[test]
7751    #[cfg(feature = "liteinst")]
7752    fn liteinst_reserved_failures_require_scheduler_cancellation() {
7753        for status in 122..=127 {
7754            assert!(liteinst_requires_forced_shutdown(ExitStatus::Exited(
7755                status
7756            )));
7757        }
7758        assert!(!liteinst_requires_forced_shutdown(ExitStatus::Exited(121)));
7759        assert!(!liteinst_requires_forced_shutdown(ExitStatus::Exited(128)));
7760    }
7761
7762    #[test]
7763    fn liteinst_backend_config_preserves_the_requested_timeslice() {
7764        // Configuration must not silently clear a requested timeslice: the
7765        // in-guest LiteInst refusal relies on seeing it.
7766        let config = super::DetConfig::default();
7767        assert!(config.max_timeslice.is_some());
7768        for backend in [Backend::Liteinst, Backend::InGuestTrap] {
7769            assert!(
7770                prepare_backend_config(config.clone(), backend)
7771                    .max_timeslice
7772                    .is_some(),
7773                "{backend:?}"
7774            );
7775        }
7776        assert!(
7777            prepare_backend_config(config, Backend::Ptrace)
7778                .max_timeslice
7779                .is_some()
7780        );
7781    }
7782
7783    #[test]
7784    fn in_guest_liteinst_cancels_killed_thread_rpcs() {
7785        let config = super::DetConfig::default();
7786        for backend in [Backend::Liteinst, Backend::InGuestTrap] {
7787            assert!(
7788                prepare_backend_config(config.clone(), backend)
7789                    .backend
7790                    .needs_killed_thread_rpc_cancellation,
7791                "{backend:?}"
7792            );
7793        }
7794        // LiteInst's cancellation changes no other backend.
7795        assert!(
7796            !prepare_backend_config(config, Backend::Ptrace)
7797                .backend
7798                .needs_killed_thread_rpc_cancellation
7799        );
7800    }
7801
7802    #[test]
7803    fn sabre_backend_configures_process_local_capabilities() {
7804        let config = super::DetConfig::default();
7805        let sabre = prepare_capability_case(config.clone(), CapabilityCase::Sabre).backend;
7806        assert!(sabre.tool_shares_guest_descriptor_table);
7807        assert!(sabre.rediscovers_descriptors_after_exec);
7808        assert!(sabre.loopback_pollers_yield_to_peers);
7809        assert!(sabre.guest_clock_reads_bypass_backend);
7810        assert!(sabre.needs_killed_thread_rpc_cancellation);
7811        assert!(sabre.reports_physical_process_exits);
7812        assert!(sabre.tracks_process_children);
7813        assert!(!sabre.runs_exit_robust_list);
7814        assert!(!sabre.requires_thread_directed_process_signals);
7815        let ptrace = prepare_capability_case(config, CapabilityCase::Ptrace).backend;
7816        assert!(!ptrace.tool_shares_guest_descriptor_table);
7817        assert!(!ptrace.rediscovers_descriptors_after_exec);
7818        assert!(!ptrace.loopback_pollers_yield_to_peers);
7819        assert!(!ptrace.guest_clock_reads_bypass_backend);
7820        assert!(!ptrace.needs_killed_thread_rpc_cancellation);
7821        assert!(!ptrace.reports_physical_process_exits);
7822        assert!(ptrace.tracks_process_children);
7823        assert!(ptrace.runs_exit_robust_list);
7824        assert!(!ptrace.requires_thread_directed_process_signals);
7825    }
7826
7827    #[test]
7828    fn kvm_reports_stored_metadata_timestamps_only_for_sequential_tool_threads() {
7829        let sequential = prepare_backend_config(
7830            super::DetConfig {
7831                sequentialize_threads: true,
7832                virtualize_metadata: true,
7833                ..super::DetConfig::default()
7834            },
7835            Backend::Kvm,
7836        );
7837        assert!(super::kvm_reports_stored_metadata_timestamps(&sequential));
7838
7839        // Without sequentialized threads the setting stays off; the
7840        // backend reports host files' kernel timestamps either way.
7841        let concurrent = super::DetConfig {
7842            sequentialize_threads: false,
7843            ..sequential.clone()
7844        };
7845        assert!(!super::kvm_reports_stored_metadata_timestamps(&concurrent));
7846
7847        // Nothing rewrites the stored values.
7848        let host_metadata = super::DetConfig {
7849            virtualize_metadata: false,
7850            ..sequential
7851        };
7852        assert!(!super::kvm_reports_stored_metadata_timestamps(
7853            &host_metadata
7854        ));
7855    }
7856
7857    #[test]
7858    fn kvm_backend_config_marks_concurrent_process_children() {
7859        let defaults = super::DetConfig::default();
7860        assert!(!defaults.sequentialize_threads);
7861        let nonsequential = prepare_capability_case(defaults, CapabilityCase::Kvm);
7862        assert!(nonsequential.backend.provides_process_signal_control);
7863        assert!(nonsequential.backend.emulates_child_waits);
7864        assert!(!may_observe_signal_dequeues(&nonsequential));
7865        assert!(!requires_signal_control(&nonsequential));
7866
7867        let config = super::DetConfig {
7868            sequentialize_threads: true,
7869            ..super::DetConfig::default()
7870        };
7871        let kvm = prepare_capability_case(config, CapabilityCase::Kvm);
7872        assert!(may_observe_signal_dequeues(&kvm));
7873        assert!(requires_signal_control(&kvm));
7874        assert!(kvm.backend.needs_killed_thread_rpc_cancellation);
7875        assert!(kvm.backend.tracks_process_children);
7876        assert!(!kvm.backend.runs_exit_robust_list);
7877        assert!(!kvm.backend.requires_thread_directed_process_signals);
7878
7879        // A controlled run needs both the backend's process-signal control and
7880        // sequentialized threads; neither alone admits one.
7881        for backend in [
7882            CapabilityCase::Ptrace,
7883            CapabilityCase::Dbt,
7884            CapabilityCase::Sabre,
7885            CapabilityCase::Liteinst,
7886            CapabilityCase::InGuestTrap,
7887            CapabilityCase::E9patch,
7888        ] {
7889            let config = super::DetConfig {
7890                sequentialize_threads: true,
7891                ..super::DetConfig::default()
7892            };
7893            let prepared = prepare_capability_case(config, backend);
7894            assert!(!may_observe_signal_dequeues(&prepared), "{backend:?}");
7895            assert!(!requires_signal_control(&prepared), "{backend:?}");
7896        }
7897    }
7898
7899    /// Whether Detcore asks the backend to install process signal control for
7900    /// `config`: the bound Reverie checks before it offers the control.
7901    fn may_observe_signal_dequeues(config: &DetConfig) -> bool {
7902        <super::Detcore as reverie::Tool>::may_observe_signal_dequeues(config)
7903    }
7904
7905    /// Whether Reverie must refuse `config` on a runner that offers no
7906    /// process signal control, as Detcore's installation did before.
7907    fn requires_signal_control(config: &DetConfig) -> bool {
7908        <super::Detcore as reverie::Tool>::requires_signal_control(config)
7909    }
7910
7911    /// The values the hand-written assignments in `prepare_backend_config` and
7912    /// `run_kvm` gave each backend before Detcore read
7913    /// [`reverie::BackendCapabilities`], transcribed field by field. A backend
7914    /// identity check became the capability it selected:
7915    /// `discover_live_file_metadata` split into the first four fields, the old
7916    /// `backend_reports_physical_process_exits` became two, and
7917    /// `backend_is_kvm` became five, of which the last two remain here:
7918    /// `refuses_nonleader_exec_with_enosys` became the backend's typed exec
7919    /// refusal (Reverie d78f770f), `failed_gettimeofday_may_store_host_time`
7920    /// the guest's storable-memory report (Reverie 8bfb450e) and
7921    /// `user_address_limit` the guest's user address limit query (Reverie
7922    /// 6f75cc4c). Three fields are newer than that transcription:
7923    /// `process_exits_complete_asynchronously` (Reverie 056c8cac), true only
7924    /// for in-guest LiteInst, whose process exits complete some time after the
7925    /// tool grants them, `reports_child_exit_publication` (Reverie 9cb6f7b7),
7926    /// true for ptrace and e9patch, whose tracer reports when the kernel
7927    /// publishes a process's exit to its parent, and
7928    /// `virtualizes_guest_sigalrm` (Reverie 7142ff8c), true only for in-guest
7929    /// LiteInst, whose runtime keeps a guest SIGALRM handler virtual (signal
7930    /// phase 1). `in-guest-trap` is in-guest LiteInst with site patching off,
7931    /// which changes how a syscall reaches the Tool and nothing the Tool host
7932    /// can do, so it has exactly in-guest LiteInst's row.
7933    /// DBT now also shares the guest descriptor table and rediscovers it after
7934    /// exec (Reverie 15374907); its poller-yield capability remains false. `reports_sigreturns` (A5 step 2) is true
7935    /// for ptrace, and so for e9patch preprocessing, whose guest Hermit runs
7936    /// on ptrace. `reports_kernel_child_exit_signals` is true for ptrace,
7937    /// e9patch and SaBRe, whose signal callbacks see the kernel's child-exit
7938    /// SIGCHLD with its siginfo and can suppress or rewrite it.
7939    /// `kernel_never_walks_robust_lists` (Reverie 93d3a168), `places_orphans`
7940    /// and `publishable_parent_death_signals` (Reverie f614c7c8) arrived with
7941    /// the Reverie pin of the first-party split; each is KVM's alone.
7942    fn golden_backend_capabilities(backend: CapabilityCase) -> serde_json::Value {
7943        let sabre = backend == CapabilityCase::Sabre;
7944        let kvm = backend == CapabilityCase::Kvm;
7945        let dbt = backend == CapabilityCase::Dbt;
7946        // LiteInst runs only through its in-guest runtime.
7947        let in_guest_liteinst = matches!(
7948            backend,
7949            CapabilityCase::Liteinst | CapabilityCase::InGuestTrap
7950        );
7951        serde_json::json!({
7952            "tool_shares_guest_descriptor_table": sabre || dbt,
7953            "rediscovers_descriptors_after_exec": sabre || dbt,
7954            "loopback_pollers_yield_to_peers": sabre,
7955            "guest_clock_reads_bypass_backend": sabre,
7956            "needs_killed_thread_rpc_cancellation": in_guest_liteinst || sabre || dbt || kvm,
7957            "reports_physical_process_exits": sabre,
7958            "process_exits_complete_asynchronously": in_guest_liteinst,
7959            "virtualizes_guest_sigalrm": in_guest_liteinst,
7960            "tracks_process_children": !dbt,
7961            "runs_exit_robust_list": matches!(backend, CapabilityCase::Ptrace | CapabilityCase::E9patch),
7962            // Reverie 93d3a168: KVM runs guest threads on its own vCPUs, so no
7963            // Linux task exit walks their robust lists; every other backend
7964            // runs each guest thread as a host task whose exit does.
7965            "kernel_never_walks_robust_lists": kvm,
7966            "requires_thread_directed_process_signals": dbt,
7967            "supports_parked_write_signal_interruption":
7968                matches!(backend, CapabilityCase::Ptrace | CapabilityCase::E9patch | CapabilityCase::Kvm),
7969            "reports_child_exit_publication":
7970                matches!(backend, CapabilityCase::Ptrace | CapabilityCase::E9patch),
7971            "reports_kernel_child_exit_signals":
7972                matches!(backend, CapabilityCase::Ptrace | CapabilityCase::E9patch | CapabilityCase::Sabre),
7973            "provides_process_signal_control": kvm,
7974            "emulates_child_waits": kvm,
7975            // A Config field Hermit set from the backend's name before it was
7976            // a stated capability. e9patch runs on ptrace and has its value.
7977            "supports_blocked_wait_signal_interruption":
7978                matches!(backend, CapabilityCase::Ptrace | CapabilityCase::E9patch),
7979            // Readings of the field above until A5 step 3c.0 split them out,
7980            // with its value on every backend.
7981            "kernel_signal_state_is_guests": matches!(backend, CapabilityCase::Ptrace | CapabilityCase::E9patch),
7982            "kernel_signal_targets_are_predictable":
7983                matches!(backend, CapabilityCase::Ptrace | CapabilityCase::E9patch),
7984            "host_task_reads_reach_guest_threads":
7985                matches!(backend, CapabilityCase::Ptrace | CapabilityCase::E9patch),
7986            "host_timed_signals_are_physical":
7987                matches!(backend, CapabilityCase::Ptrace | CapabilityCase::E9patch),
7988            // Reverie 9443a6b2 lets Tools rely on KVM's preemption timer too.
7989            "delivers_preemption_timer_events":
7990                matches!(
7991                    backend,
7992                    CapabilityCase::Ptrace | CapabilityCase::E9patch | CapabilityCase::Kvm
7993                ),
7994            "runs_guest_code_between_injections": sabre || in_guest_liteinst,
7995            "promotes_open_files_before_native_lifecycle": dbt || in_guest_liteinst,
7996            // Hermit runs e9patch preprocessing as a ptrace run (empty
7997            // trampolines keep the original syscall instruction), whose
7998            // rt_sigreturns reach the seccomp stop that reports them.
7999            "reports_sigreturns": matches!(backend, CapabilityCase::Ptrace | CapabilityCase::E9patch),
8000            // ptrace (and so e9patch) sets DR0-DR3 on x86-64; every other
8001            // backend sets no hardware watchpoints.
8002            "hardware_watchpoints": if cfg!(target_arch = "x86_64") {
8003                match backend {
8004                    CapabilityCase::Ptrace | CapabilityCase::E9patch => 4,
8005                    _ => 0,
8006                }
8007            } else {
8008                0
8009            },
8010            // Reverie 9443a6b2: ptrace unwinds remotely, DBT walks the frame
8011            // chain, SaBRe returns the trapped site; KVM and the in-guest
8012            // LiteInst runtime never return a stack.
8013            "provides_guest_backtraces": matches!(
8014                backend,
8015                CapabilityCase::Ptrace
8016                    | CapabilityCase::E9patch
8017                    | CapabilityCase::Sabre
8018                    | CapabilityCase::Dbt
8019            ),
8020            // Reverie f614c7c8: only KVM's process signal control places an
8021            // exiting process's orphans and publishes parent-death signals;
8022            // on every other backend the kernel does both itself.
8023            "places_orphans": kvm,
8024            // Signals 1 to 31 but SIGCHLD (17) and SIGSTOP, SIGTSTP, SIGTTIN
8025            // and SIGTTOU (19 to 22), as a kernel signal set.
8026            "publishable_parent_death_signals": if kvm { 0x7fc2_ffff_u64 } else { 0 },
8027        })
8028    }
8029
8030    #[test]
8031    fn backend_capabilities_match_the_golden_table() {
8032        for backend in [
8033            CapabilityCase::Ptrace,
8034            CapabilityCase::Kvm,
8035            CapabilityCase::E9patch,
8036            CapabilityCase::Liteinst,
8037            CapabilityCase::InGuestTrap,
8038            CapabilityCase::Sabre,
8039            CapabilityCase::Dbt,
8040        ] {
8041            let config = prepare_capability_case(super::DetConfig::default(), backend);
8042            assert_eq!(
8043                serde_json::to_value(config.backend).unwrap(),
8044                golden_backend_capabilities(backend),
8045                "{backend:?}"
8046            );
8047        }
8048    }
8049
8050    #[test]
8051    fn backend_capabilities_are_the_ones_each_backend_reports() {
8052        assert_eq!(
8053            super::backend_capabilities(Backend::Ptrace),
8054            <reverie_ptrace::PtraceBackend as reverie::Backend>::capabilities()
8055        );
8056        assert_eq!(
8057            super::backend_capabilities(Backend::Kvm),
8058            reverie_kvm::KvmBackend::capabilities()
8059        );
8060        #[cfg(feature = "liteinst")]
8061        {
8062            // SAFETY: the strong immutable declaration above has the public
8063            // repr(C), two-u64 descriptor ABI. This reads the real linked
8064            // object, so changing the host declaration to Fixed fails here.
8065            let backing = unsafe {
8066                std::ptr::addr_of!(super::reverie_liteinst_allocator_backing_v1)
8067                    .cast::<[u64; 2]>()
8068                    .read_volatile()
8069            };
8070            assert_eq!(backing, [1, 1], "host Hermit must declare Legacy backing");
8071            assert_eq!(
8072                super::backend_capabilities(Backend::Liteinst),
8073                <reverie_liteinst::LiteinstBackend as reverie::Backend>::capabilities()
8074            );
8075            // in-guest-trap runs the same Reverie backend.
8076            assert_eq!(
8077                super::backend_capabilities(Backend::InGuestTrap),
8078                <reverie_liteinst::LiteinstBackend as reverie::Backend>::capabilities()
8079            );
8080        }
8081    }
8082
8083    /// `Config` with its three environment-backed fields (`HERMIT_EPOCH`,
8084    /// `HERMIT_PRNG`, `HERMIT_SCHED_SEED`) pinned, so the encoding does not
8085    /// depend on the test process's environment.
8086    fn env_free_detconfig(epoch: &str, seed: &str) -> DetConfig {
8087        use clap::Parser;
8088        let mut config = DetConfig::parse_from(["hermit", epoch, seed, "--sched-seed=0"]);
8089        config.sched_seed = None;
8090        config
8091    }
8092
8093    /// Non-default values in fields that serialize as strings, floats,
8094    /// options and nested arrays. The path contains legacy key names, so an
8095    /// encoder that edited text would put a key inside a string value.
8096    fn rich_detconfig() -> DetConfig {
8097        let mut config = env_free_detconfig("--epoch=2000-12-31T23:59:59Z", "--seed=7");
8098        config.rng_seed = Some(13);
8099        config.sched_seed = Some(11);
8100        config.clock_multiplier = Some(1.5);
8101        config.chaos = true;
8102        config.record_preemptions_to = Some(std::path::PathBuf::from(
8103            "/tmp/a,\"detect_host_clock_futex_timeouts\":true,\"backend_tracks_process_children\":{",
8104        ));
8105        config.mountinfo_root_rewrites = vec![detcore_model::config::MountInfoRootRewrite {
8106            raw_mount_id: 41,
8107            deterministic_root: b"/".to_vec(),
8108            raw_root_prefix: Some(b"/tmp/x".to_vec()),
8109            deterministic_root_prefix: Some(b"/tmp".to_vec()),
8110            raw_mountpoint_prefix: None,
8111            deterministic_mountpoint_prefix: None,
8112        }];
8113        config.stacktrace_event = vec![(3, Some(std::path::PathBuf::from("/s")))];
8114        config
8115    }
8116
8117    /// The legacy configuration form, which a standalone DBT run's
8118    /// `HERMIT_DBT_DETCONFIG` may still carry, reads and writes these bytes.
8119    /// These fixtures are the output of `serde_json::to_string` on the same
8120    /// configs, captured from a build of the commit before Detcore read
8121    /// [`reverie::BackendCapabilities`], when `Config` still had one boolean
8122    /// field per backend switch. Every DBT run sequentializes threads, so the
8123    /// DBT cases do too. The fixtures carry one deliberate later change, the
8124    /// `fdinfo_unlisted_mount_ids` -> `mount_id_assignment_order` key rename.
8125    /// Preserve the historical DBT descriptor capabilities here as well as the
8126    /// historical bytes. Current DBT's independent descriptor facts require
8127    /// the typed envelope, whose round trip detcore-dbt's tests check.
8128    #[test]
8129    fn historical_dbt_capabilities_preserve_legacy_detconfig_bytes() {
8130        let sequential = |mut config: DetConfig| {
8131            config.sequentialize_threads = true;
8132            config
8133        };
8134        let default_config = || env_free_detconfig("--epoch=2026-01-01T00:00:00Z", "--seed=0");
8135        for (name, mut config, before) in [
8136            (
8137                "default",
8138                default_config(),
8139                include_str!("../tests/fixtures/dbt/detconfig-before-capabilities-default.json"),
8140            ),
8141            (
8142                "dbt",
8143                prepare_capability_case(sequential(default_config()), CapabilityCase::Dbt),
8144                include_str!("../tests/fixtures/dbt/detconfig-before-capabilities-dbt.json"),
8145            ),
8146            (
8147                "rich_dbt",
8148                prepare_capability_case(sequential(rich_detconfig()), CapabilityCase::Dbt),
8149                include_str!("../tests/fixtures/dbt/detconfig-before-capabilities-rich_dbt.json"),
8150            ),
8151        ] {
8152            config.backend.tool_shares_guest_descriptor_table = false;
8153            config.backend.rediscovers_descriptors_after_exec = false;
8154            // Read back as false, as the `Config` fields they replace were.
8155            config.backend.supports_blocked_wait_signal_interruption = false;
8156            config.backend.delivers_preemption_timer_events = false;
8157            config.backend.runs_guest_code_between_injections = false;
8158            // The legacy form carries no width for `hardware_watchpoints`;
8159            // it reads back as 0.
8160            config.backend.hardware_watchpoints = 0;
8161            assert_eq!(
8162                detcore::to_legacy_backend_json(&config).unwrap(),
8163                before,
8164                "the legacy form of the {name} config changed"
8165            );
8166            // A reader of the earlier bytes recovers the capabilities.
8167            let decoded = detcore::from_legacy_backend_json(before).unwrap();
8168            assert_eq!(decoded.backend, config.backend, "{name}");
8169            assert_eq!(
8170                decoded.sequentialize_threads, config.sequentialize_threads,
8171                "{name}"
8172            );
8173            assert_eq!(
8174                detcore::to_legacy_backend_json(&decoded).unwrap(),
8175                before,
8176                "{name}"
8177            );
8178        }
8179    }
8180
8181    /// The fifteen per-backend keys, in the order the encoding has always
8182    /// carried them.
8183    const LEGACY_BACKEND_KEYS: [&str; 15] = [
8184        "cpuid_virtualized_by_backend",
8185        "backend_supports_madvise",
8186        "discover_live_file_metadata",
8187        "detect_host_clock_futex_timeouts",
8188        "syscall_clobbers_virtualized_by_backend",
8189        "cancel_killed_thread_rpcs",
8190        "backend_reports_physical_process_exits",
8191        "backend_tracks_process_children",
8192        "backend_runs_exit_robust_list",
8193        "backend_requires_thread_directed_process_signals",
8194        "backend_is_kvm",
8195        "kvm_shared_dequeue_timers",
8196        "backend_supports_parked_write_signal_interruption",
8197        "backend_virtualizes_capability_prctls",
8198        "backend_defers_vfork_child_registration",
8199    ];
8200
8201    /// Pin the current projection onto all fifteen legacy backend keys, once
8202    /// without and once with sequentialized threads. DBT's two descriptor
8203    /// facts project to `discover_live_file_metadata=true`; that legacy bit
8204    /// cannot retain their independence from the host-timed capabilities.
8205    /// Current DBT therefore uses the separately checked typed envelope, not
8206    /// this lossy projection. LiteInst's row is its in-guest runtime. KVM's
8207    /// first two digits are the capabilities `run_kvm` formerly set itself; the
8208    /// first, `cpuid_virtualized_by_backend`, is written false for every
8209    /// backend now that no backend installs a CPUID profile of its own, and so
8210    /// is the fourteenth, `backend_virtualizes_capability_prctls`, now that
8211    /// the capability prctls reach the kernel on every backend, and so is the
8212    /// fifth, `syscall_clobbers_virtualized_by_backend`, now that Reverie's
8213    /// backend contract requires every backend to leave the syscall-clobbered
8214    /// registers as a native return would (with the exceptions it names). The
8215    /// second, `backend_supports_madvise`, is written true for every backend
8216    /// now that every backend takes every madvise advice and refuses what it
8217    /// does not implement itself, and the fifteenth,
8218    /// `backend_defers_vfork_child_registration`, false for every backend now
8219    /// that Detcore waits for a vfork child however late it registers.
8220    #[test]
8221    fn every_backend_encodes_its_current_legacy_projection() {
8222        for (backend, unsequenced, sequential) in [
8223            (CapabilityCase::Ptrace, "010000011000100", "010000011000100"),
8224            (
8225                CapabilityCase::E9patch,
8226                "010000011000100",
8227                "010000011000100",
8228            ),
8229            (
8230                CapabilityCase::Liteinst,
8231                "010001010000000",
8232                "010001010000000",
8233            ),
8234            (
8235                CapabilityCase::InGuestTrap,
8236                "010001010000000",
8237                "010001010000000",
8238            ),
8239            (CapabilityCase::Sabre, "011101110000000", "011101110000000"),
8240            (CapabilityCase::Dbt, "011001000100000", "011001000100000"),
8241            (CapabilityCase::Kvm, "010001010010100", "010001010011100"),
8242        ] {
8243            for (sequentialize_threads, expected) in [(false, unsequenced), (true, sequential)] {
8244                let config = prepare_capability_case(
8245                    DetConfig {
8246                        sequentialize_threads,
8247                        ..DetConfig::default()
8248                    },
8249                    backend,
8250                );
8251                let encoded: serde_json::Map<String, serde_json::Value> =
8252                    serde_json::from_str(&detcore::to_legacy_backend_json(&config).unwrap())
8253                        .unwrap();
8254                let digits: String = LEGACY_BACKEND_KEYS
8255                    .iter()
8256                    .map(|key| match encoded.get(*key) {
8257                        Some(serde_json::Value::Bool(true)) => '1',
8258                        Some(serde_json::Value::Bool(false)) => '0',
8259                        other => panic!("{key} encoded as {other:?}"),
8260                    })
8261                    .collect();
8262                assert_eq!(
8263                    digits, expected,
8264                    "{backend:?}, sequentialize_threads={sequentialize_threads}"
8265                );
8266            }
8267        }
8268    }
8269
8270    #[test]
8271    fn dbt_backend_config_translates_process_signals_to_host_threads() {
8272        let config = prepare_capability_case(super::DetConfig::default(), CapabilityCase::Dbt);
8273        assert!(config.backend.needs_killed_thread_rpc_cancellation);
8274        assert!(!config.backend.tracks_process_children);
8275        assert!(!config.backend.runs_exit_robust_list);
8276        assert!(config.backend.requires_thread_directed_process_signals);
8277    }
8278
8279    #[test]
8280    fn backend_write_signal_contract_is_explicit() {
8281        for (backend, supports_interruption) in [
8282            (CapabilityCase::Ptrace, true),
8283            (CapabilityCase::Dbt, false),
8284            (CapabilityCase::Kvm, true),
8285            (CapabilityCase::Sabre, false),
8286            (CapabilityCase::Liteinst, false),
8287            (CapabilityCase::InGuestTrap, false),
8288            (CapabilityCase::E9patch, true),
8289        ] {
8290            let config = prepare_capability_case(super::DetConfig::default(), backend);
8291            assert_eq!(
8292                config.backend.supports_parked_write_signal_interruption, supports_interruption,
8293                "unexpected parked-write signal support for {backend:?}"
8294            );
8295        }
8296    }
8297
8298    /// Only ptrace and KVM deliver the preemption timer, so only they let a
8299    /// chaos `sched_yield` hand its turn to a thread that may never make a
8300    /// syscall (https://github.com/rrnewton/hermit/issues/4068).
8301    #[test]
8302    fn backend_timer_delivery_is_explicit() {
8303        for (backend, delivers) in [
8304            (CapabilityCase::Ptrace, true),
8305            (CapabilityCase::Dbt, false),
8306            (CapabilityCase::Kvm, true),
8307            (CapabilityCase::Sabre, false),
8308            (CapabilityCase::Liteinst, false),
8309            (CapabilityCase::InGuestTrap, false),
8310            (CapabilityCase::E9patch, true),
8311        ] {
8312            let config = prepare_capability_case(super::DetConfig::default(), backend);
8313            assert_eq!(
8314                config.backend.delivers_preemption_timer_events, delivers,
8315                "unexpected timer delivery for {backend:?}"
8316            );
8317        }
8318    }
8319
8320    /// Only SaBRe and in-guest LiteInst run guest code inside a Tool handler,
8321    /// between two injected calls.
8322    #[test]
8323    fn backend_guest_code_between_injections_is_explicit() {
8324        for (backend, runs) in [
8325            (CapabilityCase::Ptrace, false),
8326            (CapabilityCase::Dbt, false),
8327            (CapabilityCase::Kvm, false),
8328            (CapabilityCase::Sabre, true),
8329            (CapabilityCase::Liteinst, true),
8330            (CapabilityCase::InGuestTrap, true),
8331            (CapabilityCase::E9patch, false),
8332        ] {
8333            let config = prepare_capability_case(super::DetConfig::default(), backend);
8334            assert_eq!(
8335                config.backend.runs_guest_code_between_injections, runs,
8336                "unexpected guest code between injections for {backend:?}"
8337            );
8338        }
8339    }
8340
8341    #[test]
8342    fn backend_blocked_wait_signal_contract_is_explicit() {
8343        for (backend, supports_interruption) in [
8344            (CapabilityCase::Ptrace, true),
8345            (CapabilityCase::Dbt, false),
8346            (CapabilityCase::Kvm, false),
8347            (CapabilityCase::Sabre, false),
8348            (CapabilityCase::Liteinst, false),
8349            (CapabilityCase::InGuestTrap, false),
8350            (CapabilityCase::E9patch, true),
8351        ] {
8352            let config = prepare_capability_case(super::DetConfig::default(), backend);
8353            assert_eq!(
8354                config.backend.supports_blocked_wait_signal_interruption, supports_interruption,
8355                "unexpected blocked-wait signal support for {backend:?}"
8356            );
8357        }
8358    }
8359
8360    /// A launch may pass the guest a terminal when a standard descriptor is
8361    /// one, when the launcher has a controlling terminal, or when that cannot
8362    /// be read; only a launch with neither is known not to. The device number
8363    /// is read past a command name that holds spaces and parentheses.
8364    #[test]
8365    fn a_launch_may_pass_a_terminal_unless_it_proves_it_has_none() {
8366        assert!(!super::may_pass_a_terminal(false, Some(0)));
8367        assert!(super::may_pass_a_terminal(true, Some(0)));
8368        assert!(super::may_pass_a_terminal(false, Some(34817)));
8369        assert!(super::may_pass_a_terminal(false, None));
8370
8371        let stat = "4242 (a) b (c)) S 1 4242 4242 34817 4242 4194560 0 0 0 0";
8372        assert_eq!(super::parse_controlling_terminal_device(stat), Some(34817));
8373        let stat = "4242 (hermit) S 1 4242 4242 0 -1 4194560 0 0 0 0";
8374        assert_eq!(super::parse_controlling_terminal_device(stat), Some(0));
8375        assert_eq!(
8376            super::parse_controlling_terminal_device("4242 (hermit"),
8377            None
8378        );
8379        assert_eq!(
8380            super::parse_controlling_terminal_device("4242 (hermit) S 1"),
8381            None
8382        );
8383        assert!(super::controlling_terminal_device().is_some());
8384    }
8385
8386    /// Only a backend whose blocked waits decide interruption from the kernel's
8387    /// signal state asks whether the launch may pass the guest a terminal.
8388    #[test]
8389    fn only_gated_backends_record_an_inherited_terminal() {
8390        for backend in [
8391            CapabilityCase::Dbt,
8392            CapabilityCase::Kvm,
8393            CapabilityCase::Sabre,
8394            CapabilityCase::Liteinst,
8395            CapabilityCase::InGuestTrap,
8396        ] {
8397            let config = prepare_capability_case(super::DetConfig::default(), backend);
8398            assert!(
8399                !config.guest_may_inherit_a_terminal,
8400                "unexpected terminal record for {backend:?}"
8401            );
8402        }
8403        for backend in [CapabilityCase::Ptrace, CapabilityCase::E9patch] {
8404            let config = prepare_capability_case(super::DetConfig::default(), backend);
8405            assert_eq!(
8406                config.guest_may_inherit_a_terminal,
8407                super::launcher_may_pass_a_terminal()
8408            );
8409        }
8410    }
8411
8412    #[test]
8413    fn liteinst_public_dispatch_runs_in_guest_detcore() {
8414        if Backend::Liteinst.ensure_available().is_err() {
8415            return;
8416        }
8417
8418        // In-guest LiteInst has no preemption timer and refuses a maximum
8419        // timeslice, so this run disables preemption; preemption is not what
8420        // this test checks.
8421        let config = super::DetConfig {
8422            max_timeslice: None,
8423            ..super::DetConfig::default()
8424        };
8425        let mut command = super::Command::new("/bin/echo");
8426        command.arg("hello");
8427        let output = super::run_with_output_backend(
8428            command,
8429            config.clone(),
8430            false,
8431            &None,
8432            Backend::Liteinst,
8433        )
8434        .expect("run /bin/echo through in-guest LiteInst");
8435        // Include captured output so a failed backend launch reports its diagnostics.
8436        assert_eq!(
8437            output.status,
8438            super::ExitStatus::Exited(0),
8439            "guest did not exit 0.\n--- guest stderr ---\n{}\n--- guest stdout ---\n{}",
8440            String::from_utf8_lossy(&output.stderr),
8441            String::from_utf8_lossy(&output.stdout),
8442        );
8443        assert_eq!(output.stdout, b"hello\n");
8444
8445        let status = super::run_with_backend(
8446            super::Command::new("/bin/true"),
8447            config,
8448            false,
8449            &None,
8450            Backend::Liteinst,
8451        )
8452        .expect("run /bin/true through in-guest LiteInst");
8453        assert_eq!(status, super::ExitStatus::Exited(0));
8454    }
8455
8456    #[test]
8457    fn default_and_available_backends_reflect_host_probes() {
8458        assert_eq!(Backend::default(), Backend::Ptrace);
8459        let available = Backend::available().collect::<Vec<_>>();
8460        assert_eq!(
8461            available.contains(&Backend::Ptrace),
8462            Backend::Ptrace.is_available()
8463        );
8464        assert_eq!(
8465            available.contains(&Backend::Liteinst),
8466            liteinst_unavailable_reason().is_none()
8467        );
8468        // The same runtime, so available exactly when liteinst is.
8469        assert_eq!(
8470            available.contains(&Backend::InGuestTrap),
8471            available.contains(&Backend::Liteinst)
8472        );
8473        assert_eq!(
8474            available.contains(&Backend::Kvm),
8475            kvm_device_unavailable_reason(std::path::Path::new("/dev/kvm")).is_none(),
8476        );
8477    }
8478
8479    #[test]
8480    fn dependency_probes_require_usable_paths() {
8481        let temp = tempfile::tempdir().unwrap();
8482        let reason = kvm_device_unavailable_reason(temp.path())
8483            .expect("a directory must not pass the read-write KVM device probe");
8484        assert!(reason.contains("read-write"));
8485    }
8486
8487    #[test]
8488    fn optional_backends_report_accurate_availability() {
8489        assert_eq!(
8490            Backend::Liteinst.ensure_available().is_ok(),
8491            liteinst_unavailable_reason().is_none()
8492        );
8493        assert_eq!(
8494            Backend::InGuestTrap.ensure_available().is_ok(),
8495            liteinst_unavailable_reason().is_none()
8496        );
8497
8498        match Backend::Kvm.ensure_available() {
8499            Ok(()) => assert!(
8500                kvm_device_unavailable_reason(std::path::Path::new("/dev/kvm")).is_none(),
8501                "KVM reported available without a usable /dev/kvm",
8502            ),
8503            Err(kvm_error) => {
8504                let message = kvm_error.to_string();
8505                assert!(
8506                    message.contains("/dev/kvm"),
8507                    "unexpected KVM availability error: {message}",
8508                );
8509                assert!(!message.contains("requires root privileges"));
8510            }
8511        }
8512    }
8513
8514    #[test]
8515    fn kvm_runs_dynamic_echo_through_detcore() {
8516        // The KVM backend runs its session on the calling thread (see
8517        // `run`), and a debug session overflows the test thread's default
8518        // 2 MiB stack under plain `cargo test`, so run the body on a
8519        // thread with an explicit stack.
8520        std::thread::Builder::new()
8521            .name("kvm_runs_dynamic_echo_through_detcore".to_owned())
8522            .stack_size(16 << 20)
8523            .spawn(kvm_runs_dynamic_echo_through_detcore_body)
8524            .unwrap()
8525            .join()
8526            .unwrap();
8527    }
8528
8529    fn kvm_runs_dynamic_echo_through_detcore_body() {
8530        use clap::Parser;
8531
8532        if kvm_device_unavailable_reason(std::path::Path::new("/dev/kvm")).is_some() {
8533            return;
8534        }
8535
8536        let mut command = super::Command::new("/bin/echo");
8537        command.arg("hello");
8538        let mut config = super::DetConfig::parse_from(["hermit-kvm-test"]);
8539        config.validate();
8540        let output = super::run_with_output_backend(command, config, false, &None, Backend::Kvm)
8541            .expect("run dynamic /bin/echo through KvmGuest<Detcore>");
8542
8543        // Include captured output so a failed backend launch reports its diagnostics.
8544        assert_eq!(
8545            output.status,
8546            super::ExitStatus::Exited(0),
8547            "guest did not exit 0.\n--- guest stderr ---\n{}\n--- guest stdout ---\n{}",
8548            String::from_utf8_lossy(&output.stderr),
8549            String::from_utf8_lossy(&output.stdout),
8550        );
8551        assert_eq!(output.stdout, b"hello\n");
8552        assert!(output.stderr.is_empty());
8553    }
8554
8555    // Keep the low-level vmcall transport covered independently from the ELF
8556    // process personality. Requires /dev/kvm; skips cleanly otherwise.
8557    #[test]
8558    fn detcore_drives_kvm_guest_for_synthetic_syscall() {
8559        use clap::Parser;
8560
8561        const MEMORY_SIZE: usize = 0x10_000;
8562        const ENTRY_POINT: u64 = 0x1000;
8563        const FRAME_ADDRESS: u64 = 0x2000;
8564
8565        let mut backend = match reverie_kvm::KvmBackend::new(MEMORY_SIZE) {
8566            Ok(backend) => backend,
8567            Err(error) => {
8568                eprintln!("skipping KVM Detcore experiment: cannot init VM: {error}");
8569                return;
8570            }
8571        };
8572
8573        // A guest that issues one `getpid` through the vmcall transport, then HLTs.
8574        backend
8575            .install_syscall(
8576                ENTRY_POINT,
8577                FRAME_ADDRESS,
8578                reverie_kvm::SyscallRequest::new(libc::SYS_getpid as u64, [0; 6]),
8579            )
8580            .expect("install synthetic getpid guest");
8581
8582        // Minimal deterministic Detcore config with RCB preemption disabled.
8583        let mut config =
8584            super::DetConfig::parse_from(["hermit-kvm-test", "--max-timeslice=disabled"]);
8585        config.validate();
8586
8587        let runtime = tokio::runtime::Builder::new_current_thread()
8588            .enable_all()
8589            .build()
8590            .expect("build tokio runtime");
8591
8592        let outcome = runtime.block_on(async {
8593            backend
8594                .run_with_tool::<super::Detcore, _>(
8595                    config,
8596                    // Executor: forward anything Detcore injects to the host.
8597                    |request: &reverie_kvm::SyscallRequest, _memory: &reverie_kvm::GuestMemory| {
8598                        // SAFETY: forwarding a register-only syscall (getpid) to the
8599                        // host; no guest pointers are dereferenced by the kernel.
8600                        unsafe {
8601                            libc::syscall(
8602                                request.number() as libc::c_long,
8603                                request.args()[0],
8604                                request.args()[1],
8605                                request.args()[2],
8606                                request.args()[3],
8607                                request.args()[4],
8608                                request.args()[5],
8609                            ) as i64
8610                        }
8611                    },
8612                )
8613                .await
8614        });
8615
8616        // The point of the experiment is to observe whether Detcore can be driven
8617        // to completion over KvmGuest at all; assert it did not error.
8618        outcome.expect("Detcore drove the synthetic KVM guest to completion");
8619    }
8620
8621    // Minimal fake ELF payload: the loader only needs the image to NOT start
8622    // with `#!`, and a real ELF magic makes the intent obvious.
8623    const FAKE_ELF: &[u8] = b"\x7fELF\x02\x01\x01\x00 fake elf body";
8624
8625    /// The whole contents of the open file `resolve_kvm_shebang` returned, read
8626    /// from its start.
8627    fn image_of(mut file: fs::File) -> Vec<u8> {
8628        std::io::Seek::seek(&mut file, std::io::SeekFrom::Start(0)).unwrap();
8629        let mut image = Vec::new();
8630        std::io::Read::read_to_end(&mut file, &mut image).unwrap();
8631        image
8632    }
8633
8634    fn shebang_tmpdir(tag: &str) -> PathBuf {
8635        let dir = std::env::temp_dir().join(format!(
8636            "hermit-shebang-test-{}-{}",
8637            std::process::id(),
8638            tag
8639        ));
8640        let _ = fs::remove_dir_all(&dir);
8641        fs::create_dir_all(&dir).unwrap();
8642        dir
8643    }
8644
8645    #[test]
8646    fn resolve_shebang_plain_elf_is_unchanged() {
8647        let dir = shebang_tmpdir("plain");
8648        let prog = dir.join("prog");
8649        fs::write(&prog, FAKE_ELF).unwrap();
8650
8651        let argv = vec!["prog".to_owned(), "-a".to_owned()];
8652        let (path, out_argv, file) = resolve_kvm_shebang(&prog, argv).unwrap();
8653        assert_eq!(path, prog);
8654        assert_eq!(out_argv, vec!["prog".to_owned(), "-a".to_owned()]);
8655        assert_eq!(image_of(file), FAKE_ELF);
8656        fs::remove_dir_all(&dir).unwrap();
8657    }
8658
8659    #[test]
8660    fn resolve_shebang_single_level_kernel_order() {
8661        let dir = shebang_tmpdir("single");
8662        let interp = dir.join("fakebash");
8663        fs::write(&interp, FAKE_ELF).unwrap();
8664        let script = dir.join("script");
8665        // Interpreter with a single optional argument.
8666        fs::write(&script, format!("#!{} -x\necho hi\n", interp.display())).unwrap();
8667
8668        let argv = vec!["script".to_owned(), "arg1".to_owned()];
8669        let (path, out_argv, file) = resolve_kvm_shebang(&script, argv).unwrap();
8670        assert_eq!(path, interp);
8671        // Kernel order: [interp, optarg, script_path, original args after argv[0]].
8672        assert_eq!(
8673            out_argv,
8674            vec![
8675                interp.to_string_lossy().into_owned(),
8676                "-x".to_owned(),
8677                script.to_string_lossy().into_owned(),
8678                "arg1".to_owned(),
8679            ]
8680        );
8681        assert_eq!(image_of(file), FAKE_ELF);
8682        fs::remove_dir_all(&dir).unwrap();
8683    }
8684
8685    #[test]
8686    fn resolve_shebang_nested_accumulates_like_binfmt_script() {
8687        let dir = shebang_tmpdir("nested");
8688        let interp = dir.join("fakebash");
8689        fs::write(&interp, FAKE_ELF).unwrap();
8690        let mid = dir.join("mid"); // a #!-interpreter that is itself a script
8691        fs::write(&mid, format!("#!{}\n", interp.display())).unwrap();
8692        let script = dir.join("script");
8693        fs::write(&script, format!("#!{} -e\n", mid.display())).unwrap();
8694
8695        let argv = vec!["script".to_owned(), "arg1".to_owned()];
8696        let (path, out_argv, file) = resolve_kvm_shebang(&script, argv).unwrap();
8697        assert_eq!(path, interp);
8698        // Level 1: [mid, -e, script, arg1]; level 2 prepends [interp, mid].
8699        assert_eq!(
8700            out_argv,
8701            vec![
8702                interp.to_string_lossy().into_owned(),
8703                mid.to_string_lossy().into_owned(),
8704                "-e".to_owned(),
8705                script.to_string_lossy().into_owned(),
8706                "arg1".to_owned(),
8707            ]
8708        );
8709        assert_eq!(image_of(file), FAKE_ELF);
8710        fs::remove_dir_all(&dir).unwrap();
8711    }
8712
8713    #[test]
8714    fn resolve_shebang_rejects_infinite_recursion() {
8715        let dir = shebang_tmpdir("loop");
8716        let a = dir.join("a");
8717        let b = dir.join("b");
8718        fs::write(&a, format!("#!{}\n", b.display())).unwrap();
8719        fs::write(&b, format!("#!{}\n", a.display())).unwrap();
8720
8721        let argv = vec!["a".to_owned()];
8722        assert!(resolve_kvm_shebang(&a, argv).is_err());
8723        fs::remove_dir_all(&dir).unwrap();
8724    }
8725
8726    /// The build scripts' pin reader, bracketed against the inputs that
8727    /// first-match-wins got wrong. Included rather than imported because the
8728    /// reader is build-script code with no library home.
8729    mod reverie_pin_reader {
8730        include!("../reverie_pin.rs");
8731
8732        const A: &str = "1111111111111111111111111111111111111111";
8733        const B: &str = "2222222222222222222222222222222222222222";
8734
8735        fn dep(rev: &str) -> String {
8736            format!(
8737                "reverie = {{ git = \"https://github.com/rrnewton/reverie.git\", rev = \"{rev}\" }}\n"
8738            )
8739        }
8740
8741        #[test]
8742        fn a_single_revision_is_read() {
8743            assert_eq!(parse_reverie_pin(&dep(A)).as_deref(), Some(A));
8744        }
8745
8746        #[test]
8747        fn agreeing_revisions_are_read_once() {
8748            let text = format!("{}{}", dep(A), dep(A));
8749            assert_eq!(parse_reverie_pin(&text).as_deref(), Some(A));
8750        }
8751
8752        /// A manifest halfway through a bump names two revisions. First-match-wins
8753        /// silently returned the first; the canonical rule refuses to resolve it.
8754        #[test]
8755        fn disagreeing_revisions_are_refused() {
8756            let text = format!("{}{}", dep(A), dep(B));
8757            assert_eq!(parse_reverie_pin(&text), None);
8758        }
8759
8760        /// The reported reproduction: ONE commented line above the live pin made
8761        /// the binary embed the comment's revision, so a correctly staged runtime
8762        /// would be refused by a guard naming a revision that was never the pin.
8763        #[test]
8764        fn a_commented_out_dependency_is_not_the_pin() {
8765            let text = format!("# {}{}", dep(B), dep(A));
8766            assert_eq!(
8767                parse_reverie_pin(&text).as_deref(),
8768                Some(A),
8769                "a commented-out revision must not win"
8770            );
8771        }
8772
8773        #[test]
8774        fn a_manifest_naming_no_revision_yields_none() {
8775            assert_eq!(parse_reverie_pin("[dependencies]\nlibc = \"0.2\"\n"), None);
8776        }
8777
8778        /// An unparseable Reverie line could be hiding a disagreement, so it makes
8779        /// the manifest ambiguous rather than being skipped as absent.
8780        #[test]
8781        fn an_unparseable_revision_is_ambiguous() {
8782            let text = format!(
8783                "{}reverie-x = {{ git = \"https://github.com/rrnewton/reverie.git\", rev = \"beef\" }}\n",
8784                dep(A)
8785            );
8786            assert_eq!(parse_reverie_pin(&text), None);
8787        }
8788    }
8789
8790    /// SaBRe's guest plugin has no subscriber, so it forwards DETLOG records by
8791    /// the policy the CLI hands it. For each filter the CLI builds, that policy
8792    /// must answer every emitting module as the CLI's own subscriber does,
8793    /// which is what each in-process `detlog!` callsite asks under ptrace.
8794    /// `warn,detcore::random=info` once yielded no forwarding at all, because
8795    /// only the generic `detcore` target was asked, so SaBRe dropped the
8796    /// AT_RANDOM and early getrandom records ptrace prints.
8797    #[test]
8798    fn sabre_detlog_forwarding_answers_each_target_as_the_cli_filter_does() {
8799        use detcore::detlog::ForwardPolicy;
8800        use detcore::detlog::info_enabled_for_target;
8801        use tracing::level_filters::LevelFilter;
8802        use tracing_subscriber::layer::SubscriberExt;
8803
8804        use crate::liteinst_bootstrap::EffectiveFilter;
8805
8806        let targets = [
8807            "",
8808            "detcore",
8809            "detcore::random",
8810            "detcore::randomness",
8811            "detcore::tool_local",
8812            "detcore::scheduler",
8813            "detcore::scheduler::runqueue",
8814            "detcore::syscalls::files",
8815            "hermit",
8816            "warn",
8817        ];
8818        let policy_under = |raw: &str, level: LevelFilter| {
8819            let subscriber = tracing_subscriber::registry()
8820                .with(EffectiveFilter::from_directives_lossy(raw, level).into_filter());
8821            tracing::subscriber::with_default(subscriber, || {
8822                let policy = ForwardPolicy::from_current_subscriber(raw);
8823                let subscriber_says: Vec<bool> = targets
8824                    .iter()
8825                    .map(|target| info_enabled_for_target(target))
8826                    .collect();
8827                (policy, subscriber_says)
8828            })
8829        };
8830        for (raw, level) in [
8831            ("warn,detcore::random=info", LevelFilter::WARN),
8832            ("detcore::random=info", LevelFilter::WARN),
8833            ("detcore::random=warn", LevelFilter::INFO),
8834            (
8835                "warn,detcore=info,detcore::scheduler=off",
8836                LevelFilter::WARN,
8837            ),
8838            (
8839                "warn,detcore[work]=info,detcore::random=info",
8840                LevelFilter::WARN,
8841            ),
8842            ("", LevelFilter::INFO),
8843            ("", LevelFilter::WARN),
8844        ] {
8845            let (policy, subscriber_says) = policy_under(raw, level);
8846            for (target, enabled) in targets.iter().zip(subscriber_says) {
8847                assert_eq!(
8848                    policy.forwards(target),
8849                    enabled,
8850                    "RUST_LOG={raw:?} --log={level}: policy {policy:?} disagrees on {target:?}"
8851                );
8852            }
8853        }
8854
8855        let (scoped, subscriber_says) =
8856            policy_under("warn,detcore::random=info", LevelFilter::WARN);
8857        assert!(!subscriber_says[1], "the generic detcore target is off");
8858        assert!(scoped.forwards_any());
8859        assert!(scoped.forwards("detcore::random"));
8860        assert!(!scoped.forwards("detcore::tool_local"));
8861        assert_eq!(policy_under("", LevelFilter::INFO).0, ForwardPolicy::all());
8862        assert!(!policy_under("", LevelFilter::WARN).0.forwards_any());
8863    }
8864
8865    /// In-guest LiteInst gets the per-target policy it is given next to the
8866    /// forwarding descriptor, and this build's fingerprint, in the launch
8867    /// record, with none of Hermit's variables left on the command; a policy
8868    /// that forwards nothing requests no forwarding.
8869    #[cfg(feature = "liteinst")]
8870    #[test]
8871    fn liteinst_launch_hands_the_guest_its_per_target_policy() {
8872        use std::os::fd::AsRawFd;
8873
8874        use detcore::detlog::ForwardPolicy;
8875        use detcore::in_guest_launch::InGuestLaunch;
8876        use reverie::process::Command;
8877
8878        use super::LITEINST_DETLOG_FORWARD_ENV;
8879        use super::LITEINST_DETLOG_FORWARD_POLICY_ENV;
8880        use super::forward_in_guest_detlogs_to;
8881        use super::in_guest_liteinst_launch;
8882
8883        let (sink, _peer) = std::os::unix::net::UnixDatagram::pair().unwrap();
8884        forward_in_guest_detlogs_to(sink.into());
8885        let removed = |command: &Command| {
8886            [
8887                LITEINST_DETLOG_FORWARD_ENV,
8888                LITEINST_DETLOG_FORWARD_POLICY_ENV,
8889                detcore::LITEINST_CONFIG_FINGERPRINT_ENV,
8890            ]
8891            .into_iter()
8892            .all(|key| {
8893                command
8894                    .get_envs()
8895                    .any(|(name, value)| name == std::ffi::OsStr::new(key) && value.is_none())
8896            })
8897        };
8898
8899        let scoped = ForwardPolicy::decode("0,detcore::random=1").unwrap();
8900        let mut command = Command::new("/bin/true");
8901        let (launch, duplicate) = in_guest_liteinst_launch(&mut command, &scoped).unwrap();
8902        let duplicate = duplicate.expect("a policy that forwards a target requests forwarding");
8903        assert!(removed(&command));
8904        let launch = InGuestLaunch::decode(&launch.encode()).unwrap();
8905        let passed = launch.forward_request.expect("the descriptor is passed");
8906        assert_eq!(
8907            detcore::detlog::tool_output_env_target(&passed),
8908            Some(duplicate.as_raw_fd()),
8909            "{passed}"
8910        );
8911        let sent =
8912            ForwardPolicy::decode(&launch.forward_policy.expect("the policy goes too")).unwrap();
8913        assert_eq!(sent, scoped);
8914        assert!(sent.forwards("detcore::random"));
8915        assert!(!sent.forwards("detcore::tool_local"));
8916        assert_eq!(
8917            launch.coordinator_fingerprint,
8918            Some(detcore::config_wire_fingerprint())
8919        );
8920
8921        let nothing = ForwardPolicy::decode("0").unwrap();
8922        let mut command = Command::new("/bin/true");
8923        command
8924            .env(LITEINST_DETLOG_FORWARD_ENV, "9")
8925            .env(LITEINST_DETLOG_FORWARD_POLICY_ENV, "1")
8926            .env(detcore::LITEINST_CONFIG_FINGERPRINT_ENV, "0000000000000000");
8927        let (launch, duplicate) = in_guest_liteinst_launch(&mut command, &nothing).unwrap();
8928        assert!(duplicate.is_none());
8929        assert!(removed(&command));
8930        assert_eq!(launch.forward_request, None);
8931        assert_eq!(launch.forward_policy, None);
8932    }
8933
8934    /// The forwarding policy answers as a real in-process `detlog!` record
8935    /// behaves under the CLI's filter: a record is logged only when both its
8936    /// `tracing::enabled!` hint and its event pass, and a directive with a
8937    /// field filter applies to the hint whatever its fields but to the event
8938    /// only when the event has that field. `info,<module>[{unused}]=warn`
8939    /// suppresses the record; the policy once forwarded it, because it probed
8940    /// only the event.
8941    #[test]
8942    fn detlog_forward_policy_matches_real_detlog_emission_under_field_filters() {
8943        use std::sync::Arc;
8944        use std::sync::atomic::AtomicUsize;
8945        use std::sync::atomic::Ordering;
8946
8947        use detcore::detlog::ForwardPolicy;
8948        use tracing::level_filters::LevelFilter;
8949        use tracing_subscriber::layer::SubscriberExt;
8950
8951        use crate::liteinst_bootstrap::EffectiveFilter;
8952
8953        struct Count(Arc<AtomicUsize>);
8954        impl<S: tracing::Subscriber> tracing_subscriber::Layer<S> for Count {
8955            fn on_event(
8956                &self,
8957                _event: &tracing::Event<'_>,
8958                _context: tracing_subscriber::layer::Context<'_, S>,
8959            ) {
8960                self.0.fetch_add(1, Ordering::Relaxed);
8961            }
8962        }
8963
8964        let module = module_path!();
8965        for (raw, level) in [
8966            (format!("info,{module}[{{unused}}]=warn"), LevelFilter::INFO),
8967            (
8968                format!("warn,{module}[{{unused}}]=trace"),
8969                LevelFilter::WARN,
8970            ),
8971            (
8972                format!("info,{module}[{{message}}]=warn"),
8973                LevelFilter::INFO,
8974            ),
8975            (format!("info,{module}=warn"), LevelFilter::INFO),
8976            (format!("warn,{module}=info"), LevelFilter::WARN),
8977            ("info".to_owned(), LevelFilter::INFO),
8978            ("warn".to_owned(), LevelFilter::WARN),
8979        ] {
8980            let emitted = Arc::new(AtomicUsize::new(0));
8981            let subscriber = tracing_subscriber::registry()
8982                .with(EffectiveFilter::from_directives_lossy(&raw, level).into_filter())
8983                .with(Count(emitted.clone()));
8984            let forwards = tracing::subscriber::with_default(subscriber, || {
8985                detcore::detlog!("probe record");
8986                ForwardPolicy::from_current_subscriber(&raw).forwards(module)
8987            });
8988            assert_eq!(
8989                forwards,
8990                emitted.load(Ordering::Relaxed) == 1,
8991                "RUST_LOG={raw:?} --log={level}: the policy and a real detlog! disagree"
8992            );
8993        }
8994    }
8995}
8996
8997/// Producer-owned record and raw streams for one retained verification run.
8998pub mod verify_run_record;