Skip to content
Draft
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
63 changes: 56 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 fn2(crash_type: CrashType) {
fn3(crash_type);
}

#[inline(never)]
fn fn1() {
fn2()
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,17 @@ 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));
if is_panic_mode {
let called_panic_hook_clone = Arc::clone(&called_panic_hook);
std::panic::set_hook(Box::new(move |_| {
called_panic_hook_clone.store(true, Ordering::SeqCst);
}));
}

crashtracker::init(
config,
CrashtrackerReceiverConfig::new(
Expand All @@ -100,7 +143,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 @@ -43,6 +43,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 @@ -55,6 +56,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
1 change: 1 addition & 0 deletions bin_tests/src/modes/behavior.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ 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!("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 @@ -13,3 +13,4 @@ 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;
98 changes: 98 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,98 @@
// 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::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
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<()> {
Ok(())
}

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

fn post() -> anyhow::Result<()> {
// Set up a panic hook to verify it gets called
let panic_hook_called = Arc::new(AtomicBool::new(false));
let panic_hook_called_clone = Arc::clone(&panic_hook_called);

std::panic::set_hook(Box::new(move |_panic_info| {
panic_hook_called_clone.store(true, Ordering::SeqCst);
}));

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
}
}
}

// 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);
}
}
}
}
Loading
Loading