Skip to content

Commit 2dd85ed

Browse files
committed
feat(crashtracker): capture C assert expression in crash reports
Hook __assert_fail via GOT patching (using libdd-got-hook) during crashtracker init. When a C assert() fails, the hook captures the assertion expression, file, line, and function name before the process aborts via SIGABRT. The signal handler includes this message in the crash report. Only supported on 64-bit Linux; no-op on other platforms.
1 parent 822405c commit 2dd85ed

11 files changed

Lines changed: 324 additions & 2 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bin_tests/build.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,40 @@ fn main() {
2424

2525
// Make the built shared object path available at compile time for tests/tools.
2626
println!("cargo:rustc-env=PRELOAD_LOGGER_SO={}", so_path.display());
27+
28+
// --- trigger_assert static library (Linux only) ---
29+
#[cfg(target_os = "linux")]
30+
{
31+
let assert_src = PathBuf::from("src/c/trigger_assert.c");
32+
let assert_obj = out_dir.join("trigger_assert.o");
33+
let assert_lib = out_dir.join("libtrigger_assert.a");
34+
35+
let status = Command::new("cc")
36+
.args([
37+
"-std=c11", "-fPIC", "-UNDEBUG", "-Wall", "-Wextra", "-c", "-o",
38+
])
39+
.arg(&assert_obj)
40+
.arg(&assert_src)
41+
.status()
42+
.expect("failed to compile trigger_assert.c");
43+
if !status.success() {
44+
panic!("compiling trigger_assert.c failed with status {status}");
45+
}
46+
47+
let status = Command::new("ar")
48+
.args(["rcs"])
49+
.arg(&assert_lib)
50+
.arg(&assert_obj)
51+
.status()
52+
.expect("failed to run ar");
53+
if !status.success() {
54+
panic!("creating libtrigger_assert.a failed with status {status}");
55+
}
56+
57+
println!("cargo:rustc-link-search=native={}", out_dir.display());
58+
println!("cargo:rustc-link-lib=static=trigger_assert");
59+
println!("cargo:rerun-if-changed=src/c/trigger_assert.c");
60+
}
2761
}
2862

2963
#[cfg(not(unix))]

bin_tests/src/bin/crashtracker_bin_test.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,15 @@ mod unix {
158158
"raise_sigill" => raise(Signal::SIGILL)?,
159159
"raise_sigbus" => raise(Signal::SIGBUS)?,
160160
"raise_sigsegv" => raise(Signal::SIGSEGV)?,
161+
#[cfg(target_os = "linux")]
162+
"assert_fail" => {
163+
extern "C" {
164+
fn trigger_c_assert() -> !;
165+
}
166+
// SAFETY: trigger_c_assert calls the real C assert() macro,
167+
// which expands to __assert_fail and never returns.
168+
unsafe { trigger_c_assert() }
169+
}
161170
"unhandled_exception" => {
162171
let mut stacktrace = StackTrace::new_incomplete();
163172
let mut stackframe1 = StackFrame::new();

bin_tests/src/c/trigger_assert.c

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
// Calls the real C assert() macro so that the GOT hook is exercised
5+
// through the same code path as a genuine assertion failure.
6+
// NDEBUG must NOT be defined, otherwise assert() compiles to nothing.
7+
#undef NDEBUG
8+
#include <assert.h>
9+
10+
void trigger_c_assert(void) {
11+
int test_value = 0;
12+
assert(test_value > 0);
13+
}

bin_tests/src/test_types.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ pub enum CrashType {
140140
RaiseSigSegv,
141141
/// Unhandled Exception
142142
UnhandledException,
143+
/// C assert() failure (calls __assert_fail, triggers SIGABRT)
144+
AssertFail,
143145
}
144146

145147
impl CrashType {
@@ -156,6 +158,7 @@ impl CrashType {
156158
Self::RaiseSigBus => "raise_sigbus",
157159
Self::RaiseSigSegv => "raise_sigsegv",
158160
Self::UnhandledException => "unhandled_exception",
161+
Self::AssertFail => "assert_fail",
159162
}
160163
}
161164

@@ -178,7 +181,7 @@ impl CrashType {
178181
pub const fn signal_number(self) -> i32 {
179182
match self {
180183
Self::NullDeref | Self::KillSigSegv | Self::RaiseSigSegv => 11, // SIGSEGV
181-
Self::KillSigAbrt | Self::RaiseSigAbrt => 6, // SIGABRT
184+
Self::KillSigAbrt | Self::RaiseSigAbrt | Self::AssertFail => 6, // SIGABRT
182185
Self::KillSigIll | Self::RaiseSigIll => 4, // SIGILL
183186
Self::KillSigBus | Self::RaiseSigBus => 7, // SIGBUS
184187
Self::UnhandledException => 0, // no signal
@@ -189,7 +192,7 @@ impl CrashType {
189192
pub const fn signal_name(self) -> &'static str {
190193
match self {
191194
Self::NullDeref | Self::KillSigSegv | Self::RaiseSigSegv => "SIGSEGV",
192-
Self::KillSigAbrt | Self::RaiseSigAbrt => "SIGABRT",
195+
Self::KillSigAbrt | Self::RaiseSigAbrt | Self::AssertFail => "SIGABRT",
193196
Self::KillSigIll | Self::RaiseSigIll => "SIGILL",
194197
Self::KillSigBus | Self::RaiseSigBus => "SIGBUS",
195198
Self::UnhandledException => "Unhandled Exception",
@@ -218,6 +221,7 @@ impl std::str::FromStr for CrashType {
218221
"raise_sigbus" => Ok(Self::RaiseSigBus),
219222
"raise_sigsegv" => Ok(Self::RaiseSigSegv),
220223
"unhandled_exception" => Ok(Self::UnhandledException),
224+
"assert_fail" => Ok(Self::AssertFail),
221225
_ => Err(format!("Unknown crash type: {}", s)),
222226
}
223227
}

bin_tests/tests/crashtracker_bin_test.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,44 @@ fn test_crash_tracking_bin_unhandled_exception() {
160160
run_crash_test_with_artifacts(&config, &artifacts_map, &artifacts, validator).unwrap();
161161
}
162162

163+
/// Tests that when a C `assert()` fails, the crash report contains the assertion
164+
/// expression string in the error message.
165+
#[test]
166+
#[cfg(target_os = "linux")]
167+
#[cfg_attr(miri, ignore)]
168+
fn test_crash_tracking_bin_assert_fail() {
169+
let config = CrashTestConfig::new(
170+
BuildProfile::Release,
171+
TestMode::DoNothing,
172+
CrashType::AssertFail,
173+
);
174+
let artifacts = StandardArtifacts::new(config.profile);
175+
let artifacts_map = fetch_built_artifacts(&artifacts.as_slice()).unwrap();
176+
177+
let validator: ValidatorFn = Box::new(|payload, fixtures| {
178+
PayloadValidator::new(payload)
179+
.validate_error_kind("UnixSignal")?
180+
// The assertion expression from the real C assert() macro
181+
.validate_error_message_contains("test_value > 0")?
182+
// The function name comes from the C compiler's __func__
183+
.validate_error_message_contains("trigger_c_assert")?;
184+
185+
// Validate SIGABRT signal info
186+
let sig_info = &payload["sig_info"];
187+
let signo_hr = sig_info["si_signo_human_readable"].as_str().unwrap_or("");
188+
anyhow::ensure!(
189+
signo_hr.contains("SIGABRT"),
190+
"Expected SIGABRT in signal info, got: {signo_hr}"
191+
);
192+
193+
validate_telemetry(&fixtures.crash_telemetry_path, "assert_fail")?;
194+
195+
Ok(())
196+
});
197+
198+
run_crash_test_with_artifacts(&config, &artifacts_map, &artifacts, validator).unwrap();
199+
}
200+
163201
/// Tests that when `collect_all_threads` is enabled and the crash is reported via
164202
/// `report_unhandled_exception`, the crash report contains entries in `error.threads`
165203
/// for background threads with valid stack traces.
@@ -1772,6 +1810,10 @@ fn assert_siginfo_message(sig_info: &Value, crash_typ: &str) {
17721810
|| sig_info.is_object() && sig_info.as_object().is_none_or(|m| m.is_empty())
17731811
);
17741812
}
1813+
"assert_fail" => {
1814+
assert_eq!(sig_info["si_signo"], libc::SIGABRT);
1815+
assert_eq!(sig_info["si_signo_human_readable"], "SIGABRT");
1816+
}
17751817
_ => panic!("unexpected crash_typ {crash_typ}"),
17761818
}
17771819
}
@@ -1906,6 +1948,10 @@ fn assert_telemetry_message(crash_telemetry: &[u8], crash_typ: &str) {
19061948
"unhandled_exception" => {
19071949
// Unhandled exceptions have no signal info tags
19081950
}
1951+
"assert_fail" => {
1952+
assert!(tags.contains("si_signo_human_readable:SIGABRT"), "{tags:?}");
1953+
assert!(tags.contains("si_signo:6"), "{tags:?}");
1954+
}
19091955
_ => panic!("{crash_typ}"),
19101956
}
19111957

libdd-crashtracker/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ cxx = ["dep:cxx", "dep:cxx-build"]
4141
# Should be kept in sync with the libdatadog symbolizer crate (also using blasesym)
4242
blazesym = "=0.2.3"
4343

44+
[target.'cfg(all(target_os = "linux", target_pointer_width = "64"))'.dependencies]
45+
libdd-got-hook = { version = "1.0.0", path = "../libdd-got-hook" }
46+
4447
[target.'cfg(target_os = "linux")'.dependencies]
4548
libdd-libunwind-sys = { version = "1.0.2" }
4649

libdd-crashtracker/src/collector/api.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ pub fn init(
8686
Receiver::update_stored_config(receiver_config)?;
8787
register_crash_handlers(&config)?;
8888
register_panic_hook()?;
89+
super::assert_interceptor::install_assert_hook();
8990
enable();
9091
Ok(())
9192
}

0 commit comments

Comments
 (0)