use crate::{
crash_info::{CrashInfo, CrashInfoBuilder, ErrorKind, SigInfo, Span, StackFrame, Ucontext},
receiver::debug_logger::{DebugLogger, ReceiverIssue},
runtime_callback::RuntimeStack,
shared::constants::*,
CrashtrackerConfiguration, StackTrace,
};
use anyhow::Context;
use libdd_telemetry::data::LogLevel;
use serde::{Deserialize, Serialize};
use std::time::{Duration, Instant};
use tokio::io::AsyncBufReadExt;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
struct RuntimeStackFrame {
#[serde(default, skip_serializing_if = "Option::is_none")]
line: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
column: Option<u32>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
function: Vec<u8>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
type_name: Vec<u8>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
file: Vec<u8>,
}
impl From<RuntimeStackFrame> for StackFrame {
fn from(value: RuntimeStackFrame) -> Self {
let mut stack_frame = StackFrame::new();
stack_frame.function = if value.function.is_empty() {
None
} else {
Some(String::from_utf8_lossy(&value.function).to_string())
};
stack_frame.type_name = if value.type_name.is_empty() {
None
} else {
Some(String::from_utf8_lossy(&value.type_name).to_string())
};
stack_frame.file = if value.file.is_empty() {
None
} else {
Some(String::from_utf8_lossy(&value.file).to_string())
};
stack_frame.line = value.line;
stack_frame.column = value.column;
stack_frame
}
}
#[derive(Debug)]
pub(crate) enum StdinState {
AdditionalTags,
Config,
Counters,
Done,
File(String, Vec<String>),
Kind,
Metadata,
ProcInfo,
SigInfo,
SpanIds,
StackTrace,
TraceIds,
Ucontext,
Waiting,
WholeStackTrace,
RuntimeStackFrame(Vec<StackFrame>),
RuntimeStackString(Vec<String>),
Message,
}
fn process_line(
builder: &mut CrashInfoBuilder,
config: &mut Option<CrashtrackerConfiguration>,
line: &str,
state: StdinState,
debug_logger: &DebugLogger,
) -> anyhow::Result<StdinState> {
let next = match state {
StdinState::AdditionalTags if line.starts_with(DD_CRASHTRACK_END_ADDITIONAL_TAGS) => {
StdinState::Waiting
}
StdinState::AdditionalTags => {
let additional_tags: Vec<String> = serde_json::from_str(line)?;
builder.with_experimental_additional_tags(additional_tags)?;
StdinState::AdditionalTags
}
StdinState::Config if line.starts_with(DD_CRASHTRACK_END_CONFIG) => StdinState::Waiting,
StdinState::Config => {
if config.is_some() {
eprintln!("Unexpected double config");
}
*config = Some(serde_json::from_str(line)?);
StdinState::Config
}
StdinState::Counters if line.starts_with(DD_CRASHTRACK_END_COUNTERS) => StdinState::Waiting,
StdinState::Counters => {
let v: serde_json::Value = serde_json::from_str(line)?;
let map = v.as_object().context("Expected map type value")?;
anyhow::ensure!(map.len() == 1);
let (key, val) = map
.iter()
.next()
.context("we know there is one value here")?;
let val = val.as_i64().context("Vals are ints")?;
builder.with_counter(key.clone(), val)?;
StdinState::Counters
}
StdinState::WholeStackTrace if line.starts_with(DD_CRASHTRACK_END_WHOLE_STACKTRACE) => {
StdinState::Waiting
}
StdinState::WholeStackTrace => {
let stacktrace: StackTrace = serde_json::from_str(line)?;
builder.with_stack(stacktrace)?;
StdinState::WholeStackTrace
}
StdinState::Done => {
builder.with_log_message(
format!("Unexpected line after crashreport is done: {line}"),
true,
)?;
StdinState::Done
}
StdinState::File(filename, lines) if line.starts_with(DD_CRASHTRACK_END_FILE) => {
builder.with_file_and_contents(filename, lines)?;
StdinState::Waiting
}
StdinState::File(name, mut contents) => {
contents.push(line.to_string());
StdinState::File(name, contents)
}
StdinState::Kind if line.starts_with(DD_CRASHTRACK_END_KIND) => StdinState::Waiting,
StdinState::Kind => {
let kind: ErrorKind = serde_json::from_str(line)?;
builder.with_kind(kind)?;
StdinState::Kind
}
StdinState::Metadata if line.starts_with(DD_CRASHTRACK_END_METADATA) => StdinState::Waiting,
StdinState::Metadata => {
let metadata = serde_json::from_str(line)?;
builder.with_metadata(metadata)?;
StdinState::Metadata
}
StdinState::ProcInfo if line.starts_with(DD_CRASHTRACK_END_PROCINFO) => StdinState::Waiting,
StdinState::ProcInfo => {
let proc_info = serde_json::from_str(line)?;
builder.with_proc_info(proc_info)?;
StdinState::ProcInfo
}
StdinState::RuntimeStackFrame(frames)
if line.starts_with(DD_CRASHTRACK_END_RUNTIME_STACK_FRAME) =>
{
let runtime_stack = RuntimeStack {
format: "Datadog Runtime Callback 1.0".to_string(),
frames,
stacktrace_string: None,
};
builder.with_experimental_runtime_stack(runtime_stack)?;
StdinState::Waiting
}
StdinState::RuntimeStackFrame(mut frames) => {
let frame_json: RuntimeStackFrame = serde_json::from_str(line)?;
frames.push(frame_json.into());
StdinState::RuntimeStackFrame(frames)
}
StdinState::RuntimeStackString(lines)
if line.starts_with(DD_CRASHTRACK_END_RUNTIME_STACK_STRING) =>
{
let runtime_stack = RuntimeStack {
format: "Datadog Runtime Callback 1.0".to_string(),
frames: vec![],
stacktrace_string: Some(lines.join("\n")),
};
builder.with_experimental_runtime_stack(runtime_stack)?;
StdinState::Waiting
}
StdinState::RuntimeStackString(mut lines) => {
lines.push(line.to_string());
StdinState::RuntimeStackString(lines)
}
StdinState::SigInfo if line.starts_with(DD_CRASHTRACK_END_SIGINFO) => StdinState::Waiting,
StdinState::SigInfo => {
let sig_info: SigInfo = serde_json::from_str(line)?;
if !builder.has_message() {
let message = format!(
"Process terminated with {:?} ({:?})",
sig_info.si_code_human_readable, sig_info.si_signo_human_readable
);
builder.with_message(message)?;
}
builder.with_timestamp_now()?;
builder.with_sig_info(sig_info)?;
builder.with_incomplete(true)?;
StdinState::SigInfo
}
StdinState::Message if line.starts_with(DD_CRASHTRACK_END_MESSAGE) => StdinState::Waiting,
StdinState::Message => {
let unescaped = line.replace("\\n", "\n").replace("\\r", "\r");
builder.with_message(unescaped)?;
StdinState::Message
}
StdinState::SpanIds if line.starts_with(DD_CRASHTRACK_END_SPAN_IDS) => StdinState::Waiting,
StdinState::SpanIds => {
let span_ids: Vec<Span> = serde_json::from_str(line)?;
builder.with_span_ids(span_ids)?;
StdinState::SpanIds
}
StdinState::StackTrace if line.starts_with(DD_CRASHTRACK_END_STACKTRACE) => {
builder.with_stack_set_complete()?;
StdinState::Waiting
}
StdinState::StackTrace => {
let frame = serde_json::from_str(line)?;
builder.with_stack_frame(frame, true)?;
StdinState::StackTrace
}
StdinState::TraceIds if line.starts_with(DD_CRASHTRACK_END_TRACE_IDS) => {
StdinState::Waiting
}
StdinState::TraceIds => {
let trace_ids: Vec<Span> = serde_json::from_str(line)?;
builder.with_trace_ids(trace_ids)?;
StdinState::TraceIds
}
StdinState::Ucontext if line.starts_with(DD_CRASHTRACK_END_UCONTEXT) => StdinState::Waiting,
StdinState::Ucontext => {
let ucontext: Ucontext = serde_json::from_str(line)?;
builder.with_ucontext(ucontext)?;
StdinState::Ucontext
}
StdinState::Waiting if line.starts_with(DD_CRASHTRACK_BEGIN_ADDITIONAL_TAGS) => {
StdinState::AdditionalTags
}
StdinState::Waiting if line.starts_with(DD_CRASHTRACK_BEGIN_CONFIG) => StdinState::Config,
StdinState::Waiting if line.starts_with(DD_CRASHTRACK_BEGIN_COUNTERS) => {
StdinState::Counters
}
StdinState::Waiting if line.starts_with(DD_CRASHTRACK_BEGIN_FILE) => {
let (_, filename) = line.split_once(' ').unwrap_or(("", "MISSING_FILENAME"));
StdinState::File(filename.to_string(), vec![])
}
StdinState::Waiting if line.starts_with(DD_CRASHTRACK_BEGIN_KIND) => StdinState::Kind,
StdinState::Waiting if line.starts_with(DD_CRASHTRACK_BEGIN_METADATA) => {
StdinState::Metadata
}
StdinState::Waiting if line.starts_with(DD_CRASHTRACK_BEGIN_PROCINFO) => {
StdinState::ProcInfo
}
StdinState::Waiting if line.starts_with(DD_CRASHTRACK_BEGIN_SIGINFO) => StdinState::SigInfo,
StdinState::Waiting if line.starts_with(DD_CRASHTRACK_BEGIN_MESSAGE) => StdinState::Message,
StdinState::Waiting if line.starts_with(DD_CRASHTRACK_BEGIN_SPAN_IDS) => {
StdinState::SpanIds
}
StdinState::Waiting if line.starts_with(DD_CRASHTRACK_BEGIN_STACKTRACE) => {
StdinState::StackTrace
}
StdinState::Waiting if line.starts_with(DD_CRASHTRACK_BEGIN_RUNTIME_STACK_STRING) => {
StdinState::RuntimeStackString(vec![])
}
StdinState::Waiting if line.starts_with(DD_CRASHTRACK_BEGIN_RUNTIME_STACK_FRAME) => {
StdinState::RuntimeStackFrame(vec![])
}
StdinState::Waiting if line.starts_with(DD_CRASHTRACK_BEGIN_TRACE_IDS) => {
StdinState::TraceIds
}
StdinState::Waiting if line.starts_with(DD_CRASHTRACK_BEGIN_UCONTEXT) => {
StdinState::Ucontext
}
StdinState::Waiting if line.starts_with(DD_CRASHTRACK_BEGIN_WHOLE_STACKTRACE) => {
StdinState::WholeStackTrace
}
StdinState::Waiting if line.starts_with(DD_CRASHTRACK_DONE) => {
builder.with_incomplete(false)?;
StdinState::Done
}
StdinState::Waiting => {
let msg = format!("Unexpected line while receiving crashreport: {line}");
builder.with_log_message(msg.clone(), true)?;
debug_logger.emit(
ReceiverIssue::UnexpectedLine,
&builder.uuid.to_string(),
msg,
LogLevel::Warn,
);
StdinState::Waiting
}
};
Ok(next)
}
pub(crate) async fn receive_report_from_stream(
timeout: Duration,
stream: &mut (impl AsyncBufReadExt + std::marker::Unpin),
) -> anyhow::Result<Option<(CrashtrackerConfiguration, CrashInfo)>> {
let mut builder = CrashInfoBuilder::new();
let mut stdin_state = StdinState::Waiting;
let mut config: Option<CrashtrackerConfiguration> = None;
let mut debug_logger = DebugLogger::new(None, None);
let mut crash_ping_sent = false;
let mut lines = stream.lines();
let mut deadline = None;
let mut remaining_timeout = Duration::MAX;
loop {
debug_logger.update(config.as_ref(), builder.metadata.as_ref());
if !crash_ping_sent && builder.is_ping_ready() {
if let Some(ref config_ref) = config {
let config_clone = config_ref.clone();
crash_ping_sent = true;
let crash_ping = builder.build_crash_ping()?;
tokio::task::spawn(async move {
if let Err(e) = crash_ping
.upload_to_endpoint_async(config_clone.endpoint())
.await
{
eprintln!("Failed to send crash ping: {e}");
}
});
} else {
eprintln!("No config found, skipping crash ping");
}
}
let next_line = tokio::time::timeout(remaining_timeout, lines.next_line()).await;
let Ok(next_line) = next_line else {
builder.with_log_message(format!("Timeout: {next_line:?}"), true)?;
debug_logger.emit(
ReceiverIssue::Timeout,
&builder.uuid.to_string(),
format!("Timeout while waiting for crash report input: {next_line:?}"),
LogLevel::Warn,
);
break;
};
let Ok(next_line) = next_line else {
builder.with_log_message(format!("IO Error: {next_line:?}"), true)?;
debug_logger.emit(
ReceiverIssue::IoError,
&builder.uuid.to_string(),
format!("IO error while reading crash report input: {next_line:?}"),
LogLevel::Warn,
);
break;
};
let Some(next_line) = next_line else { break };
match process_line(
&mut builder,
&mut config,
&next_line,
stdin_state,
&debug_logger,
) {
Ok(next_state) => {
stdin_state = next_state;
if matches!(stdin_state, StdinState::Done) {
break;
}
}
Err(e) => {
builder.with_log_message(
format!("Unable to process line: {next_line}. Error: {e}"),
true,
)?;
debug_logger.emit(
ReceiverIssue::ProcessLine,
&builder.uuid.to_string(),
format!("Unable to process line: {next_line}. Error: {e}"),
LogLevel::Warn,
);
break;
}
}
if let Some(deadline) = deadline {
remaining_timeout = deadline - Instant::now()
} else {
deadline = Some(Instant::now() + timeout);
remaining_timeout = timeout;
}
}
if !builder.has_data() {
debug_logger
.emit_and_wait(
ReceiverIssue::NoData,
&builder.uuid.to_string(),
"Receiver received no data".to_string(),
LogLevel::Warn,
)
.await;
return Ok(None);
}
enrich_thread_name(&mut builder)?;
builder.with_os_info_this_machine()?;
let config = config.context("Missing crashtracker configuration")?;
for filename in config.additional_files() {
if let Err(e) = builder.with_file(filename.clone()) {
builder.with_log_message(e.to_string(), true)?;
debug_logger.emit(
ReceiverIssue::AttachAdditionalFile,
&builder.uuid.to_string(),
format!("Unable to attach additional file {filename:?}: {e}"),
LogLevel::Warn,
);
}
}
#[cfg(target_os = "linux")]
if config.collect_all_threads() {
if let Some(proc_info) = builder.proc_info.as_ref() {
let parent_pid = proc_info.pid;
let crashing_tid = proc_info.tid;
let remaining_budget = deadline
.map(|d| d.saturating_duration_since(Instant::now()))
.unwrap_or(Duration::ZERO);
if let Err(e) = collect_and_add_thread_contexts(
&mut builder,
&config,
parent_pid,
crashing_tid,
remaining_budget,
) {
let _ = builder
.with_log_message(format!("Failed to collect thread contexts: {e}"), true);
}
}
}
let crash_info = builder.build()?;
if crash_info.incomplete {
debug_logger.emit(
ReceiverIssue::IncompleteStacktrace,
&crash_info.uuid,
"CrashInfo stacktrace incomplete".to_string(),
LogLevel::Warn,
);
}
Ok(Some((config, crash_info)))
}
#[cfg(target_os = "linux")]
fn collect_and_add_thread_contexts(
builder: &mut CrashInfoBuilder,
config: &CrashtrackerConfiguration,
parent_pid: u32,
crashing_tid: Option<u32>,
budget: Duration,
) -> anyhow::Result<()> {
use crate::crash_info::{StackTrace, ThreadData};
use crate::receiver::ptrace_collector::stream_thread_contexts;
let crashing_tid = crashing_tid.unwrap_or(0) as i32;
let parent_pid = parent_pid as i32;
let crash_site = builder.ucontext.as_ref().and_then(crash_site_registers);
let mut collected_threads = Vec::new();
let incomplete = stream_thread_contexts(
parent_pid,
crashing_tid,
config.max_threads(),
budget,
|tid, captured_context| {
let (name, state) = read_thread_stat(parent_pid, tid);
let name = name.unwrap_or_else(|| tid.to_string());
let mut stack = match captured_context {
Some(ctx) => ctx.stack_trace.clone(),
None => StackTrace::new_incomplete(),
};
let crashed = tid == crashing_tid;
if crashed {
if let Some((ip, sp)) = crash_site {
drop_frames_above_crash_site(&mut stack, ip, sp);
}
}
collected_threads.push(ThreadData {
crashed,
name,
stack,
state,
});
},
)?;
if incomplete {
let _ = builder.with_counter("threads_incomplete".to_string(), 1);
}
let _ = builder.with_threads(collected_threads);
Ok(())
}
#[cfg(target_os = "linux")]
fn crash_site_registers(ucontext: &Ucontext) -> Option<(u64, u64)> {
let (ip_name, sp_name) = match ucontext.arch.as_str() {
"x86_64" => ("rip", "rsp"),
"aarch64" => ("pc", "sp"),
_ => return None,
};
let ip = parse_hex_address(ucontext.registers.get(ip_name)?)?;
let sp = parse_hex_address(ucontext.registers.get(sp_name)?)?;
Some((ip, sp))
}
#[cfg(target_os = "linux")]
fn parse_hex_address(value: &str) -> Option<u64> {
u64::from_str_radix(value.trim_start_matches("0x"), 16).ok()
}
#[cfg(target_os = "linux")]
fn drop_frames_above_crash_site(stack: &mut StackTrace, ip: u64, sp: u64) {
let is_crash_site = |frame: &StackFrame| {
frame.ip.as_deref().and_then(parse_hex_address) == Some(ip)
&& frame.sp.as_deref().and_then(parse_hex_address) == Some(sp)
};
if let Some(crash_site) = stack.frames.iter().position(is_crash_site) {
stack.frames.drain(..crash_site);
}
}
#[cfg(target_os = "linux")]
fn read_thread_stat(pid: i32, tid: i32) -> (Option<String>, Option<String>) {
let content = match std::fs::read_to_string(format!("/proc/{pid}/task/{tid}/stat")) {
Ok(c) => c,
Err(_) => return (None, None),
};
let Some(name_start) = content.find('(') else {
return (None, None);
};
let Some(name_end) = content.rfind(')') else {
return (None, None);
};
let name = Some(content[name_start + 1..name_end].to_string());
let state = content[name_end + 1..]
.split_whitespace()
.next()
.map(|s| s.to_string());
(name, state)
}
#[cfg(target_os = "linux")]
fn enrich_thread_name(builder: &mut CrashInfoBuilder) -> anyhow::Result<()> {
use std::{fs, path::PathBuf};
if builder.error.thread_name.is_some() {
return Ok(());
}
let Some(proc_info) = builder.proc_info.as_ref() else {
return Ok(());
};
let Some(tid) = proc_info.tid else {
return Ok(());
};
let pid = proc_info.pid;
let path = PathBuf::from(format!("/proc/{pid}/task/{tid}/comm"));
let Ok(comm) = fs::read_to_string(&path) else {
return Ok(());
};
let thread_name = comm.trim_end_matches('\n');
if thread_name.is_empty() {
return Ok(());
}
builder.with_thread_name(thread_name.to_string())?;
Ok(())
}
#[cfg(not(target_os = "linux"))]
fn enrich_thread_name(_builder: &mut CrashInfoBuilder) -> anyhow::Result<()> {
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
async fn serve_one_request(listener: tokio::net::TcpListener, marker: &str) -> String {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let (mut socket, _) = listener.accept().await.expect("accept");
let mut request = Vec::new();
let mut chunk = [0u8; 4096];
loop {
let n = socket.read(&mut chunk).await.expect("read");
if n == 0 {
break;
}
request.extend_from_slice(&chunk[..n]);
if String::from_utf8_lossy(&request).contains(marker) {
break;
}
}
let _ = socket
.write_all(b"HTTP/1.1 200 OK\r\ncontent-length: 0\r\n\r\n")
.await;
let _ = socket.flush().await;
String::from_utf8_lossy(&request).to_string()
}
#[tokio::test]
#[cfg_attr(miri, ignore)]
async fn test_receive_report_no_data_sends_debug_log() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
std::env::set_var(
"DD_TRACE_AGENT_URL",
format!("http://{}", listener.local_addr().unwrap()),
);
let server = tokio::spawn(async move {
tokio::time::timeout(
Duration::from_secs(5),
serve_one_request(listener, "no_data"),
)
.await
.expect("no telemetry request received")
});
let (sender, receiver) = tokio::net::UnixStream::pair().unwrap();
drop(sender);
let mut stream = tokio::io::BufReader::new(receiver);
let report = receive_report_from_stream(Duration::from_secs(1), &mut stream)
.await
.unwrap();
assert!(report.is_none());
let request = server.await.unwrap();
assert!(
request.contains("receiver_issue:no_data"),
"no_data tag missing from telemetry request: {request}"
);
assert!(
request.contains("Receiver received no data"),
"no_data message missing from telemetry request: {request}"
);
}
#[test]
fn test_stdin_state_waiting_to_message() {
let mut builder = CrashInfoBuilder::new();
let mut config = None;
let state = StdinState::Waiting;
let line = DD_CRASHTRACK_BEGIN_MESSAGE;
let next_state = process_line(
&mut builder,
&mut config,
line,
state,
&DebugLogger::disabled(),
)
.unwrap();
assert!(matches!(next_state, StdinState::Message));
}
#[test]
fn test_stdin_state_message_content() {
let mut builder = CrashInfoBuilder::new();
let mut config = None;
let state = StdinState::Message;
let message_line = "program panicked";
let next_state = process_line(
&mut builder,
&mut config,
message_line,
state,
&DebugLogger::disabled(),
)
.unwrap();
assert!(matches!(next_state, StdinState::Message));
assert!(builder.has_message());
}
#[test]
fn test_stdin_state_message_to_waiting() {
let mut builder = CrashInfoBuilder::new();
let mut config = None;
let state = StdinState::Message;
let line = DD_CRASHTRACK_END_MESSAGE;
let next_state = process_line(
&mut builder,
&mut config,
line,
state,
&DebugLogger::disabled(),
)
.unwrap();
assert!(matches!(next_state, StdinState::Waiting));
}
#[test]
fn test_message_state_with_empty_line() {
let mut builder = CrashInfoBuilder::new();
let mut config = None;
let state = StdinState::Message;
let empty_line = "";
let result = process_line(
&mut builder,
&mut config,
empty_line,
state,
&DebugLogger::disabled(),
);
assert!(result.is_ok());
}
#[test]
fn test_message_state_with_multiline_content() {
let mut builder = CrashInfoBuilder::new();
let mut config = None;
let state = process_line(
&mut builder,
&mut config,
"Line 1 of panic",
StdinState::Message,
&DebugLogger::disabled(),
)
.unwrap();
assert!(matches!(state, StdinState::Message));
}
#[test]
fn test_message_state_full_workflow() {
let mut builder = CrashInfoBuilder::new();
let mut config = None;
let mut state = StdinState::Waiting;
state = process_line(
&mut builder,
&mut config,
DD_CRASHTRACK_BEGIN_MESSAGE,
state,
&DebugLogger::disabled(),
)
.unwrap();
assert!(matches!(state, StdinState::Message));
state = process_line(
&mut builder,
&mut config,
"test panic message",
state,
&DebugLogger::disabled(),
)
.unwrap();
assert!(matches!(state, StdinState::Message));
assert!(builder.has_message());
state = process_line(
&mut builder,
&mut config,
DD_CRASHTRACK_END_MESSAGE,
state,
&DebugLogger::disabled(),
)
.unwrap();
assert!(matches!(state, StdinState::Waiting));
}
#[test]
fn test_stacktrace_empty_workflow() {
let mut builder = CrashInfoBuilder::new();
let mut config = None;
let mut state = StdinState::Waiting;
state = process_line(
&mut builder,
&mut config,
DD_CRASHTRACK_BEGIN_STACKTRACE,
state,
&DebugLogger::disabled(),
)
.unwrap();
assert!(matches!(state, StdinState::StackTrace));
state = process_line(
&mut builder,
&mut config,
DD_CRASHTRACK_END_STACKTRACE,
state,
&DebugLogger::disabled(),
)
.unwrap();
assert!(matches!(state, StdinState::Waiting));
let stack = builder.error.stack.as_ref().expect("Stack should exist");
assert!(stack.frames.is_empty());
assert!(
stack.incomplete,
"Stack should be marked incomplete when no frames were captured"
);
assert!(builder
.log_messages
.as_ref()
.map(|msgs| msgs
.iter()
.any(|msg| msg.contains("No native stack frames received")))
.unwrap_or(false));
}
#[test]
fn test_stacktrace_with_frames_workflow() {
let mut builder = CrashInfoBuilder::new();
let mut config = None;
let mut state = StdinState::Waiting;
state = process_line(
&mut builder,
&mut config,
DD_CRASHTRACK_BEGIN_STACKTRACE,
state,
&DebugLogger::disabled(),
)
.unwrap();
assert!(matches!(state, StdinState::StackTrace));
let frame_json = r#"{"ip":"0x1234"}"#;
state = process_line(
&mut builder,
&mut config,
frame_json,
state,
&DebugLogger::disabled(),
)
.unwrap();
assert!(matches!(state, StdinState::StackTrace));
state = process_line(
&mut builder,
&mut config,
DD_CRASHTRACK_END_STACKTRACE,
state,
&DebugLogger::disabled(),
)
.unwrap();
assert!(matches!(state, StdinState::Waiting));
let stack = builder.error.stack.as_ref().expect("Stack should exist");
assert_eq!(stack.frames.len(), 1);
assert!(!stack.incomplete, "Stack should be marked complete");
assert_eq!(stack.frames[0].ip, Some("0x1234".to_string()));
}
#[test]
fn test_message_with_escaped_sentinel_does_not_inject() {
let mut builder = CrashInfoBuilder::new();
let mut config = None;
let mut state = StdinState::Waiting;
state = process_line(
&mut builder,
&mut config,
DD_CRASHTRACK_BEGIN_MESSAGE,
state,
&DebugLogger::disabled(),
)
.unwrap();
assert!(matches!(state, StdinState::Message));
let sanitized_line = format!(
"Exception 'Evil'\\n{}\\n{}\\n{{}}\\n{}",
DD_CRASHTRACK_END_MESSAGE, DD_CRASHTRACK_BEGIN_CONFIG, DD_CRASHTRACK_END_CONFIG,
);
state = process_line(
&mut builder,
&mut config,
&sanitized_line,
state,
&DebugLogger::disabled(),
)
.unwrap();
assert!(
matches!(state, StdinState::Message),
"escaped sentinels must not trigger state transitions"
);
state = process_line(
&mut builder,
&mut config,
DD_CRASHTRACK_END_MESSAGE,
state,
&DebugLogger::disabled(),
)
.unwrap();
assert!(matches!(state, StdinState::Waiting));
assert!(
config.is_none(),
"no config section should have been parsed"
);
assert!(builder.has_message());
}
}
#[cfg(all(test, target_os = "linux"))]
mod crashing_thread_tests {
use super::*;
use std::collections::HashMap;
fn frame(ip: &str, sp: &str) -> StackFrame {
StackFrame {
ip: Some(ip.to_string()),
sp: Some(sp.to_string()),
..StackFrame::new()
}
}
fn ucontext(arch: &str, registers: &[(&str, &str)]) -> Ucontext {
Ucontext {
arch: arch.to_string(),
registers: registers
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect::<HashMap<_, _>>(),
raw: None,
}
}
fn parked_in_handler() -> StackTrace {
StackTrace::from_frames(
vec![
frame("0x7ddee3ca126f", "0x7ddee2f7cbe0"), frame("0x7ddee2b4f356", "0x7ddee2f7cc10"), frame("0x7ddee2b527db", "0x7ddee2f7cc80"), frame("0x7ddee3be1050", "0x7ddee2f7d4c0"), frame("0x7ddee3c2feec", "0x7ddee2f7da80"), frame("0x7ddee3be1050", "0x7ddee2f7dac0"),
frame("0x7ddee2e9ccc0", "0x7ffe06975aa0"),
],
false,
)
}
#[test]
fn crash_site_frame_becomes_the_first_frame() {
let mut stack = parked_in_handler();
drop_frames_above_crash_site(&mut stack, 0x7ddee3c2feec, 0x7ddee2f7da80);
assert_eq!(stack.frames.len(), 3);
assert_eq!(stack.frames[0].ip.as_deref(), Some("0x7ddee3c2feec"));
assert_eq!(stack.frames[0].sp.as_deref(), Some("0x7ddee2f7da80"));
}
#[test]
fn alternate_signal_stack_above_thread_stack_is_still_trimmed() {
let mut stack = StackTrace::from_frames(
vec![
frame("0x1000", "0xffff0000"), frame("0x1010", "0xffff0040"),
frame("0x2000", "0x7ffe0000"), frame("0x2010", "0x7ffe0040"),
],
false,
);
drop_frames_above_crash_site(&mut stack, 0x2000, 0x7ffe0000);
assert_eq!(stack.frames.len(), 2);
assert_eq!(stack.frames[0].ip.as_deref(), Some("0x2000"));
}
#[test]
fn zero_padded_registers_match_unpadded_frames() {
let registers = crash_site_registers(&ucontext(
"x86_64",
&[("rip", "0x00007ddee3c2feec"), ("rsp", "0x00007ddee2f7da80")],
))
.expect("x86_64 registers should parse");
let mut stack = parked_in_handler();
drop_frames_above_crash_site(&mut stack, registers.0, registers.1);
assert_eq!(stack.frames.len(), 3);
}
#[test]
fn stack_is_untouched_when_no_frame_matches() {
let mut stack = parked_in_handler();
let before = stack.frames.clone();
drop_frames_above_crash_site(&mut stack, 0xdead, 0xbeef);
assert_eq!(stack.frames, before);
}
#[test]
fn stack_is_untouched_when_it_already_starts_at_the_crash_site() {
let mut stack = StackTrace::from_frames(
vec![frame("0x2000", "0x7ffe0000"), frame("0x2010", "0x7ffe0040")],
false,
);
let before = stack.frames.clone();
drop_frames_above_crash_site(&mut stack, 0x2000, 0x7ffe0000);
assert_eq!(stack.frames, before);
}
#[test]
fn matching_ip_alone_does_not_end_the_prefix() {
let mut stack = StackTrace::from_frames(
vec![
frame("0x2000", "0x7ffe0000"), frame("0x2000", "0x7ffe0040"), ],
false,
);
drop_frames_above_crash_site(&mut stack, 0x2000, 0x7ffe0040);
assert_eq!(stack.frames.len(), 1);
assert_eq!(stack.frames[0].sp.as_deref(), Some("0x7ffe0040"));
}
#[test]
fn registers_are_read_per_architecture() {
assert_eq!(
crash_site_registers(&ucontext("x86_64", &[("rip", "0x10"), ("rsp", "0x20")])),
Some((0x10, 0x20))
);
assert_eq!(
crash_site_registers(&ucontext("aarch64", &[("pc", "0x10"), ("sp", "0x20")])),
Some((0x10, 0x20))
);
assert_eq!(
crash_site_registers(&ucontext("riscv64", &[("pc", "0x10"), ("sp", "0x20")])),
None
);
assert_eq!(
crash_site_registers(&ucontext("x86_64", &[("rip", "0x10")])),
None,
"a ucontext missing the stack pointer yields no crash site"
);
}
}