Skip to content
Open
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/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ name = "bin_tests"
version = "0.1.0"
edition = "2021"
publish = false
build = "build.rs"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

Expand Down
30 changes: 30 additions & 0 deletions bin_tests/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright 2025-Present Datadog, Inc.
// SPDX-License-Identifier: Apache-2.0

#[cfg(unix)]
fn main() {
use std::env;
use std::path::PathBuf;
use std::process::Command;

let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR not set"));
let src = PathBuf::from("preload/preload.c");
let so_path = out_dir.join("libpreload_logger.so");

let status = Command::new("cc")
.args(["-std=gnu11", "-fPIC", "-shared", "-Wall", "-Wextra", "-o"])
.arg(&so_path)
.arg(&src)
.status()
.expect("failed to spawn cc");

if !status.success() {
panic!("compiling preload.c failed with status {status}");
}

// Make the built shared object path available at compile time for tests/tools.
println!("cargo:rustc-env=PRELOAD_LOGGER_SO={}", so_path.display());
}

#[cfg(not(unix))]
fn main() {}
162 changes: 162 additions & 0 deletions bin_tests/preload/preload.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/
// SPDX-License-Identifier: Apache-2.0

#define _GNU_SOURCE
#include <dlfcn.h>
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <signal.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>

#ifdef __GLIBC__
#include <execinfo.h>
#define DD_HAVE_EXECINFO 1
#else
#define DD_HAVE_EXECINFO 0
#endif

static void *(*real_malloc)(size_t) = NULL;
static void (*real_free)(void *) = NULL;
static void *(*real_calloc)(size_t, size_t) = NULL;
static void *(*real_realloc)(void *, size_t) = NULL;
static pthread_once_t init_once = PTHREAD_ONCE_INIT;

// We should load all the real symbols on library load
static void init_function_ptrs(void) {
if (real_malloc == NULL) {
real_malloc = dlsym(RTLD_NEXT, "malloc");
real_free = dlsym(RTLD_NEXT, "free");
real_calloc = dlsym(RTLD_NEXT, "calloc");
real_realloc = dlsym(RTLD_NEXT, "realloc");
}
}

__attribute__((constructor)) static void preload_ctor(void) {
pthread_once(&init_once, init_function_ptrs);
}

static int log_fd = -1;
// Flag to indicate we are currently in the collector; we should only
// detect allocations when we are in the collector.
// Must be thread-local: the collector work runs on a single thread; other
// threads in the process should not be considered "collector" and should
// not trip the detector.
static __thread int collector_marked = 0;

// Called by the collector process to enable detection in the collector only
void dd_preload_logger_mark_collector(void) {
collector_marked = 1;
if (log_fd >= 0 || !collector_marked) {
// Already initialized or not a collector
return;
}
}

static void write_int(int fd, long value) {
char buf[32];
int i = 0;

if (value == 0) {
write(fd, "0", 1);
return;
}

if (value < 0) {
write(fd, "-", 1);
value = -value;
}

while (value > 0 && i < (int)sizeof(buf)) {
buf[i++] = '0' + (value % 10);
value /= 10;
}

for (int j = i - 1; j >= 0; j--) {
write(fd, &buf[j], 1);
}
}

// This function MUST be async signal safe
static void capture_and_report_allocation(const char *func_name) {
const char *path = "/tmp/preload_detector.log";
log_fd = open(path, O_CREAT | O_WRONLY | O_TRUNC, 0644);
if (log_fd >= 0) {
pid_t pid = getpid();
long tid = syscall(SYS_gettid);

write(log_fd,
"[FATAL] Dangerous allocation detected in collector!\n",
52);

write(log_fd, " Function: ", 12);
write(log_fd, func_name, strlen(func_name));

write(log_fd, "\n PID: ", 8);
write_int(log_fd, pid);

write(log_fd, "\n TID: ", 8);
write_int(log_fd, tid);

close(log_fd);
log_fd = -1;
abort();
}
}

void *malloc(size_t size) {
if (real_malloc == NULL) {
errno = ENOMEM;
return NULL;
}

if (collector_marked) {
capture_and_report_allocation("malloc");
}

void *ptr = real_malloc(size);
return ptr;
}

void free(void *ptr) {
if (real_free == NULL) {
return;
}

// free is generally safe; we'll allow free operations without failing
real_free(ptr);
}

void *calloc(size_t nmemb, size_t size) {
if (real_calloc == NULL) {
errno = ENOMEM;
return NULL;
}

if (collector_marked) {
capture_and_report_allocation("calloc");
}

void *ptr = real_calloc(nmemb, size);
return ptr;
}

void *realloc(void *ptr, size_t size) {
if (real_realloc == NULL) {
errno = ENOMEM;
return NULL;
}

if (collector_marked) {
capture_and_report_allocation("realloc");
}

void *new_ptr = real_realloc(ptr, size);
return new_ptr;
}
42 changes: 37 additions & 5 deletions bin_tests/src/bin/crashtracker_bin_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ mod unix {
};
use std::env;
use std::path::Path;
use std::process;
use std::time::Duration;

use libdd_common::{tag, Endpoint};
Expand All @@ -43,33 +44,64 @@ mod unix {
}

pub fn main() -> anyhow::Result<()> {
let mut args = env::args().skip(1);
let raw_args: Vec<String> = env::args().collect();
let mut args = raw_args.iter().skip(1);
let output_url = args.next().context("Unexpected number of arguments")?;
let receiver_binary = args.next().context("Unexpected number of arguments")?;
let output_dir = args.next().context("Unexpected number of arguments")?;
let mode_str = args.next().context("Unexpected number of arguments")?;
let crash_typ = args.next().context("Missing crash type")?;
anyhow::ensure!(args.next().is_none(), "unexpected extra arguments");

// For preload logger mode, ensure we actually start with LD_PRELOAD applied.
// Setting LD_PRELOAD after startup has no effect on the current process,
// so re-exec only if we weren't born with it
if mode_str == "runtime_preload_logger" && env::var_os("LD_PRELOAD").is_none() {
if let Some(so_path) = option_env!("PRELOAD_LOGGER_SO") {
let status = process::Command::new(&raw_args[0])
.args(&raw_args[1..])
.env("LD_PRELOAD", so_path)
.status()
.context("failed to re-exec with LD_PRELOAD")?;
let code = status.code().unwrap_or(1);
process::exit(code);
}
}

let stderr_filename = format!("{output_dir}/out.stderr");
let stdout_filename = format!("{output_dir}/out.stdout");
let output_dir: &Path = output_dir.as_ref();

let endpoint = if output_url.is_empty() {
None
} else {
Some(Endpoint::from_slice(&output_url))
Some(Endpoint::from_slice(output_url))
};

// The configuration can be modified by a Behavior (testing plan), so it is mut here.
// Unlike a normal harness, in this harness tests are run in individual processes, so race
// issues are avoided.
let stacktrace_collection = match env::var("DD_TEST_STACKTRACE_COLLECTION") {
Ok(val) => match val.as_str() {
"disabled" => crashtracker::StacktraceCollection::Disabled,
"without_symbols" => crashtracker::StacktraceCollection::WithoutSymbols,
"inprocess_symbols" => {
crashtracker::StacktraceCollection::EnabledWithInprocessSymbols
}
"receiver_symbols" => {
crashtracker::StacktraceCollection::EnabledWithSymbolsInReceiver
}
_ => crashtracker::StacktraceCollection::WithoutSymbols,
},
Err(_) => crashtracker::StacktraceCollection::WithoutSymbols,
};

let mut config = CrashtrackerConfiguration::new(
vec![],
true,
true,
endpoint,
crashtracker::StacktraceCollection::WithoutSymbols,
stacktrace_collection,
crashtracker::default_signals(),
Some(TEST_COLLECTOR_TIMEOUT),
Some("".to_string()),
Expand All @@ -92,7 +124,7 @@ mod unix {
};

// Set the behavior of the test, run setup, and do the pre-init test
let behavior = get_behavior(&mode_str);
let behavior = get_behavior(mode_str);
behavior.setup(output_dir, &mut config)?;
behavior.pre(output_dir)?;

Expand All @@ -101,7 +133,7 @@ mod unix {
CrashtrackerReceiverConfig::new(
vec![],
env::vars().collect(),
receiver_binary,
receiver_binary.to_string(),
Some(stderr_filename),
Some(stdout_filename),
)?,
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 @@ -137,6 +137,7 @@ pub fn get_behavior(mode_str: &str) -> Box<dyn Behavior> {
"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),
"runtime_preload_logger" => Box::new(test_000_donothing::Test),
_ => panic!("Unknown mode: {mode_str}"),
}
}
Expand Down
25 changes: 25 additions & 0 deletions bin_tests/src/test_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//! across different test scenarios.

use crate::{
build_artifacts,
test_types::{CrashType, TestMode},
validation::{read_and_parse_crash_payload, validate_std_outputs, PayloadValidator},
ArtifactType, ArtifactsBuild, BuildProfile,
Expand Down Expand Up @@ -270,6 +271,30 @@ where
Ok(())
}

/// Minimal runner for scenarios where the process may not emit a crash report
/// (preload allocation detector). It just runs the binary and waits.
pub fn run_crash_no_op(config: &CrashTestConfig) -> Result<()> {
let artifacts = StandardArtifacts::new(config.profile);
let artifacts_map = build_artifacts(&artifacts.as_slice())?;
let fixtures = TestFixtures::new()?;

let mut cmd = process::Command::new(&artifacts_map[&artifacts.crashtracker_bin]);
cmd.arg(format!("file://{}", fixtures.crash_profile_path.display()))
.arg(&artifacts_map[&artifacts.crashtracker_receiver])
.arg(&fixtures.output_dir)
.arg(config.mode.as_str())
.arg(config.crash_type.as_str());

for (key, val) in &config.env_vars {
cmd.env(key, val);
}

let mut child = cmd.spawn().context("Failed to spawn test process")?;
let _ = child.wait();

Ok(())
}

/// Validates the process exit status matches expectations for the crash type.
fn assert_exit_status(exit_status: process::ExitStatus, crash_type: CrashType) -> Result<()> {
let expected_success = crash_type.expects_success();
Expand Down
4 changes: 4 additions & 0 deletions bin_tests/src/test_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub enum TestMode {
RuntimeCallbackFrame,
RuntimeCallbackString,
RuntimeCallbackFrameInvalidUtf8,
RuntimePreloadLogger,
}

impl TestMode {
Expand All @@ -37,6 +38,7 @@ impl TestMode {
Self::RuntimeCallbackFrame => "runtime_callback_frame",
Self::RuntimeCallbackString => "runtime_callback_string",
Self::RuntimeCallbackFrameInvalidUtf8 => "runtime_callback_frame_invalid_utf8",
Self::RuntimePreloadLogger => "runtime_preload_logger",
}
}

Expand All @@ -56,6 +58,7 @@ impl TestMode {
Self::RuntimeCallbackFrame,
Self::RuntimeCallbackString,
Self::RuntimeCallbackFrameInvalidUtf8,
Self::RuntimePreloadLogger,
]
}
}
Expand Down Expand Up @@ -84,6 +87,7 @@ impl std::str::FromStr for TestMode {
"runtime_callback_frame" => Ok(Self::RuntimeCallbackFrame),
"runtime_callback_string" => Ok(Self::RuntimeCallbackString),
"runtime_callback_frame_invalid_utf8" => Ok(Self::RuntimeCallbackFrameInvalidUtf8),
"runtime_preload_logger" => Ok(Self::RuntimePreloadLogger),
_ => Err(format!("Unknown test mode: {}", s)),
}
}
Expand Down
Loading
Loading