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
27 changes: 11 additions & 16 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions bin_tests/tests/crashtracker_bin_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -646,8 +646,13 @@ fn test_crash_tracking_app(crash_type: &str) {
let sig_info = &payload["sig_info"];
let error = &payload["error"];

let kind = error["kind"]
.as_str()
.expect("error.kind should be a string");

match crash_type_owned.as_str() {
"panic" => {
assert_eq!(kind, "Panic", "Expected error kind 'Panic', got '{kind}'");
let message = error["message"].as_str().unwrap();
assert!(
message.contains("Process panicked with message") && message.contains("program panicked"),
Expand All @@ -656,6 +661,7 @@ fn test_crash_tracking_app(crash_type: &str) {
);
}
"segfault" => {
assert_eq!(kind, "UnixSignal", "Expected error kind 'UnixSignal', got '{kind}'");
assert_error_message(&error["message"], sig_info);
}
_ => unreachable!("Invalid crash type: {}", crash_type_owned),
Expand Down
1 change: 1 addition & 0 deletions libdd-crashtracker/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ symbolic-common = { version = "12.8.0", default-features = false }
tokio = { version = "1.23", features = ["rt", "macros", "io-std", "io-util"] }
uuid = { version = "1.4.1", features = ["v4", "serde"] }
thiserror = "1.0"
backtrace = "0.3.76"

[target.'cfg(windows)'.dependencies]
windows = { version = "0.59.0", features = ["Win32_System_Diagnostics_Debug", "Win32_System_Diagnostics_ToolHelp", "Win32_System_ErrorReporting", "Win32_System_Kernel", "Win32_System_ProcessStatus", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_Security"] }
Expand Down
16 changes: 4 additions & 12 deletions libdd-crashtracker/src/collector/collector_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ use libdd_common::timeout::TimeoutManager;

use super::emitters::{emit_crashreport, CrashKindData};
use crate::shared::configuration::CrashtrackerConfiguration;
use libc::{siginfo_t, ucontext_t};
use libdd_common::unix_utils::{alt_fork, terminate};
use nix::sys::signal::{self, SaFlags, SigAction, SigHandler, SigSet};
use std::os::unix::io::RawFd;
Expand All @@ -30,9 +29,7 @@ impl Collector {
config: &CrashtrackerConfiguration,
config_str: &str,
metadata_str: &str,
message_ptr: *mut String,
sig_info: *const siginfo_t,
ucontext: *const ucontext_t,
crash_kind: CrashKindData,
) -> Result<Self, CollectorSpawnError> {
// When we spawn the child, our pid becomes the ppid.
// SAFETY: This function has no safety requirements.
Expand All @@ -49,9 +46,7 @@ impl Collector {
config,
config_str,
metadata_str,
message_ptr,
sig_info,
ucontext,
crash_kind,
receiver.handle.uds_fd,
pid,
tid,
Expand Down Expand Up @@ -89,9 +84,7 @@ pub(crate) fn run_collector_child(
config: &CrashtrackerConfiguration,
config_str: &str,
metadata_str: &str,
message_ptr: *mut String,
sig_info: *const siginfo_t,
ucontext: *const ucontext_t,
crash_kind: CrashKindData,
uds_fd: RawFd,
ppid: libc::pid_t,
crashing_tid: libc::pid_t,
Expand All @@ -117,8 +110,7 @@ pub(crate) fn run_collector_child(
config,
config_str,
metadata_str,
message_ptr,
CrashKindData::UnixSignal { sig_info, ucontext },
crash_kind,
ppid,
crashing_tid,
);
Expand Down
167 changes: 137 additions & 30 deletions libdd-crashtracker/src/collector/crash_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ use super::collector_manager::Collector;
use super::receiver_manager::Receiver;
use super::saguard::{SaGuard, SuppressionMode};
use super::signal_handler_manager::chain_signal_handler;
use crate::collector::emitters::CrashKindData;
use crate::crash_info::Metadata;
use crate::shared::configuration::CrashtrackerConfiguration;
use crate::StackTrace;
use crate::{StackFrame, StackTrace};
use errno::{errno, set_errno};
use libc::{c_void, siginfo_t, ucontext_t};
use libdd_common::timeout::TimeoutManager;
Expand Down Expand Up @@ -44,6 +45,7 @@ use std::sync::atomic::{AtomicBool, AtomicPtr, AtomicU64};
static METADATA: AtomicPtr<(Metadata, String)> = AtomicPtr::new(ptr::null_mut());
static CONFIG: AtomicPtr<(CrashtrackerConfiguration, String)> = AtomicPtr::new(ptr::null_mut());
static PANIC_MESSAGE: AtomicPtr<String> = AtomicPtr::new(ptr::null_mut());
static PANIC_CALLSTACK: AtomicPtr<StackTrace> = AtomicPtr::new(ptr::null_mut());

type PanicHook = Box<dyn Fn(&PanicHookInfo<'_>) + Send + Sync>;
static PREVIOUS_PANIC_HOOK: AtomicPtr<PanicHook> = AtomicPtr::new(ptr::null_mut());
Expand Down Expand Up @@ -102,6 +104,52 @@ fn format_message(
}
}

unsafe fn capture_panic_callstack() -> anyhow::Result<()> {
let mut stacktrace = StackTrace::new_incomplete();
backtrace::trace_unsynchronized(|bt_frame| {
let mut frame = StackFrame::new();
frame.with_ip(bt_frame.ip() as usize);
frame.with_symbol_address(bt_frame.symbol_address() as usize);
if let Some(module_base_address) = bt_frame.module_base_address() {
frame.with_module_base_address(module_base_address as usize);
}
frame.with_sp(bt_frame.sp() as usize);
let _ = stacktrace.push_frame(frame, true);
true
});

stacktrace.set_complete()?;

let stacktrace_ptr = PANIC_CALLSTACK.swap(Box::into_raw(Box::new(stacktrace)), SeqCst);
// stacktrace_ptr should be null, but just in case.
if !stacktrace_ptr.is_null() {
unsafe {
std::mem::drop(Box::from_raw(stacktrace_ptr));
}
}
Ok(())
}

fn capture_panic_message(panic_info: &PanicHookInfo<'_>) {
// Extract panic message from payload (supports &str and String)
let message = if let Some(&s) = panic_info.payload().downcast_ref::<&str>() {
format_message("message", s, panic_info.location())
} else if let Some(s) = panic_info.payload().downcast_ref::<String>() {
format_message("message", s.as_str(), panic_info.location())
} else {
// For non-string types, use a generic message
format_message("unknown type", "", panic_info.location())
};

let message_ptr = PANIC_MESSAGE.swap(Box::into_raw(Box::new(message)), SeqCst);
// message_ptr should be null, but just in case.
if !message_ptr.is_null() {
unsafe {
std::mem::drop(Box::from_raw(message_ptr));
}
}
}

/// Register the panic hook.
///
/// This function is used to register the panic hook and store the previous hook.
Expand All @@ -122,24 +170,8 @@ pub fn register_panic_hook() -> anyhow::Result<()> {
let old_hook_ptr = Box::into_raw(Box::new(old_hook));
PREVIOUS_PANIC_HOOK.swap(old_hook_ptr, SeqCst);
panic::set_hook(Box::new(|panic_info| {
// Extract panic message from payload (supports &str and String)
let message = if let Some(&s) = panic_info.payload().downcast_ref::<&str>() {
format_message("message", s, panic_info.location())
} else if let Some(s) = panic_info.payload().downcast_ref::<String>() {
format_message("message", s.as_str(), panic_info.location())
} else {
// For non-string types, use a generic message
format_message("unknown type", "", panic_info.location())
};

// Store the message, cleaning up any old message
let message_ptr = PANIC_MESSAGE.swap(Box::into_raw(Box::new(message)), SeqCst);
// message_ptr should be null, but just in case.
if !message_ptr.is_null() {
unsafe {
std::mem::drop(Box::from_raw(message_ptr));
}
}
let _ = unsafe { capture_panic_callstack() };
capture_panic_message(panic_info);

call_previous_panic_hook(panic_info);
}));
Expand Down Expand Up @@ -298,20 +330,22 @@ fn handle_posix_signal_impl(
// The collector child process will handle converting this to a String after forking.
// Leak of the message pointer is ok here.
let message_ptr = PANIC_MESSAGE.swap(ptr::null_mut(), SeqCst);
let callstack_ptr = PANIC_CALLSTACK.swap(ptr::null_mut(), SeqCst);

let crash_kind = if !message_ptr.is_null() {
CrashKindData::Panic {
stacktrace: callstack_ptr,
message: message_ptr,
}
} else {
CrashKindData::UnixSignal { sig_info, ucontext }
};

let timeout_manager = TimeoutManager::new(config.timeout());

let receiver = Receiver::from_crashtracker_config(config)?;

let collector = Collector::spawn(
&receiver,
config,
config_str,
metadata_string,
message_ptr,
sig_info,
ucontext,
)?;
let collector = Collector::spawn(&receiver, config, config_str, metadata_string, crash_kind)?;

// We're done. Wrap up our interaction with the receiver.
collector.finish(&timeout_manager);
Expand Down Expand Up @@ -453,8 +487,10 @@ pub fn report_unhandled_exception(
&config,
&config_str,
&metadata_str,
message_ptr,
super::emitters::CrashKindData::UnhandledException { stacktrace },
super::emitters::CrashKindData::UnhandledException {
stacktrace,
message: message_ptr,
},
pid,
tid,
);
Expand Down Expand Up @@ -572,6 +608,77 @@ mod tests {
}
}

fn make_test_stacktrace() -> StackTrace {
let mut st = StackTrace::new_incomplete();
let mut frame = StackFrame::new();
frame.with_ip(0xdead);
frame.with_sp(0xbeef);
let _ = st.push_frame(frame, true);
st.set_complete().unwrap();
st
}

fn clear_panic_callstack() {
let ptr = PANIC_CALLSTACK.swap(ptr::null_mut(), SeqCst);
if !ptr.is_null() {
unsafe { drop(Box::from_raw(ptr)) };
}
}

#[test]
fn test_panic_callstack_storage_and_retrieval() {
clear_panic_callstack();
let test_stacktrace = make_test_stacktrace();
let stacktrace_ptr = Box::into_raw(Box::new(test_stacktrace.clone()));

let old_ptr = PANIC_CALLSTACK.swap(stacktrace_ptr, SeqCst);
assert!(old_ptr.is_null());

let retrieved_ptr = PANIC_CALLSTACK.swap(ptr::null_mut(), SeqCst);
assert!(!retrieved_ptr.is_null());

unsafe {
let retrieved_stacktrace = *Box::from_raw(retrieved_ptr);
assert_eq!(retrieved_stacktrace, test_stacktrace);
}
}

#[test]
fn test_panic_callstack_null_handling() {
clear_panic_callstack();

let callstack_ptr = PANIC_CALLSTACK.load(SeqCst);
assert!(callstack_ptr.is_null());

let old_ptr = PANIC_CALLSTACK.swap(ptr::null_mut(), SeqCst);
assert!(old_ptr.is_null());
}

#[test]
fn test_panic_callstack_replacement() {
clear_panic_callstack();
let stacktrace1 = make_test_stacktrace();
let mut stacktrace2 = make_test_stacktrace();
let mut extra_frame = StackFrame::new();
extra_frame.with_ip(0xcafe);
stacktrace2.frames.push(extra_frame);

let ptr1 = Box::into_raw(Box::new(stacktrace1));
let ptr2 = Box::into_raw(Box::new(stacktrace2.clone()));

PANIC_CALLSTACK.store(ptr1, SeqCst);
let old_ptr = PANIC_CALLSTACK.swap(ptr2, SeqCst);

assert_eq!(old_ptr, ptr1);

unsafe {
drop(Box::from_raw(old_ptr));
let final_ptr = PANIC_CALLSTACK.swap(ptr::null_mut(), SeqCst);
let final_stacktrace = *Box::from_raw(final_ptr);
assert_eq!(final_stacktrace, stacktrace2);
}
}

#[test]
fn test_metadata_update_atomic() {
// Test that metadata updates are atomic
Expand Down
Loading
Loading