Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions libdd-crashtracker/src/receiver/ptrace_collector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,13 @@ fn wait_for_stop(tid: libc::pid_t, deadline: Instant) -> Result<(), PtraceError>
/// to enter ptrace-stop state before returning.
///
/// `stop_deadline` bounds how long we poll for the stop event.
///
/// After the thread enters ptrace-stop, this function also polls until the
/// instruction pointer is non-zero. On older kernels (CentOS 7 kernel 3.10,
/// CentOS 8 kernel 4.18) there is a race where `waitpid` returns WIFSTOPPED
/// but the thread's register state hasn't been fully flushed to the
/// ptrace-accessible area yet. Reading registers in that window yields zeros,
/// which causes libunwind to produce an empty stack trace.
fn attach_thread(tid: libc::pid_t, stop_deadline: Instant) -> Result<(), PtraceError> {
// PTRACE_SEIZE attaches without stopping the thread
let result = unsafe {
Expand Down Expand Up @@ -163,9 +170,41 @@ fn attach_thread(tid: libc::pid_t, stop_deadline: Instant) -> Result<(), PtraceE
let _ = detach_thread(tid);
return Err(e);
}

// On older kernels, the register state may not be
// immediately readable after waitpid reports the stop. Spin briefly
// until PEEKUSER returns a non-zero IP, proving registers are committed.
wait_for_registers(tid, stop_deadline);

Ok(())
}

/// Poll the thread's instruction pointer via PTRACE_PEEKUSER until it is
/// non-zero or the deadline expires. On modern kernels this returns on the
/// first iteration; on older ones, it may take a few microseconds.
fn wait_for_registers(tid: libc::pid_t, deadline: Instant) {
#[cfg(target_arch = "x86_64")]
const IP_OFFSET: libc::c_long = 16 * std::mem::size_of::<libc::c_long>() as libc::c_long; // RIP

#[cfg(target_arch = "aarch64")]
const IP_OFFSET: libc::c_long = 32 * std::mem::size_of::<libc::c_long>() as libc::c_long; // PC

const SPIN_SLEEP: Duration = Duration::from_micros(100);

loop {
unsafe { *libc::__errno_location() = 0 };
let ip = unsafe { libc::ptrace(libc::PTRACE_PEEKUSER, tid as libc::c_long, IP_OFFSET, 0) };
// PEEKUSER returns the register value; errno==0 means success.
if ip != 0 && unsafe { *libc::__errno_location() } == 0 {
return;
}
if Instant::now() >= deadline {
return;
}
std::thread::sleep(SPIN_SLEEP);
}
}

fn detach_thread(tid: libc::pid_t) -> Result<(), PtraceError> {
// SAFETY: PTRACE_DETACH is valid for a currently-traced thread
let result = unsafe {
Expand Down
Loading