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, SignalNames, StacktraceCollection};
use backtrace::Frame;
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),
}
unsafe fn emit_backtrace_by_frames(
w: &mut impl Write,
resolve_frames: StacktraceCollection,
fault_ip: usize,
) -> Result<(), EmitterError> {
writeln!(w, "{DD_CRASHTRACK_BEGIN_STACKTRACE}")?;
fn emit_absolute_addresses(w: &mut impl Write, frame: &Frame) -> Result<(), EmitterError> {
write!(w, "\"ip\": \"{:?}\"", frame.ip())?;
if let Some(module_base_address) = frame.module_base_address() {
write!(w, ", \"module_base_address\": \"{module_base_address:?}\"",)?;
}
write!(w, ", \"sp\": \"{:?}\"", frame.sp())?;
write!(w, ", \"symbol_address\": \"{:?}\"", frame.symbol_address())?;
Ok(())
}
let mut ip_found = false;
loop {
backtrace::trace_unsynchronized(|frame| {
let ip = frame.ip();
if ip as usize == fault_ip {
ip_found = true;
}
if !ip_found {
return true;
}
if resolve_frames == StacktraceCollection::EnabledWithInprocessSymbols {
backtrace::resolve_frame_unsynchronized(frame, |symbol| {
#[allow(clippy::unwrap_used)]
write!(w, "{{").unwrap();
#[allow(clippy::unwrap_used)]
emit_absolute_addresses(w, frame).unwrap();
if let Some(column) = symbol.colno() {
#[allow(clippy::unwrap_used)]
write!(w, ", \"column\": {column}").unwrap();
}
if let Some(file) = symbol.filename() {
#[allow(clippy::unwrap_used)]
write!(w, ", \"file\": {file:?}").unwrap();
}
if let Some(function) = symbol.name() {
#[allow(clippy::unwrap_used)]
write!(w, ", \"function\": \"{function}\"").unwrap();
}
if let Some(line) = symbol.lineno() {
#[allow(clippy::unwrap_used)]
write!(w, ", \"line\": {line}").unwrap();
}
#[allow(clippy::unwrap_used)]
writeln!(w, "}}").unwrap();
#[allow(clippy::unwrap_used)]
w.flush().unwrap();
});
} else {
#[allow(clippy::unwrap_used)]
write!(w, "{{").unwrap();
#[allow(clippy::unwrap_used)]
emit_absolute_addresses(w, frame).unwrap();
#[allow(clippy::unwrap_used)]
writeln!(w, "}}").unwrap();
#[allow(clippy::unwrap_used)]
w.flush().unwrap();
}
true });
if ip_found {
break;
}
ip_found = true;
}
writeln!(w, "{DD_CRASHTRACK_END_STACKTRACE}")?;
w.flush()?;
Ok(())
}
pub(crate) fn emit_crashreport(
pipe: &mut impl Write,
config: &CrashtrackerConfiguration,
config_str: &str,
metadata_string: &str,
sig_info: *const siginfo_t,
ucontext: *const ucontext_t,
ppid: i32,
) -> Result<(), EmitterError> {
emit_metadata(pipe, metadata_string)?;
emit_config(pipe, config_str)?;
emit_siginfo(pipe, sig_info)?;
emit_ucontext(pipe, ucontext)?;
emit_procinfo(pipe, ppid)?;
emit_counters(pipe)?;
emit_spans(pipe)?;
consume_and_emit_additional_tags(pipe)?;
emit_traces(pipe)?;
#[cfg(target_os = "linux")]
emit_proc_self_maps(pipe)?;
if config.resolve_frames() != StacktraceCollection::Disabled {
let fault_ip = extract_ip(ucontext);
unsafe { emit_backtrace_by_frames(pipe, config.resolve_frames(), fault_ip)? };
}
if is_runtime_callback_registered() {
emit_runtime_stack(pipe)?;
}
writeln!(pipe, "{DD_CRASHTRACK_DONE}")?;
pipe.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_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 emit_procinfo(w: &mut impl Write, pid: i32) -> Result<(), EmitterError> {
writeln!(w, "{DD_CRASHTRACK_BEGIN_PROCINFO}")?;
writeln!(w, "{{\"pid\": {pid} }}")?;
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}")?;
writeln!(w, "{:?}", unsafe { *ucontext })?;
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 mcontext = unsafe { *ucontext }.uc_mcontext;
writeln!(w, "{DD_CRASHTRACK_BEGIN_UCONTEXT}")?;
write!(w, "{:?}", unsafe { *ucontext })?;
if !mcontext.is_null() {
write!(w, ", {:?}", unsafe { *mcontext })?;
}
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(())
}
fn extract_ip(ucontext: *const ucontext_t) -> usize {
unsafe {
#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
return (*(*ucontext).uc_mcontext).__ss.__rip as usize;
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
return (*(*ucontext).uc_mcontext).__ss.__pc as usize;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
return (*ucontext).uc_mcontext.gregs[libc::REG_RIP as usize] as usize;
#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
return (*ucontext).uc_mcontext.pc as usize;
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str;
#[inline(never)]
fn inner_test_emit_backtrace_with_symbols(collection: StacktraceCollection) -> Vec<u8> {
let mut ip_of_test_fn = 0;
let mut skip = 3;
unsafe {
backtrace::trace_unsynchronized(|frame| {
ip_of_test_fn = frame.ip() as usize;
skip -= 1;
skip > 0
})
};
let mut buf = Vec::new();
unsafe {
emit_backtrace_by_frames(&mut buf, collection, ip_of_test_fn).expect("to work ;-)");
}
buf
}
#[test]
#[cfg_attr(miri, ignore)]
fn test_emit_backtrace_disabled() {
let buf = inner_test_emit_backtrace_with_symbols(StacktraceCollection::Disabled);
let out = str::from_utf8(&buf).expect("to be valid UTF8");
assert!(out.contains("BEGIN_STACKTRACE"));
assert!(out.contains("END_STACKTRACE"));
assert!(out.contains("\"ip\":"));
assert!(
!out.contains("\"column\":"),
"'column' key must not be emitted"
);
assert!(!out.contains("\"file\":"), "'file' key must not be emitted");
assert!(
!out.contains("\"function\":"),
"'function' key must not be emitted"
);
assert!(!out.contains("\"line\":"), "'line' key must not be emitted");
}
#[test]
#[cfg_attr(miri, ignore)]
fn test_emit_backtrace_with_symbols() {
let buf = inner_test_emit_backtrace_with_symbols(
StacktraceCollection::EnabledWithInprocessSymbols,
);
let out = str::from_utf8(&buf).expect("to be valid UTF8");
assert!(out.contains("BEGIN_STACKTRACE"));
assert!(out.contains("END_STACKTRACE"));
assert!(out.contains("\"column\":"), "'column' key missing");
assert!(out.contains("\"file\":"), "'file' key missing");
assert!(out.contains("\"function\":"), "'function' key missing");
assert!(out.contains("\"line\":"), "'line' key missing");
assert!(
!out.contains("emitters::emit_backtrace_by_frames"),
"crashtracker itself must be filtered, found 'backtrace::backtrace::libunwind'"
);
assert!(
!out.contains("backtrace::backtrace"),
"crashtracker itself must be filtered away, found 'backtrace::backtrace'"
);
}
}