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