@@ -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.
370393const 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