Skip to content
Merged
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 Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions bin_tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ libc = "0.2"
nix = { version = "0.29", features = ["signal", "socket"] }
hex = "0.4"
os_info = "3.7.0"
regex = "1.0"

[dev-dependencies]
serial_test = "3.2"
Expand Down
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 non-string types is handled gracefully
// 2. The message format is: "Process panicked with unknown type (<location>)"
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