1use std::sync::OnceLock;
41
42use reverie::Guest;
43
44use crate::Detcore;
45use crate::record_or_replay::RecordOrReplay;
46
47pub type RefusedSignalControls = (u64, bool);
51
52static SOURCE: OnceLock<fn() -> RefusedSignalControls> = OnceLock::new();
53
54pub fn install(source: fn() -> RefusedSignalControls) -> bool {
59 SOURCE.set(source).is_ok()
60}
61
62fn take() -> RefusedSignalControls {
65 SOURCE.get().map_or((0, false), |source| source())
66}
67
68pub(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
84pub(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
98pub(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
117pub(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
125pub(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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
150pub(crate) enum ReservedArming {
151 FcntlSetsig,
153 TimerCreate,
155 PrctlPdeathsig,
157}
158
159impl ReservedArming {
160 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 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
185pub(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
204pub(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
220pub(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
242pub(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
259pub(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
277pub(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
299pub(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
318pub(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
339pub(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
358pub(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
377pub(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
406pub(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
424pub(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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
463pub(crate) enum StopSendRefusal {
464 Sigstop,
466 DefaultAction,
469 StatusUnreadable,
472}
473
474pub(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
506fn 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 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 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 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}