Skip to main content

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}