Skip to content

Commit 1c4e0bc

Browse files
committed
First pass, need to isolate only collector from test harness
1 parent b9320a3 commit 1c4e0bc

File tree

9 files changed

+253
-2
lines changed

9 files changed

+253
-2
lines changed

bin_tests/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ name = "bin_tests"
66
version = "0.1.0"
77
edition = "2021"
88
publish = false
9+
build = "build.rs"
910

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

bin_tests/build.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright 2025-Present Datadog, Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
#[cfg(unix)]
5+
fn main() {
6+
use std::env;
7+
use std::path::PathBuf;
8+
use std::process::Command;
9+
10+
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR not set"));
11+
let src = PathBuf::from("preload/malloc_logger.c");
12+
let so_path = out_dir.join("libmalloc_logger.so");
13+
14+
let status = Command::new("cc")
15+
.args(["-fPIC", "-shared", "-Wall", "-Wextra", "-o"])
16+
.arg(&so_path)
17+
.arg(&src)
18+
.status()
19+
.expect("failed to spawn cc");
20+
21+
if !status.success() {
22+
panic!("compiling malloc_logger.c failed with status {status}");
23+
}
24+
25+
// Make the built shared object path available at compile time for tests/tools.
26+
println!("cargo:rustc-env=MALLOC_LOGGER_SO={}", so_path.display());
27+
}
28+
29+
#[cfg(not(unix))]
30+
fn main() {}

bin_tests/preload/malloc_logger.c

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
#define _GNU_SOURCE
2+
#include <dlfcn.h>
3+
#include <errno.h>
4+
#include <fcntl.h>
5+
#include <pthread.h>
6+
#include <signal.h>
7+
#include <stddef.h>
8+
#include <stdio.h>
9+
#include <stdlib.h>
10+
#include <string.h>
11+
#include <sys/syscall.h>
12+
#include <sys/types.h>
13+
#include <unistd.h>
14+
15+
static void *(*real_malloc)(size_t) = NULL;
16+
static void (*real_free)(void *) = NULL;
17+
static void *(*real_calloc)(size_t, size_t) = NULL;
18+
static void *(*real_realloc)(void *, size_t) = NULL;
19+
static int log_fd = -1;
20+
static pthread_once_t init_once = PTHREAD_ONCE_INIT;
21+
22+
static int is_enabled(void) {
23+
const char *v = getenv("MALLOC_LOG_ENABLED");
24+
return v && v[0] == '1';
25+
}
26+
27+
static void init_logger(void) {
28+
const char *path = getenv("MALLOC_LOG_PATH");
29+
if (path == NULL || path[0] == '\0') {
30+
path = "/tmp/malloc_logger.log";
31+
}
32+
33+
log_fd = open(path, O_CREAT | O_WRONLY | O_TRUNC, 0644);
34+
real_malloc = dlsym(RTLD_NEXT, "malloc");
35+
real_free = dlsym(RTLD_NEXT, "free");
36+
real_calloc = dlsym(RTLD_NEXT, "calloc");
37+
real_realloc = dlsym(RTLD_NEXT, "realloc");
38+
39+
// No logging here; deferred until enabled.
40+
}
41+
42+
__attribute__((constructor)) static void preload_ctor(void) {
43+
pthread_once(&init_once, init_logger);
44+
}
45+
46+
__attribute__((destructor)) static void preload_dtor(void) {
47+
if (log_fd >= 0) {
48+
close(log_fd);
49+
}
50+
}
51+
52+
static void log_line(const char *tag, size_t size, void *ptr) {
53+
if (log_fd < 0) {
54+
return;
55+
}
56+
57+
if (!is_enabled()) {
58+
return;
59+
}
60+
61+
char buf[200];
62+
pid_t pid = getpid();
63+
long tid = syscall(SYS_gettid);
64+
int len;
65+
66+
if (strcmp(tag, "malloc") == 0) {
67+
len = snprintf(buf, sizeof(buf), "pid=%d tid=%ld malloc size=%zu ptr=%p\n", pid, tid, size, ptr);
68+
} else if (strcmp(tag, "calloc") == 0) {
69+
len = snprintf(buf, sizeof(buf), "pid=%d tid=%ld calloc size=%zu ptr=%p\n", pid, tid, size, ptr);
70+
} else if (strcmp(tag, "realloc") == 0) {
71+
len = snprintf(buf, sizeof(buf), "pid=%d tid=%ld realloc size=%zu ptr=%p\n", pid, tid, size, ptr);
72+
} else { // free
73+
len = snprintf(buf, sizeof(buf), "pid=%d tid=%ld free ptr=%p\n", pid, tid, ptr);
74+
}
75+
76+
if (len > 0) {
77+
(void)write(log_fd, buf, (size_t)len);
78+
}
79+
}
80+
81+
void *malloc(size_t size) {
82+
pthread_once(&init_once, init_logger);
83+
84+
if (real_malloc == NULL) {
85+
errno = ENOMEM;
86+
return NULL;
87+
}
88+
89+
void *ptr = real_malloc(size);
90+
log_line("malloc", size, ptr);
91+
return ptr;
92+
}
93+
94+
void free(void *ptr) {
95+
pthread_once(&init_once, init_logger);
96+
97+
if (real_free == NULL) {
98+
return;
99+
}
100+
101+
log_line("free", 0, ptr);
102+
real_free(ptr);
103+
}
104+
105+
void *calloc(size_t nmemb, size_t size) {
106+
pthread_once(&init_once, init_logger);
107+
108+
if (real_calloc == NULL) {
109+
errno = ENOMEM;
110+
return NULL;
111+
}
112+
113+
void *ptr = real_calloc(nmemb, size);
114+
log_line("calloc", nmemb * size, ptr);
115+
return ptr;
116+
}
117+
118+
void *realloc(void *ptr, size_t size) {
119+
pthread_once(&init_once, init_logger);
120+
121+
if (real_realloc == NULL) {
122+
errno = ENOMEM;
123+
return NULL;
124+
}
125+
126+
void *new_ptr = real_realloc(ptr, size);
127+
log_line("realloc", size, new_ptr);
128+
return new_ptr;
129+
}

bin_tests/src/bin/crashtracker_bin_test.rs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ mod unix {
1919
};
2020
use std::env;
2121
use std::path::Path;
22+
use std::process;
2223
use std::time::Duration;
2324

2425
use libdd_common::{tag, Endpoint};
@@ -43,14 +44,37 @@ mod unix {
4344
}
4445

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

56+
// For malloc logger mode, re-exec with LD_PRELOAD but keep logging disabled
57+
// until the Behavior explicitly enables it (MALLOC_LOG_ENABLED=1). This
58+
// ensures the logger is loaded before any allocator calls while avoiding
59+
// harness noise.
60+
if mode_str == "runtime_malloc_logger"
61+
&& env::var_os("LD_PRELOAD").is_none()
62+
&& env::var_os("MALLOC_LOGGER_BOOTSTRAPPED").is_none()
63+
{
64+
if let Some(so_path) = option_env!("MALLOC_LOGGER_SO") {
65+
let status = process::Command::new(&raw_args[0])
66+
.args(&raw_args[1..])
67+
.env("LD_PRELOAD", so_path)
68+
.env("MALLOC_LOGGER_BOOTSTRAPPED", "1")
69+
.env("MALLOC_LOG_ENABLED", "0")
70+
.status()
71+
.context("failed to re-exec with LD_PRELOAD")?;
72+
73+
let code = status.code().unwrap_or(1);
74+
process::exit(code);
75+
}
76+
}
77+
5478
let stderr_filename = format!("{output_dir}/out.stderr");
5579
let stdout_filename = format!("{output_dir}/out.stdout");
5680
let output_dir: &Path = output_dir.as_ref();
@@ -101,7 +125,7 @@ mod unix {
101125
CrashtrackerReceiverConfig::new(
102126
vec![],
103127
env::vars().collect(),
104-
receiver_binary,
128+
receiver_binary.to_string(),
105129
Some(stderr_filename),
106130
Some(stdout_filename),
107131
)?,

bin_tests/src/modes/behavior.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ pub fn get_behavior(mode_str: &str) -> Box<dyn Behavior> {
134134
"runtime_callback_frame_invalid_utf8" => {
135135
Box::new(test_012_runtime_callback_frame_invalid_utf8::Test)
136136
}
137+
"runtime_malloc_logger" => Box::new(test_013_runtime_malloc_logger::Test),
137138
_ => panic!("Unknown mode: {mode_str}"),
138139
}
139140
}

bin_tests/src/modes/unix/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ pub mod test_009_prechain_with_abort;
1313
pub mod test_010_runtime_callback_frame;
1414
pub mod test_011_runtime_callback_string;
1515
pub mod test_012_runtime_callback_frame_invalid_utf8;
16+
pub mod test_013_runtime_malloc_logger;
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/
2+
// SPDX-License-Identifier: Apache-2.0
3+
//
4+
// Exercise the collector under an LD_PRELOAD malloc logger.
5+
6+
use crate::modes::behavior::Behavior;
7+
use libdd_crashtracker::CrashtrackerConfiguration;
8+
use std::path::Path;
9+
10+
pub struct Test;
11+
12+
impl Behavior for Test {
13+
fn setup(
14+
&self,
15+
output_dir: &Path,
16+
_config: &mut CrashtrackerConfiguration,
17+
) -> anyhow::Result<()> {
18+
use anyhow::Context;
19+
20+
// Ensure the collector loads the malloc logger via LD_PRELOAD.
21+
if std::env::var("LD_PRELOAD").is_err() {
22+
let so_path = option_env!("MALLOC_LOGGER_SO")
23+
.context("MALLOC_LOGGER_SO not set; rebuild bin_tests?")?;
24+
std::env::set_var("LD_PRELOAD", so_path);
25+
}
26+
27+
// Direct malloc log into the test output directory.
28+
let log_path = output_dir.join("malloc_logger.log");
29+
std::env::set_var("MALLOC_LOG_PATH", &log_path);
30+
// Enable logging now that path is set.
31+
std::env::set_var("MALLOC_LOG_ENABLED", "1");
32+
let _ = std::fs::remove_file(&log_path);
33+
Ok(())
34+
}
35+
36+
fn pre(&self, _output_dir: &Path) -> anyhow::Result<()> {
37+
// The collector (this process) should already be running with LD_PRELOAD.
38+
// Drop LD_PRELOAD before spawning the receiver to keep the preload scoped
39+
// to the collector only.
40+
std::env::remove_var("LD_PRELOAD");
41+
Ok(())
42+
}
43+
44+
fn post(&self, _output_dir: &Path) -> anyhow::Result<()> {
45+
Ok(())
46+
}
47+
}

bin_tests/src/test_types.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub enum TestMode {
1818
RuntimeCallbackFrame,
1919
RuntimeCallbackString,
2020
RuntimeCallbackFrameInvalidUtf8,
21+
RuntimeMallocLogger,
2122
}
2223

2324
impl TestMode {
@@ -37,6 +38,7 @@ impl TestMode {
3738
Self::RuntimeCallbackFrame => "runtime_callback_frame",
3839
Self::RuntimeCallbackString => "runtime_callback_string",
3940
Self::RuntimeCallbackFrameInvalidUtf8 => "runtime_callback_frame_invalid_utf8",
41+
Self::RuntimeMallocLogger => "runtime_malloc_logger",
4042
}
4143
}
4244

@@ -56,6 +58,7 @@ impl TestMode {
5658
Self::RuntimeCallbackFrame,
5759
Self::RuntimeCallbackString,
5860
Self::RuntimeCallbackFrameInvalidUtf8,
61+
Self::RuntimeMallocLogger,
5962
]
6063
}
6164
}
@@ -84,6 +87,7 @@ impl std::str::FromStr for TestMode {
8487
"runtime_callback_frame" => Ok(Self::RuntimeCallbackFrame),
8588
"runtime_callback_string" => Ok(Self::RuntimeCallbackString),
8689
"runtime_callback_frame_invalid_utf8" => Ok(Self::RuntimeCallbackFrameInvalidUtf8),
90+
"runtime_malloc_logger" => Ok(Self::RuntimeMallocLogger),
8791
_ => Err(format!("Unknown test mode: {}", s)),
8892
}
8993
}

bin_tests/tests/crashtracker_bin_test.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,20 @@ fn test_crash_tracking_bin_no_runtime_callback() {
177177
run_crash_test_with_artifacts(&config, &artifacts_map, &artifacts, validator).unwrap();
178178
}
179179

180+
// Manual/diagnostic malloc logger check. This is ignored by default to keep the
181+
// regular bin test suite fast and hermetic. Run with:
182+
// cargo test --test crashtracker_bin_test -- --ignored manual_runtime_malloc_logger
183+
// Log output is written to tmp/malloc_logger.log.
184+
#[test]
185+
#[ignore]
186+
fn manual_runtime_malloc_logger() {
187+
run_standard_crash_test_refactored(
188+
BuildProfile::Release,
189+
TestMode::RuntimeMallocLogger,
190+
CrashType::NullDeref,
191+
);
192+
}
193+
180194
#[test]
181195
#[cfg_attr(miri, ignore)]
182196
fn test_crash_tracking_bin_runtime_callback_frame_invalid_utf8() {

0 commit comments

Comments
 (0)