use crate::collector::additional_tags::consume_and_emit_additional_tags;
use crate::collector::counters::emit_counters;
use crate::collector::spans::{emit_spans, emit_traces};
use crate::runtime_callback::{
get_registered_callback, invoke_runtime_callback_with_writer, is_runtime_callback_registered,
CallbackData,
};
use crate::shared::constants::*;
use crate::{
translate_si_code, CrashtrackerConfiguration, ErrorKind, SignalNames, StackTrace,
StacktraceCollection,
};
use libc::{siginfo_t, ucontext_t};
use std::{
fs::File,
io::{Read, Write},
};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum EmitterError {
#[error("Failed to write to output: {0}")]
WriteError(#[from] std::io::Error),
#[error("Failed to open file: {0}")]
FileOpenError(std::io::Error),
#[error("Null pointer provided for ucontext")]
NullUcontext,
#[error("Null pointer provided for siginfo")]
NullSiginfo,
#[error("Counter error: {0}")]
CounterError(#[from] crate::collector::counters::CounterError),
#[error("Atomic set error: {0}")]
AtomicSetError(#[from] crate::collector::atomic_set::AtomicSetError),
#[error("Serialization error: {0}")]
SerializationError(#[from] serde_json::Error),
}
pub(crate) enum CrashKindData {
UnixSignal {
sig_info: *const siginfo_t,
ucontext: *const ucontext_t,
},
UnhandledException {
stacktrace: StackTrace,
},
}
impl CrashKindData {
fn error_kind(&self) -> ErrorKind {
match self {
CrashKindData::UnixSignal { .. } => ErrorKind::UnixSignal,
CrashKindData::UnhandledException { .. } => ErrorKind::UnhandledException,
}
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn emit_crashreport(
pipe: &mut impl Write,
config: &CrashtrackerConfiguration,
config_str: &str,
metadata_string: &str,
message: Option<&str>,
crash: CrashKindData,
ppid: i32,
crashing_tid: libc::pid_t,
) -> Result<(), EmitterError> {
emit_config(pipe, config_str)?;
emit_message(pipe, message)?;
match &crash {
CrashKindData::UnixSignal { sig_info, .. } => {
emit_siginfo(pipe, *sig_info)?;
}
CrashKindData::UnhandledException { .. } => {
}
}
emit_kind(pipe, &crash.error_kind())?;
emit_metadata(pipe, metadata_string)?;
emit_procinfo(pipe, ppid, crashing_tid)?;
emit_counters(pipe)?;
emit_spans(pipe)?;
consume_and_emit_additional_tags(pipe)?;
emit_traces(pipe)?;
#[cfg(target_os = "linux")]
emit_proc_self_maps(pipe)?;
match crash {
CrashKindData::UnixSignal { ucontext, .. } => {
emit_ucontext(pipe, ucontext)?;
if config.resolve_frames() != StacktraceCollection::Disabled {
unsafe { emit_backtrace_by_frames(pipe, config.resolve_frames(), ucontext)? };
}
if is_runtime_callback_registered() {
emit_runtime_stack(pipe)?;
}
}
CrashKindData::UnhandledException { stacktrace } => {
unsafe { emit_whole_stacktrace(pipe, stacktrace)? };
}
}
writeln!(pipe, "{DD_CRASHTRACK_DONE}")?;
pipe.flush()?;
Ok(())
}
unsafe fn emit_backtrace_by_frames(
w: &mut impl Write,
resolve_frames: StacktraceCollection,
ucontext: *const ucontext_t,
) -> Result<(), EmitterError> {
writeln!(w, "{DD_CRASHTRACK_BEGIN_STACKTRACE}")?;
#[cfg(target_os = "macos")]
{
let _ = resolve_frames;
unsafe { emit_macos_backtrace_from_ucontext(w, ucontext)? };
}
#[cfg(target_os = "linux")]
unsafe {
emit_backtrace_via_libunwind(w, resolve_frames, ucontext)?
};
writeln!(w, "{DD_CRASHTRACK_END_STACKTRACE}")?;
w.flush()?;
Ok(())
}
#[cfg(target_os = "linux")]
unsafe fn emit_backtrace_via_libunwind(
w: &mut impl Write,
resolve_frames: StacktraceCollection,
ucontext: *const ucontext_t,
) -> Result<(), EmitterError> {
use libdd_libunwind_sys::{
unw_get_proc_name, unw_get_reg, unw_init_local2, unw_step, UnwCursor, UnwWord, UNW_REG_FP,
UNW_REG_IP, UNW_REG_SP,
};
if ucontext.is_null() {
return Ok(());
}
let mut cursor: UnwCursor = unsafe { core::mem::zeroed() };
let ret = unsafe { unw_init_local2(&mut cursor, ucontext as *mut _, 1) };
if ret != 0 {
return Ok(());
}
const MAX_FRAMES: usize = 512;
for _ in 0..MAX_FRAMES {
let mut ip: UnwWord = 0;
let mut sp: UnwWord = 0;
let mut fp: UnwWord = 0;
if unsafe { unw_get_reg(&mut cursor, UNW_REG_IP, &mut ip) } != 0 || ip == 0 {
break;
}
let _ = unsafe { unw_get_reg(&mut cursor, UNW_REG_SP, &mut sp) };
let _ = unsafe { unw_get_reg(&mut cursor, UNW_REG_FP, &mut fp) };
write!(w, "{{\"ip\": \"0x{ip:x}\"")?;
write!(w, ", \"sp\": \"0x{sp:x}\"")?;
write!(w, ", \"fp\": \"0x{fp:x}\"")?;
let mut dl_info: libc::Dl_info = unsafe { core::mem::zeroed() };
if unsafe { libc::dladdr(ip as *const libc::c_void, &mut dl_info) } != 0 {
if !dl_info.dli_fbase.is_null() {
write!(w, ", \"module_base_address\": \"{:?}\"", dl_info.dli_fbase)?;
}
if !dl_info.dli_saddr.is_null() {
write!(w, ", \"symbol_address\": \"{:?}\"", dl_info.dli_saddr)?;
}
}
if resolve_frames == StacktraceCollection::EnabledWithInprocessSymbols {
let mut name_buf: [libc::c_char; 256] = [0; 256];
if unsafe {
unw_get_proc_name(
&mut cursor,
name_buf.as_mut_ptr(),
name_buf.len(),
core::ptr::null_mut(),
)
} == 0
{
let name = unsafe { core::ffi::CStr::from_ptr(name_buf.as_ptr()) };
if let Ok(s) = name.to_str() {
write!(w, ", \"function\": \"{s}\"")?;
}
}
}
writeln!(w, "}}")?;
w.flush()?;
if unsafe { unw_step(&mut cursor) } <= 0 {
break;
}
}
Ok(())
}
#[cfg(target_os = "macos")]
unsafe fn emit_macos_backtrace_from_ucontext(
w: &mut impl Write,
ucontext: *const ucontext_t,
) -> Result<(), EmitterError> {
if ucontext.is_null() {
return Ok(());
}
let mcontext = unsafe { (*ucontext).uc_mcontext };
if mcontext.is_null() {
return Ok(());
}
let thread = unsafe { libc::pthread_self() };
let stack_top = unsafe { libc::pthread_get_stackaddr_np(thread) } as usize;
let stack_size = unsafe { libc::pthread_get_stacksize_np(thread) };
let stack_bottom = stack_top.saturating_sub(stack_size);
let in_stack_bounds = |addr: usize, len: usize| -> bool {
let end = addr.saturating_add(len);
addr >= stack_bottom && end <= stack_top
};
let ss = unsafe { &(*mcontext).__ss };
#[cfg(target_arch = "aarch64")]
let (pc, mut fp) = (ss.__pc as usize, ss.__fp as usize);
#[cfg(target_arch = "x86_64")]
let (pc, mut fp) = (ss.__rip as usize, ss.__rbp as usize);
unsafe { emit_frame_with_dladdr(w, pc)? };
const MAX_FRAMES: usize = 512;
for _ in 0..MAX_FRAMES {
if fp == 0 || fp % core::mem::align_of::<usize>() != 0 {
break;
}
if !in_stack_bounds(fp, 2 * core::mem::size_of::<usize>()) {
break;
}
let next_fp = unsafe { *(fp as *const usize) };
let return_addr = unsafe { *((fp + core::mem::size_of::<usize>()) as *const usize) };
if return_addr == 0 {
break;
}
unsafe { emit_frame_with_dladdr(w, return_addr)? };
if next_fp <= fp {
break;
}
fp = next_fp;
}
Ok(())
}
#[cfg(target_os = "macos")]
unsafe fn emit_frame_with_dladdr(w: &mut impl Write, ip: usize) -> Result<(), EmitterError> {
let mut info: libc::Dl_info = unsafe { core::mem::zeroed() };
let resolved = unsafe { libc::dladdr(ip as *const libc::c_void, &mut info) } != 0;
write!(w, "{{\"ip\": \"0x{ip:x}\"")?;
if resolved {
if !info.dli_fbase.is_null() {
write!(w, ", \"module_base_address\": \"{:?}\"", info.dli_fbase)?;
}
if !info.dli_saddr.is_null() {
write!(w, ", \"symbol_address\": \"{:?}\"", info.dli_saddr)?;
}
if !info.dli_sname.is_null() {
let name = unsafe { core::ffi::CStr::from_ptr(info.dli_sname) };
if let Ok(s) = name.to_str() {
write!(w, ", \"function\": \"{s}\"")?;
}
}
}
writeln!(w, "}}")?;
w.flush()?;
Ok(())
}
unsafe fn emit_whole_stacktrace(
w: &mut impl Write,
stacktrace: StackTrace,
) -> Result<(), EmitterError> {
writeln!(w, "{DD_CRASHTRACK_BEGIN_WHOLE_STACKTRACE}")?;
let _ = serde_json::to_writer(&mut *w, &stacktrace);
writeln!(w)?;
writeln!(w, "{DD_CRASHTRACK_END_WHOLE_STACKTRACE}")?;
w.flush()?;
Ok(())
}
fn emit_config(w: &mut impl Write, config_str: &str) -> Result<(), EmitterError> {
writeln!(w, "{DD_CRASHTRACK_BEGIN_CONFIG}")?;
writeln!(w, "{config_str}")?;
writeln!(w, "{DD_CRASHTRACK_END_CONFIG}")?;
w.flush()?;
Ok(())
}
fn emit_kind<W: std::io::Write>(w: &mut W, kind: &ErrorKind) -> Result<(), EmitterError> {
writeln!(w, "{DD_CRASHTRACK_BEGIN_KIND}")?;
let _ = serde_json::to_writer(&mut *w, kind);
writeln!(w)?;
writeln!(w, "{DD_CRASHTRACK_END_KIND}")?;
w.flush()?;
Ok(())
}
fn emit_metadata(w: &mut impl Write, metadata_str: &str) -> Result<(), EmitterError> {
writeln!(w, "{DD_CRASHTRACK_BEGIN_METADATA}")?;
writeln!(w, "{metadata_str}")?;
writeln!(w, "{DD_CRASHTRACK_END_METADATA}")?;
w.flush()?;
Ok(())
}
fn write_sanitized_message_line(w: &mut impl Write, message: &str) -> Result<(), EmitterError> {
if message.starts_with("DD_CRASHTRACK_") {
w.write_all(b" ")?;
}
let bytes = message.as_bytes();
let mut start = 0;
for (i, &byte) in bytes.iter().enumerate() {
let escape: Option<&[u8]> = match byte {
b'\n' => Some(b"\\n"),
b'\r' => Some(b"\\r"),
_ => None,
};
if let Some(replacement) = escape {
if start < i {
w.write_all(&bytes[start..i])?;
}
w.write_all(replacement)?;
start = i + 1;
}
}
if start < bytes.len() {
w.write_all(&bytes[start..])?;
}
w.write_all(b"\n")?;
Ok(())
}
fn emit_message(w: &mut impl Write, message: Option<&str>) -> Result<(), EmitterError> {
if let Some(msg) = message {
if !msg.trim().is_empty() {
writeln!(w, "{DD_CRASHTRACK_BEGIN_MESSAGE}")?;
write_sanitized_message_line(w, msg)?;
writeln!(w, "{DD_CRASHTRACK_END_MESSAGE}")?;
w.flush()?;
}
}
Ok(())
}
fn emit_procinfo(w: &mut impl Write, pid: i32, tid: libc::pid_t) -> Result<(), EmitterError> {
writeln!(w, "{DD_CRASHTRACK_BEGIN_PROCINFO}")?;
writeln!(w, "{{\"pid\": {pid}, \"tid\": {tid} }}")?;
writeln!(w, "{DD_CRASHTRACK_END_PROCINFO}")?;
w.flush()?;
Ok(())
}
#[cfg(target_os = "linux")]
fn emit_proc_self_maps(w: &mut impl Write) -> Result<(), EmitterError> {
emit_text_file(w, "/proc/self/maps")?;
Ok(())
}
#[cfg(target_os = "linux")]
fn emit_ucontext(w: &mut impl Write, ucontext: *const ucontext_t) -> Result<(), EmitterError> {
if ucontext.is_null() {
return Err(EmitterError::NullUcontext);
}
writeln!(w, "{DD_CRASHTRACK_BEGIN_UCONTEXT}")?;
let uc = unsafe { &*ucontext };
#[cfg(target_arch = "x86_64")]
{
let gregs = &uc.uc_mcontext.gregs;
write!(w, "{{\"arch\": \"x86_64\", \"registers\": {{")?;
write!(w, "\"rip\": \"0x{:016x}\"", gregs[libc::REG_RIP as usize])?;
write!(w, ", \"rsp\": \"0x{:016x}\"", gregs[libc::REG_RSP as usize])?;
write!(w, ", \"rbp\": \"0x{:016x}\"", gregs[libc::REG_RBP as usize])?;
write!(w, ", \"rax\": \"0x{:016x}\"", gregs[libc::REG_RAX as usize])?;
write!(w, ", \"rbx\": \"0x{:016x}\"", gregs[libc::REG_RBX as usize])?;
write!(w, ", \"rcx\": \"0x{:016x}\"", gregs[libc::REG_RCX as usize])?;
write!(w, ", \"rdx\": \"0x{:016x}\"", gregs[libc::REG_RDX as usize])?;
write!(w, ", \"rsi\": \"0x{:016x}\"", gregs[libc::REG_RSI as usize])?;
write!(w, ", \"rdi\": \"0x{:016x}\"", gregs[libc::REG_RDI as usize])?;
write!(w, ", \"r8\": \"0x{:016x}\"", gregs[libc::REG_R8 as usize])?;
write!(w, ", \"r9\": \"0x{:016x}\"", gregs[libc::REG_R9 as usize])?;
write!(w, ", \"r10\": \"0x{:016x}\"", gregs[libc::REG_R10 as usize])?;
write!(w, ", \"r11\": \"0x{:016x}\"", gregs[libc::REG_R11 as usize])?;
write!(w, ", \"r12\": \"0x{:016x}\"", gregs[libc::REG_R12 as usize])?;
write!(w, ", \"r13\": \"0x{:016x}\"", gregs[libc::REG_R13 as usize])?;
write!(w, ", \"r14\": \"0x{:016x}\"", gregs[libc::REG_R14 as usize])?;
write!(w, ", \"r15\": \"0x{:016x}\"", gregs[libc::REG_R15 as usize])?;
write!(w, "}}, \"raw\": \"{:?}\"", uc)?;
writeln!(w, "}}")?;
}
#[cfg(target_arch = "aarch64")]
{
let mc = &uc.uc_mcontext;
write!(w, "{{\"arch\": \"aarch64\", \"registers\": {{")?;
write!(w, "\"pc\": \"0x{:016x}\"", mc.pc)?;
write!(w, ", \"sp\": \"0x{:016x}\"", mc.sp)?;
for i in 0..31 {
write!(w, ", \"x{}\": \"0x{:016x}\"", i, mc.regs[i])?;
}
write!(w, "}}, \"raw\": \"{:?}\"", uc)?;
writeln!(w, "}}")?;
}
writeln!(w, "{DD_CRASHTRACK_END_UCONTEXT}")?;
w.flush()?;
Ok(())
}
fn emit_runtime_stack(w: &mut impl Write) -> Result<(), EmitterError> {
let callback = unsafe { get_registered_callback() };
let callback = match callback {
Some(callback) => callback,
None => return Ok(()),
};
match callback {
CallbackData::Frame(_) => emit_runtime_stack_by_frames(w),
CallbackData::StacktraceString(_) => emit_runtime_stack_by_stacktrace_string(w),
}
}
fn emit_runtime_stack_by_frames(w: &mut impl Write) -> Result<(), EmitterError> {
writeln!(w, "{DD_CRASHTRACK_BEGIN_RUNTIME_STACK_FRAME}")?;
unsafe { invoke_runtime_callback_with_writer(w)? };
writeln!(w, "{DD_CRASHTRACK_END_RUNTIME_STACK_FRAME}")?;
w.flush()?;
Ok(())
}
fn emit_runtime_stack_by_stacktrace_string(w: &mut impl Write) -> Result<(), EmitterError> {
writeln!(w, "{DD_CRASHTRACK_BEGIN_RUNTIME_STACK_STRING}")?;
unsafe { invoke_runtime_callback_with_writer(w)? };
writeln!(w, "{DD_CRASHTRACK_END_RUNTIME_STACK_STRING}")?;
w.flush()?;
Ok(())
}
#[cfg(target_os = "macos")]
fn emit_ucontext(w: &mut impl Write, ucontext: *const ucontext_t) -> Result<(), EmitterError> {
if ucontext.is_null() {
return Err(EmitterError::NullUcontext);
}
let uc = unsafe { &*ucontext };
let mcontext = uc.uc_mcontext;
writeln!(w, "{DD_CRASHTRACK_BEGIN_UCONTEXT}")?;
if mcontext.is_null() {
write!(w, "{{\"arch\": \"")?;
#[cfg(target_arch = "x86_64")]
write!(w, "x86_64")?;
#[cfg(target_arch = "aarch64")]
write!(w, "aarch64")?;
write!(w, "\", \"registers\": {{}}")?;
write!(w, ", \"raw\": \"{:?}\"", uc)?;
writeln!(w, "}}")?;
} else {
let mc = unsafe { &*mcontext };
let ss = &mc.__ss;
#[cfg(target_arch = "x86_64")]
{
write!(w, "{{\"arch\": \"x86_64\", \"registers\": {{")?;
write!(w, "\"rip\": \"0x{:016x}\"", ss.__rip)?;
write!(w, ", \"rsp\": \"0x{:016x}\"", ss.__rsp)?;
write!(w, ", \"rbp\": \"0x{:016x}\"", ss.__rbp)?;
write!(w, ", \"rax\": \"0x{:016x}\"", ss.__rax)?;
write!(w, ", \"rbx\": \"0x{:016x}\"", ss.__rbx)?;
write!(w, ", \"rcx\": \"0x{:016x}\"", ss.__rcx)?;
write!(w, ", \"rdx\": \"0x{:016x}\"", ss.__rdx)?;
write!(w, ", \"rsi\": \"0x{:016x}\"", ss.__rsi)?;
write!(w, ", \"rdi\": \"0x{:016x}\"", ss.__rdi)?;
write!(w, ", \"r8\": \"0x{:016x}\"", ss.__r8)?;
write!(w, ", \"r9\": \"0x{:016x}\"", ss.__r9)?;
write!(w, ", \"r10\": \"0x{:016x}\"", ss.__r10)?;
write!(w, ", \"r11\": \"0x{:016x}\"", ss.__r11)?;
write!(w, ", \"r12\": \"0x{:016x}\"", ss.__r12)?;
write!(w, ", \"r13\": \"0x{:016x}\"", ss.__r13)?;
write!(w, ", \"r14\": \"0x{:016x}\"", ss.__r14)?;
write!(w, ", \"r15\": \"0x{:016x}\"", ss.__r15)?;
write!(w, "}}, \"raw\": \"{:?}, {:?}\"", uc, mc)?;
writeln!(w, "}}")?;
}
#[cfg(target_arch = "aarch64")]
{
write!(w, "{{\"arch\": \"aarch64\", \"registers\": {{")?;
write!(w, "\"pc\": \"0x{:016x}\"", ss.__pc)?;
write!(w, ", \"sp\": \"0x{:016x}\"", ss.__sp)?;
write!(w, ", \"fp\": \"0x{:016x}\"", ss.__fp)?;
write!(w, ", \"lr\": \"0x{:016x}\"", ss.__lr)?;
for i in 0..29 {
write!(w, ", \"x{}\": \"0x{:016x}\"", i, ss.__x[i])?;
}
write!(w, "}}, \"raw\": \"{:?}, {:?}\"", uc, mc)?;
writeln!(w, "}}")?;
}
}
writeln!(w, "{DD_CRASHTRACK_END_UCONTEXT}")?;
w.flush()?;
Ok(())
}
fn emit_siginfo(w: &mut impl Write, sig_info: *const siginfo_t) -> Result<(), EmitterError> {
if sig_info.is_null() {
return Err(EmitterError::NullSiginfo);
}
let si_signo = unsafe { (*sig_info).si_signo };
let si_signo_human_readable: SignalNames = si_signo.into();
let si_addr: Option<usize> = match si_signo {
libc::SIGILL | libc::SIGFPE | libc::SIGSEGV | libc::SIGBUS | libc::SIGTRAP => {
Some(unsafe { (*sig_info).si_addr() as usize })
}
_ => None,
};
let si_code = unsafe { (*sig_info).si_code };
let si_code_human_readable = translate_si_code(si_signo, si_code);
writeln!(w, "{DD_CRASHTRACK_BEGIN_SIGINFO}")?;
write!(w, "{{")?;
write!(w, "\"si_code\": {si_code}")?;
write!(
w,
", \"si_code_human_readable\": \"{si_code_human_readable:?}\""
)?;
write!(w, ", \"si_signo\": {si_signo}")?;
write!(
w,
", \"si_signo_human_readable\": \"{si_signo_human_readable:?}\""
)?;
if let Some(si_addr) = si_addr {
write!(w, ", \"si_addr\": \"{si_addr:#018x}\"")?;
}
writeln!(w, "}}")?;
writeln!(w, "{DD_CRASHTRACK_END_SIGINFO}")?;
w.flush()?;
Ok(())
}
#[allow(dead_code)]
fn emit_text_file(w: &mut impl Write, path: &str) -> Result<(), EmitterError> {
let mut file = File::open(path).map_err(EmitterError::FileOpenError)?;
const BUFFER_LEN: usize = 512;
let mut buffer = [0u8; BUFFER_LEN];
writeln!(w, "{DD_CRASHTRACK_BEGIN_FILE} {path}")?;
loop {
let read_count = file.read(&mut buffer)?;
w.write_all(&buffer[..read_count])?;
if read_count == 0 {
break;
}
}
writeln!(w, "\n{DD_CRASHTRACK_END_FILE} \"{path}\"")?;
w.flush()?;
Ok(())
}
#[cfg(test)]
mod tests {
use crate::StackFrame;
use super::*;
use alloc::str;
#[test]
fn test_emit_complete_stacktrace() {
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();
let mut buf = Vec::new();
unsafe { emit_whole_stacktrace(&mut buf, stacktrace).expect("to work ;-)") };
let out = str::from_utf8(&buf).expect("to be valid UTF8");
assert!(out.contains("\"ip\":\"0x4d2\""));
assert!(out.contains("\"function\":\"test_function1\""));
assert!(out.contains("\"file\":\"test_file1\""));
assert!(out.contains("\"ip\":\"0x162e\""));
assert!(out.contains("\"function\":\"test_function2\""));
assert!(out.contains("\"file\":\"test_file2\""));
}
#[test]
fn test_emit_message_none() {
let mut buf = Vec::new();
emit_message(&mut buf, None).expect("to work ;-)");
assert!(buf.is_empty());
}
#[test]
fn test_emit_message() {
let message = "test message";
let mut buf = Vec::new();
emit_message(&mut buf, Some(message)).expect("to work ;-)");
let out = str::from_utf8(&buf).expect("to be valid UTF8");
assert!(out.contains("BEGIN_MESSAGE"));
assert!(out.contains("END_MESSAGE"));
assert!(out.contains(message));
}
#[test]
fn test_emit_message_empty_string() {
let mut buf = Vec::new();
emit_message(&mut buf, Some("")).expect("to work");
assert!(buf.is_empty());
}
#[test]
fn test_emit_message_whitespace_only() {
let mut buf = Vec::new();
emit_message(&mut buf, Some(" \n\t ")).expect("to work");
assert!(buf.is_empty());
}
#[test]
fn test_emit_message_with_leading_trailing_whitespace() {
let message_with_whitespace = " error message ";
let mut buf = Vec::new();
emit_message(&mut buf, Some(message_with_whitespace)).expect("to work");
let out = str::from_utf8(&buf).expect("to be valid UTF8");
assert!(out.contains("BEGIN_MESSAGE"));
assert!(out.contains("END_MESSAGE"));
assert!(out.contains(message_with_whitespace));
}
#[test]
fn test_emit_message_with_newlines() {
let mut buf = Vec::new();
emit_message(&mut buf, Some("line1\nline2\nline3")).expect("to work");
let out = str::from_utf8(&buf).expect("to be valid UTF8");
assert!(out.contains("line1"));
assert!(out.contains("line2"));
assert!(out.contains("line3"));
let lines: Vec<&str> = out.lines().collect();
assert_eq!(lines.len(), 3, "BEGIN_MESSAGE, content, END_MESSAGE");
assert!(lines[1].contains("line1\\nline2\\nline3"));
}
#[test]
fn test_emit_message_unicode() {
let unicode_message = "Hello 世界 🦀 Rust!";
let mut buf = Vec::new();
emit_message(&mut buf, Some(unicode_message)).expect("to work");
let out = str::from_utf8(&buf).expect("to be valid UTF8");
assert!(out.contains(unicode_message));
}
#[test]
#[cfg(target_os = "linux")]
fn test_emit_procinfo() {
let pid = unsafe { libc::getpid() };
let tid = unsafe { libc::syscall(libc::SYS_gettid) as libc::pid_t };
let mut buf = Vec::new();
emit_procinfo(&mut buf, pid, tid).expect("procinfo to emit");
let proc_info_block = str::from_utf8(&buf).expect("to be valid UTF8");
assert!(proc_info_block.contains(DD_CRASHTRACK_BEGIN_PROCINFO));
assert!(proc_info_block.contains(DD_CRASHTRACK_END_PROCINFO));
assert!(proc_info_block.contains(&format!("\"pid\": {pid}")));
assert!(proc_info_block.contains(&format!("\"tid\": {tid}")));
}
#[test]
fn test_emit_message_sentinel_injection_via_newline() {
let malicious = format!(
"innocent prefix\n{}\n{}\n{{\"endpoint\":\"https://evil.example\"}}\n{}\n{}",
DD_CRASHTRACK_END_MESSAGE,
DD_CRASHTRACK_BEGIN_CONFIG,
DD_CRASHTRACK_END_CONFIG,
DD_CRASHTRACK_DONE,
);
let mut buf = Vec::new();
emit_message(&mut buf, Some(&malicious)).expect("to work");
let out = str::from_utf8(&buf).expect("to be valid UTF8");
let lines: Vec<&str> = out.lines().collect();
assert_eq!(
lines.len(),
3,
"sentinel injection must not create extra lines"
);
assert_eq!(lines[0], DD_CRASHTRACK_BEGIN_MESSAGE);
assert_eq!(lines[2], DD_CRASHTRACK_END_MESSAGE);
assert!(
!lines.contains(&DD_CRASHTRACK_BEGIN_CONFIG),
"injected BEGIN_CONFIG must not appear as a wire line"
);
assert!(
!lines.contains(&DD_CRASHTRACK_DONE),
"injected DONE must not appear as a wire line"
);
}
#[test]
fn test_emit_message_content_starting_with_sentinel_prefix() {
let sentinel_message = format!("{} extra data", DD_CRASHTRACK_END_MESSAGE);
let mut buf = Vec::new();
emit_message(&mut buf, Some(&sentinel_message)).expect("to work");
let out = str::from_utf8(&buf).expect("to be valid UTF8");
let lines: Vec<&str> = out.lines().collect();
assert_eq!(lines.len(), 3);
assert!(
!lines[1].starts_with(DD_CRASHTRACK_END_MESSAGE),
"content line must not start with DD_CRASHTRACK_END_MESSAGE, got: {}",
lines[1]
);
}
#[test]
fn test_write_sanitized_message_line_basic() {
let mut buf = Vec::new();
write_sanitized_message_line(&mut buf, "hello").unwrap();
assert_eq!(buf, b"hello\n");
buf.clear();
write_sanitized_message_line(&mut buf, "line1\nline2").unwrap();
assert_eq!(buf, b"line1\\nline2\n");
buf.clear();
write_sanitized_message_line(&mut buf, "a\r\nb").unwrap();
assert_eq!(buf, b"a\\r\\nb\n");
}
#[test]
fn test_write_sanitized_message_line_sentinel_prefix() {
let input = format!("{} injected", DD_CRASHTRACK_END_MESSAGE);
let mut buf = Vec::new();
write_sanitized_message_line(&mut buf, &input).unwrap();
let out = str::from_utf8(&buf).unwrap();
assert!(!out.starts_with(DD_CRASHTRACK_END_MESSAGE));
assert!(out.starts_with(' '));
}
#[test]
fn test_emit_message_very_long() {
let long_message = "x".repeat(100000); let mut buf = Vec::new();
emit_message(&mut buf, Some(&long_message)).expect("to work");
let out = str::from_utf8(&buf).expect("to be valid UTF8");
assert!(out.contains(&long_message[..100])); }
#[test]
#[cfg(target_os = "linux")]
fn test_emit_backtrace_via_libunwind_null_ucontext() {
let mut buf = Vec::new();
unsafe {
emit_backtrace_via_libunwind(
&mut buf,
StacktraceCollection::WithoutSymbols,
core::ptr::null(),
)
.expect("should handle null ucontext gracefully");
}
assert!(buf.is_empty());
}
#[test]
#[cfg(target_os = "linux")]
#[cfg_attr(miri, ignore)]
fn test_emit_backtrace_via_libunwind_unw_init_failure() {
let context: libc::ucontext_t = unsafe { core::mem::zeroed() };
let mut buf = Vec::new();
unsafe {
emit_backtrace_via_libunwind(&mut buf, StacktraceCollection::WithoutSymbols, &context)
.expect("should handle unw_init_local2 failure gracefully");
}
assert!(
buf.is_empty(),
"Function should return early on unw_init_local2 failure"
);
}
#[test]
fn test_emit_ucontext_null_pointer() {
let mut buf = Vec::new();
let result = emit_ucontext(&mut buf, core::ptr::null());
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), EmitterError::NullUcontext));
assert!(buf.is_empty());
}
#[test]
#[cfg(target_os = "linux")]
fn test_emit_ucontext_linux_valid() {
let mut context: libc::ucontext_t = unsafe { core::mem::zeroed() };
#[cfg(target_arch = "x86_64")]
{
context.uc_mcontext.gregs[libc::REG_RIP as usize] = 0x12345678;
context.uc_mcontext.gregs[libc::REG_RSP as usize] = 0x87654321;
context.uc_mcontext.gregs[libc::REG_RBP as usize] = 0xABCDEF00;
}
#[cfg(target_arch = "aarch64")]
{
context.uc_mcontext.pc = 0x12345678;
context.uc_mcontext.sp = 0x87654321;
context.uc_mcontext.regs[0] = 0xABCDEF00;
}
let mut buf = Vec::new();
emit_ucontext(&mut buf, &context).expect("emit_ucontext should succeed");
let output = str::from_utf8(&buf).expect("output should be valid UTF-8");
assert!(output.contains(crate::shared::constants::DD_CRASHTRACK_BEGIN_UCONTEXT));
assert!(output.contains(crate::shared::constants::DD_CRASHTRACK_END_UCONTEXT));
#[cfg(target_arch = "x86_64")]
{
assert!(output.contains("\"arch\": \"x86_64\""));
assert!(output.contains("\"registers\""));
assert!(output.contains("\"rip\""));
assert!(output.contains("\"rsp\""));
assert!(output.contains("\"rbp\""));
assert!(output.contains("\"rax\""));
assert!(output.contains("0x0000000012345678")); assert!(output.contains("0x0000000087654321")); assert!(output.contains("0x00000000abcdef00")); }
#[cfg(target_arch = "aarch64")]
{
assert!(output.contains("\"arch\": \"aarch64\""));
assert!(output.contains("\"registers\""));
assert!(output.contains("\"pc\""));
assert!(output.contains("\"sp\""));
assert!(output.contains("\"x0\""));
assert!(output.contains("0x0000000012345678")); assert!(output.contains("0x0000000087654321")); assert!(output.contains("0x00000000abcdef00")); }
assert!(output.contains("\"raw\""));
let start_marker = crate::shared::constants::DD_CRASHTRACK_BEGIN_UCONTEXT;
let end_marker = crate::shared::constants::DD_CRASHTRACK_END_UCONTEXT;
let start_pos = output.find(start_marker).unwrap() + start_marker.len() + 1; let end_pos = output.find(end_marker).unwrap();
let json_part = output[start_pos..end_pos].trim();
let parsed: serde_json::Value =
serde_json::from_str(json_part).expect("JSON between markers should be valid");
assert!(parsed.is_object());
assert!(parsed["arch"].is_string());
assert!(parsed["registers"].is_object());
assert!(parsed["raw"].is_string());
}
#[test]
#[cfg(target_os = "macos")]
#[cfg_attr(miri, ignore)]
fn test_emit_ucontext_macos_valid() {
use libc::__darwin_ucontext;
let mut context: __darwin_ucontext = unsafe { core::mem::zeroed() };
let mut mcontext: libc::__darwin_mcontext64 = unsafe { core::mem::zeroed() };
context.uc_mcontext = &mut mcontext as *mut libc::__darwin_mcontext64;
#[cfg(target_arch = "x86_64")]
{
unsafe {
(*context.uc_mcontext).__ss.__rip = 0x12345678;
(*context.uc_mcontext).__ss.__rsp = 0x87654321;
(*context.uc_mcontext).__ss.__rbp = 0xABCDEF00;
}
}
#[cfg(target_arch = "aarch64")]
{
unsafe {
(*context.uc_mcontext).__ss.__pc = 0x12345678;
(*context.uc_mcontext).__ss.__sp = 0x87654321;
(*context.uc_mcontext).__ss.__fp = 0xABCDEF00;
}
}
let mut buf = Vec::new();
emit_ucontext(&mut buf, &context as *const __darwin_ucontext)
.expect("emit_ucontext should succeed");
let output = str::from_utf8(&buf).expect("output should be valid UTF-8");
assert!(output.contains(crate::shared::constants::DD_CRASHTRACK_BEGIN_UCONTEXT));
assert!(output.contains(crate::shared::constants::DD_CRASHTRACK_END_UCONTEXT));
#[cfg(target_arch = "x86_64")]
{
assert!(output.contains("\"arch\": \"x86_64\""));
assert!(output.contains("\"registers\""));
assert!(output.contains("\"rip\""));
assert!(output.contains("\"rsp\""));
assert!(output.contains("\"rbp\""));
assert!(output.contains("0x0000000012345678")); assert!(output.contains("0x0000000087654321")); assert!(output.contains("0x00000000abcdef00")); }
#[cfg(target_arch = "aarch64")]
{
assert!(output.contains("\"arch\": \"aarch64\""));
assert!(output.contains("\"registers\""));
assert!(output.contains("\"pc\""));
assert!(output.contains("\"sp\""));
assert!(output.contains("\"fp\""));
assert!(output.contains("0x0000000012345678")); assert!(output.contains("0x0000000087654321")); assert!(output.contains("0x00000000abcdef00")); }
assert!(output.contains("\"raw\""));
}
#[test]
#[cfg(target_os = "macos")]
#[cfg_attr(miri, ignore)]
fn test_emit_ucontext_macos_null_mcontext() {
let mut context: libc::ucontext_t = unsafe { core::mem::zeroed() };
context.uc_mcontext = core::ptr::null_mut();
let mut buf = Vec::new();
emit_ucontext(&mut buf, &context).expect("emit_ucontext should succeed with null mcontext");
let output = str::from_utf8(&buf).expect("output should be valid UTF-8");
assert!(output.contains(crate::shared::constants::DD_CRASHTRACK_BEGIN_UCONTEXT));
assert!(output.contains(crate::shared::constants::DD_CRASHTRACK_END_UCONTEXT));
assert!(output.contains("\"registers\": {}"));
assert!(output.contains("\"raw\""));
#[cfg(target_arch = "x86_64")]
assert!(output.contains("\"arch\": \"x86_64\""));
#[cfg(target_arch = "aarch64")]
assert!(output.contains("\"arch\": \"aarch64\""));
}
}