Skip to content

Commit

Permalink
Make unix signal-handler signal-safe
Browse files Browse the repository at this point in the history
Also re-enable it in release-builds.
  • Loading branch information
hulthe committed Nov 13, 2024
1 parent c838835 commit 96038b9
Show file tree
Hide file tree
Showing 3 changed files with 251 additions and 131 deletions.
7 changes: 6 additions & 1 deletion mullvad-daemon/src/exception_logging/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::sync::atomic::AtomicBool;

#[cfg(windows)]
mod win;

Expand All @@ -8,4 +10,7 @@ pub use win::enable;
mod unix;

#[cfg(unix)]
pub use unix::enable;
pub use unix::{enable, set_log_file};

// TODO: name and docs
pub static ENABLE_BACKTRACE: AtomicBool = AtomicBool::new(true);
360 changes: 232 additions & 128 deletions mullvad-daemon/src/exception_logging/unix.rs
Original file line number Diff line number Diff line change
@@ -1,145 +1,249 @@
//! Installs signal handlers to catch critical program faults and logs them. See [`enable`].
//!
//! NOTE: The signal handlers are disabled in release-builds. See docs on
//! `handler::logging_fault_handler` for reasoning.

pub use handler::enable;

#[cfg(not(debug_assertions))]
mod handler {
/// This is a no-op on release-builds; it does NOT install a signal handler.
pub fn enable() {
// No fault handler in release-builds.
//! Install signal handlers to catch critical program faults and log them. See [`enable`].
#![warn(clippy::undocumented_unsafe_blocks)]

use libc::siginfo_t;
use nix::sys::signal::{sigaction, SaFlags, SigAction, SigHandler, SigSet, Signal};

use core::fmt::{self};
use std::{
backtrace::Backtrace,
ffi::{c_int, c_void, CString},
os::fd::{FromRawFd, RawFd},
sync::{
atomic::{AtomicBool, Ordering},
Once, OnceLock,
},
};

use crate::exception_logging::ENABLE_BACKTRACE;

/// Write fault to this file.
static LOG_FILE_PATH: OnceLock<CString> = OnceLock::new();

/// The signals we install handlers for.
const FAULT_SIGNALS: [Signal; 5] = [
// Access to invalid memory address
Signal::SIGBUS,
// Floating point exception
Signal::SIGFPE,
// Illegal instructors
Signal::SIGILL,
// Invalid memory reference
Signal::SIGSEGV,
// Bad syscall
Signal::SIGSYS,
];

/// Set the file path used for fault handler logging.
pub fn set_log_file(file_path: impl Into<CString>) {
if let Err(_file_path) = LOG_FILE_PATH.set(file_path.into()) {
panic!("set_log_file may not be called more than once");
}
}

#[cfg(debug_assertions)]
mod handler {
use libc::siginfo_t;
use nix::sys::signal::{sigaction, SaFlags, SigAction, SigHandler, SigSet, Signal};

use std::{
backtrace::Backtrace,
ffi::{c_int, c_void},
sync::{
atomic::{AtomicBool, Ordering},
Once,
/// Install signal handlers to catch critical program faults, log them, and exit the process.
pub fn enable() {
static INIT_ONCE: Once = Once::new();

INIT_ONCE.call_once(|| {
// XXX: SA_ONSTACK tells the signal handler to use an alternate stack, if one is available.
// The purpose of an alternate stack is to have somewhere to execute the signal handler
// in the case of a stack overflow. I.e. if an alternate stack hasn't been configured,
// stack overflows will silently cause the process to exit with code SIGSEGV.
//
// XXX: `libc::sigaltstack` can be used to set up an alternate stack on the current thread.
// The default behaviour of the Rust runtime is to set up alternate stacks for the
// main thread, and for every thread spawned using `std::thread`. However, Rust will
// not do this if any signal handlers have been configured before the Rust runtime is
// initialized (note: the initialization happens before main() is called). For
// example, if any Go code is linked into this binary, the Go runtime will probably
// be initialized first, and will set up it's own signal handlers.
//
// XXX: Go requires this flag to be set for all signal handlers.
// https://github.com/golang/go/blob/d6fb0ab2/src/os/signal/doc.go
let sig_handler_flags = SaFlags::SA_ONSTACK;

let signal_action = SigAction::new(
SigHandler::SigAction(fault_handler),
sig_handler_flags,
SigSet::empty(),
);

for signal in &FAULT_SIGNALS {
// SAFETY: `fault_handler` is signal-safe.
if let Err(err) = unsafe { sigaction(*signal, &signal_action) } {
log::error!("Failed to install signal handler for {}: {}", signal, err);
}
}
});
}

/// Signal handler to catch signals that are used to indicate unrecoverable errors in the daemon.
extern "C" fn fault_handler(
signum: c_int,
_siginfo: *mut siginfo_t,
_thread_context_ptr: *mut c_void,
) {
// XXX: This function is a signal handler, meaning it must be signal safe.
// For a detailed definition, see https://man7.org/linux/man-pages/man7/signal-safety.7.html
// The short version is:
// - This function must be re-entrant.
// - This function must only call functions that are signal-safe.
// - The man-page provides a list of posix-functions that are signal-safe. (These can be found
// in the `libc`-crate)

let code: c_int = match log_fault_to_file(signum) {
// Signal numbers are positive integers
Ok(()) => signum,

// map error to error-codes
Err(err) => match err {
FaultHandlerErr::UnknownSignal => signum,
FaultHandlerErr::Open => -2,
FaultHandlerErr::Write => -3,
FaultHandlerErr::FSync => -4,
FaultHandlerErr::Reentrancy => -5,
},
};

/// The signals we install handlers for.
const FAULT_SIGNALS: [Signal; 5] = [
// Access to invalid memory address
Signal::SIGBUS,
// Floating point exception
Signal::SIGFPE,
// Illegal instructors
Signal::SIGILL,
// Invalid memory reference
Signal::SIGSEGV,
// Bad syscall
Signal::SIGSYS,
];

/// Install the signal handlers (debug-builds only).
pub fn enable() {
static INIT_ONCE: Once = Once::new();

INIT_ONCE.call_once(|| {
// Setup alt stack for signal handlers to be executed in.
// If the daemon ever needs to be compiled for architectures where memory can't be
// writeable and executable, the following block of code has to be disabled.
// This will also mean that stack overflows may be silent and undetectable
// in logs.
let sig_handler_flags = {
// The kernel will use the first properly aligned address, so alignment is not an
// issue.
let alt_stack = vec![0u8; libc::SIGSTKSZ];
let stack_t = libc::stack_t {
ss_sp: alt_stack.as_ptr() as *mut c_void,
ss_flags: 0,
ss_size: alt_stack.len(),
};
let ret = unsafe { libc::sigaltstack(&stack_t, std::ptr::null_mut()) };
if ret != 0 {
log::error!(
"Failed to set alternative stack: {}",
std::io::Error::last_os_error()
);
SaFlags::empty()
} else {
std::mem::forget(alt_stack);
SaFlags::SA_ONSTACK
}
};
// SIGNAL-SAFETY: This function is listed in `man 7 signal-safety`.
// SAFETY: This function is trivially safe to call.
unsafe { libc::_exit(code) }
}

let signal_action = SigAction::new(
SigHandler::SigAction(fault_handler),
sig_handler_flags,
SigSet::empty(),
);

for signal in &FAULT_SIGNALS {
// SAFETY: fault_handler is NOT signal-safe (see logging_fault_handler). We still
// use it, but only in development builds because it makes debugging
// easier.
if let Err(err) = unsafe { sigaction(*signal, &signal_action) } {
log::error!("Failed to install signal handler for {}: {}", signal, err);
}
}
});
/// Call from a signal handler to try to print the signal (and optionally a backtrace).
///
/// The output is written to stdout, and to a file if [set_exception_logging_file] was called.
fn log_fault_to_file(signum: c_int) -> Result<(), FaultHandlerErr> {
// XXX: This function must be signal-safe. See notes in `fn fault_handler`

// Guard against reentrancy, which can happen if this fault handler triggers another fault.
static REENTRANT: AtomicBool = AtomicBool::new(false);
if !REENTRANT.swap(true, Ordering::SeqCst) {
return Err(FaultHandlerErr::Reentrancy);
}

/// Signal handler to catch signals that are used to indicate unrecoverable errors in the
/// daemon.
extern "C" fn fault_handler(
signum: c_int,
_siginfo: *mut siginfo_t,
_thread_context_ptr: *mut c_void,
) {
// SAFETY: This function is known to be potentially unsound and should not be used in prod,
// but we keep it in debug-builds because debugging SIGSEGV faults is a PITA.
// See logging_fault_handler docs for more info.
unsafe { logging_fault_handler(signum) };
// SAFETY: calling `write` on stdout is always safe, even if it's been closed.
let stdout = unsafe { LibcWriter::from_raw_fd(libc::STDOUT_FILENO) };
log_fault_to_writer(signum, stdout)?;

// SIGNAL-SAFETY: OnceLock::get is atomic and non-blocking.
if let Some(log_file) = LOG_FILE_PATH.get() {
let open_flags = libc::O_WRONLY | libc::O_APPEND;

// SIGNAL-SAFETY: This function is listed in `man 7 signal-safety`.
// SAFETY: `path` is a null-terminated string.
let log_file: RawFd = match unsafe { libc::open(log_file.as_ptr(), open_flags) } {
..0 => return Err(FaultHandlerErr::Open),
fd => fd,
};

// SAFETY: `log_file` is an open file descriptor; it's not closed until the process exits.
let mut log_file = unsafe { LibcWriter::from_raw_fd(log_file) };

log_fault_to_writer(signum, &mut log_file)?;
log_file.flush()?;
}

/// Call from a signal handler to [log] the signal, and the current backtrace.
///
/// See also: [fault_handler].
///
/// # SAFETY
/// Calling this function from a signal handler is potentially unsound. This is because is
/// performs functions that are not "signal-safe", for example: `process::exit` and writing
/// to to stdout. See also: <https://man7.org/linux/man-pages/man7/signal-safety.7.html>.
///
/// For this reason, this handler is only used in debug-builds.
// TODO: Consider rewriting this function to e.g. use a pipe to exfiltrate the backtrace to
// another process that can write the backtrace to tho log file. `write` is signal-safe.
unsafe fn logging_fault_handler(signum: c_int) {
// Guard against reentrancy, which can happen if this fault handler triggers another fault.
static REENTRANCY_GUARD: AtomicBool = AtomicBool::new(false);
if REENTRANCY_GUARD.swap(true, Ordering::SeqCst) {
// `process::abort` is signal-safe, unlike `process::exit`.
std::process::abort();
Ok(())
}

/// Call from a signal handler to try to write the signal (and optionally a backtrace).
///
/// The output is written to the writer passed in as an argument.
fn log_fault_to_writer(signum: c_int, mut w: impl fmt::Write) -> Result<(), FaultHandlerErr> {
// SIGNAL-SAFETY: Signal::try_from(i32) is signal-safe
let signal: Signal = match Signal::try_from(signum) {
Ok(signal) => signal,
Err(_) => {
// SIGNAL-SAFETY: formatting an i32 is signal-safe.
writeln!(w, "Signal handler triggered by unknown signal: {signum}")?;
return Err(FaultHandlerErr::UnknownSignal);
}
};

let signal: Signal = match Signal::try_from(signum) {
Ok(signal) => signal,
Err(err) => {
log::error!(
"Signal handler triggered by unknown signal {}, exiting: {}",
signum,
err
);
std::process::exit(2);
}
};
// SIGNAL-SAFETY:
// `writeln` resolves to calls to <LibcWriter as io::Write>::write, which is signal-safe
// as_str is const and formatting a &str is signal-safe
writeln!(w, "Caught signal {}", signal.as_str())?;
writeln!(w, "Backtrace:")?;

// Formatting a `Backtrace` is NOT signal-safe.
if ENABLE_BACKTRACE.load(Ordering::SeqCst) {
writeln!(w, "{}", Backtrace::force_capture())?;
}

Ok(())
}

enum FaultHandlerErr {
/// Signal handler received an unknown signal number.
UnknownSignal,

/// A call to `libc::open` failed.
Open,

log::error!("Caught signal {}", signal);
log::error!("Backtrace:");
for line in format!("{}", Backtrace::force_capture()).lines() {
log::error!("{line}");
/// A call to `libc::write` failed.
Write,

/// A call to `libc::fsync` failed.
FSync,

/// Signal handler was called reentrantly.
Reentrancy,
}

/// A wrapper type that implements `fmt::Write` for a file descriptor through `libc::write`.
struct LibcWriter {
file_descriptor: RawFd,
}

impl LibcWriter {
/// Call `libc::fsync` on the file descriptor.
pub fn flush(&self) -> Result<(), FaultHandlerErr> {
match unsafe { libc::fsync(self.file_descriptor) } {
..0 => Err(FaultHandlerErr::FSync),
_ => Ok(()),
}
std::process::exit(2);
}
}

impl FromRawFd for LibcWriter {
/// Wrap a file descriptor in a [LibcWriter].
///
/// # Safety
/// The file descriptor must refer to an opened file, or to stdout/stderr.
/// In the case of a file, it must remain open for the lifetime of the [LibcWriter].
unsafe fn from_raw_fd(file_descriptor: RawFd) -> Self {
Self { file_descriptor }
}
}

impl fmt::Write for LibcWriter {
fn write_str(&mut self, string: &str) -> fmt::Result {
let mut bytes = string.as_bytes();
while !bytes.is_empty() {
let ptr = bytes.as_ptr() as *const c_void;

// SAFETY:
// - `self.file_descriptor` is an open file descriptor.
// - `ptr` points to the start of `bytes`
let n = match unsafe { libc::write(self.file_descriptor, ptr, bytes.len()) } {
n if n > 0 => n as usize,
_ => return Err(fmt::Error),
};

bytes = &bytes[n..];
}

Ok(())
}
}

impl From<fmt::Error> for FaultHandlerErr {
/// Convert a [fmt::Error] into a [FaultHandlerErr::Write].
/// [fmt::Error] is returned by [fmt::Write]-methods.
fn from(_: fmt::Error) -> Self {
FaultHandlerErr::Write
}
}
Loading

0 comments on commit 96038b9

Please sign in to comment.