Skip to content

Commit c8ebe5f

Browse files
committed
Fail test on any detection and print out stacktrace
1 parent 1e98ec3 commit c8ebe5f

File tree

3 files changed

+105
-65
lines changed

3 files changed

+105
-65
lines changed

bin_tests/preload/preload.c

Lines changed: 63 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44
#define _GNU_SOURCE
55
#include <dlfcn.h>
66
#include <errno.h>
7+
#include <execinfo.h>
78
#include <fcntl.h>
89
#include <pthread.h>
10+
#include <signal.h>
911
#include <stddef.h>
1012
#include <stdio.h>
13+
#include <stdlib.h>
1114
#include <string.h>
1215
#include <sys/syscall.h>
1316
#include <sys/types.h>
@@ -18,6 +21,7 @@ static void (*real_free)(void *) = NULL;
1821
static void *(*real_calloc)(size_t, size_t) = NULL;
1922
static void *(*real_realloc)(void *, size_t) = NULL;
2023
static pthread_once_t init_once = PTHREAD_ONCE_INIT;
24+
static pthread_once_t detection_once = PTHREAD_ONCE_INIT;
2125

2226
// We should load all the real symbols on library load
2327
static void init_function_ptrs(void) {
@@ -35,64 +39,65 @@ __attribute__((constructor)) static void preload_ctor(void) {
3539

3640
static int log_fd = -1;
3741
// Flag to indicate we are currently in the collector; We should only
38-
// log when we are in the collector.
42+
// detect allocations when we are in the collector.
3943
static int collector_marked = 0;
44+
// Flag to track if we've already detected and reported an allocation
45+
// This guards against reentrancy of the detection logic.
46+
static int allocation_detected = 0;
4047

41-
42-
static void init_logger(void) {
48+
// Called by the collector process to enable detection in the collector only
49+
void dd_preload_logger_mark_collector(void) {
50+
collector_marked = 1;
4351
if (log_fd >= 0 || !collector_marked) {
4452
// Already initialized or not a collector
4553
return;
4654
}
47-
48-
const char *path = "/tmp/preload_logger.log";
49-
log_fd = open(path, O_CREAT | O_WRONLY | O_TRUNC, 0644);
50-
if (log_fd >= 0) {
51-
char buf[256];
52-
pid_t pid = getpid();
53-
int len = snprintf(buf, sizeof(buf), "[DEBUG] Collector logger initialized pid=%d, fd=%d, path=%s\n",
54-
pid, log_fd, path);
55-
write(log_fd, buf, len);
56-
}
57-
}
58-
59-
// Called by the collector process to scope logging to the collector only
60-
void dd_preload_logger_mark_collector(void) {
61-
collector_marked = 1;
62-
63-
init_logger();
64-
65-
if (log_fd >= 0) {
66-
char buf[256];
67-
pid_t pid = getpid();
68-
int len = snprintf(buf, sizeof(buf), "[DEBUG] Marked as collector, pid=%d, fd=%d\n", pid, log_fd);
69-
write(log_fd, buf, len);
70-
}
7155
}
7256

73-
static void log_line(const char *tag, size_t size, void *ptr) {
74-
if (log_fd < 0) {
75-
return;
57+
static void capture_and_report_allocation(const char *func_name) {
58+
// Only report once using atomic compare-and-swap
59+
if (__sync_bool_compare_and_swap(&allocation_detected, 0, 1)) {
60+
const char *path = "/tmp/preload_detector.log";
61+
log_fd = open(path, O_CREAT | O_WRONLY | O_TRUNC, 0644);
62+
if (log_fd >= 0) {
63+
char buf[4096];
64+
pid_t pid = getpid();
65+
long tid = syscall(SYS_gettid);
66+
67+
// Log the detection
68+
int len = snprintf(buf, sizeof(buf),
69+
"[FATAL] Dangerous allocation detected in collector!\n"
70+
" Function: %s\n"
71+
" PID: %d\n"
72+
" TID: %ld\n"
73+
" Stacktrace:\n",
74+
func_name, pid, tid);
75+
write(log_fd, buf, len);
76+
77+
// Capture and log stacktrace
78+
void *array[100];
79+
int size = backtrace(array, 100);
80+
char **strings = backtrace_symbols(array, size);
81+
82+
if (strings != NULL) {
83+
for (int i = 0; i < size; i++) {
84+
len = snprintf(buf, sizeof(buf), " #%d %s\n", i, strings[i]);
85+
write(log_fd, buf, len);
86+
}
87+
// Note: we can't use free() here since we're in an allocation hook
88+
// backtrace_symbols uses malloc internally, so we have a small leak
89+
// but this is acceptable since this only happens once and we guard
90+
// against it anyways
91+
}
92+
93+
fsync(log_fd);
94+
close(log_fd);
95+
log_fd = -1;
96+
}
7697
}
7798

78-
char buf[256];
79-
pid_t pid = getpid();
80-
long tid = syscall(SYS_gettid);
81-
int len = 0;
82-
83-
if (strcmp(tag, "malloc") == 0) {
84-
len = snprintf(buf, sizeof(buf), "pid=%d tid=%ld malloc size=%zu ptr=%p\n", pid, tid, size, ptr);
85-
} else if (strcmp(tag, "calloc") == 0) {
86-
len = snprintf(buf, sizeof(buf), "pid=%d tid=%ld calloc size=%zu ptr=%p\n", pid, tid, size, ptr);
87-
} else if (strcmp(tag, "realloc") == 0) {
88-
len = snprintf(buf, sizeof(buf), "pid=%d tid=%ld realloc size=%zu ptr=%p\n", pid, tid, size, ptr);
89-
} else if (strcmp(tag, "free") == 0) {
90-
len = snprintf(buf, sizeof(buf), "pid=%d tid=%ld free ptr=%p\n", pid, tid, ptr);
91-
}
92-
93-
if (len > 0) {
94-
(void)write(log_fd, buf, (size_t)len);
95-
}
99+
// Don't abort. let the collector continue so it can finish writing the crash report
100+
// The test will check for the log file and fail if allocations were detected
96101
}
97102

98103
void *malloc(size_t size) {
@@ -101,10 +106,11 @@ void *malloc(size_t size) {
101106
return NULL;
102107
}
103108

104-
void *ptr = real_malloc(size);
105109
if (collector_marked) {
106-
log_line("malloc", size, ptr);
110+
capture_and_report_allocation("malloc");
107111
}
112+
113+
void *ptr = real_malloc(size);
108114
return ptr;
109115
}
110116

@@ -113,9 +119,7 @@ void free(void *ptr) {
113119
return;
114120
}
115121

116-
if (collector_marked) {
117-
log_line("free", 0, ptr);
118-
}
122+
// free is generally safe; we'll allow free operations without failing
119123
real_free(ptr);
120124
}
121125

@@ -125,10 +129,11 @@ void *calloc(size_t nmemb, size_t size) {
125129
return NULL;
126130
}
127131

128-
void *ptr = real_calloc(nmemb, size);
129132
if (collector_marked) {
130-
log_line("calloc", nmemb * size, ptr);
133+
capture_and_report_allocation("calloc");
131134
}
135+
136+
void *ptr = real_calloc(nmemb, size);
132137
return ptr;
133138
}
134139

@@ -138,9 +143,10 @@ void *realloc(void *ptr, size_t size) {
138143
return NULL;
139144
}
140145

141-
void *new_ptr = real_realloc(ptr, size);
142146
if (collector_marked) {
143-
log_line("realloc", size, new_ptr);
147+
capture_and_report_allocation("realloc");
144148
}
149+
150+
void *new_ptr = real_realloc(ptr, size);
145151
return new_ptr;
146152
}

bin_tests/tests/crashtracker_bin_test.rs

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -180,18 +180,48 @@ fn test_crash_tracking_bin_no_runtime_callback() {
180180
run_crash_test_with_artifacts(&config, &artifacts_map, &artifacts, validator).unwrap();
181181
}
182182

183-
// Manual/diagnostic preload logger check. This is ignored by default to keep the
184-
// regular bin test suite fast and hermetic. Run with:
185-
// cargo test --test crashtracker_bin_test -- --ignored manual_runtime_preload_logger
186-
// Log output is written to tmp/preload_logger.log.
183+
// Test that verifies the collector process doesn't perform dangerous allocations.
184+
// Uses LD_PRELOAD to detect malloc/calloc/realloc calls in the collector and fails
185+
// if any are detected, capturing a stacktrace for debugging.
187186
#[test]
188-
#[ignore]
189-
fn manual_runtime_preload_logger() {
190-
run_standard_crash_test_refactored(
191-
BuildProfile::Release,
187+
#[cfg_attr(miri, ignore)]
188+
#[cfg(target_os = "linux")] // LD_PRELOAD is Linux-specific
189+
fn test_collector_no_allocations() {
190+
let config = CrashTestConfig::new(
191+
BuildProfile::Debug,
192192
TestMode::RuntimePreloadLogger,
193193
CrashType::NullDeref,
194194
);
195+
let artifacts = StandardArtifacts::new(config.profile);
196+
let artifacts_map = build_artifacts(&artifacts.as_slice()).unwrap();
197+
198+
let validator: ValidatorFn = Box::new(|payload, _fixtures| {
199+
// First validate that the crash report was properly generated
200+
PayloadValidator::new(payload).validate_counters()?;
201+
202+
let detector_log_path = PathBuf::from("/tmp/preload_detector.log");
203+
204+
if detector_log_path.exists() {
205+
let log_content = fs::read_to_string(&detector_log_path)
206+
.context("Failed to read preload detector log")?;
207+
208+
// Clean up the log file first
209+
let _ = fs::remove_file(&detector_log_path);
210+
211+
eprintln!("=== DANGEROUS ALLOCATION DETECTED IN COLLECTOR ===");
212+
eprintln!("{}", log_content);
213+
214+
anyhow::bail!(
215+
"Collector performed dangerous allocation! Check the log above for stacktrace."
216+
);
217+
}
218+
219+
// If we get here, no dangerous allocations were detected, we shuld pass
220+
Ok(())
221+
});
222+
223+
// Run the test - we expect it to complete normally without the collector aborting
224+
run_crash_test_with_artifacts(&config, &artifacts_map, &artifacts, validator).unwrap();
195225
}
196226

197227
#[test]

libdd-crashtracker/src/collector/emitters.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,10 @@ pub(crate) fn emit_crashreport(
143143
ucontext: *const ucontext_t,
144144
ppid: i32,
145145
) -> Result<(), EmitterError> {
146+
let mut test = Vec::new();
147+
test.push(config_str);
148+
test.push(metadata_string);
149+
test.push("lol");
146150
// The following order is important in order to emit the crash ping:
147151
// - receiver expects the config
148152
// - then message if any

0 commit comments

Comments
 (0)