#![cfg(unix)]
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::crash_info::Metadata;
use crate::shared::configuration::CrashtrackerConfiguration;
use crate::StackTrace;
use core::ptr;
use core::sync::atomic::Ordering::{Acquire, Relaxed, SeqCst};
use core::sync::atomic::{AtomicBool, AtomicI32, AtomicPtr, AtomicU64};
use errno::{errno, set_errno};
use libc::{c_void, pid_t, siginfo_t, ucontext_t};
use libdd_common::timeout::TimeoutManager;
use std::os::fd::OwnedFd;
use std::os::unix::io::{AsRawFd, FromRawFd};
use std::os::unix::net::UnixStream;
use std::panic;
use std::panic::PanicHookInfo;
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());
type PanicHook = Box<dyn Fn(&PanicHookInfo<'_>) + Send + Sync>;
static PREVIOUS_PANIC_HOOK: AtomicPtr<PanicHook> = AtomicPtr::new(ptr::null_mut());
static EXPECTED_RECEIVER_PID: AtomicI32 = AtomicI32::new(0);
pub fn set_expected_receiver_pid(pid: pid_t) {
EXPECTED_RECEIVER_PID.store(pid, Relaxed);
}
pub fn get_expected_receiver_pid() -> pid_t {
EXPECTED_RECEIVER_PID.load(Relaxed)
}
#[derive(Debug, thiserror::Error)]
pub enum CrashHandlerError {
#[error("No crashtracking config available")]
NoConfig,
#[error("No crashtracking metadata available")]
NoMetadata,
#[error("Failed to spawn receiver: {0}")]
ReceiverSpawnError(#[from] super::receiver_manager::ReceiverError),
#[error("Failed to spawn collector: {0}")]
CollectorSpawnError(#[from] super::collector_manager::CollectorSpawnError),
}
pub fn update_metadata(metadata: Metadata) -> anyhow::Result<()> {
let metadata_string = serde_json::to_string(&metadata)?;
let box_ptr = Box::into_raw(Box::new((metadata, metadata_string)));
let old = METADATA.swap(box_ptr, SeqCst);
if !old.is_null() {
unsafe {
core::mem::drop(Box::from_raw(old));
}
}
Ok(())
}
fn format_message(
category: &str,
panic_message: &str,
location: Option<&panic::Location>,
) -> String {
let base = if panic_message.is_empty() {
format!("Process panicked with {}", category)
} else {
format!("Process panicked with {} \"{}\"", category, panic_message)
};
match location {
Some(loc) => format!("{} ({}:{}:{})", base, loc.file(), loc.line(), loc.column()),
None => base,
}
}
pub fn register_panic_hook() -> anyhow::Result<()> {
if !PREVIOUS_PANIC_HOOK.load(SeqCst).is_null() {
return Ok(());
}
let old_hook = panic::take_hook();
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| {
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 {
format_message("unknown type", "", panic_info.location())
};
let message_ptr = PANIC_MESSAGE.swap(Box::into_raw(Box::new(message)), SeqCst);
if !message_ptr.is_null() {
unsafe {
core::mem::drop(Box::from_raw(message_ptr));
}
}
call_previous_panic_hook(panic_info);
}));
Ok(())
}
fn call_previous_panic_hook(panic_info: &PanicHookInfo<'_>) {
let old_hook_ptr = PREVIOUS_PANIC_HOOK.load(SeqCst);
if !old_hook_ptr.is_null() {
unsafe {
let old_hook = &*old_hook_ptr;
old_hook(panic_info);
}
}
}
pub fn update_config(config: CrashtrackerConfiguration) -> anyhow::Result<()> {
let config_string = serde_json::to_string(&config)?;
let box_ptr = Box::into_raw(Box::new((config, config_string)));
let old = CONFIG.swap(box_ptr, SeqCst);
if !old.is_null() {
unsafe {
core::mem::drop(Box::from_raw(old));
}
}
Ok(())
}
pub(crate) extern "C" fn handle_posix_sigaction(
signum: i32,
sig_info: *mut siginfo_t,
ucontext: *mut c_void,
) {
let errno = errno();
let _ = handle_posix_signal_impl(sig_info, ucontext as *mut ucontext_t);
set_errno(errno);
unsafe { chain_signal_handler(signum, sig_info, ucontext) };
}
static ENABLED: AtomicBool = AtomicBool::new(true);
pub fn disable() {
ENABLED.store(false, SeqCst);
}
pub fn enable() {
ENABLED.store(true, SeqCst);
}
fn handle_posix_signal_impl(
sig_info: *const siginfo_t,
ucontext: *const ucontext_t,
) -> Result<(), CrashHandlerError> {
if !ENABLED.load(SeqCst) {
return Ok(());
}
static NUM_TIMES_CALLED: AtomicU64 = AtomicU64::new(0);
if NUM_TIMES_CALLED.fetch_add(1, SeqCst) > 0 {
return Ok(());
}
#[cfg(target_os = "linux")]
{
super::api::mark_preload_logger_collector();
}
let _sa_guard = SaGuard::new_with_modes(&[
(
nix::sys::signal::Signal::SIGCHLD,
SuppressionMode::BlockOnly,
),
(
nix::sys::signal::Signal::SIGPIPE,
SuppressionMode::IgnoreAndBlock,
),
]);
let config_ptr = take_config_ptr();
if config_ptr.is_null() {
return Err(CrashHandlerError::NoConfig);
}
let (config, config_str) = unsafe { &*config_ptr };
let metadata_ptr = take_metadata_ptr();
if metadata_ptr.is_null() {
return Err(CrashHandlerError::NoMetadata);
}
let (_metadata, metadata_string) = unsafe { &*metadata_ptr };
let panic_message_ptr = PANIC_MESSAGE.swap(ptr::null_mut(), Acquire);
let message: Option<&str> = if !panic_message_ptr.is_null() {
Some(unsafe { &*panic_message_ptr })
} else {
#[cfg(all(target_os = "linux", target_pointer_width = "64"))]
{
super::assert_interceptor::take_assert_message()
}
#[cfg(not(all(target_os = "linux", target_pointer_width = "64")))]
{
None
}
};
let timeout_manager = TimeoutManager::new(config.timeout());
let receiver = Receiver::from_crashtracker_config(config)?;
#[cfg(target_os = "linux")]
if config.collect_all_threads() {
grant_ptracer_permission(&receiver);
}
let collector = Collector::spawn(
&receiver,
config,
config_str,
metadata_string,
message,
sig_info,
ucontext,
)?;
collector.finish(&timeout_manager);
receiver.finish(&timeout_manager);
Ok(())
}
fn take_metadata_ptr() -> *mut (crate::crash_info::Metadata, String) {
METADATA.swap(ptr::null_mut(), SeqCst)
}
fn take_config_ptr() -> *mut (
crate::shared::configuration::CrashtrackerConfiguration,
String,
) {
CONFIG.swap(ptr::null_mut(), SeqCst)
}
fn take_metadata() -> Option<(crate::crash_info::Metadata, String)> {
let ptr = take_metadata_ptr();
if ptr.is_null() {
None
} else {
Some(*unsafe { Box::from_raw(ptr) })
}
}
fn take_config() -> Option<(
crate::shared::configuration::CrashtrackerConfiguration,
String,
)> {
let ptr = take_config_ptr();
if ptr.is_null() {
None
} else {
Some(*unsafe { Box::from_raw(ptr) })
}
}
#[cfg(target_os = "linux")]
fn grant_ptracer_permission(receiver: &Receiver) {
let ptracer_pid = match receiver.handle.pid {
Some(pid) => pid,
None => {
let expected_pid = get_expected_receiver_pid();
if expected_pid <= 0 {
0
} else {
let mut cred: libc::ucred = unsafe { core::mem::zeroed() };
let mut len = core::mem::size_of::<libc::ucred>() as libc::socklen_t;
let ret = unsafe {
libc::getsockopt(
receiver.handle.uds_fd,
libc::SOL_SOCKET,
libc::SO_PEERCRED,
&mut cred as *mut _ as *mut libc::c_void,
&mut len,
)
};
if ret == 0 && cred.pid == expected_pid {
cred.pid
} else {
0
}
}
}
};
if ptracer_pid > 0 {
unsafe {
libc::prctl(libc::PR_SET_PTRACER, ptracer_pid as libc::c_ulong);
}
}
}
pub fn report_unhandled_exception(
exception_type: Option<&str>,
exception_message: Option<&str>,
stacktrace: StackTrace,
) -> Result<(), CrashHandlerError> {
disable();
let (config, config_str) = take_config().ok_or(CrashHandlerError::NoConfig)?;
let (_metadata, metadata_str) = take_metadata().ok_or(CrashHandlerError::NoMetadata)?;
let receiver = Receiver::from_crashtracker_config(&config)?;
#[cfg(target_os = "linux")]
if config.collect_all_threads() {
grant_ptracer_permission(&receiver);
}
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>")
.replace('\n', "\\n")
.replace('\r', "\\r");
let error_message_str = exception_message
.unwrap_or("<no message>")
.replace('\n', "\\n")
.replace('\r', "\\r");
let message = format!(
"Process was terminated due to an unhandled exception of type '{error_type_str}'. \
Message: {error_message_str}"
);
let poll_fd = unsafe { OwnedFd::from_raw_fd(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,
Some(message.as_str()),
super::emitters::CrashKindData::UnhandledException { stacktrace },
pid,
tid,
);
}
let finish_handle =
super::process_handle::ProcessHandle::new(poll_fd.as_raw_fd(), receiver_pid);
finish_handle.finish(&timeout_manager);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use core::time::Duration;
fn make_test_metadata() -> Metadata {
Metadata {
library_name: "test-lib".to_string(),
library_version: "1.0.0".to_string(),
family: "test-family".to_string(),
tags: vec![],
}
}
fn make_test_config() -> CrashtrackerConfiguration {
let builder = CrashtrackerConfiguration::builder();
builder.timeout(Duration::from_secs(1)).build().unwrap()
}
fn clear_metadata() {
let ptr = METADATA.swap(ptr::null_mut(), SeqCst);
if !ptr.is_null() {
unsafe { drop(Box::from_raw(ptr)) };
}
}
fn clear_config() {
let ptr = CONFIG.swap(ptr::null_mut(), SeqCst);
if !ptr.is_null() {
unsafe { drop(Box::from_raw(ptr)) };
}
}
#[test]
fn test_register_panic_hook() {
assert!(PREVIOUS_PANIC_HOOK.load(SeqCst).is_null());
let result = register_panic_hook();
assert!(result.is_ok());
assert!(!PREVIOUS_PANIC_HOOK.load(SeqCst).is_null());
}
#[test]
fn test_panic_message_storage_and_retrieval() {
let test_message = "test panic message".to_string();
let message_ptr = Box::into_raw(Box::new(test_message.clone()));
let old_ptr = PANIC_MESSAGE.swap(message_ptr, SeqCst);
assert!(old_ptr.is_null());
let retrieved_ptr = PANIC_MESSAGE.swap(ptr::null_mut(), SeqCst);
assert!(!retrieved_ptr.is_null());
unsafe {
let retrieved_message = *Box::from_raw(retrieved_ptr);
assert_eq!(retrieved_message, test_message);
}
}
#[test]
fn test_panic_message_null_handling() {
PANIC_MESSAGE.store(ptr::null_mut(), SeqCst);
let message_ptr = PANIC_MESSAGE.load(SeqCst);
assert!(message_ptr.is_null());
let old_ptr = PANIC_MESSAGE.swap(ptr::null_mut(), SeqCst);
assert!(old_ptr.is_null());
}
#[test]
fn test_panic_message_replacement() {
let message1 = "first message".to_string();
let message2 = "second message".to_string();
let ptr1 = Box::into_raw(Box::new(message1));
let ptr2 = Box::into_raw(Box::new(message2.clone()));
PANIC_MESSAGE.store(ptr1, SeqCst);
let old_ptr = PANIC_MESSAGE.swap(ptr2, SeqCst);
assert_eq!(old_ptr, ptr1);
unsafe {
drop(Box::from_raw(old_ptr));
let final_ptr = PANIC_MESSAGE.swap(ptr::null_mut(), SeqCst);
let final_message = *Box::from_raw(final_ptr);
assert_eq!(final_message, message2);
}
}
#[test]
fn test_metadata_update_atomic() {
let metadata = Metadata {
library_name: "test".to_string(),
library_version: "1.0.0".to_string(),
family: "test_family".to_string(),
tags: vec![],
};
let result = update_metadata(metadata.clone());
assert!(result.is_ok());
let metadata_ptr = METADATA.load(SeqCst);
assert!(!metadata_ptr.is_null());
unsafe {
let (stored_metadata, _) = &*metadata_ptr;
assert_eq!(stored_metadata.library_name, "test");
}
}
#[test]
fn test_format_message_with_message_and_location() {
let location = panic::Location::caller();
let result = format_message("message", "test panic", Some(location));
assert!(result.starts_with("Process panicked with message \"test panic\" ("));
assert!(result.contains(&format!("{}:", location.file())));
assert!(result.contains(&format!(":{}", location.line())));
assert!(result.ends_with(&format!("{})", location.column())));
}
#[test]
fn test_format_message_with_message_no_location() {
let result = format_message("message", "test panic", None);
assert_eq!(result, "Process panicked with message \"test panic\"");
}
#[test]
fn test_format_message_empty_message_with_location() {
let location = panic::Location::caller();
let result = format_message("unknown type", "", Some(location));
assert!(result.starts_with("Process panicked with unknown type ("));
assert!(result.contains(&format!("{}:", location.file())));
assert!(result.ends_with(&format!("{})", location.column())));
}
#[test]
fn test_format_message_empty_message_no_location() {
let result = format_message("unknown type", "", None);
assert_eq!(result, "Process panicked with unknown type");
}
#[test]
fn test_format_message_different_categories() {
let result1 = format_message("message", "test", None);
assert_eq!(result1, "Process panicked with message \"test\"");
let result2 = format_message("unknown type", "", None);
assert_eq!(result2, "Process panicked with unknown type");
let result3 = format_message("custom category", "content", None);
assert_eq!(result3, "Process panicked with custom category \"content\"");
}
#[test]
fn test_format_message_with_special_characters() {
let result = format_message("message", "test \"quoted\" 'text'", None);
assert_eq!(
result,
"Process panicked with message \"test \"quoted\" 'text'\""
);
}
#[test]
fn test_take_metadata_ptr_returns_null_when_unset() {
clear_metadata();
assert!(take_metadata_ptr().is_null());
}
#[test]
fn test_take_metadata_ptr_takes_value_and_leaves_null() {
clear_metadata();
update_metadata(make_test_metadata()).unwrap();
let ptr = take_metadata_ptr();
assert!(!ptr.is_null());
assert!(take_metadata_ptr().is_null());
unsafe { drop(Box::from_raw(ptr)) };
}
#[test]
fn test_take_metadata_ptr_preserves_data() {
clear_metadata();
let metadata = make_test_metadata();
update_metadata(metadata.clone()).unwrap();
let ptr = take_metadata_ptr();
assert!(!ptr.is_null());
let (stored_metadata, stored_json) = unsafe { &*ptr };
assert_eq!(stored_metadata.library_name, metadata.library_name);
assert_eq!(stored_metadata.library_version, metadata.library_version);
assert_eq!(stored_metadata.family, metadata.family);
assert!(!stored_json.is_empty());
assert!(serde_json::from_str::<serde_json::Value>(stored_json).is_ok());
unsafe { drop(Box::from_raw(ptr)) };
}
#[test]
fn test_take_config_ptr_returns_null_when_unset() {
clear_config();
assert!(take_config_ptr().is_null());
}
#[test]
fn test_take_config_ptr_takes_value_and_leaves_null() {
clear_config();
update_config(make_test_config()).unwrap();
let ptr = take_config_ptr();
assert!(!ptr.is_null());
assert!(take_config_ptr().is_null());
unsafe { drop(Box::from_raw(ptr)) };
}
#[test]
fn test_take_metadata_returns_none_when_unset() {
clear_metadata();
assert!(take_metadata().is_none());
}
#[test]
fn test_take_metadata_returns_value_and_leaves_none() {
clear_metadata();
let metadata = make_test_metadata();
update_metadata(metadata.clone()).unwrap();
let (taken_metadata, taken_json) = take_metadata().expect("should return Some");
assert_eq!(taken_metadata.library_name, metadata.library_name);
assert_eq!(taken_metadata.library_version, metadata.library_version);
assert_eq!(taken_metadata.family, metadata.family);
assert!(!taken_json.is_empty());
assert!(take_metadata().is_none());
}
#[test]
fn test_take_config_returns_none_when_unset() {
clear_config();
assert!(take_config().is_none());
}
#[test]
fn test_take_config_returns_value_and_leaves_none() {
clear_config();
let config = make_test_config();
update_config(config.clone()).unwrap();
let (taken_config, taken_json) = take_config().expect("should return Some");
assert_eq!(taken_config, config);
assert!(!taken_json.is_empty());
assert!(serde_json::from_str::<serde_json::Value>(&taken_json).is_ok());
assert!(take_config().is_none());
}
}