Skip to content

Commit edd03dc

Browse files
committed
Harden ptrace retries
1 parent e91210e commit edd03dc

1 file changed

Lines changed: 68 additions & 17 deletions

File tree

libdd-crashtracker/src/receiver/ptrace_collector.rs

Lines changed: 68 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -173,15 +173,22 @@ fn attach_thread(tid: libc::pid_t, stop_deadline: Instant) -> Result<(), PtraceE
173173
// On older kernels, the register state may not be
174174
// immediately readable after waitpid reports the stop. Spin briefly
175175
// until PEEKUSER returns a non-zero IP, proving registers are committed.
176-
wait_for_registers(tid, stop_deadline);
176+
if !wait_for_registers(tid, stop_deadline) {
177+
let _ = detach_thread(tid);
178+
return Err(PtraceError::Attach(tid, libc::ETIMEDOUT));
179+
}
177180

178181
Ok(())
179182
}
180183

181184
/// Poll the thread's instruction pointer using PTRACE_PEEKUSER until it is
182185
/// non-zero or the deadline expires. On modern kernels this should return on the
183186
/// first iteration; on older ones, it may take a few microseconds.
184-
fn wait_for_registers(tid: libc::pid_t, deadline: Instant) {
187+
///
188+
/// Returns `true` if a non-zero IP was observed or the check is not applicable
189+
/// (PTRACE_PEEKUSER unsupported on the architecture), `false` if the
190+
/// deadline expired without reading a valid IP on a platform that supports it.
191+
fn wait_for_registers(tid: libc::pid_t, deadline: Instant) -> bool {
185192
#[cfg(target_arch = "x86_64")]
186193
const IP_OFFSET: libc::c_long = 16 * std::mem::size_of::<libc::c_long>() as libc::c_long; // RIP
187194

@@ -190,17 +197,33 @@ fn wait_for_registers(tid: libc::pid_t, deadline: Instant) {
190197

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

200+
// First probe: if PTRACE_PEEKUSER returns EIO, the kernel doesn't support it
201+
// In that case, skip the check. libunwind uses PTRACE_GETREGSET which works
202+
// regardless, and modern kernels commit register state synchronously on ptrace-stop.
203+
unsafe { *libc::__errno_location() = 0 };
204+
let ip = unsafe { libc::ptrace(libc::PTRACE_PEEKUSER, tid as libc::c_long, IP_OFFSET, 0) };
205+
let errno = unsafe { *libc::__errno_location() };
206+
if errno == libc::EIO {
207+
return true;
208+
}
209+
if ip != 0 && errno == 0 {
210+
return true;
211+
}
212+
193213
loop {
214+
if Instant::now() >= deadline {
215+
return false;
216+
}
217+
std::thread::sleep(SPIN_SLEEP);
194218
unsafe { *libc::__errno_location() = 0 };
195219
let ip = unsafe { libc::ptrace(libc::PTRACE_PEEKUSER, tid as libc::c_long, IP_OFFSET, 0) };
196-
// PEEKUSER returns the register value; errno==0 means success.
197-
if ip != 0 && unsafe { *libc::__errno_location() } == 0 {
198-
return;
220+
let errno = unsafe { *libc::__errno_location() };
221+
if errno == libc::EIO {
222+
return true;
199223
}
200-
if Instant::now() >= deadline {
201-
return;
224+
if ip != 0 && errno == 0 {
225+
return true;
202226
}
203-
std::thread::sleep(SPIN_SLEEP);
204227
}
205228
}
206229

@@ -369,6 +392,40 @@ pub fn capture_thread_context(
369392
/// Maximum time to wait for a single thread to enter ptrace-stop.
370393
const STOP_TIMEOUT_PER_THREAD: Duration = Duration::from_millis(50);
371394

395+
/// Number of retry attempts for a failed capture_thread_context call.
396+
const CAPTURE_RETRIES: u32 = 1;
397+
398+
/// Delay between retry attempts.
399+
const RETRY_DELAY: Duration = Duration::from_millis(5);
400+
401+
/// Attempt to capture a thread context with retries on transient failures.
402+
///
403+
/// On some kernels, ptrace operations can fail intermittently due to scheduling
404+
/// races or brief permission windows. Retrying once after a short delay resolves
405+
/// most transient issues without significantly impacting overall collection time.
406+
fn capture_with_retry(
407+
tid: libc::pid_t,
408+
resolve_frames: crate::StacktraceCollection,
409+
addr_space: UnwAddrSpaceT,
410+
overall_deadline: Instant,
411+
) -> Option<CapturedThreadContext> {
412+
for attempt in 0..=CAPTURE_RETRIES {
413+
let now = Instant::now();
414+
if now >= overall_deadline {
415+
break;
416+
}
417+
let stop_deadline = (now + STOP_TIMEOUT_PER_THREAD).min(overall_deadline);
418+
match capture_thread_context(tid, resolve_frames, addr_space, stop_deadline) {
419+
Ok(ctx) => return Some(ctx),
420+
Err(_) if attempt < CAPTURE_RETRIES => {
421+
std::thread::sleep(RETRY_DELAY);
422+
}
423+
Err(_) => break,
424+
}
425+
}
426+
None
427+
}
428+
372429
/// Stream thread contexts to a callback one at a time.
373430
///
374431
/// For each thread in the process the callback receives the TID and an optional
@@ -412,9 +469,8 @@ where
412469

413470
// Process the crashing thread first so it is never dropped by the cap.
414471
if crashing_tid != 0 && tids.contains(&crashing_tid) {
415-
let stop_deadline = (Instant::now() + STOP_TIMEOUT_PER_THREAD).min(overall_deadline);
416472
let context =
417-
capture_thread_context(crashing_tid, resolve_frames, addr_space, stop_deadline).ok();
473+
capture_with_retry(crashing_tid, resolve_frames, addr_space, overall_deadline);
418474
callback(crashing_tid, context.as_ref());
419475
processed += 1;
420476
}
@@ -423,16 +479,11 @@ where
423479
if tid == crashing_tid {
424480
continue;
425481
}
426-
let now = Instant::now();
427-
if now >= overall_deadline || processed >= max_threads {
482+
if Instant::now() >= overall_deadline || processed >= max_threads {
428483
break;
429484
}
430485

431-
// Cap the per-thread stop wait at STOP_TIMEOUT_PER_THREAD but never
432-
// past the overall deadline, so one thread can't consume the budget.
433-
let stop_deadline = (now + STOP_TIMEOUT_PER_THREAD).min(overall_deadline);
434-
435-
let context = capture_thread_context(tid, resolve_frames, addr_space, stop_deadline).ok();
486+
let context = capture_with_retry(tid, resolve_frames, addr_space, overall_deadline);
436487
callback(tid, context.as_ref());
437488
processed += 1;
438489
}

0 commit comments

Comments
 (0)