diff --git a/Cargo.lock b/Cargo.lock index 6bb527bf96..081f5256c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -477,6 +477,7 @@ version = "0.1.0" dependencies = [ "anyhow", "current_platform", + "errno", "hex", "libc", "libdd-common", @@ -2974,6 +2975,7 @@ dependencies = [ "criterion", "cxx", "cxx-build", + "errno", "goblin", "http", "libc", diff --git a/bin_tests/Cargo.toml b/bin_tests/Cargo.toml index 7182075b48..43e40161e4 100644 --- a/bin_tests/Cargo.toml +++ b/bin_tests/Cargo.toml @@ -21,6 +21,7 @@ tempfile = "3.3" serde_json = { version = "1.0" } strum = { version = "0.26.2", features = ["derive"] } libc = "0.2" +errno = "0.3" nix = { version = "0.29", features = ["signal", "socket"] } hex = "0.4" os_info = "3.14.0" diff --git a/bin_tests/src/modes/behavior.rs b/bin_tests/src/modes/behavior.rs index e8b3f340c3..969f8dc59f 100644 --- a/bin_tests/src/modes/behavior.rs +++ b/bin_tests/src/modes/behavior.rs @@ -137,6 +137,7 @@ pub fn get_behavior(mode_str: &str) -> Box { "panic_hook_after_fork" => Box::new(test_013_panic_hook_after_fork::Test), "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), "runtime_preload_logger" => Box::new(test_000_donothing::Test), _ => panic!("Unknown mode: {mode_str}"), } diff --git a/bin_tests/src/modes/unix/mod.rs b/bin_tests/src/modes/unix/mod.rs index 2b8c2b0f2d..73638943c2 100644 --- a/bin_tests/src/modes/unix/mod.rs +++ b/bin_tests/src/modes/unix/mod.rs @@ -16,3 +16,4 @@ pub mod test_012_runtime_callback_frame_invalid_utf8; 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; diff --git a/bin_tests/src/modes/unix/test_016_errno_preservation.rs b/bin_tests/src/modes/unix/test_016_errno_preservation.rs new file mode 100644 index 0000000000..b821f06d5c --- /dev/null +++ b/bin_tests/src/modes/unix/test_016_errno_preservation.rs @@ -0,0 +1,118 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 +// +// Checks that crashtracker's signal handler preserves errno across its +// execution. The handler saves errno on entry and restores it before chaining, +// so a chained handler should see the same errno that was current when the +// signal fired. +// +// Expected operation +// 1. post() sets errno to EXPECTED_ERRNO (42) +// 2. SIGSEGV is triggered +// 3. crashtracker handles the crash, then restores errno and chains this test's SIGSEGV handler +// 4. this test's SIGSEGV handler reads errno, writes either "PRESERVED" or "MISMATCHED" to +// ERRNO_STATUS_FILENAME, then raises SIGABRT to exit +// +// The integration test reads ERRNO_STATUS_FILENAME and asserts "PRESERVED" +use crate::modes::behavior::Behavior; + +use errno::{errno, set_errno, Errno}; +use libc; +use libdd_crashtracker::CrashtrackerConfiguration; +use nix::{ + sys::signal::{self, kill, SaFlags, SigAction, SigHandler, SigSet, Signal}, + unistd::Pid, +}; +use std::ffi::CString; +use std::path::Path; +use std::sync::atomic::{AtomicPtr, Ordering}; + +/// The errno value we set before the crash and expect to see in the chained handler. +const EXPECTED_ERRNO: i32 = 42; + +pub const ERRNO_STATUS_FILENAME: &str = "errno_status"; +static STATUS_PATH: AtomicPtr = AtomicPtr::new(std::ptr::null_mut()); + +pub struct Test; + +impl Behavior for Test { + fn setup( + &self, + output_dir: &Path, + _config: &mut CrashtrackerConfiguration, + ) -> anyhow::Result<()> { + let path = output_dir.join(ERRNO_STATUS_FILENAME); + let cpath = CString::new(path.as_os_str().as_encoded_bytes())?; + crate::modes::behavior::set_atomic(&STATUS_PATH, cpath); + setup() + } + + fn pre(&self, _output_dir: &Path) -> anyhow::Result<()> { + Ok(()) + } + + fn post(&self, _output_dir: &Path) -> anyhow::Result<()> { + // Set errno to known value right before crash + set_errno(Errno(EXPECTED_ERRNO)); + Ok(()) + } +} + +extern "C" fn segv_sigaction( + _signum: i32, + _sig_info: *mut libc::siginfo_t, + _ucontext: *mut libc::c_void, +) { + let actual_errno = errno().0; + + let path_ptr = STATUS_PATH.load(Ordering::SeqCst); + if !path_ptr.is_null() { + let cpath = unsafe { &*path_ptr }; + // open/write/close are async signal safe + unsafe { + let fd = libc::open( + cpath.as_ptr(), + libc::O_WRONLY | libc::O_CREAT | libc::O_TRUNC, + 0o644, + ); + if fd >= 0 { + let msg = if actual_errno == EXPECTED_ERRNO { + b"PRESERVED" as &[u8] + } else { + b"MISMATCHED" + }; + libc::write(fd, msg.as_ptr() as *const libc::c_void, msg.len()); + libc::close(fd); + } + } + } + + let _ = kill(Pid::this(), Signal::SIGABRT); +} + +extern "C" fn abort_sigaction( + _signum: i32, + _sig_info: *mut libc::siginfo_t, + _ucontext: *mut libc::c_void, +) { + unsafe { + libc::_exit(128 + _signum); + } +} + +pub fn setup() -> anyhow::Result<()> { + let sig_action = SigAction::new( + SigHandler::SigAction(segv_sigaction), + SaFlags::empty(), + SigSet::empty(), + ); + let _ = unsafe { signal::sigaction(signal::SIGSEGV, &sig_action) }?; + + let sig_action = SigAction::new( + SigHandler::SigAction(abort_sigaction), + SaFlags::empty(), + SigSet::empty(), + ); + let _ = unsafe { signal::sigaction(signal::SIGABRT, &sig_action) }?; + Ok(()) +} diff --git a/bin_tests/src/test_types.rs b/bin_tests/src/test_types.rs index ec3faf235c..1c47aac864 100644 --- a/bin_tests/src/test_types.rs +++ b/bin_tests/src/test_types.rs @@ -19,6 +19,7 @@ pub enum TestMode { RuntimeCallbackString, RuntimeCallbackFrameInvalidUtf8, RuntimePreloadLogger, + ErrnoPreservation, } impl TestMode { @@ -39,6 +40,7 @@ impl TestMode { Self::RuntimeCallbackString => "runtime_callback_string", Self::RuntimeCallbackFrameInvalidUtf8 => "runtime_callback_frame_invalid_utf8", Self::RuntimePreloadLogger => "runtime_preload_logger", + Self::ErrnoPreservation => "errno_preservation", } } @@ -59,6 +61,7 @@ impl TestMode { Self::RuntimeCallbackString, Self::RuntimeCallbackFrameInvalidUtf8, Self::RuntimePreloadLogger, + Self::ErrnoPreservation, ] } } @@ -88,6 +91,7 @@ impl std::str::FromStr for TestMode { "runtime_callback_string" => Ok(Self::RuntimeCallbackString), "runtime_callback_frame_invalid_utf8" => Ok(Self::RuntimeCallbackFrameInvalidUtf8), "runtime_preload_logger" => Ok(Self::RuntimePreloadLogger), + "errno_preservation" => Ok(Self::ErrnoPreservation), _ => Err(format!("Unknown test mode: {}", s)), } } diff --git a/bin_tests/tests/crashtracker_bin_test.rs b/bin_tests/tests/crashtracker_bin_test.rs index 88f2a8a5e5..230e10cd47 100644 --- a/bin_tests/tests/crashtracker_bin_test.rs +++ b/bin_tests/tests/crashtracker_bin_test.rs @@ -96,6 +96,33 @@ fn run_standard_crash_test_refactored( // These tests below use the new infrastructure but require custom validation logic // that doesn't fit the simple macro-generated pattern. +#[test] +#[cfg_attr(miri, ignore)] +fn test_crash_tracking_bin_errno_preservation() { + use bin_tests::modes::unix::test_016_errno_preservation::ERRNO_STATUS_FILENAME; + + let config = CrashTestConfig::new( + BuildProfile::Release, + TestMode::ErrnoPreservation, + CrashType::NullDeref, + ); + let artifacts = StandardArtifacts::new(config.profile); + let artifacts_map = build_artifacts(&artifacts.as_slice()).unwrap(); + + let validator: ValidatorFn = Box::new(|_payload, fixtures| { + let status_path = fixtures.output_dir.join(ERRNO_STATUS_FILENAME); + let content = fs::read_to_string(&status_path) + .context("reading errno_status file; signal handler may not have written it")?; + assert_eq!( + content, "PRESERVED", + "errno was not preserved across crashtracker signal handler (got {content:?})" + ); + Ok(()) + }); + + run_crash_test_with_artifacts(&config, &artifacts_map, &artifacts, validator).unwrap(); +} + #[test] #[cfg_attr(miri, ignore)] fn test_crash_tracking_bin_unhandled_exception() { diff --git a/libdd-crashtracker/Cargo.toml b/libdd-crashtracker/Cargo.toml index b4e34250d0..ebfa26a737 100644 --- a/libdd-crashtracker/Cargo.toml +++ b/libdd-crashtracker/Cargo.toml @@ -48,6 +48,7 @@ libdd-libunwind-sys = { version = "0.1.0", path = "../libdd-libunwind-sys" } anyhow = "1.0" chrono = {version = "0.4", default-features = false, features = ["std", "clock", "serde"]} cxx = { version = "1.0", optional = true } +errno = "0.3" libdd-common = { version = "3.0.0", path = "../libdd-common" } libdd-telemetry = { version = "3.0.0", path = "../libdd-telemetry" } http = "1.1" diff --git a/libdd-crashtracker/src/collector/crash_handler.rs b/libdd-crashtracker/src/collector/crash_handler.rs index 3a51f0d9ba..ece91c755c 100644 --- a/libdd-crashtracker/src/collector/crash_handler.rs +++ b/libdd-crashtracker/src/collector/crash_handler.rs @@ -9,6 +9,7 @@ use super::signal_handler_manager::chain_signal_handler; use crate::crash_info::Metadata; use crate::shared::configuration::CrashtrackerConfiguration; use crate::StackTrace; +use errno::{errno, set_errno}; use libc::{c_void, siginfo_t, ucontext_t}; use libdd_common::timeout::TimeoutManager; use std::os::fd::OwnedFd; @@ -193,10 +194,17 @@ pub(crate) extern "C" fn handle_posix_sigaction( sig_info: *mut siginfo_t, ucontext: *mut c_void, ) { + // Save errno + let errno = errno(); + // Handle the signal. Note this has a guard to ensure that we only generate // one crash report per process. let _ = handle_posix_signal_impl(sig_info, ucontext as *mut ucontext_t); + + // Restore errno + set_errno(errno); // SAFETY: No preconditions. + unsafe { chain_signal_handler(signum, sig_info, ucontext) }; }