Skip to main content

coreshift_core/
signal.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/
4
5//! Signal and shutdown helpers.
6//!
7//! This module provides small process-global signal utilities intended for
8//! low-level daemons and worker processes that want explicit signal handling
9//! without a heavier runtime.
10
11use crate::CoreError;
12use crate::error::syscall_ret;
13use crate::fd::Fd;
14use std::sync::atomic::{AtomicBool, AtomicPtr, Ordering};
15
16pub type SignalSet = libc::sigset_t;
17pub type ThreadId = libc::pthread_t;
18
19pub const SIGINT: i32 = libc::SIGINT;
20pub const SIGTERM: i32 = libc::SIGTERM;
21pub const SIGPIPE: i32 = libc::SIGPIPE;
22pub const SIGKILL: i32 = libc::SIGKILL;
23pub const SIGUSR1: i32 = libc::SIGUSR1;
24pub const SIGUSR2: i32 = libc::SIGUSR2;
25pub const SIGCHLD: i32 = libc::SIGCHLD;
26pub const SIGHUP: i32 = libc::SIGHUP;
27
28/// Type alias for the kernel signal info structure read from a `signalfd`.
29pub type SignalfdSiginfo = libc::signalfd_siginfo;
30
31/// Set a signal's disposition to SIG_IGN.
32///
33/// # Safety
34/// Changes process-global signal disposition.
35pub unsafe fn signal_ignore(sig: i32) {
36    unsafe { libc::signal(sig, libc::SIG_IGN) };
37}
38
39/// Send a signal to a process (libc `kill(2)`), re-exported so callers need
40/// not depend on `libc` directly. `pid` is the usual `kill` semantics: >0 a
41/// single process, 0 the calling process group, -1 the caller's group.
42pub fn kill(pid: i32, sig: i32) -> Result<(), CoreError> {
43    syscall_ret(unsafe { libc::kill(pid, sig) }, "kill")
44}
45
46static SHUTDOWN_FLAG_PTR: AtomicPtr<AtomicBool> = AtomicPtr::new(std::ptr::null_mut());
47
48extern "C" fn shutdown_signal_handler(_sig: libc::c_int) {
49    let flag = SHUTDOWN_FLAG_PTR.load(Ordering::Relaxed);
50    if !flag.is_null() {
51        unsafe {
52            (*flag).store(true, Ordering::Release);
53        }
54    }
55}
56
57/// Install SIGINT and SIGTERM handlers that flip a shared shutdown flag.
58///
59/// This is intended for simple daemon shutdown loops that want a reusable
60/// signal hook without direct `sigaction(2)` setup. The handlers are
61/// process-global and remain installed until replaced by another install.
62/// Use [`install_shutdown_flag_guard`] when the previous process-global
63/// handlers must be restored automatically.
64///
65/// ### Reactor Compatibility
66/// This function uses standard Unix `signal()`/`sigaction()` handlers and is
67/// **not** directly compatible with the `Reactor`. For event-loop based
68/// applications, prefer using [`SignalRuntime::signalfd_new`].
69///
70/// ### Fork Safety
71/// Signal handlers are inherited by the child. The shutdown flag pointer is
72/// also inherited. If the child process receives SIGINT/SIGTERM, it will
73/// attempt to flip the flag in its own address space at the same virtual
74/// address.
75///
76/// ### Errors
77/// - `EINVAL`: Invalid signal number.
78pub fn install_shutdown_flag(flag: &'static AtomicBool) -> Result<(), CoreError> {
79    install_shutdown_flag_inner(flag).map(|_| ())
80}
81
82/// Guard that restores previous SIGINT/SIGTERM handlers and shutdown flag on drop.
83///
84/// ### Fork Safety
85/// The guard is owned by the process that created it. If the process forks,
86/// the child will also have a copy of the guard, but dropping it in the child
87/// will restore handlers in the child's context only.
88pub struct ShutdownFlagGuard {
89    old_sigint: libc::sigaction,
90    old_sigterm: libc::sigaction,
91    old_flag: *mut AtomicBool,
92}
93
94impl Drop for ShutdownFlagGuard {
95    fn drop(&mut self) {
96        // Block SIGINT/SIGTERM while the flag pointer and handlers are swapped
97        // back (CORE-M11): a signal landing between the flag store and the
98        // handler restore would hit the old flag pointer with new handlers (or
99        // the new flag with old handlers), silently setting the wrong flag.
100        let _blocked = SignalRuntime::blocked([SIGINT, SIGTERM]);
101        SHUTDOWN_FLAG_PTR.store(self.old_flag, Ordering::Release);
102        let _ = restore_signal_handler(SIGTERM, &self.old_sigterm);
103        let _ = restore_signal_handler(SIGINT, &self.old_sigint);
104    }
105}
106
107/// Install SIGINT and SIGTERM handlers and return a restore guard.
108///
109/// Dropping the guard restores the previous handlers and previous shutdown
110/// flag pointer. This is the scoped form for tests and callers that do not
111/// want the global convenience behavior of [`install_shutdown_flag`].
112pub fn install_shutdown_flag_guard(
113    flag: &'static AtomicBool,
114) -> Result<ShutdownFlagGuard, CoreError> {
115    let (old_sigint, old_sigterm, old_flag) = install_shutdown_flag_inner(flag)?;
116    Ok(ShutdownFlagGuard {
117        old_sigint,
118        old_sigterm,
119        old_flag,
120    })
121}
122
123fn install_shutdown_flag_inner(
124    flag: &'static AtomicBool,
125) -> Result<(libc::sigaction, libc::sigaction, *mut AtomicBool), CoreError> {
126    // Block SIGINT/SIGTERM for the whole install (CORE-M11): a signal arriving
127    // between the SIGINT handler install, the SIGTERM handler install, and the
128    // flag store would hit `shutdown_signal_handler` with a stale/null flag
129    // pointer — silently dropped or setting the wrong flag. The mask is
130    // restored when the guard is dropped at the end of this fn.
131    let _blocked = SignalRuntime::blocked([SIGINT, SIGTERM]);
132    let old_flag = SHUTDOWN_FLAG_PTR.load(Ordering::Acquire);
133    let old_sigint = install_signal_handler(SIGINT)?;
134    match install_signal_handler(SIGTERM) {
135        Ok(old_sigterm) => {
136            SHUTDOWN_FLAG_PTR.store(
137                flag as *const AtomicBool as *mut AtomicBool,
138                Ordering::Release,
139            );
140            Ok((old_sigint, old_sigterm, old_flag))
141        }
142        Err(err) => {
143            restore_signal_handler(SIGINT, &old_sigint)?;
144            Err(err)
145        }
146    }
147}
148
149/// Return whether a shutdown flag was flipped by the installed handler.
150#[inline]
151pub fn shutdown_requested(flag: &AtomicBool) -> bool {
152    flag.load(Ordering::Acquire)
153}
154
155fn install_signal_handler(sig: libc::c_int) -> Result<libc::sigaction, CoreError> {
156    let mut action: libc::sigaction = unsafe { std::mem::zeroed() };
157    let mut old_action: libc::sigaction = unsafe { std::mem::zeroed() };
158    action.sa_sigaction = shutdown_signal_handler as *const () as usize;
159    action.sa_flags = 0;
160    unsafe { libc::sigemptyset(&mut action.sa_mask) };
161
162    let ret = unsafe { libc::sigaction(sig, &action, &mut old_action) };
163    if ret == -1 {
164        Err(last_sigaction_error(sig))
165    } else {
166        Ok(old_action)
167    }
168}
169
170fn restore_signal_handler(sig: libc::c_int, old_action: &libc::sigaction) -> Result<(), CoreError> {
171    let ret = unsafe { libc::sigaction(sig, old_action, std::ptr::null_mut()) };
172    if ret == -1 {
173        Err(last_sigaction_error(sig))
174    } else {
175        Ok(())
176    }
177}
178
179fn last_sigaction_error(sig: libc::c_int) -> CoreError {
180    let op = match sig {
181        SIGINT => "sigaction(SIGINT)",
182        SIGTERM => "sigaction(SIGTERM)",
183        _ => "sigaction",
184    };
185    let code = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
186    CoreError::sys(code, op)
187}
188
189/// Utilities for process signal management.
190pub struct SignalRuntime;
191
192impl SignalRuntime {
193    /// Create an empty signal set.
194    pub fn empty_set() -> SignalSet {
195        let mut set: SignalSet = unsafe { std::mem::zeroed() };
196        unsafe { libc::sigemptyset(&mut set) };
197        set
198    }
199
200    /// Create a signal set containing the specified signals.
201    ///
202    /// ### Errors
203    /// - `EINVAL`: One of the signal numbers is invalid.
204    pub fn set_with(signals: &[i32]) -> Result<SignalSet, CoreError> {
205        let mut set: SignalSet = unsafe { std::mem::zeroed() };
206        unsafe { libc::sigemptyset(&mut set) };
207        for &sig in signals {
208            let ret = unsafe { libc::sigaddset(&mut set, sig) };
209            if ret == -1 {
210                return Err(CoreError::sys(libc::EINVAL, "sigaddset"));
211            }
212        }
213        Ok(set)
214    }
215
216    /// Block the specified signals for the current thread and return the previous mask.
217    ///
218    /// ### Errors
219    /// - `EINVAL`: `how` or `signals` is invalid.
220    pub fn block_current_thread(signals: &SignalSet) -> Result<SignalSet, CoreError> {
221        let mut previous = Self::empty_set();
222        let result = unsafe { libc::pthread_sigmask(libc::SIG_BLOCK, signals, &mut previous) };
223        if result == 0 {
224            Ok(previous)
225        } else {
226            Err(CoreError::sys(result, "pthread_sigmask(SIG_BLOCK)"))
227        }
228    }
229
230    /// Restore the current thread signal mask.
231    ///
232    /// ### Errors
233    /// - `EINVAL`: `mask` is invalid.
234    pub fn restore_current_thread(mask: &SignalSet) -> Result<(), CoreError> {
235        let result =
236            unsafe { libc::pthread_sigmask(libc::SIG_SETMASK, mask, std::ptr::null_mut()) };
237        if result == 0 {
238            Ok(())
239        } else {
240            Err(CoreError::sys(result, "pthread_sigmask(SIG_SETMASK)"))
241        }
242    }
243
244    /// Wait synchronously for one of the supplied signals.
245    ///
246    /// ### Errors
247    /// - `EINVAL`: `signals` contains invalid signal numbers.
248    pub fn wait(signals: &SignalSet) -> Result<i32, CoreError> {
249        let mut received_signal = 0;
250        let result = unsafe { libc::sigwait(signals, &mut received_signal) };
251        if result == 0 {
252            Ok(received_signal)
253        } else {
254            Err(CoreError::sys(result, "sigwait"))
255        }
256    }
257
258    /// Deliver a signal to a specific thread.
259    ///
260    /// ### Errors
261    /// - `EINVAL`: Invalid signal number.
262    /// - `ESRCH`: The thread ID is invalid or the thread has terminated.
263    pub fn interrupt_thread(thread: ThreadId, signal: i32) -> Result<(), CoreError> {
264        let result = unsafe { libc::pthread_kill(thread, signal) };
265        if result == 0 {
266            Ok(())
267        } else {
268            Err(CoreError::sys(result, "pthread_kill"))
269        }
270    }
271
272    /// Block or unblock signals for the current thread and return the previous mask.
273    pub fn set_current_thread_mask(how: i32, signals: &SignalSet) -> Result<SignalSet, CoreError> {
274        let mut previous = Self::empty_set();
275        let result = unsafe { libc::pthread_sigmask(how, signals, &mut previous) };
276        if result == 0 {
277            Ok(previous)
278        } else {
279            let op = match how {
280                libc::SIG_BLOCK => "pthread_sigmask(SIG_BLOCK)",
281                libc::SIG_UNBLOCK => "pthread_sigmask(SIG_UNBLOCK)",
282                libc::SIG_SETMASK => "pthread_sigmask(SIG_SETMASK)",
283                _ => "pthread_sigmask",
284            };
285            Err(CoreError::sys(result, op))
286        }
287    }
288
289    /// Block the given signals on the current thread and return a guard that
290    /// restores the previous mask on drop.
291    ///
292    /// Used to close the install/restore race on signal-handler swaps
293    /// (CORE-M11): while the guard is alive, SIGINT/SIGTERM cannot land in the
294    /// half-swapped state.
295    pub fn blocked(signals: impl IntoIterator<Item = i32>) -> BlockedSignals {
296        let mut set = Self::empty_set();
297        for sig in signals {
298            unsafe { libc::sigaddset(&mut set, sig) };
299        }
300        let previous = Self::set_current_thread_mask(libc::SIG_BLOCK, &set)
301            .unwrap_or_else(|_| Self::empty_set());
302        BlockedSignals { previous }
303    }
304
305    /// Unblock all signals for the current thread.
306    ///
307    /// # Warning (CORE-M12)
308    /// This is only correct in the fork-child context it currently serves
309    /// ([`spawn::fork`](crate::spawn::fork)): it sets the *entire* mask to the
310    /// empty set, unblocking signals a live `signalfd` thread may depend on. A
311    /// future caller's blocked signal would get default disposition and could
312    /// kill the process. Do not use outside a single-threaded fork child; for a
313    /// targeted change use [`block_current_thread`](Self::block_current_thread)
314    /// with the specific set instead.
315    pub fn unblock_all() -> Result<(), CoreError> {
316        let empty_mask = Self::empty_set();
317        let r =
318            unsafe { libc::pthread_sigmask(libc::SIG_SETMASK, &empty_mask, std::ptr::null_mut()) };
319        if r != 0 {
320            Err(CoreError::sys(r, "pthread_sigmask(SIG_SETMASK)"))
321        } else {
322            Ok(())
323        }
324    }
325
326    /// Create a new `signalfd` for the specified signal set.
327    ///
328    /// The descriptor is created with `SFD_CLOEXEC` and `SFD_NONBLOCK` set.
329    /// Callers are responsible for blocking the signals in the set before
330    /// reading from the `signalfd`.
331    ///
332    /// ### Fork Safety
333    /// The descriptor is `O_CLOEXEC` and will be closed in the child after `exec`.
334    ///
335    /// ### Errors
336    /// - `EINVAL`: `signals` is invalid.
337    /// - `EMFILE`: Process limit on open file descriptors hit.
338    /// - `ENFILE`: System-wide limit on open files hit.
339    ///
340    /// # Example
341    /// ```no_run
342    /// # use coreshift_core::signal::{SignalRuntime, SIGUSR1};
343    /// let signals = SignalRuntime::set_with(&[SIGUSR1]).unwrap();
344    /// SignalRuntime::block_current_thread(&signals).unwrap();
345    /// let sfd = SignalRuntime::signalfd_new(&signals).unwrap();
346    /// ```
347    pub fn signalfd_new(signals: &SignalSet) -> Result<Fd, CoreError> {
348        let fd = unsafe { libc::signalfd(-1, signals, libc::SFD_NONBLOCK | libc::SFD_CLOEXEC) };
349        syscall_ret(fd, "signalfd")?;
350        Fd::new(fd, "signalfd")
351    }
352
353    /// Register a process-wide handler for a single signal.
354    ///
355    /// This is a low-level wrapper around `sigaction(2)`.
356    ///
357    /// ### Fork Safety
358    /// Signal handlers are inherited across `fork`.
359    ///
360    /// ### Errors
361    /// - `EINVAL`: Invalid signal number.
362    ///
363    /// # Example
364    /// ```no_run
365    /// # use coreshift_core::signal::{SignalRuntime, SIGUSR1};
366    /// extern "C" fn handler(_: i32) {}
367    /// SignalRuntime::register_handler(SIGUSR1, handler).unwrap();
368    /// ```
369    pub fn register_handler(
370        sig: i32,
371        handler: extern "C" fn(i32),
372    ) -> Result<libc::sigaction, CoreError> {
373        let mut action: libc::sigaction = unsafe { std::mem::zeroed() };
374        let mut old_action: libc::sigaction = unsafe { std::mem::zeroed() };
375        action.sa_sigaction = handler as *const () as usize;
376        action.sa_flags = 0;
377        unsafe { libc::sigemptyset(&mut action.sa_mask) };
378
379        let ret = unsafe { libc::sigaction(sig, &action, &mut old_action) };
380        if ret == -1 {
381            let code = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
382            Err(CoreError::sys(code, "sigaction"))
383        } else {
384            Ok(old_action)
385        }
386    }
387
388    /// Reset a signal to its default kernel handler.
389    ///
390    /// ### Errors
391    /// - `EINVAL`: Invalid signal number.
392    pub fn reset_default(sig: i32) -> Result<(), CoreError> {
393        let prev = unsafe { libc::signal(sig, libc::SIG_DFL) };
394        if prev == libc::SIG_ERR {
395            Err(CoreError::sys(
396                std::io::Error::last_os_error().raw_os_error().unwrap_or(0),
397                "signal(SIG_DFL)",
398            ))
399        } else {
400            Ok(())
401        }
402    }
403
404    /// Reset every signal that is currently ignored (`SIG_IGN`) to its default
405    /// disposition (`SIG_DFL`).
406    ///
407    /// `execve(2)` preserves ignored dispositions across the exec, so a child
408    /// that inherits `SIG_IGN` for e.g. `SIGINT`/`SIGQUIT`/`SIGHUP` from a
409    /// backgrounded parent stays immune to terminal control signals forever.
410    /// Interactive shells pass that state on to their own children, which makes
411    /// Ctrl-C (SIGINT) and Ctrl-\ (SIGQUIT) unable to interrupt foreground
412    /// jobs. A spawned process should start with normal signal handling unless
413    /// explicitly configured otherwise.
414    ///
415    /// Called from the spawn child before `execve`; `SIGKILL`/`SIGSTOP`
416    /// (which cannot be changed) are skipped. Errors are tolerated — the goal
417    /// is best-effort normalization, not a spawn failure.
418    pub fn reset_ignored_to_default() {
419        const MAX_SIG: i32 = 64; // Linux NSIG - 1 (signals 1..=64; libc::NSIG is not exported on all targets)
420        for sig in 1..=MAX_SIG {
421            if sig == libc::SIGKILL || sig == libc::SIGSTOP {
422                continue;
423            }
424            let mut cur: libc::sigaction = unsafe { std::mem::zeroed() };
425            if unsafe { libc::sigaction(sig, std::ptr::null(), &mut cur) } != 0 {
426                continue;
427            }
428            if cur.sa_sigaction == libc::SIG_IGN {
429                unsafe { libc::signal(sig, libc::SIG_DFL) };
430            }
431        }
432    }
433}
434
435/// RAII guard restoring the previous thread signal mask on drop.
436///
437/// Produced by [`SignalRuntime::blocked`]; used to close the signal-handler
438/// swap race in [`install_shutdown_flag_guard`] (CORE-M11).
439pub struct BlockedSignals {
440    previous: SignalSet,
441}
442
443impl Drop for BlockedSignals {
444    fn drop(&mut self) {
445        let _ = SignalRuntime::restore_current_thread(&self.previous);
446    }
447}