detcore_model/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//! Widely-shared type definitions.
10
11pub mod backend_engagement;
12pub mod build_info;
13pub mod host_capability;
14
15/// Exit status for a run HERMIT DELIBERATELY REFUSED, as distinct from one
16/// where hermit itself broke.
17///
18/// ⚠️ "REFUSED" AND "BROKE" DEMAND OPPOSITE RESPONSES, AND THEY WERE THE SAME
19/// NUMBER. A fail-closed policy stopping a run is hermit working correctly: the
20/// operator must read the refusal and change their program or their flags.
21/// `HERMIT_INTERNAL_FAILURE_EXIT` (125) says hermit is broken and the operator
22/// must file a bug. Reporting the first as the second sends a reader looking
23/// for a defect in a shutdown path that behaved exactly as designed.
24///
25/// ⚠️ WHY IT IS NOT 1, WHICH IS WHAT THIS PATH USED TO EXIT.
26/// `unrecoverable_shutdown` called `std::process::exit(1)`, so the container
27/// child exited 1 and the parent's classifier — whose arm is documented "the
28/// child died with a status it did not pick" — turned a status hermit HAD
29/// picked into `class=container-child-exit` and 125. Restoring 1 would fix the
30/// misclassification and reintroduce a worse one: 1 is the commonest guest exit
31/// status, so it cannot distinguish "hermit refused" from "your program
32/// returned 1".
33///
34/// ⚠️ WHY 122. It sits immediately below the reserved band (123 safehermit log
35/// cap, 124 deadline, 125 hermit broke, 126/127 GNU exec-level), keeping the
36/// reserved codes contiguous, and it is the cheapest possible narrowing of the
37/// guest range. See the full allocation above the constants in
38/// `hermit-cli/src/lib.rs`.
39///
40/// ⚠️ IT LIVES IN `detcore-model` BECAUSE BOTH SIDES NEED IT. `detcore` emits it
41/// and `hermit-cli` recognises it; `detcore-model` is the only crate both
42/// depend on. A copy on each side is exactly the defect that left eight cli
43/// tests asserting a stale exit status for a day after the product moved.
44pub const HERMIT_POLICY_REFUSAL_EXIT: i32 = 122;
45
46// ⚠️ THE VALUE IS PINNED, NOT ONLY NAMED. `tests/cli.rs` and the allocation
47// table both assert 122; a one-character edit here would move every consumer
48// with it and nothing would fail. 0 is called out separately because it is the
49// dangerous drift: at 0 a refusal would report SUCCESS.
50const _: () = assert!(
51 HERMIT_POLICY_REFUSAL_EXIT == 122,
52 "122 keeps the reserved band contiguous below 123/124/125/126/127"
53);
54const _: () = assert!(
55 HERMIT_POLICY_REFUSAL_EXIT != 0,
56 "a refusal exiting 0 would report success"
57);
58
59/// The shell's base for "killed by signal N": a process killed by signal `N` is
60/// conventionally reported as `128 + N`.
61pub const SIGNAL_EXIT_BASE: i32 = 128;
62
63/// The status a signal-terminated run reports, `128 + signo`.
64///
65/// ⚠️ A SIGNAL DEATH IS NOT A POLICY REFUSAL, AND FOR ONE RELEASE IT WAS SPELLED
66/// AS ONE. `sigint_instakill` is one of four `unrecoverable_shutdown` callers.
67/// The other three are fail-closed policy decisions where
68/// `HERMIT_POLICY_REFUSAL_EXIT` is right: hermit examined the run and refused it.
69/// This one is an OPERATOR INTERRUPT — hermit refused nothing, somebody pressed
70/// Ctrl-C — so reporting it as a refusal tells the reader hermit made a decision
71/// it did not make. hermit#2659 moved all four off `exit(1)` together, which was
72/// an improvement for all four (they had been reported as `125`, "hermit broke")
73/// and correct for only three.
74///
75/// ⚠️ IT ALSO PUT TWO MEANINGS ON 122, WHICH IS THE DEFECT THIS REMOVES. With the
76/// SIGINT path exiting `HERMIT_POLICY_REFUSAL_EXIT`, 122 meant both "a fail-closed
77/// policy stopped the run" and "the operator killed it" — a legible number with
78/// two conditions behind it, which is the exact family `125` was split up to end.
79///
80/// ⚠️ WHY THIS IS NOT A GENUINE `WIFSIGNALED` STATUS. The faithful way to honour a
81/// signal is restore `SIG_DFL`, unblock, re-raise — and it is measurably
82/// unavailable here for the same reason it is unavailable to
83/// `on_container_init_stop_signal`: a self-sent signal does not come from an
84/// ancestor namespace, so a namespace init discards it exactly like the original
85/// and survives. `128 + signo` is the closest honest approximation, and it is the
86/// spelling that function already uses — this shares its convention rather than
87/// inventing a second one.
88pub const fn signal_exit_status(signo: i32) -> i32 {
89 SIGNAL_EXIT_BASE + signo
90}
91
92/// `128 + SIGINT`. Spelled from the shared helper so it cannot drift from the
93/// band, and pinned below so it cannot drift from 130.
94pub const HERMIT_SIGINT_DEATH_EXIT: i32 = signal_exit_status(2);
95
96/// The highest signal number this recognises, `SIGRTMAX` on Linux.
97const MAX_SIGNO: i32 = 64;
98
99/// Recover the signal from a `128 + signo` status, or `None` if it is not in the
100/// band.
101///
102/// ⚠️ THE PARENT NEEDS THIS BECAUSE THE CHILD CANNOT SEND A REAL SIGNAL STATUS.
103/// Without it the classifier's catch-all treats every non-refusal child exit as
104/// unaccounted and reports `125`, so making the SIGINT path exit `130` on its own
105/// would have moved Ctrl-C from "hermit refused" to "hermit broke" — worse than
106/// what it replaced. Emitting the code and recognising the code are one change.
107///
108/// ⚠️ IT IS DELIBERATELY A BAND AND NOT A SINGLE VALUE. `sigint_instakill` is not
109/// the only producer: `on_container_init_stop_signal` already exits
110/// `128 + signo` for SIGTERM, SIGINT and SIGHUP (143, 130, 129), so keying on 130
111/// alone would leave the other two misreported as internal failures.
112///
113/// The upper bound is `SIGRTMAX`; above it, a status is a guest's own number and
114/// not a signal this can honestly name.
115pub const fn signal_from_exit_status(status: i32) -> Option<i32> {
116 if status > SIGNAL_EXIT_BASE && status <= SIGNAL_EXIT_BASE + MAX_SIGNO {
117 Some(status - SIGNAL_EXIT_BASE)
118 } else {
119 None
120 }
121}
122
123// ⚠️ THE ROUND TRIP IS PINNED, INCLUDING THE EDGES. 128 is excluded (there is no
124// signal 0) and the refusal code must not be readable as a signal, or the two
125// classifier arms would both match and the order would silently decide meaning.
126const _: () = assert!(matches!(signal_from_exit_status(130), Some(2)));
127const _: () = assert!(matches!(signal_from_exit_status(143), Some(15)));
128const _: () = assert!(signal_from_exit_status(SIGNAL_EXIT_BASE).is_none());
129// ⚠️ THE UPPER EDGE, WHICH WAS UNPINNED WHILE THIS COMMENT CLAIMED IT WAS NOT.
130// agent(hermit-005)'s codex lane proved it vacuous: `MAX_SIGNO` 64 -> 15 left
131// every test green, so nothing held the top of the band. 192 is `128 + SIGRTMAX`
132// and is the last status that IS a signal; 193 is the first that is not and
133// belongs to the guest. A band needs both edges or it has one.
134const _: () = assert!(
135 matches!(
136 signal_from_exit_status(SIGNAL_EXIT_BASE + MAX_SIGNO),
137 Some(MAX_SIGNO)
138 ),
139 "128 + SIGRTMAX is the last status in the band"
140);
141const _: () = assert!(
142 signal_from_exit_status(SIGNAL_EXIT_BASE + MAX_SIGNO + 1).is_none(),
143 "above SIGRTMAX a status is the guest's own number, not a signal"
144);
145const _: () = assert!(
146 signal_from_exit_status(HERMIT_POLICY_REFUSAL_EXIT).is_none(),
147 "a policy refusal must not also parse as a signal death"
148);
149
150// ⚠️ PINNED AGAINST BOTH FAILURE DIRECTIONS. The first pin catches the band
151// moving; the second catches the far worse edit, because the whole point of the
152// change is that these two values are DIFFERENT. If a refactor ever collapsed
153// them the build fails here rather than silently restoring the ambiguity.
154const _: () = assert!(
155 HERMIT_SIGINT_DEATH_EXIT == 130,
156 "128 + SIGINT(2); the shell convention every other tool reports"
157);
158const _: () = assert!(
159 HERMIT_SIGINT_DEATH_EXIT != HERMIT_POLICY_REFUSAL_EXIT,
160 "a signal death and a policy refusal must not share a code -- that collision is the defect"
161);
162
163pub mod collections;
164pub mod config;
165pub mod fd;
166pub mod futex;
167pub mod happens_before;
168pub mod network_trace;
169pub mod pedigree;
170pub mod pid;
171pub mod procfs;
172pub mod schedule;
173pub mod summary;
174pub mod time;