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
26 changes: 26 additions & 0 deletions bin_tests/src/bin/crashtracker_bin_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ mod unix {
use libdd_common::{tag, Endpoint};
use libdd_crashtracker::{
self as crashtracker, CrashtrackerConfiguration, CrashtrackerReceiverConfig, Metadata,
StackFrame, StackTrace,
};

const TEST_COLLECTOR_TIMEOUT: Duration = Duration::from_secs(15);
Expand Down Expand Up @@ -154,6 +155,31 @@ mod unix {
"raise_sigill" => raise(Signal::SIGILL)?,
"raise_sigbus" => raise(Signal::SIGBUS)?,
"raise_sigsegv" => raise(Signal::SIGSEGV)?,
"unhandled_exception" => {
let mut stacktrace = StackTrace::new_incomplete();
let mut stackframe1 = StackFrame::new();
stackframe1.with_ip(1234);
stackframe1.with_function("test_function1".to_string());
stackframe1.with_file("test_file1".to_string());

let mut stackframe2 = StackFrame::new();
stackframe2.with_ip(5678);
stackframe2.with_function("test_function2".to_string());
stackframe2.with_file("test_file2".to_string());

stacktrace.push_frame(stackframe1, true).unwrap();
stacktrace.push_frame(stackframe2, true).unwrap();

stacktrace.set_complete().unwrap();

crashtracker::report_unhandled_exception(
Some("RuntimeException"),
Some("an exception occured"),
stacktrace,
)?;

process::exit(0);
}
_ => anyhow::bail!("Unexpected crash_typ: {crash_typ}"),
}
crashtracker::end_op(crashtracker::OpTypes::ProfilerCollectingSample)?;
Expand Down
13 changes: 12 additions & 1 deletion bin_tests/src/test_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ pub enum CrashType {
RaiseSigBus,
/// Raise SIGSEGV
RaiseSigSegv,
/// Unhandled Exception
UnhandledException,
}

impl CrashType {
Expand All @@ -129,6 +131,7 @@ impl CrashType {
Self::RaiseSigIll => "raise_sigill",
Self::RaiseSigBus => "raise_sigbus",
Self::RaiseSigSegv => "raise_sigsegv",
Self::UnhandledException => "unhandled_exception",
}
}

Expand All @@ -138,7 +141,11 @@ impl CrashType {
pub const fn expects_success(self) -> bool {
matches!(
self,
Self::KillSigBus | Self::KillSigSegv | Self::RaiseSigBus | Self::RaiseSigSegv
Self::KillSigBus
| Self::KillSigSegv
| Self::RaiseSigBus
| Self::RaiseSigSegv
| Self::UnhandledException
)
}

Expand All @@ -150,6 +157,7 @@ impl CrashType {
Self::KillSigAbrt | Self::RaiseSigAbrt => 6, // SIGABRT
Self::KillSigIll | Self::RaiseSigIll => 4, // SIGILL
Self::KillSigBus | Self::RaiseSigBus => 7, // SIGBUS
Self::UnhandledException => 0, // no signal
}
}

Expand All @@ -160,6 +168,7 @@ impl CrashType {
Self::KillSigAbrt | Self::RaiseSigAbrt => "SIGABRT",
Self::KillSigIll | Self::RaiseSigIll => "SIGILL",
Self::KillSigBus | Self::RaiseSigBus => "SIGBUS",
Self::UnhandledException => "Unhandled Exception",
}
}
}
Expand All @@ -184,6 +193,7 @@ impl std::str::FromStr for CrashType {
"raise_sigill" => Ok(Self::RaiseSigIll),
"raise_sigbus" => Ok(Self::RaiseSigBus),
"raise_sigsegv" => Ok(Self::RaiseSigSegv),
"unhandled_exception" => Ok(Self::UnhandledException),
_ => Err(format!("Unknown crash type: {}", s)),
}
}
Expand Down Expand Up @@ -220,5 +230,6 @@ mod tests {
assert!(!CrashType::KillSigAbrt.expects_success());
assert!(CrashType::KillSigBus.expects_success());
assert!(CrashType::KillSigSegv.expects_success());
assert!(CrashType::UnhandledException.expects_success());
}
}
39 changes: 39 additions & 0 deletions bin_tests/tests/crashtracker_bin_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,39 @@ fn run_standard_crash_test_refactored(
// These tests below use the new infrastructure but require custom validation logic
// that doesn't fit the simple macro-generated pattern.

#[test]
#[cfg_attr(miri, ignore)]
fn test_crash_tracking_bin_unhandled_exception() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly, I should probably add more checks for the other fields

let config = CrashTestConfig::new(
BuildProfile::Release,
TestMode::DoNothing,
CrashType::UnhandledException,
);
let artifacts = StandardArtifacts::new(config.profile);
let artifacts_map = build_artifacts(&artifacts.as_slice()).unwrap();

let validator: ValidatorFn = Box::new(|payload, _fixtures| {
PayloadValidator::new(payload)
.validate_counters()?
.validate_error_kind("UnhandledException")?
.validate_error_message_contains("Process was terminated due to an unhandled exception of type 'RuntimeException'. Message: \"an exception occured\"")?
// The two frames emitted in the bin: test_function1 and test_function2
.validate_callstack_functions(&["test_function1", "test_function2"])?;

// Unhandled exceptions have no signal info
let sig_info = &payload["sig_info"];
assert!(
sig_info.is_null()
|| sig_info.is_object() && sig_info.as_object().is_none_or(|m| m.is_empty()),
"Expected no sig_info for unhandled exception, got: {sig_info:?}"
);

Ok(())
});

run_crash_test_with_artifacts(&config, &artifacts_map, &artifacts, validator).unwrap();
}

#[test]
#[cfg_attr(miri, ignore)]
fn test_crash_tracking_bin_runtime_callback_frame() {
Expand Down Expand Up @@ -1027,6 +1060,12 @@ fn assert_siginfo_message(sig_info: &Value, crash_typ: &str) {
assert_eq!(sig_info["si_signo"], libc::SIGILL);
assert_eq!(sig_info["si_signo_human_readable"], "SIGILL");
}
"unhandled_exception" => {
assert!(
sig_info.is_null()
|| sig_info.is_object() && sig_info.as_object().is_none_or(|m| m.is_empty())
);
}
_ => panic!("unexpected crash_typ {crash_typ}"),
}
}
Expand Down
7 changes: 5 additions & 2 deletions libdd-crashtracker/src/collector/collector_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use libdd_common::timeout::TimeoutManager;

use super::emitters::emit_crashreport;
use crate::shared::configuration::CrashtrackerConfiguration;
use crate::ErrorKind;
use libc::{siginfo_t, ucontext_t};
use libdd_common::unix_utils::{alt_fork, terminate};
use nix::sys::signal::{self, SaFlags, SigAction, SigHandler, SigSet};
Expand Down Expand Up @@ -118,10 +119,12 @@ pub(crate) fn run_collector_child(
config_str,
metadata_str,
message_ptr,
sig_info,
ucontext,
Some(sig_info),
None, // stacktrace is none; this is collected in the signal handler
Some(ucontext),
ppid,
crashing_tid,
ErrorKind::UnixSignal,
);
if let Err(e) = report {
eprintln!("Failed to flush crash report: {e}");
Expand Down
118 changes: 118 additions & 0 deletions libdd-crashtracker/src/collector/crash_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ use super::receiver_manager::Receiver;
use super::signal_handler_manager::chain_signal_handler;
use crate::crash_info::Metadata;
use crate::shared::configuration::CrashtrackerConfiguration;
use crate::{ErrorKind, StackTrace};
use libc::{c_void, siginfo_t, ucontext_t};
use libdd_common::timeout::TimeoutManager;
use std::os::unix::{io::FromRawFd, net::UnixStream};
use std::panic;
use std::panic::PanicHookInfo;
use std::ptr;
Expand Down Expand Up @@ -301,6 +303,122 @@ fn handle_posix_signal_impl(
Ok(())
}

/// Gets a clone of the current metadata, if set.
/// Unlike the signal handler path, this reads without consuming the stored value.
///
/// SAFETY:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this necessary or can we just consume the config and metadata like we do for signal based path? 🤔

/// This function must not be called concurrently with `update_metadata`.
fn get_metadata() -> Option<(crate::crash_info::Metadata, String)> {
let ptr = METADATA.load(SeqCst);
if ptr.is_null() {
None
} else {
// Safety: ptr was created by Box::into_raw in update_metadata
let (metadata, metadata_string) = unsafe { &*ptr };
Some((metadata.clone(), metadata_string.clone()))
}
}

/// Gets a clone of the current config, if set.
/// Unlike the signal handler path, this reads without consuming the stored value.
///
/// SAFETY:
/// This function must not be called concurrently with `update_config`.
fn get_config() -> Option<(
crate::shared::configuration::CrashtrackerConfiguration,
String,
)> {
let ptr = CONFIG.load(SeqCst);
if ptr.is_null() {
None
} else {
// Safety: ptr was created by Box::into_raw in update_config
let (config, config_string) = unsafe { &*ptr };
Some((config.clone(), config_string.clone()))
}
}

/// This function is designed to be when a program is at a terminal state
/// and the application wants to report an unhandled exception to the crashtracker
///
/// Preconditions:
/// - The crashtracker must be started
/// - The stacktrace must be valid
///
/// This function will spawn the receiver process and call an emit function to pipe over
/// the crash data. We don't use the collector process because we are not in a signal handler
/// Rather, we call emit_crashreport directly and pipe over data to the receiver
pub fn report_unhandled_exception(
exception_type: Option<&str>,
exception_message: Option<&str>,
stacktrace: StackTrace,
) -> Result<(), CrashHandlerError> {
let Some((config, config_str)) = get_config() else {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an explicit contract with the caller. Error if not upheld

return Err(CrashHandlerError::NoConfig);
};
let Some((_metadata, metadata_str)) = get_metadata() else {
return Err(CrashHandlerError::NoMetadata);
};

// Turn crashtracker off to prevent a recursive crash report emission
// We do not turn it back on because this function is not intended to be used as
// a recurring mechanism to report exceptions. We expect the application to exit
// after
disable();

let unix_socket_path = config.unix_socket_path().as_deref().unwrap_or_default();

let receiver = if unix_socket_path.is_empty() {
Receiver::spawn_from_stored_config()?
} else {
Receiver::from_socket(unix_socket_path)?
};

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

let pid = unsafe { libc::getpid() };
let tid = libdd_common::threading::get_current_thread_id() as libc::pid_t;

let error_type_str = exception_type.unwrap_or("<unknown>");
let error_message_str = exception_message.unwrap_or("<no message>");
let message = format!(
"Process was terminated due to an unhandled exception of type '{error_type_str}'. \
Message: \"{error_message_str}\""
);

let message_ptr = Box::into_raw(Box::new(message));

// Duplicate the socket fd so we can poll for receiver completion after we close the write end.
// UnixStream::from_raw_fd takes ownership of uds_fd, so we need a separate fd to poll.
let poll_fd = unsafe { libc::dup(receiver.handle.uds_fd) };
let receiver_pid = receiver.handle.pid;

{
let mut unix_stream = unsafe { UnixStream::from_raw_fd(receiver.handle.uds_fd) };
let _ = super::emitters::emit_crashreport(
&mut unix_stream,
&config,
&config_str,
&metadata_str,
message_ptr,
None,
Some(stacktrace),
None,
pid,
tid,
ErrorKind::UnhandledException,
);
// unix_stream is dropped here, closing the write end of the socket.
// This signals EOF to the receiver so it can finish writing the crash report.
}

// Wait for the receiver to signal it is done (POLLHUP on the dup'd fd), then reap it.
let finish_handle = super::process_handle::ProcessHandle::new(poll_fd, receiver_pid);
finish_handle.finish(&timeout_manager);
unsafe { libc::close(poll_fd) };

Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
Expand Down
Loading
Loading