use std::ptr;
use std::time::{Duration, Instant};
use libdd_libunwind_sys::{
unw_get_reg_remote, unw_init_remote, unw_step_remote, UnwAddrSpace, UnwCursor, UnwWord,
UptInfo, UNW_REG_IP, UNW_REG_SP,
};
use crate::crash_info::{StackFrame, StackTrace};
const MAX_FRAMES: usize = 512;
pub struct CapturedThreadContext {
pub stack_trace: StackTrace,
}
#[derive(Debug)]
pub enum PtraceError {
Enumeration(std::io::Error),
Attach(libc::pid_t, i32),
Detach(libc::pid_t, i32),
}
impl std::fmt::Display for PtraceError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PtraceError::Enumeration(e) => write!(f, "Failed to enumerate threads: {}", e),
PtraceError::Attach(tid, errno) => {
write!(f, "Failed to attach to thread {}: errno {}", tid, errno)
}
PtraceError::Detach(tid, errno) => {
write!(f, "Failed to detach from thread {}: errno {}", tid, errno)
}
}
}
}
impl std::error::Error for PtraceError {}
pub fn enumerate_threads(pid: libc::pid_t) -> Result<Vec<libc::pid_t>, PtraceError> {
let task_dir = format!("/proc/{}/task", pid);
let entries = std::fs::read_dir(&task_dir).map_err(PtraceError::Enumeration)?;
let mut tids = Vec::new();
for entry in entries {
let entry = entry.map_err(PtraceError::Enumeration)?;
if let Ok(name) = entry.file_name().into_string() {
if let Ok(tid) = name.parse::<libc::pid_t>() {
tids.push(tid);
}
}
}
Ok(tids)
}
fn wait_for_stop(tid: libc::pid_t, deadline: Instant) -> Result<(), PtraceError> {
const POLL_SLEEP: Duration = Duration::from_millis(2);
loop {
let mut status = 0i32;
let ret = unsafe { libc::waitpid(tid, &mut status, libc::__WALL | libc::WNOHANG) };
if ret == tid as libc::pid_t {
if libc::WIFSTOPPED(status) {
return Ok(());
}
return Err(PtraceError::Attach(tid, unsafe {
*libc::__errno_location()
}));
} else if ret == 0 {
if Instant::now() >= deadline {
return Err(PtraceError::Attach(tid, libc::ETIMEDOUT));
}
std::thread::sleep(POLL_SLEEP);
} else {
return Err(PtraceError::Attach(tid, unsafe {
*libc::__errno_location()
}));
}
}
}
fn attach_thread(tid: libc::pid_t, stop_deadline: Instant) -> Result<(), PtraceError> {
let result = unsafe {
libc::ptrace(
libc::PTRACE_SEIZE,
tid as libc::c_long,
ptr::null_mut::<libc::c_void>(),
ptr::null_mut::<libc::c_void>(),
)
};
if result == -1 {
let errno = unsafe { *libc::__errno_location() };
return Err(PtraceError::Attach(tid, errno));
}
let result = unsafe {
libc::ptrace(
libc::PTRACE_INTERRUPT,
tid as libc::c_long,
ptr::null_mut::<libc::c_void>(),
ptr::null_mut::<libc::c_void>(),
)
};
if result == -1 {
let errno = unsafe { *libc::__errno_location() };
let _ = detach_thread(tid);
return Err(PtraceError::Attach(tid, errno));
}
if let Err(e) = wait_for_stop(tid, stop_deadline) {
let _ = detach_thread(tid);
return Err(e);
}
let _ = wait_for_registers(tid, stop_deadline);
Ok(())
}
fn wait_for_registers(tid: libc::pid_t, deadline: Instant) -> bool {
#[cfg(target_arch = "x86_64")]
const IP_OFFSET: libc::c_long = 16 * std::mem::size_of::<libc::c_long>() as libc::c_long;
#[cfg(target_arch = "aarch64")]
const IP_OFFSET: libc::c_long = 32 * std::mem::size_of::<libc::c_long>() as libc::c_long;
const SPIN_SLEEP: Duration = Duration::from_micros(100);
unsafe { *libc::__errno_location() = 0 };
let ip = unsafe { libc::ptrace(libc::PTRACE_PEEKUSER, tid as libc::c_long, IP_OFFSET, 0) };
let errno = unsafe { *libc::__errno_location() };
if errno == libc::EIO {
return true;
}
if ip != 0 && errno == 0 {
return true;
}
loop {
if Instant::now() >= deadline {
return false;
}
std::thread::sleep(SPIN_SLEEP);
unsafe { *libc::__errno_location() = 0 };
let ip = unsafe { libc::ptrace(libc::PTRACE_PEEKUSER, tid as libc::c_long, IP_OFFSET, 0) };
let errno = unsafe { *libc::__errno_location() };
if errno == libc::EIO {
return true;
}
if ip != 0 && errno == 0 {
return true;
}
}
}
fn detach_thread(tid: libc::pid_t) -> Result<(), PtraceError> {
let result = unsafe {
libc::ptrace(
libc::PTRACE_DETACH,
tid as libc::c_long,
ptr::null_mut::<libc::c_void>(),
ptr::null_mut::<libc::c_void>(),
)
};
if result == -1 {
let errno = unsafe { *libc::__errno_location() };
if errno != libc::ESRCH {
return Err(PtraceError::Detach(tid, errno));
}
}
unsafe {
libc::waitpid(tid, ptr::null_mut(), libc::__WALL | libc::WNOHANG);
}
Ok(())
}
fn unwind_remote_thread(tid: libc::pid_t, addr_space: &UnwAddrSpace) -> StackTrace {
let Some(upt_info) = UptInfo::new(tid) else {
return StackTrace::new_incomplete();
};
let mut cursor: UnwCursor = unsafe { std::mem::zeroed() };
let ret = unsafe { unw_init_remote(&mut cursor, addr_space.as_ptr(), upt_info.as_ptr()) };
if ret != 0 {
return StackTrace::new_incomplete();
}
let mut frames = Vec::new();
for _ in 0..MAX_FRAMES {
let mut ip: UnwWord = 0;
let mut sp: UnwWord = 0;
if unsafe { unw_get_reg_remote(&mut cursor, UNW_REG_IP, &mut ip) } != 0 || ip == 0 {
break;
}
let _ = unsafe { unw_get_reg_remote(&mut cursor, UNW_REG_SP, &mut sp) };
frames.push(StackFrame {
ip: Some(format!("0x{:x}", ip)),
sp: Some(format!("0x{:x}", sp)),
..StackFrame::new()
});
if unsafe { unw_step_remote(&mut cursor) } <= 0 {
break;
}
}
StackTrace::from_frames(frames, false)
}
pub fn capture_thread_context(
tid: libc::pid_t,
addr_space: &UnwAddrSpace,
stop_deadline: Instant,
) -> Result<CapturedThreadContext, PtraceError> {
attach_thread(tid, stop_deadline)?;
let stack_trace = unwind_remote_thread(tid, addr_space);
let _ = detach_thread(tid);
Ok(CapturedThreadContext { stack_trace })
}
const STOP_TIMEOUT_PER_THREAD: Duration = Duration::from_millis(200);
const RETRY_BASE_DELAY: Duration = Duration::from_millis(10);
const MAX_RETRIES: u32 = 3;
fn is_transient_ptrace_error(err: &PtraceError) -> bool {
matches!(err, PtraceError::Attach(_, libc::ETIMEDOUT))
}
fn capture_with_retry(
tid: libc::pid_t,
addr_space: &UnwAddrSpace,
overall_deadline: Instant,
) -> Option<CapturedThreadContext> {
for attempt in 0..=MAX_RETRIES {
let thread_deadline = (Instant::now() + STOP_TIMEOUT_PER_THREAD).min(overall_deadline);
match capture_thread_context(tid, addr_space, thread_deadline) {
Ok(ctx) if !ctx.stack_trace.frames.is_empty() => return Some(ctx),
Ok(_) => {} Err(ref e) if is_transient_ptrace_error(e) => {} Err(_) => return None, }
if attempt == MAX_RETRIES {
break;
}
let delay = RETRY_BASE_DELAY * 2u32.saturating_pow(attempt);
if Instant::now() + delay >= overall_deadline {
break;
}
std::thread::sleep(delay);
}
None
}
pub fn stream_thread_contexts<F>(
parent_pid: libc::pid_t,
crashing_tid: libc::pid_t,
max_threads: usize,
timeout: Duration,
mut callback: F,
) -> Result<bool, PtraceError>
where
F: FnMut(libc::pid_t, Option<&CapturedThreadContext>),
{
let overall_deadline = Instant::now() + timeout;
let tids = enumerate_threads(parent_pid)?;
let total_eligible = tids.len();
let mut processed = 0;
let Some(addr_space) = UnwAddrSpace::new() else {
return Ok(true); };
if crashing_tid != 0 && tids.contains(&crashing_tid) {
let context = capture_with_retry(crashing_tid, &addr_space, overall_deadline);
callback(crashing_tid, context.as_ref());
processed += 1;
}
for tid in tids {
if tid == crashing_tid {
continue;
}
if Instant::now() >= overall_deadline || processed >= max_threads {
break;
}
let context = capture_with_retry(tid, &addr_space, overall_deadline);
callback(tid, context.as_ref());
processed += 1;
}
let incomplete = processed < total_eligible;
Ok(incomplete)
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Arc, Barrier};
use std::time::Duration;
fn current_tid() -> libc::pid_t {
unsafe { libc::syscall(libc::SYS_gettid) as libc::pid_t }
}
#[test]
fn enumerate_includes_current_thread() {
let pid = std::process::id() as libc::pid_t;
let tids = enumerate_threads(pid).expect("enumerate_threads should succeed for self");
assert!(tids.contains(&pid), "main thread TID {pid} not in {tids:?}");
}
#[test]
fn enumerate_rejects_nonexistent_pid() {
assert!(enumerate_threads(0).is_err());
}
#[test]
#[cfg_attr(miri, ignore)]
fn enumerate_discovers_spawned_thread() {
let barrier = Arc::new(Barrier::new(2));
let b = Arc::clone(&barrier);
let (tx, rx) = std::sync::mpsc::channel();
let handle = std::thread::spawn(move || {
tx.send(current_tid()).unwrap();
b.wait();
});
let spawned_tid = rx.recv().unwrap();
let pid = std::process::id() as libc::pid_t;
let tids = enumerate_threads(pid).expect("enumerate_threads should succeed");
assert!(
tids.contains(&spawned_tid),
"spawned TID {spawned_tid} should appear in {tids:?}"
);
barrier.wait();
handle.join().unwrap();
}
#[test]
#[cfg_attr(miri, ignore)]
fn capture_context_produces_frames() {
let barrier = Arc::new(Barrier::new(2));
let b = Arc::clone(&barrier);
let (tx, rx) = std::sync::mpsc::channel();
let handle = std::thread::spawn(move || {
tx.send(current_tid()).unwrap();
b.wait();
});
let tid = rx.recv().unwrap();
let Some(addr_space) = UnwAddrSpace::new() else {
eprintln!("skipping ptrace test (UnwAddrSpace::new failed)");
barrier.wait();
handle.join().unwrap();
return;
};
match capture_thread_context(tid, &addr_space, Instant::now() + Duration::from_secs(5)) {
Err(e) => eprintln!("skipping ptrace test (ptrace unavailable): {e}"),
Ok(ctx) => assert!(
!ctx.stack_trace.frames.is_empty(),
"expected at least one frame from a running thread"
),
}
barrier.wait();
handle.join().unwrap();
}
#[test]
#[cfg_attr(miri, ignore)]
fn stream_respects_max_threads_limit() {
let barrier = Arc::new(Barrier::new(4));
let mut handles = Vec::new();
for _ in 0..3 {
let b = Arc::clone(&barrier);
handles.push(std::thread::spawn(move || {
b.wait();
}));
}
barrier.wait();
let mut collected = 0usize;
let _ = stream_thread_contexts(
std::process::id() as libc::pid_t,
current_tid(),
2,
Duration::from_secs(5),
|_tid, _ctx| collected += 1,
);
assert!(collected <= 3, "collected {collected}, expected <= 3");
for h in handles {
h.join().unwrap();
}
}
#[test]
#[cfg_attr(miri, ignore)]
fn stream_includes_all_threads() {
let barrier = Arc::new(Barrier::new(2));
let b: Arc<Barrier> = Arc::clone(&barrier);
let (tx, rx) = std::sync::mpsc::channel();
let handle = std::thread::spawn(move || {
tx.send(current_tid()).unwrap();
b.wait();
});
let worker_tid = rx.recv().unwrap();
let mut seen_worker = false;
let mut seen_self = false;
let self_tid = current_tid();
let _ = stream_thread_contexts(
std::process::id() as libc::pid_t,
self_tid,
64,
Duration::from_secs(5),
|tid, _ctx| {
if tid == worker_tid {
seen_worker = true;
}
if tid == self_tid {
seen_self = true;
}
},
);
assert!(seen_worker, "worker thread should appear in callbacks");
assert!(seen_self, "current thread should appear in callbacks");
barrier.wait();
handle.join().unwrap();
}
}