reverie/signal.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//! Backend-neutral signal delivery metadata.
10
11use serde::Deserialize;
12use serde::Serialize;
13
14use crate::Pid;
15use crate::Tid;
16use crate::error::Errno;
17
18/// Size of Linux's userspace `siginfo_t` representation on supported targets.
19pub const SIGNAL_INFO_SIZE: usize = 128;
20
21/// Receiver state after accepting one process-directed child-exit event.
22#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
23pub enum ChildExitSignalDisposition {
24 /// Explicit `SIG_IGN` suppressed generation; no event was inserted.
25 Ignored,
26 /// The event is pending and currently blocked by the receiver's mask.
27 PendingBlocked,
28 /// The event is eligible at the receiver's signal-delivery boundary.
29 ///
30 /// The Tool hook and current disposition still determine whether a guest
31 /// handler runs. This is not a promise of handler execution or `EINTR`.
32 PendingEligible,
33}
34
35/// Why a child-exit event was refused before changing backend state.
36#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
37pub enum ChildExitSignalErrorKind {
38 /// This backend, execution context, or producer class is unsupported.
39 Unsupported,
40 /// Metadata or the current receiver identity is invalid.
41 Invalid,
42 /// An internal backend operation failed before publication.
43 Backend,
44}
45
46/// Complete result of a backend's child-exit pending-state operation.
47///
48/// A caller must distinguish refusal before publication from failure after
49/// publication. Retrying a post-publication failure may lose the first siginfo
50/// or report a delivery that never reached the receiver. Neither failure is an
51/// ordinary guest syscall result. These generations identify the signal's
52/// disposition generation, not a scheduler operation or delivery identifier.
53#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
54pub enum ChildExitSignalOutcome {
55 /// The bounded receiver operation completed successfully.
56 Accepted {
57 /// Whether generation was suppressed, blocked, or eligible.
58 disposition: ChildExitSignalDisposition,
59 /// The signal's disposition/pending generation at this operation.
60 pending_generation: u64,
61 /// An already pending standard signal retained its original siginfo.
62 coalesced: bool,
63 },
64 /// Neither pending state nor signalfd readiness changed.
65 RejectedBeforeCommit {
66 /// Machine-readable failure class, independent of diagnostic text.
67 kind: ChildExitSignalErrorKind,
68 /// The original errno.
69 errno: Errno,
70 },
71 /// Insertion or coalescing committed before a readiness update failed.
72 FailedAfterCommit {
73 /// The original readiness-update errno.
74 errno: Errno,
75 /// The generation under which insertion or coalescing committed.
76 pending_generation: u64,
77 },
78}
79
80/// Current disposition of a process-pending alarm, before Tool filtering.
81#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
82pub enum ProcessAlarmSignalDisposition {
83 /// The Tool may observe the signal, but this disposition runs no handler.
84 Ignored,
85 /// A caught disposition; delivery and interruption have not occurred.
86 Caught,
87 /// The default SIGALRM action terminates the process when delivered.
88 DefaultFatal,
89}
90
91/// State captured when a process-alarm pending operation commits.
92///
93/// This is a publication receipt, not a dequeue identity, timer rearm, Tool
94/// observation, handler execution, or authorization to return `EINTR`. A later
95/// mask or disposition change may invalidate this eligibility snapshot.
96#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
97pub struct ProcessAlarmSignalReceipt {
98 /// Whether the sole receiver currently blocks SIGALRM.
99 pub blocked: bool,
100 /// The disposition at publication, independently of the receiver's mask.
101 pub disposition: ProcessAlarmSignalDisposition,
102 /// The SIGALRM disposition/pending generation, not a delivery counter.
103 pub pending_generation: u64,
104 /// An existing shared standard signal retained its first complete siginfo.
105 pub coalesced: bool,
106}
107
108/// Why a process-alarm operation was refused without changing backend state.
109#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
110pub enum ProcessAlarmSignalErrorKind {
111 /// This backend, callback, receiver configuration or producer is unsupported.
112 Unsupported,
113 /// The complete metadata or current receiver identity is invalid.
114 Invalid,
115 /// A backend operation failed before publication.
116 Backend,
117}
118
119/// Complete result of publishing a process-pending SIGALRM.
120///
121/// Accepted alarms always belong to the shared process pending set, including
122/// blocked or ignored alarms. A Tool still observes eligible ignored signals.
123/// Retrying a failure after publication as if it were a refusal is incorrect.
124#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
125pub enum ProcessAlarmSignalOutcome {
126 /// Shared pending state and its signalfd readiness were published.
127 Accepted(ProcessAlarmSignalReceipt),
128 /// Neither pending state nor signalfd readiness changed.
129 RejectedBeforeCommit {
130 /// Failure class independent of diagnostic text.
131 kind: ProcessAlarmSignalErrorKind,
132 /// Original errno.
133 errno: Errno,
134 },
135 /// Shared insertion/coalescing committed before a readiness update failed.
136 FailedAfterCommit {
137 /// Original readiness-update errno.
138 errno: Errno,
139 /// The published state; some readiness updates may also have completed.
140 receipt: ProcessAlarmSignalReceipt,
141 },
142}
143
144/// Identifies both the selected guest task and whether a signal was originally
145/// process-directed or thread-directed.
146#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
147pub enum SignalTarget {
148 /// A process-directed signal. The backend or determinizing tool selected a
149 /// concrete thread in `pid` before deferring the event to that guest.
150 Process {
151 /// The destination thread group.
152 pid: Pid,
153 },
154 /// A signal directed to one exact thread.
155 Thread {
156 /// The destination thread group.
157 pid: Pid,
158 /// The destination thread.
159 tid: Tid,
160 },
161}
162
163/// A signal selected for deterministic delivery to one stopped guest thread.
164///
165/// Unlike [`nix::sys::signal::Signal`], this representation retains Linux's
166/// raw signal numbers 1 through 64, the complete 128-byte `siginfo_t`, and the
167/// process-versus-thread provenance needed by an out-of-process backend. It is
168/// intentionally an event selected by the caller, not a request for a backend
169/// to perform process-wide target selection.
170#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
171#[serde(try_from = "SignalEventWire", into = "SignalEventWire")]
172pub struct SignalEvent {
173 signal: u8,
174 siginfo: [u8; SIGNAL_INFO_SIZE],
175 target: SignalTarget,
176}
177
178#[derive(Serialize, Deserialize)]
179struct SignalEventWire {
180 signal: i32,
181 siginfo: Vec<u8>,
182 target: SignalTarget,
183}
184
185impl From<SignalEvent> for SignalEventWire {
186 fn from(event: SignalEvent) -> Self {
187 Self {
188 signal: event.signal(),
189 siginfo: event.siginfo().to_vec(),
190 target: event.target(),
191 }
192 }
193}
194
195impl TryFrom<SignalEventWire> for SignalEvent {
196 type Error = Errno;
197 fn try_from(wire: SignalEventWire) -> Result<Self, Self::Error> {
198 let info = wire.siginfo.try_into().map_err(|_| Errno::EINVAL)?;
199 Self::new(wire.signal, info, wire.target)
200 }
201}
202
203impl SignalEvent {
204 /// Creates a coherent signal event, rejecting numbers outside Linux's 1
205 /// through 64 signal namespace and a `siginfo_t` whose `si_signo` differs
206 /// from the separately supplied signal number.
207 pub fn new(
208 signal: i32,
209 siginfo: [u8; SIGNAL_INFO_SIZE],
210 target: SignalTarget,
211 ) -> Result<Self, Errno> {
212 if !(1..=64).contains(&signal)
213 || i32::from_ne_bytes(siginfo[0..4].try_into().expect("siginfo signo bytes")) != signal
214 {
215 return Err(Errno::EINVAL);
216 }
217 Ok(Self {
218 signal: signal as u8,
219 siginfo,
220 target,
221 })
222 }
223
224 /// Returns the raw Linux signal number.
225 pub const fn signal(self) -> i32 {
226 self.signal as i32
227 }
228
229 /// Returns the complete Linux `siginfo_t` bytes.
230 pub const fn siginfo(self) -> [u8; SIGNAL_INFO_SIZE] {
231 self.siginfo
232 }
233
234 /// Returns the target and original direction of the event.
235 pub const fn target(self) -> SignalTarget {
236 self.target
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243
244 fn siginfo(signal: i32) -> [u8; SIGNAL_INFO_SIZE] {
245 let mut info = [0; SIGNAL_INFO_SIZE];
246 info[0..4].copy_from_slice(&signal.to_ne_bytes());
247 info
248 }
249
250 #[test]
251 fn signal_event_preserves_raw_realtime_number_payload_and_target() {
252 let mut info = siginfo(64);
253 for (index, byte) in info[4..].iter_mut().enumerate() {
254 *byte = index as u8;
255 }
256 let target = SignalTarget::Thread {
257 pid: Pid::from_raw(41),
258 tid: Pid::from_raw(42),
259 };
260 let event = SignalEvent::new(64, info, target).unwrap();
261
262 assert_eq!(event.signal(), 64);
263 assert_eq!(event.siginfo(), info);
264 assert_eq!(event.target(), target);
265 }
266
267 #[test]
268 fn signal_event_rejects_invalid_or_incoherent_numbers() {
269 let target = SignalTarget::Process {
270 pid: Pid::from_raw(7),
271 };
272 for signal in [i32::MIN, -1, 0, 65, i32::MAX] {
273 assert_eq!(
274 SignalEvent::new(signal, siginfo(libc::SIGUSR1), target),
275 Err(Errno::EINVAL),
276 "accepted invalid signal {signal}",
277 );
278 }
279 assert_eq!(
280 SignalEvent::new(libc::SIGUSR2, siginfo(libc::SIGUSR1), target),
281 Err(Errno::EINVAL),
282 "accepted an event whose signal and siginfo.si_signo disagree",
283 );
284 }
285}