Skip to main content

detcore/
refused_signal_controls.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//! Guest signal controls an in-guest runtime refused before Detcore saw the
10//! call, reported once per run.
11//!
12//! On the in-guest backends the runtime answers a guest's handler-installing
13//! `rt_sigaction`, and an alternate signal stack, with `EPERM` itself
14//! (reverie-inguest `protect_runtime_control`): a guest handler would run
15//! outside the Tool's view. Until A5 step 3c makes guest handlers supported,
16//! each refusal is announced once per run, by name, so that a run whose guest
17//! carries on after the `EPERM` (many do, Rust's standard library among them)
18//! is not silently different from Linux.
19//!
20//! The runtime records each refusal; the runtime crate that hosts Detcore
21//! installs the function that takes them ([`install`]); Detcore takes them at
22//! each syscall entry (`report`) and tells the coordinator, which prints one
23//! WARN per refused signal and one for the alternate stack, per run. No other
24//! backend installs a source, so nothing is reported there.
25//!
26//! A refusal is reported at the process's next syscall Detcore sees, which is
27//! its exit call at the latest when it exits normally. A process that dies
28//! before then reports nothing: one killed from outside, and one that faults
29//! in the signal it asked to handle (SIGSEGV's default action is delivered by
30//! the runtime while it traps instructions and by the kernel otherwise; the
31//! kernel ends a process on SIGBUS, SIGFPE or SIGILL), because neither path
32//! reaches Detcore; a thread's exit hook does not run on those deaths either,
33//! so it is not used.
34//!
35//! The texts of Detcore's own refusals of the in-guest runtime's reserved
36//! signals (`Config::in_guest_reserved_signals`: a send to another process,
37//! or an arming) live here too. Detcore decides those itself, at the call,
38//! and prints each through its guest WARN path (`crate::guest_log`).
39
40use std::sync::OnceLock;
41
42use reverie::Guest;
43
44use crate::Detcore;
45use crate::record_or_replay::RecordOrReplay;
46
47/// What a runtime refused since it was last asked: the signals whose new
48/// action it refused, bit `n - 1` for signal `n`, and whether it refused an
49/// alternate signal stack.
50pub type RefusedSignalControls = (u64, bool);
51
52static SOURCE: OnceLock<fn() -> RefusedSignalControls> = OnceLock::new();
53
54/// Installs `source`, which takes the signal controls the runtime refused
55/// since it was last called. The runtime crate calls this once per process
56/// image; a forked child inherits it. Returns false if one was installed
57/// already.
58pub fn install(source: fn() -> RefusedSignalControls) -> bool {
59    SOURCE.set(source).is_ok()
60}
61
62/// Takes what the runtime refused since the last call: nothing where no
63/// source is installed.
64fn take() -> RefusedSignalControls {
65    SOURCE.get().map_or((0, false), |source| source())
66}
67
68/// At a syscall's entry, tells the coordinator what the runtime refused since
69/// the last report (`GlobalRequest::AnnounceRefusedSignalControls`). The
70/// message carries no logical time and is answered before any accounting, so
71/// whether a run reports changes neither its schedule nor its records.
72pub(crate) async fn report<G, T>(guest: &mut G)
73where
74    G: Guest<Detcore<T>>,
75    T: RecordOrReplay,
76{
77    let (actions, alternate_stack) = take();
78    if actions == 0 && !alternate_stack {
79        return;
80    }
81    crate::tool_global::announce_refused_signal_controls(guest, actions, alternate_stack).await;
82}
83
84/// The WARN for a signal whose new action the runtime refused: a handler, or
85/// any action for a signal the runtime keeps for itself (SIGSYS, and SIGSEGV
86/// while it traps instructions).
87pub(crate) fn handler_refused_warning(signum: i32) -> String {
88    let name = signal_name(signum);
89    format!(
90        "HERMIT_SIGNAL_HANDLER_REFUSED signum={signum} ({name}): the guest asked for a new \
91         action for it (a handler, or any action for a signal the in-guest runtime keeps for \
92         itself) and received EPERM, so the handler was not installed: in-guest signal \
93         handlers are unsupported until A5 step 3c (https://github.com/rrnewton/hermit/issues/4485). \
94         A {name} the guest receives takes the action it had before."
95    )
96}
97
98/// The WARN for a guest handler that Detcore refused with EPERM because the
99/// run's threads are not sequentialized (A5 step 3c slice 0g, design 16.2):
100/// `caller` is the calling thread. Printed for every refused install, through
101/// Detcore's guest WARN path (`crate::guest_log`), like the reserved-signal
102/// refusals: Detcore decides it itself, at the call.
103pub(crate) fn unsequentialized_handler_refused_warning(signum: i32, caller: i32) -> String {
104    let name = signal_name(signum);
105    format!(
106        "HERMIT_UNSEQUENTIALIZED_HANDLER_REFUSED signum={signum} ({name}) caller={caller}: the \
107         guest asked for a handler and received EPERM, so the handler was not installed: threads \
108         are not sequentialized (--no-sequentialize-threads, or --strace-only, which turns \
109         sequentialization off; for a library caller, Config::sequentialize_threads is false), \
110         and an in-guest guest handler needs Detcore's scheduler to run one guest thread at a \
111         time. A named refusal, not Linux's behaviour (A5 step 3c, \
112         https://github.com/rrnewton/hermit/issues/4485). A {name} the guest receives takes the \
113         action it had before."
114    )
115}
116
117/// The WARN for an alternate signal stack change the runtime refused.
118pub(crate) fn alternate_stack_refused_warning() -> String {
119    "HERMIT_SIGNAL_ALTSTACK_REFUSED: the guest asked to set or change its alternate signal \
120     stack and received EPERM, so it was not installed: in-guest signal handlers are \
121     unsupported until A5 step 3c (https://github.com/rrnewton/hermit/issues/4485)."
122        .to_string()
123}
124
125/// The WARN for a send of a reserved in-guest signal
126/// (`Config::in_guest_reserved_signals`) from the caller's process to
127/// another, which Detcore refused with EPERM: `call` is the system call,
128/// `sender` the calling thread and `target` the process it named. Printed for
129/// every refused send, through Detcore's guest WARN path
130/// (`crate::guest_log`), not once per run: each names its own target.
131pub(crate) fn reserved_signal_send_refused_warning(
132    call: &str,
133    signum: i32,
134    sender: i32,
135    target: i32,
136) -> String {
137    let name = signal_name(signum);
138    format!(
139        "HERMIT_RESERVED_SIGNAL_SEND_REFUSED signum={signum} ({name}) call={call} sender={sender} \
140         target=process {target}: received EPERM and nothing was sent. The in-guest runtime keeps \
141         {name} for itself, and a {name} from another process would run the runtime's handler in \
142         process {target} outside the schedule. A named refusal, not Linux's behaviour \
143         (A5 step 3c, https://github.com/rrnewton/hermit/issues/4485)."
144    )
145}
146
147/// A call that arms a notification signal, as the reserved-signal refusal
148/// names it (`Detcore::refuse_reserved_signal_arming`).
149#[derive(Clone, Copy, Debug, PartialEq, Eq)]
150pub(crate) enum ReservedArming {
151    /// `fcntl(F_SETSIG)`: the descriptor's I/O signal.
152    FcntlSetsig,
153    /// `timer_create` with a signal notification.
154    TimerCreate,
155    /// `prctl(PR_SET_PDEATHSIG)`: the parent-death signal.
156    PrctlPdeathsig,
157}
158
159impl ReservedArming {
160    /// The call and operation, as the WARN names them.
161    pub(crate) fn call(self) -> &'static str {
162        match self {
163            Self::FcntlSetsig => "fcntl(F_SETSIG)",
164            Self::TimerCreate => "timer_create",
165            Self::PrctlPdeathsig => "prctl(PR_SET_PDEATHSIG)",
166        }
167    }
168
169    /// Who would send the signal, and when.
170    fn sender(self) -> &'static str {
171        match self {
172            Self::FcntlSetsig => {
173                "the kernel would send it for the descriptor's I/O events, at moments host timing \
174                 decides"
175            }
176            Self::TimerCreate => "Detcore's virtual timer would deliver it at each expiry",
177            Self::PrctlPdeathsig => {
178                "the kernel would send it when the parent thread exits, at a moment host timing \
179                 decides"
180            }
181        }
182    }
183}
184
185/// The WARN for an arming of a reserved in-guest signal as a notification
186/// signal by `arming`, which Detcore refused with EPERM.
187pub(crate) fn reserved_signal_arming_refused_warning(
188    arming: ReservedArming,
189    signum: i32,
190    caller: i32,
191) -> String {
192    let name = signal_name(signum);
193    let call = arming.call();
194    let sender = arming.sender();
195    format!(
196        "HERMIT_RESERVED_SIGNAL_ARMING_REFUSED signum={signum} ({name}) call={call} \
197         caller={caller}: received EPERM and nothing was armed. The in-guest runtime keeps {name} \
198         for itself: {sender}, and the runtime's handler, not the guest's action, would take it. \
199         A named refusal, not Linux's behaviour \
200         (A5 step 3c, https://github.com/rrnewton/hermit/issues/4485)."
201    )
202}
203
204/// The run-wide stopping refusal (A5 design D49) for a syscall that, under
205/// the in-guest delivery gate, finished with one of the kernel's internal
206/// restart codes other than `ERESTARTSYS` although no wait recorded a signal
207/// ending it (design D40): the in-guest runtime would hand the raw code to
208/// the guest, since only `ERESTARTSYS` is re-run there. `ERESTARTSYS` itself
209/// passes through as that re-run.
210pub(crate) fn restart_code_without_signal_refusal(call: &str, code: &str, caller: i32) -> String {
211    format!(
212        "hermit refused to continue the run (HERMIT_RESTART_CODE_WITHOUT_SIGNAL): {call} in \
213         thread {caller} finished with {code} under the in-guest delivery gate, but no wait \
214         recorded a signal that ended it, so the in-guest runtime would return the kernel's \
215         internal restart code to the guest, which Linux never does \
216         (A5 design D40, https://github.com/rrnewton/hermit/issues/4485)."
217    )
218}
219
220/// The run-wide stopping refusal (A5 design D49) for a wait whose end the
221/// restart checkpoint cannot resolve (`wait_signal_end::DequeueWalk::resolve`,
222/// design D40): a caught signal is delivered first and a later delivery
223/// (`behind`) could end the process, depending on the caught action, and the
224/// query of that action failed. Fail closed: the run ends rather than guess.
225pub(crate) fn caught_action_unreadable_refusal(
226    call: &str,
227    caught: i32,
228    behind: i32,
229    caller: i32,
230) -> String {
231    format!(
232        "hermit refused to continue the run (HERMIT_CAUGHT_ACTION_UNREADABLE): {call} in \
233         thread {caller} ended for caught signal {caught} ({}) with signal {behind} ({}) \
234         deliverable behind it, and the caught signal's action could not be read, so whether \
235         the process dies at the return cannot be decided (A5 design D40, \
236         https://github.com/rrnewton/hermit/issues/4485).",
237        signal_name(caught),
238        signal_name(behind),
239    )
240}
241
242/// The run-wide stopping refusal (A5 design D49, with D48) for a delivery
243/// loop that dequeues a second instance of caught signal `signal` after
244/// `SA_RESETHAND` reset its action to its default, a stop
245/// (`wait_signal_end::WalkRefusal::ResetToStop`): the in-guest backend cannot
246/// stop a guest process on the schedule.
247pub(crate) fn signal_reset_to_stop_refusal(call: &str, signal: i32, caller: i32) -> String {
248    format!(
249        "hermit refused to continue the run (HERMIT_SIGNAL_RESET_TO_STOP): {call} in thread \
250         {caller} ended for caught signal {signal} ({}), whose SA_RESETHAND resets it to its \
251         default action, a stop, and a second instance of it is pending unblocked, which Linux \
252         would then deliver by stopping the process; the in-guest backend cannot stop a guest \
253         process on the schedule (A5 design D40, D48, \
254         https://github.com/rrnewton/hermit/issues/4485).",
255        signal_name(signal),
256    )
257}
258
259/// The run-wide stopping refusal (A5 design D49) for a caught real-time
260/// signal whose action has `SA_RESETHAND` and leaves the signal unblocked
261/// after its delivery (`wait_signal_end::WalkRefusal::RealtimeInstances`):
262/// another queued instance of it would be dequeued at its default action,
263/// termination, and `/proc` shows only that one instance or more is pending,
264/// so whether the process dies at the return cannot be decided.
265pub(crate) fn realtime_instances_unknown_refusal(call: &str, signal: i32, caller: i32) -> String {
266    format!(
267        "hermit refused to continue the run (HERMIT_REALTIME_INSTANCES_UNKNOWN): {call} in \
268         thread {caller} ended for caught real-time signal {signal} ({}), whose SA_RESETHAND \
269         resets it to its default action, termination, while it stays unblocked after its \
270         delivery; a second queued instance would then end the process, and how many \
271         instances are queued cannot be read (A5 design D40, \
272         https://github.com/rrnewton/hermit/issues/4485).",
273        signal_name(signal),
274    )
275}
276
277/// The run-wide stopping refusal (A5 design D49) for a caught end whose
278/// restart table restarts the call with a code other than `ERESTARTSYS`
279/// (D40): the restart needs the register rewind and post-handler delivery of
280/// step 3c.2, and the in-guest Tool host would hand the raw code to the
281/// guest. Fail closed.
282pub(crate) fn restart_after_handler_refusal(
283    call: &str,
284    signal: i32,
285    code: &str,
286    caller: i32,
287) -> String {
288    format!(
289        "hermit refused to continue the run (HERMIT_RESTART_AFTER_HANDLER): {call} in thread \
290         {caller} finished with {code} and caught signal {signal} ({}) ends it, so Linux would \
291         run the handler and then restart the call, which needs a register rewind and a \
292         delivery after the handler that the in-guest delivery gate does not provide before A5 \
293         step 3c.2; the guest would otherwise see the kernel's internal restart code (A5 design \
294         D40, https://github.com/rrnewton/hermit/issues/4485).",
295        signal_name(signal),
296    )
297}
298
299/// The run-wide stopping refusal (A5 design D49) for a wait under a
300/// temporary signal mask (`rt_sigsuspend`, a masked `pselect6`, `ppoll`,
301/// `epoll_pwait` or `epoll_pwait2`) that caught signal `signal` would end
302/// under the in-guest delivery gate (step 3c.1d): Linux would run its handler
303/// with the temporary mask in force and restore the thread's own mask only at
304/// the handler's `rt_sigreturn`, which step 3c.w provides. 3c.1 never reaches
305/// it: phase 1 refuses every temporary-mask call in a process that handles
306/// SIGALRM (design F8), and no other guest handler is admitted. Fail closed.
307pub(crate) fn temporary_mask_caught_end_refusal(caller: i32, signal: i32) -> String {
308    format!(
309        "hermit refused to continue the run (HERMIT_TEMPORARY_MASK_CAUGHT_END): a wait under a \
310         temporary signal mask in thread {caller} would end for caught signal {signal} ({}), \
311         whose handler Linux runs under that mask; the in-guest delivery gate does not run \
312         guest handlers under a temporary mask before A5 step 3c.w (A5 design D39, \
313         https://github.com/rrnewton/hermit/issues/4485).",
314        signal_name(signal),
315    )
316}
317
318/// The run-wide stopping refusal (A5 design D49) for an argument of `call`
319/// under the in-guest delivery gate (step 3c.1d, round-1 ruling 4) that
320/// Detcore's own read could not reach although Linux's copy would not fault
321/// (a write-only mapping, a byte below a grow-down mapping), or whose
322/// caller's mappings could not be read to tell: Linux's answer is not known,
323/// so the run ends rather than answer EFAULT where Linux would not.
324pub(crate) fn unreadable_syscall_argument_refusal(
325    call: &str,
326    what: &str,
327    address: usize,
328    caller: i32,
329) -> String {
330    format!(
331        "hermit refused to continue the run (HERMIT_UNREADABLE_SYSCALL_ARGUMENT): {call} in \
332         thread {caller} names {what} at {address:#x}, which Hermit cannot read although \
333         Linux's copy would not fault there (or the caller's mappings could not be read to \
334         tell), so Linux's answer is not known (A5 design D39, \
335         https://github.com/rrnewton/hermit/issues/4485)."
336    )
337}
338
339/// The run-wide stopping refusal (A5 design D49) of a `/proc` status read
340/// under the in-guest delivery gate whose thread's guest mask the scheduler
341/// holds as unknown (A5 step 3c.1e A, IR-18): it was readmitted on a rejoin
342/// that returned under its own mask but published none, so neither the mask
343/// its call published (possibly a temporary one Linux has taken down) nor
344/// the kernel's `SigBlk` (the in-guest runtime's mask once it has one) is the
345/// guest's. Linux's answer is not known, so the run ends rather than show
346/// either. Defect-only once step 3c.1e B guarantees a published mask on every
347/// gated backend.
348pub(crate) fn guest_mask_unknown_refusal(reader: i32, thread: i32) -> String {
349    format!(
350        "hermit refused to continue the run (HERMIT_GUEST_MASK_UNKNOWN): thread {reader} reads \
351         the /proc status of thread {thread}, whose signal mask Hermit does not know (it \
352         returned from a call outside the schedule under its own mask without publishing it), \
353         so SigBlk cannot be shown as Linux shows it (A5 step 3c.1e, IR-18, \
354         https://github.com/rrnewton/hermit/issues/4485)."
355    )
356}
357
358/// The run-wide stopping refusal (A5 design D49) of a masked wait's end under
359/// the in-guest delivery gate (A5 step 3c.1e A, D37's R2) where one ignored
360/// real-time signal `signal`, outside the call's temporary mask, has both an
361/// instance kept pending from the call's entry and one sent to the same
362/// `queue` while the temporary mask was published: Linux keeps the first and
363/// discarded the second at send (`sig_ignored`, kernel/signal.c:104-120 at
364/// v6.12), but the queue's pending bit cannot tell them apart, and a removal
365/// would dequeue the older one first. Fail closed.
366pub(crate) fn ignored_realtime_instances_refusal(caller: i32, signal: i32, queue: &str) -> String {
367    format!(
368        "hermit refused to continue the run (HERMIT_IGNORED_REALTIME_INSTANCES): a wait under a \
369         temporary signal mask in thread {caller} ends with ignored real-time signal {signal} \
370         ({}) pending in its {queue} queue both from before the call and from a send during \
371         it, which Linux keeps and discards respectively; Hermit cannot remove only the second \
372         (A5 step 3c.1e, D37, https://github.com/rrnewton/hermit/issues/4485).",
373        signal_name(signal),
374    )
375}
376
377/// The WARN of A5 step 3c.1e A, IR-15: a two-phase delivery's announcement
378/// of a default-action signal, or a predicted death, for a thread of a
379/// process that is the init of a PID namespace it created with
380/// `CLONE_NEWPID`. Linux marks such an init `SIGNAL_UNKILLABLE`
381/// (kernel/fork.c:2558-2560 at v6.12): a signal whose action is `SIG_DFL` is
382/// ignored when it is sent unless blocked (`sig_task_ignored`,
383/// kernel/signal.c:92-94, through `sig_ignored`, :104-120 and :111), and a
384/// blocked one is discarded at dequeue (`get_signal`, :2843-2845). SIGKILL
385/// and SIGSTOP are ignored the same way unless the send is forced
386/// (`!(force && sig_kernel_only(sig))`, :93), which it is from an ancestor
387/// PID namespace or the kernel (`send_signal_locked`, :1217-1224), a sender
388/// the hook does not see. No default-action delivery or death can be
389/// predicted for any of them; the hook is refused with `EPROTONOSUPPORT`.
390pub(crate) fn pid_namespace_init_prediction_refused_warning(
391    caller: i32,
392    hook: &str,
393    signal: i32,
394) -> String {
395    format!(
396        "hermit refused an in-guest delivery (HERMIT_PID_NAMESPACE_INIT_PREDICTION): {hook} for \
397         signal {signal} ({}), which takes its default action, in thread {caller}, whose \
398         process is the init of a PID namespace it created, where Linux ignores or discards \
399         a default-action signal (SIGNAL_UNKILLABLE), SIGKILL and SIGSTOP too unless sent \
400         from an ancestor PID namespace or the kernel; the hook returns \
401         EPROTONOSUPPORT (A5 step 3c.1e, IR-15, https://github.com/rrnewton/hermit/issues/4485).",
402        signal_name(signal),
403    )
404}
405
406/// The named refusal (A5 design D39) of a masked `pselect6` under the
407/// in-guest delivery gate that Detcore cannot serve on staged copies: more
408/// than 64 descriptors and a nonzero timeout with nothing ready (Detcore polls
409/// only the first word of each set on its internal path, and the kernel would
410/// sleep under the temporary mask, which the in-guest runtime cannot let it
411/// install), or more than 1024 descriptors to copy. `why` says which. The
412/// call fails with ENOSYS after Linux's own argument checks (and, for the
413/// first, a first poll that found nothing ready and no signal that ends it).
414pub(crate) fn masked_pselect6_nfds_refused_warning(caller: i32, nfds: i32, why: &str) -> String {
415    format!(
416        "hermit refused a call (HERMIT_MASKED_PSELECT6_NFDS_REFUSED): pselect6 in thread \
417         {caller} with a temporary signal mask and {nfds} descriptors, with {why}, would need \
418         the kernel to wait or copy under that mask, which the in-guest delivery gate cannot \
419         run; the call returns ENOSYS (A5 design D39, \
420         https://github.com/rrnewton/hermit/issues/4485)."
421    )
422}
423
424/// The run-wide stopping refusal (A5 design D44(b) through D49) for a send
425/// of `signum` to thread `target`, blocked in a kernel-run call (a vfork
426/// parent's wait when `vfork`) whose continuation turn has not been granted:
427/// the call runs in the kernel under the in-guest runtime's mask, so the
428/// signal would stay pending and the call would never end for it, where Linux
429/// interrupts it. `call` is the sending call (or "the scheduler" for its own
430/// send), `sender` the sending thread. `unreadable`: the target's dispositions
431/// could not be read, which refuses, fail closed.
432pub(crate) fn kernel_run_signal_refusal(
433    call: &str,
434    signum: i32,
435    sender: Option<i32>,
436    target: i32,
437    vfork: bool,
438    unreadable: bool,
439) -> String {
440    let name = signal_name(signum);
441    let sender = sender.map_or_else(String::new, |sender| format!(" by thread {sender}"));
442    let blocked_in = if vfork {
443        "a vfork parent's wait for its child"
444    } else {
445        "a call the kernel runs outside the schedule"
446    };
447    let why = if unreadable {
448        "its signal dispositions could not be read, so the send is refused, fail closed"
449    } else {
450        "Linux would interrupt the call for it, but the in-guest runtime's mask holds it pending \
451         for as long as the call blocks"
452    };
453    format!(
454        "hermit refused to continue the run (HERMIT_KERNEL_RUN_SIGNAL_REFUSED): signal {signum} \
455         ({name}) sent{sender} through {call} to thread {target}, which is blocked in {blocked_in} \
456         and has not been granted its continuation turn: {why} \
457         (A5 design D44, https://github.com/rrnewton/hermit/issues/4485)."
458    )
459}
460
461/// Why a stop-signal send was refused (`stop_signal_send_refused_warning`).
462#[derive(Clone, Copy, Debug, PartialEq, Eq)]
463pub(crate) enum StopSendRefusal {
464    /// SIGSTOP, which always stops.
465    Sigstop,
466    /// SIGTSTP, SIGTTIN or SIGTTOU while the target's action is `SIG_DFL`, as
467    /// its `/proc/<tid>/status` `SigIgn`/`SigCgt` say in the sender's turn.
468    DefaultAction,
469    /// The target's `/proc/<tid>/status` could not be read, so whether its
470    /// action is `SIG_DFL` is unknown: refused, fail closed.
471    StatusUnreadable,
472}
473
474/// The WARN for a send of a stop signal that Detcore refused with EPERM
475/// under the in-guest delivery gate (A5 design D48, IR-4): `call` is the
476/// system call, `sender` the calling thread, `target` the thread or process it
477/// named. A program that ignores the EPERM and then waits for the target with
478/// `waitpid(WUNTRACED)` waits until the run's wall timeout (IR-4(c)).
479pub(crate) fn stop_signal_send_refused_warning(
480    call: &str,
481    signum: i32,
482    sender: i32,
483    target: i32,
484    why: StopSendRefusal,
485) -> String {
486    let name = signal_name(signum);
487    let why = match why {
488        StopSendRefusal::Sigstop => "SIGSTOP always stops its target".to_string(),
489        StopSendRefusal::DefaultAction => format!(
490            "the target's action for {name} is SIG_DFL (its /proc status SigIgn and SigCgt, read in \
491             the sender's turn), which stops it"
492        ),
493        StopSendRefusal::StatusUnreadable => format!(
494            "the target's /proc status could not be read, so whether its action for {name} is \
495             SIG_DFL is unknown, and the send is refused, fail closed"
496        ),
497    };
498    format!(
499        "HERMIT_STOP_SIGNAL_SEND_REFUSED signum={signum} ({name}) call={call} sender={sender} \
500         target={target}: received EPERM and nothing was sent: {why}, and the in-guest backend \
501         cannot stop a guest process on the schedule. A named refusal, not Linux's behaviour \
502         (A5 design D48, https://github.com/rrnewton/hermit/issues/4485)."
503    )
504}
505
506/// `signum`'s name: `SIGUSR1`, or, for a realtime signal, its kernel number
507/// (C libraries reserve different numbers of them, so `SIGRTMIN+n` would
508/// depend on the guest's).
509fn signal_name(signum: i32) -> String {
510    match nix::sys::signal::Signal::try_from(signum) {
511        Ok(signal) => signal.as_str().to_string(),
512        Err(_) => format!("realtime signal {signum}"),
513    }
514}
515
516#[cfg(test)]
517mod tests {
518    use super::*;
519
520    #[test]
521    fn the_warnings_name_the_signal_the_errno_and_why() {
522        let usr1 = handler_refused_warning(libc::SIGUSR1);
523        assert!(usr1.starts_with("HERMIT_SIGNAL_HANDLER_REFUSED signum=10 (SIGUSR1): "));
524        let stack = alternate_stack_refused_warning();
525        assert!(stack.starts_with("HERMIT_SIGNAL_ALTSTACK_REFUSED: "));
526        for text in [&usr1, &stack] {
527            assert!(text.contains("received EPERM"), "{text}");
528            assert!(text.contains("was not installed"), "{text}");
529            assert!(
530                text.contains("in-guest signal handlers are unsupported until A5 step 3c"),
531                "{text}"
532            );
533        }
534        assert!(handler_refused_warning(36).contains("(realtime signal 36)"));
535
536        let send = reserved_signal_send_refused_warning("tgkill", libc::SIGSYS, 4, 5);
537        assert!(
538            send.starts_with(
539                "HERMIT_RESERVED_SIGNAL_SEND_REFUSED signum=31 (SIGSYS) call=tgkill sender=4 \
540                 target=process 5: received EPERM"
541            ),
542            "{send}"
543        );
544        let arming =
545            reserved_signal_arming_refused_warning(ReservedArming::FcntlSetsig, libc::SIGSEGV, 4);
546        assert!(
547            arming.starts_with(
548                "HERMIT_RESERVED_SIGNAL_ARMING_REFUSED signum=11 (SIGSEGV) \
549                 call=fcntl(F_SETSIG) caller=4: received EPERM"
550            ),
551            "{arming}"
552        );
553        // Each arming names who would send the signal: host timing for the
554        // kernel's I/O and parent-death signals, never for Detcore's timers.
555        assert!(
556            arming.contains("at moments host timing decides"),
557            "{arming}"
558        );
559        let timer =
560            reserved_signal_arming_refused_warning(ReservedArming::TimerCreate, libc::SIGSYS, 4);
561        assert!(timer.contains("call=timer_create caller=4"), "{timer}");
562        assert!(
563            timer.contains("Detcore's virtual timer would deliver it"),
564            "{timer}"
565        );
566        assert!(!timer.contains("host timing"), "{timer}");
567        let pdeathsig = reserved_signal_arming_refused_warning(
568            ReservedArming::PrctlPdeathsig,
569            libc::SIGTRAP,
570            4,
571        );
572        assert!(
573            pdeathsig.contains("call=prctl(PR_SET_PDEATHSIG) caller=4"),
574            "{pdeathsig}"
575        );
576        assert!(
577            pdeathsig.contains("when the parent thread exits"),
578            "{pdeathsig}"
579        );
580        let unsequentialized = unsequentialized_handler_refused_warning(libc::SIGALRM, 4);
581        assert!(
582            unsequentialized.starts_with(
583                "HERMIT_UNSEQUENTIALIZED_HANDLER_REFUSED signum=14 (SIGALRM) caller=4: the guest \
584                 asked for a handler and received EPERM, so the handler was not installed: \
585                 threads are not sequentialized"
586            ),
587            "{unsequentialized}"
588        );
589        // It names what turns sequentialization off, for the CLI and for a
590        // library caller.
591        for cause in [
592            "--no-sequentialize-threads",
593            "--strace-only",
594            "Config::sequentialize_threads",
595        ] {
596            assert!(unsequentialized.contains(cause), "{unsequentialized}");
597        }
598        for text in [&send, &arming, &timer, &pdeathsig, &unsequentialized] {
599            assert!(
600                text.contains("A named refusal, not Linux's behaviour"),
601                "{text}"
602            );
603            assert!(
604                text.contains("https://github.com/rrnewton/hermit/issues/4485"),
605                "{text}"
606            );
607            for backend in ["liteinst", "LiteInst", "in-guest-trap", "sabre", "SaBRe"] {
608                assert!(!text.contains(backend), "{text}");
609            }
610        }
611        // No backend is named: the text is the same wherever it is printed.
612        for text in [usr1, stack] {
613            for backend in ["liteinst", "LiteInst", "in-guest-trap", "sabre", "SaBRe"] {
614                assert!(!text.contains(backend), "{text}");
615            }
616        }
617    }
618}