Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bin_tests/src/modes/behavior.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ pub fn get_behavior(mode_str: &str) -> Box<dyn Behavior> {
"panic_hook_string" => Box::new(test_014_panic_hook_string::Test),
"panic_hook_unknown_type" => Box::new(test_015_panic_hook_unknown_type::Test),
"errno_preservation" => Box::new(test_016_errno_preservation::Test),
"sigchld_sigpipe_saguard" => Box::new(test_017_sigchld_sigpipe_saguard::Test),
"runtime_preload_logger" => Box::new(test_000_donothing::Test),
_ => panic!("Unknown mode: {mode_str}"),
}
Expand Down
1 change: 1 addition & 0 deletions bin_tests/src/modes/unix/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ pub mod test_013_panic_hook_after_fork;
pub mod test_014_panic_hook_string;
pub mod test_015_panic_hook_unknown_type;
pub mod test_016_errno_preservation;
pub mod test_017_sigchld_sigpipe_saguard;
175 changes: 175 additions & 0 deletions bin_tests/src/modes/unix/test_017_sigchld_sigpipe_saguard.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/
// SPDX-License-Identifier: Apache-2.0
//
// Integration test for SaGuard during crash handling.
//
// Verifies that SIGCHLD and SIGPIPE handlers installed by the application are
// not invoked during crash handling (because SaGuard suppresses them), even
// though the crash handler spawns child processes (SIGCHLD) and writes to
// pipes (SIGPIPE).
//
// Expected operation:
// 1. setup() installs custom SIGCHLD and SIGPIPE handlers that write marker files when invoked
// 2. pre() verifies the handlers actually work as a baseline check
// 3. post() cleans up marker files and sets the output targets to "crash_sigchld" and
// "crash_sigpipe" files. If the SaGuard is working, the crash handler will suppress these
// signals and the marker files will not be created
//
// The integration test asserts that "crash_sigchld" and "crash_sigpipe" do
// not exist after the crash
use crate::modes::behavior::Behavior;
use crate::modes::behavior::{
atom_to_clone, file_write_msg, fileat_content_equals, remove_permissive, removeat_permissive,
set_atomic, trigger_sigpipe,
};

use libc;
use libdd_crashtracker::CrashtrackerConfiguration;
use std::path::{Path, PathBuf};
use std::sync::atomic::AtomicPtr;

pub const CRASH_SIGCHLD_FILENAME: &str = "crash_sigchld";
pub const CRASH_SIGPIPE_FILENAME: &str = "crash_sigpipe";

pub struct Test;

impl Behavior for Test {
fn setup(
&self,
output_dir: &Path,
_config: &mut CrashtrackerConfiguration,
) -> anyhow::Result<()> {
setup(output_dir)
}

fn pre(&self, output_dir: &Path) -> anyhow::Result<()> {
// verify SIGCHLD handler fires
verify_sigchld(output_dir, "pre_sigchld.check")?;

// verify SIGPIPE handler fires
verify_sigpipe(output_dir, "pre_sigpipe.check")?;

Ok(())
}

fn post(&self, output_dir: &Path) -> anyhow::Result<()> {
removeat_permissive(output_dir, CRASH_SIGCHLD_FILENAME);
removeat_permissive(output_dir, CRASH_SIGPIPE_FILENAME);

// Point the handlers at the crash-time marker files
// If SaGuard works, these files wont be created during crash handling
set_atomic(
&SIGCHLD_OUTPUT_FILE,
output_dir.join(CRASH_SIGCHLD_FILENAME),
);
set_atomic(
&SIGPIPE_OUTPUT_FILE,
output_dir.join(CRASH_SIGPIPE_FILENAME),
);
Ok(())
}
}

static SIGCHLD_OUTPUT_FILE: AtomicPtr<PathBuf> = AtomicPtr::new(std::ptr::null_mut());
static SIGPIPE_OUTPUT_FILE: AtomicPtr<PathBuf> = AtomicPtr::new(std::ptr::null_mut());

extern "C" fn sigchld_handler(_: libc::c_int) {
let ofile = match atom_to_clone(&SIGCHLD_OUTPUT_FILE) {
Ok(f) => f,
_ => return,
};
file_write_msg(&ofile, "SIGCHLD_FIRED").ok();
}

extern "C" fn sigpipe_handler(_: libc::c_int) {
let ofile = match atom_to_clone(&SIGPIPE_OUTPUT_FILE) {
Ok(f) => f,
_ => return,
};
file_write_msg(&ofile, "SIGPIPE_FIRED").ok();
}

fn verify_sigchld(output_dir: &Path, filename: &str) -> anyhow::Result<()> {
set_atomic(&SIGCHLD_OUTPUT_FILE, output_dir.join(filename));

match unsafe { libc::fork() } {
-1 => anyhow::bail!("Failed to fork"),
0 => unsafe {
libc::_exit(0);
},
_ => {
// Wait for child to exit
loop {
let mut status: libc::c_int = 0;
if -1 == unsafe { libc::waitpid(-1, &mut status, libc::WNOHANG) } {
break;
}
}
}
}

match fileat_content_equals(output_dir, filename, "SIGCHLD_FIRED") {
Ok(true) => (),
_ => anyhow::bail!("SIGCHLD handler did not fire during baseline check"),
}

remove_permissive(&output_dir.join(filename));
set_atomic(&SIGCHLD_OUTPUT_FILE, output_dir.join("INVALID"));
Ok(())
}

fn verify_sigpipe(output_dir: &Path, filename: &str) -> anyhow::Result<()> {
set_atomic(&SIGPIPE_OUTPUT_FILE, output_dir.join(filename));

trigger_sigpipe()?;

match fileat_content_equals(output_dir, filename, "SIGPIPE_FIRED") {
Ok(true) => (),
_ => anyhow::bail!("SIGPIPE handler did not fire during baseline check"),
}

remove_permissive(&output_dir.join(filename));
set_atomic(&SIGPIPE_OUTPUT_FILE, output_dir.join("INVALID"));
Ok(())
}

pub fn setup(output_dir: &Path) -> anyhow::Result<()> {
let mut sigset: libc::sigset_t = unsafe { std::mem::zeroed() };
unsafe {
libc::sigemptyset(&mut sigset);
}

// Install SIGCHLD handler
let sigchld_action = libc::sigaction {
sa_sigaction: sigchld_handler as *const () as usize,
sa_mask: sigset,
sa_flags: libc::SA_RESTART | libc::SA_SIGINFO,
#[cfg(target_os = "linux")]
sa_restorer: None,
};
unsafe {
if libc::sigaction(libc::SIGCHLD, &sigchld_action, std::ptr::null_mut()) != 0 {
anyhow::bail!("Failed to set up SIGCHLD handler");
}
}

// Install SIGPIPE handler
let sigpipe_action = libc::sigaction {
sa_sigaction: sigpipe_handler as *const () as usize,
sa_mask: sigset,
sa_flags: libc::SA_RESTART | libc::SA_SIGINFO,
#[cfg(target_os = "linux")]
sa_restorer: None,
};
unsafe {
if libc::sigaction(libc::SIGPIPE, &sigpipe_action, std::ptr::null_mut()) != 0 {
anyhow::bail!("Failed to set up SIGPIPE handler");
}
}

// Initialize output file pointers to INVALID
set_atomic(&SIGCHLD_OUTPUT_FILE, output_dir.join("INVALID"));
set_atomic(&SIGPIPE_OUTPUT_FILE, output_dir.join("INVALID"));

Ok(())
}
4 changes: 4 additions & 0 deletions bin_tests/src/test_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub enum TestMode {
RuntimeCallbackFrameInvalidUtf8,
RuntimePreloadLogger,
ErrnoPreservation,
SigChldSigPipeSaGuard,
}

impl TestMode {
Expand All @@ -41,6 +42,7 @@ impl TestMode {
Self::RuntimeCallbackFrameInvalidUtf8 => "runtime_callback_frame_invalid_utf8",
Self::RuntimePreloadLogger => "runtime_preload_logger",
Self::ErrnoPreservation => "errno_preservation",
Self::SigChldSigPipeSaGuard => "sigchld_sigpipe_saguard",
}
}

Expand All @@ -62,6 +64,7 @@ impl TestMode {
Self::RuntimeCallbackFrameInvalidUtf8,
Self::RuntimePreloadLogger,
Self::ErrnoPreservation,
Self::SigChldSigPipeSaGuard,
]
}
}
Expand Down Expand Up @@ -92,6 +95,7 @@ impl std::str::FromStr for TestMode {
"runtime_callback_frame_invalid_utf8" => Ok(Self::RuntimeCallbackFrameInvalidUtf8),
"runtime_preload_logger" => Ok(Self::RuntimePreloadLogger),
"errno_preservation" => Ok(Self::ErrnoPreservation),
"sigchld_sigpipe_saguard" => Ok(Self::SigChldSigPipeSaGuard),
_ => Err(format!("Unknown test mode: {}", s)),
}
}
Expand Down
42 changes: 41 additions & 1 deletion bin_tests/tests/crashtracker_bin_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,46 @@ fn test_crash_tracking_bin_errno_preservation() {
run_crash_test_with_artifacts(&config, &artifacts_map, &artifacts, validator).unwrap();
}

#[test]
#[cfg_attr(miri, ignore)]
fn test_crash_tracking_bin_sigchld_sigpipe_saguard() {
use bin_tests::modes::unix::test_017_sigchld_sigpipe_saguard::{
CRASH_SIGCHLD_FILENAME, CRASH_SIGPIPE_FILENAME,
};

let config = CrashTestConfig::new(
BuildProfile::Release,
TestMode::SigChldSigPipeSaGuard,
CrashType::NullDeref,
);
let artifacts = StandardArtifacts::new(config.profile);
let artifacts_map = build_artifacts(&artifacts.as_slice()).unwrap();

let validator: ValidatorFn = Box::new(|_payload, fixtures| {
// SaGuard shouldve suppressed SIGCHLD and SIGPIPE during crash handling,
// so the marker files shouldnt exist
let sigchld_path = fixtures.output_dir.join(CRASH_SIGCHLD_FILENAME);
let sigpipe_path = fixtures.output_dir.join(CRASH_SIGPIPE_FILENAME);

assert!(
!sigchld_path.exists(),
"SIGCHLD handler fired during crash handling; SaGuard did not suppress it. \
File {:?} should not exist.",
sigchld_path
);
assert!(
!sigpipe_path.exists(),
"SIGPIPE handler fired during crash handling; SaGuard did not suppress it. \
File {:?} should not exist.",
sigpipe_path
);

Ok(())
});

run_crash_test_with_artifacts(&config, &artifacts_map, &artifacts, validator).unwrap();
}

#[test]
#[cfg_attr(miri, ignore)]
fn test_crash_tracking_bin_unhandled_exception() {
Expand Down Expand Up @@ -299,7 +339,7 @@ fn test_collector_no_allocations_stacktrace_modes() {
let _ = fs::remove_file(&detector_log_path);

let config = CrashTestConfig::new(
BuildProfile::Debug,
BuildProfile::Release,
TestMode::RuntimePreloadLogger,
CrashType::NullDeref,
)
Expand Down
10 changes: 10 additions & 0 deletions libdd-crashtracker/src/collector/crash_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

use super::collector_manager::Collector;
use super::receiver_manager::Receiver;
use super::saguard::SaGuard;
use super::signal_handler_manager::chain_signal_handler;
use crate::crash_info::Metadata;
use crate::shared::configuration::CrashtrackerConfiguration;
Expand Down Expand Up @@ -263,6 +264,15 @@ fn handle_posix_signal_impl(
return Ok(());
}

// Block SIGCHLD and SIGPIPE during crash handling. Our collector spawns child processes
// and writes to pipes, both of which can generate these signals. Rather than risk
// re-entering a signal handler or aborting due to SIGPIPE, suppress them for the
// duration and restore when the guard drops
let _sa_guard = SaGuard::new(&[
nix::sys::signal::Signal::SIGCHLD,
nix::sys::signal::Signal::SIGPIPE,
]);

// Take config and metadata out of global storage.
// We borrow via raw pointer and intentionally leak (do not reconstruct the Box) to avoid
// calling `drop`, and therefore `free`, inside a signal handler, which is not
Expand Down
1 change: 1 addition & 0 deletions libdd-crashtracker/src/collector/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ mod crash_handler;
mod emitters;
mod process_handle;
mod receiver_manager;
mod saguard;
mod signal_handler_manager;
mod spans;

Expand Down
73 changes: 73 additions & 0 deletions libdd-crashtracker/src/collector/saguard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,76 @@ impl<const N: usize> Drop for SaGuard<N> {
);
}
}

#[cfg(test)]
mod tests {
use super::*;
use nix::sys::signal::{self, Signal};
use nix::unistd::Pid;
use std::sync::atomic::{AtomicBool, Ordering};

#[test]
#[cfg_attr(miri, ignore)]
fn signal_is_ignored_while_guard_is_active() {
let _guard = SaGuard::<1>::new(&[Signal::SIGUSR1]).unwrap();

// Send SIGUSR1 to the process. The default action is to terminate, so if
// the guard didn't set SIG_IGN this test process would die
signal::kill(Pid::this(), Signal::SIGUSR1).unwrap();
}

/// After the guard is dropped, the original handler should be restored.
/// Install a custom handler, create a guard,drop the guard, then send the
/// signal and verify the custom handler fires
#[test]
#[cfg_attr(miri, ignore)]
fn original_handler_restored_after_drop() {
static HANDLER_CALLED: AtomicBool = AtomicBool::new(false);

extern "C" fn custom_handler(_: libc::c_int) {
HANDLER_CALLED.store(true, Ordering::SeqCst);
}

// Install a custom handler
let custom_action = SigAction::new(
SigHandler::Handler(custom_handler),
SaFlags::empty(),
signal::SigSet::empty(),
);
let prev = unsafe { signal::sigaction(Signal::SIGUSR2, &custom_action).unwrap() };

// Create then drop the guard (dropped when out of scope)
{
let _guard = SaGuard::<1>::new(&[Signal::SIGUSR2]).unwrap();
signal::kill(Pid::this(), Signal::SIGUSR2).unwrap();
assert!(
!HANDLER_CALLED.load(Ordering::SeqCst),
"custom handler should not fire while guard is active"
);
}
// Guard is dropped; custom handler should be restored
HANDLER_CALLED.store(false, Ordering::SeqCst);
unsafe {
libc::raise(Signal::SIGUSR2 as libc::c_int);
}
assert!(
HANDLER_CALLED.load(Ordering::SeqCst),
"custom handler should fire after guard is dropped"
);

// Restore original handler
unsafe {
signal::sigaction(Signal::SIGUSR2, &prev).unwrap();
}
}

#[test]
#[cfg_attr(miri, ignore)]
fn multiple_signals_ignored() {
let _guard = SaGuard::<2>::new(&[Signal::SIGUSR1, Signal::SIGUSR2]).unwrap();

// Both signals should be safely ignored
signal::kill(Pid::this(), Signal::SIGUSR1).unwrap();
signal::kill(Pid::this(), Signal::SIGUSR2).unwrap();
}
}
Loading