Skip to content
Draft
65 changes: 58 additions & 7 deletions bin_tests/src/bin/crashing_test_app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ mod unix {
use anyhow::ensure;
use anyhow::Context;
use std::env;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;

use libdd_common::{tag, Endpoint};
Expand All @@ -23,8 +25,25 @@ mod unix {

const TEST_COLLECTOR_TIMEOUT: Duration = Duration::from_secs(10);

#[inline(never)]
unsafe fn fn3() {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CrashType {
Segfault,
Panic,
}

impl std::str::FromStr for CrashType {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"segfault" => Ok(CrashType::Segfault),
"panic" => Ok(CrashType::Panic),
_ => anyhow::bail!("Invalid crash type: {s}"),
}
}
}

#[inline(always)]
unsafe fn cause_segfault() {
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
{
std::arch::asm!("mov eax, [0]", options(nostack));
Expand All @@ -37,13 +56,25 @@ mod unix {
}

#[inline(never)]
fn fn2() {
unsafe { fn3() }
fn fn3(crash_type: CrashType) {
match crash_type {
CrashType::Segfault => {
unsafe { cause_segfault() };
}
CrashType::Panic => {
panic!("program panicked");
}
}
}

#[inline(never)]
fn fn1() {
fn2()
fn fn2(crash_type: CrashType) {
fn3(crash_type);
}

#[inline(never)]
fn fn1(crash_type: CrashType) {
fn2(crash_type);
}

#[inline(never)]
Expand All @@ -53,6 +84,7 @@ mod unix {
let output_url = args.next().context("Unexpected number of arguments 1")?;
let receiver_binary = args.next().context("Unexpected number of arguments 2")?;
let output_dir = args.next().context("Unexpected number of arguments 3")?;
let crash_type = args.next().context("Unexpected number of arguments 4")?;
anyhow::ensure!(args.next().is_none(), "unexpected extra arguments");

let stderr_filename = format!("{output_dir}/out.stderr");
Expand Down Expand Up @@ -88,6 +120,19 @@ mod unix {
.collect(),
};

let crash_type = crash_type.parse().context("Invalid crash type")?;
let is_panic_mode = matches!(crash_type, CrashType::Panic);

let called_panic_hook = Arc::new(AtomicBool::new(false));
let old_hook = std::panic::take_hook();
if is_panic_mode {
let called_panic_hook_clone = Arc::clone(&called_panic_hook);
std::panic::set_hook(Box::new(move |panic_info| {
called_panic_hook_clone.store(true, Ordering::SeqCst);
old_hook(panic_info);
}));
}

crashtracker::init(
config,
CrashtrackerReceiverConfig::new(
Expand All @@ -100,7 +145,13 @@ mod unix {
metadata,
)?;

fn1();
fn1(crash_type);

// If the panic hook was chained, it should have been called.
anyhow::ensure!(
!is_panic_mode || called_panic_hook.load(Ordering::SeqCst),
"panic hook was not called"
);
Ok(())
}
}
14 changes: 14 additions & 0 deletions bin_tests/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ pub struct ArtifactsBuild {
pub artifact_type: ArtifactType,
pub build_profile: BuildProfile,
pub triple_target: Option<String>,
pub panic_abort: Option<bool>,
}

fn inner_build_artifact(c: &ArtifactsBuild) -> anyhow::Result<PathBuf> {
Expand All @@ -58,6 +59,19 @@ fn inner_build_artifact(c: &ArtifactsBuild) -> anyhow::Result<PathBuf> {
ArtifactType::ExecutablePackage | ArtifactType::CDylib => build_cmd.arg("-p"),
ArtifactType::Bin => build_cmd.arg("--bin"),
};

if let Some(panic_abort) = c.panic_abort {
if panic_abort {
let existing_rustflags = std::env::var("RUSTFLAGS").unwrap_or_default();
let new_rustflags = if existing_rustflags.is_empty() {
"-C panic=abort".to_string()
} else {
format!("{} -C panic=abort", existing_rustflags)
};
build_cmd.env("RUSTFLAGS", new_rustflags);
}
}

build_cmd.arg(&c.name);

let output = build_cmd.output().unwrap();
Expand Down
3 changes: 3 additions & 0 deletions bin_tests/src/modes/behavior.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@ pub fn get_behavior(mode_str: &str) -> Box<dyn Behavior> {
"runtime_callback_frame_invalid_utf8" => {
Box::new(test_012_runtime_callback_frame_invalid_utf8::Test)
}
"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),
_ => panic!("Unknown mode: {mode_str}"),
}
}
Expand Down
3 changes: 3 additions & 0 deletions bin_tests/src/modes/unix/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ pub mod test_009_prechain_with_abort;
pub mod test_010_runtime_callback_frame;
pub mod test_011_runtime_callback_string;
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;
120 changes: 120 additions & 0 deletions bin_tests/src/modes/unix/test_013_panic_hook_after_fork.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/
// SPDX-License-Identifier: Apache-2.0
//
// Test that panic hooks registered before fork() continue to work in child processes.
// This validates that:
// 1. The panic hook survives fork()
// 2. The panic message is captured in the child process
// 3. The crash report is correctly generated
use crate::modes::behavior::Behavior;
use libdd_crashtracker::{self as crashtracker, CrashtrackerConfiguration};
use nix::sys::wait::{waitpid, WaitStatus};
use nix::unistd::Pid;
use std::fs;
use std::path::Path;
use std::time::{Duration, Instant};

pub struct Test;

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

fn pre(&self, output_dir: &Path) -> anyhow::Result<()> {
pre(output_dir)
}

fn post(&self, output_dir: &Path) -> anyhow::Result<()> {
post(output_dir)
}
}

fn pre(output_dir: &Path) -> anyhow::Result<()> {
let old_hook = std::panic::take_hook();
let output_dir = output_dir.to_path_buf();

// Set up a panic hook BEFORE crashtracker::init to verify the hook chain works
std::panic::set_hook(Box::new(move |panic_info| {
// Mark that our custom hook was called by writing a marker file
// This works across fork() because it's persistent storage
let marker_path = output_dir.join("panic_hook_called.marker");
let _ = fs::write(marker_path, "hook was called");

// Call the previous hook (usually the default panic hook)
old_hook(panic_info);
}));

Ok(())
}

fn post(output_dir: &Path) -> anyhow::Result<()> {
match unsafe { libc::fork() } {
-1 => {
anyhow::bail!("Failed to fork");
}
0 => {
// Child - panic with a specific message
// The crashtracker should capture both the panic hook execution
// and the panic message
crashtracker::begin_op(crashtracker::OpTypes::ProfilerCollectingSample)?;

// Give parent time to set up wait
std::thread::sleep(Duration::from_millis(10));

panic!("child panicked after fork - hook should fire");
}
pid => {
// Parent - wait for child to panic and crash
let start_time = Instant::now();
let max_wait = Duration::from_secs(5);

loop {
match waitpid(Pid::from_raw(pid), None)? {
WaitStatus::StillAlive => {
if start_time.elapsed() > max_wait {
anyhow::bail!("Child process did not exit within 5 seconds");
}
std::thread::sleep(Duration::from_millis(10));
}
WaitStatus::Exited(_pid, exit_code) => {
// Child exited - this is what we expect after panic
eprintln!("Child exited with code: {}", exit_code);
break;
}
WaitStatus::Signaled(_pid, signal, _) => {
// Child was killed by signal (also acceptable for panic)
eprintln!("Child killed by signal: {:?}", signal);
break;
}
_ => {
// Other status - continue waiting
}
}
}

// Verify that our custom panic hook was called by checking for the marker file
// This proves that the hook chain works correctly:
// crashtracker's hook -> our custom hook -> default hook
let marker_path = output_dir.join("panic_hook_called.marker");

if !marker_path.exists() {
anyhow::bail!(
"Custom panic hook was not called - hook chaining failed! \
Expected marker file at: {}",
marker_path.display()
);
}

// Parent exits with error code to indicate test completion
// The test harness will verify the crash report contains the panic message
unsafe {
libc::_exit(1);
}
}
}
}
31 changes: 31 additions & 0 deletions bin_tests/src/modes/unix/test_014_panic_hook_string.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/
// SPDX-License-Identifier: Apache-2.0
//
// Test that panic hooks work correctly with String payloads (not just &str).
// This validates that:
// 1. String panic payloads are correctly captured
// 2. The panic message format is "Process panicked with message: <msg>"
use crate::modes::behavior::Behavior;
use libdd_crashtracker::CrashtrackerConfiguration;
use std::path::Path;

pub struct Test;

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

fn pre(&self, _output_dir: &Path) -> anyhow::Result<()> {
Ok(())
}

fn post(&self, _output_dir: &Path) -> anyhow::Result<()> {
let dynamic_value = 42;
panic!("Panic with value: {}", dynamic_value);
}
}
30 changes: 30 additions & 0 deletions bin_tests/src/modes/unix/test_015_panic_hook_unknown_type.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/
// SPDX-License-Identifier: Apache-2.0
//
// Test that panic hooks work correctly with unknown types via panic_any.
// This validates that:
// 1. panic_any() with custom types is handled gracefully
// 2. The panic message format is "Process panicked with type: <type_name>"
use crate::modes::behavior::Behavior;
use libdd_crashtracker::CrashtrackerConfiguration;
use std::path::Path;

pub struct Test;

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

fn pre(&self, _output_dir: &Path) -> anyhow::Result<()> {
Ok(())
}

fn post(&self, _output_dir: &Path) -> anyhow::Result<()> {
std::panic::panic_any(42i32);
}
}
15 changes: 6 additions & 9 deletions bin_tests/src/test_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,9 @@ where
/// This is more flexible than `run_crash_test_with_artifacts` and allows for:
/// - Custom binary selection (e.g., crashing_test_app instead of crashtracker_bin_test)
/// - Custom command arguments
/// - Custom exit status expectations
///
/// Note: This function always expects the test to crash (exit with non-success status).
/// All current uses of this function test crash scenarios, not successful exits.
///
/// # Example
/// ```no_run
Expand Down Expand Up @@ -223,7 +225,6 @@ where
/// .arg(&artifacts_map[&receiver])
/// .arg(&fixtures.output_dir);
/// },
/// false, // expect crash (not success)
/// |payload, _fixtures| {
/// // Custom validation
/// Ok(())
Expand All @@ -235,7 +236,6 @@ where
pub fn run_custom_crash_test<CB, V>(
binary_path: &std::path::Path,
command_builder: CB,
expect_success: bool,
validator: V,
) -> Result<()>
where
Expand All @@ -251,13 +251,10 @@ where

let exit_status = crate::timeit!("exit after signal", { p.wait()? });

// Validate exit status
let actual_success = exit_status.success();
// Validate exit status - custom crash tests always expect failure
anyhow::ensure!(
expect_success == actual_success,
"Exit status mismatch: expected success={}, got success={} (exit code: {:?})",
expect_success,
actual_success,
!exit_status.success(),
"Expected test to crash (non-success exit), but it succeeded with code: {:?}",
exit_status.code()
);

Expand Down
Loading
Loading