rigless 0.0.2

Virtual hardware-in-the-loop runtime: run unmodified firmware on an emulated MCU with every peripheral, chip model, and unit of virtual time in Rust; needs the rigless-sidecar package from rigless-labs/qemu
Documentation
// Copyright (c) 2026 Rigless Labs.
// SPDX-License-Identifier: Apache-2.0
//! SIGINT and SIGTERM end a run through its error path, so every `Drop` runs, and the process then dies of the
//! signal the way it would have with no handler at all.
#![expect(
    unsafe_code,
    reason = "signal dispositions are set through libc; the handler only stores to atomics"
)]

use std::io;
use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};

/// Set by the handler; the sidecar's next wait ends with `BackendError::Cancelled`.
pub(crate) static CANCEL: AtomicBool = AtomicBool::new(false);
static RECEIVED: AtomicI32 = AtomicI32::new(0);

const ENDING_SIGNALS: [libc::c_int; 2] = [libc::SIGINT, libc::SIGTERM];

extern "C" fn record(signal: libc::c_int) {
    RECEIVED.store(signal, Ordering::Relaxed);
    CANCEL.store(true, Ordering::Release);
}

/// Routes SIGINT and SIGTERM to [`CANCEL`]. A signal the parent left ignored, as a shell does for a background
/// job, stays ignored.
pub(crate) fn install() -> io::Result<()> {
    for signal in ENDING_SIGNALS {
        // SAFETY: all zero is a valid sigaction: the empty mask, no flags, and the default handler.
        let mut current: libc::sigaction = unsafe { std::mem::zeroed() };
        // SAFETY: `current` is a sigaction to write into, and a null new action only reads the current one.
        if unsafe { libc::sigaction(signal, std::ptr::null(), &raw mut current) } != 0 {
            return Err(io::Error::last_os_error());
        }
        if current.sa_sigaction == libc::SIG_IGN {
            continue;
        }
        // SAFETY: as for `current`.
        let mut action: libc::sigaction = unsafe { std::mem::zeroed() };
        let handler: extern "C" fn(libc::c_int) = record;
        action.sa_sigaction = handler as libc::sighandler_t;
        action.sa_flags = libc::SA_RESTART;
        // SAFETY: `record` only stores to atomics, which is async-signal-safe, and `action` is fully initialised.
        if unsafe { libc::sigaction(signal, &raw const action, std::ptr::null_mut()) } != 0 {
            return Err(io::Error::last_os_error());
        }
    }
    Ok(())
}

/// Restores the recorded signal's default disposition and raises it, so the parent sees a process ended by that
/// signal. Returns only if the signal did not end the process.
pub(crate) fn die_of_recorded_signal() -> io::Error {
    let signal = RECEIVED.load(Ordering::Relaxed);
    // SAFETY: SIG_DFL is a valid disposition for any signal.
    unsafe { libc::signal(signal, libc::SIG_DFL) };
    // SAFETY: raising a signal has no memory-safety conditions.
    unsafe { libc::raise(signal) };
    io::Error::other(format!(
        "signal {signal} was raised but did not end the process"
    ))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn an_installed_handler_records_the_signal_and_sets_cancel() {
        install().unwrap();
        // SAFETY: `install` routed SIGINT to `record`, which only stores to atomics.
        assert_eq!(unsafe { libc::raise(libc::SIGINT) }, 0);
        assert!(CANCEL.load(Ordering::Acquire));
        assert_eq!(RECEIVED.load(Ordering::Relaxed), libc::SIGINT);
    }
}