@@ -55,11 +55,38 @@ unsafe fn emit_backtrace_by_frames(
5555 w : & mut impl Write ,
5656 resolve_frames : StacktraceCollection ,
5757 fault_ip : usize ,
58+ ucontext : * const ucontext_t ,
5859) -> Result < ( ) , EmitterError > {
5960 // https://docs.rs/backtrace/latest/backtrace/index.html
6061 writeln ! ( w, "{DD_CRASHTRACK_BEGIN_STACKTRACE}" ) ?;
6162
62- // Absolute addresses appear to be safer to collect during a crash than debug info.
63+ // On macOS, backtrace::trace_unsynchronized fails in forked children because
64+ // macOS restricts many APIs after fork-without-exec. Walk the frame pointer
65+ // chain directly from the saved ucontext registers instead. The parent's
66+ // stack memory is still readable in the forked child
67+ #[ cfg( target_os = "macos" ) ]
68+ {
69+ let _ = ( resolve_frames, fault_ip) ;
70+ emit_backtrace_from_ucontext ( w, ucontext) ?;
71+ }
72+
73+ #[ cfg( not( target_os = "macos" ) ) ]
74+ {
75+ let _ = ucontext;
76+ emit_backtrace_via_library ( w, resolve_frames, fault_ip) ?;
77+ }
78+
79+ writeln ! ( w, "{DD_CRASHTRACK_END_STACKTRACE}" ) ?;
80+ w. flush ( ) ?;
81+ Ok ( ( ) )
82+ }
83+
84+ #[ allow( dead_code) ] // used from tests on macOS, from emit_backtrace_by_frames on other platforms
85+ unsafe fn emit_backtrace_via_library (
86+ w : & mut impl Write ,
87+ resolve_frames : StacktraceCollection ,
88+ fault_ip : usize ,
89+ ) -> Result < ( ) , EmitterError > {
6390 fn emit_absolute_addresses ( w : & mut impl Write , frame : & Frame ) -> Result < ( ) , EmitterError > {
6491 write ! ( w, "\" ip\" : \" {:?}\" " , frame. ip( ) ) ?;
6592 if let Some ( module_base_address) = frame. module_base_address ( ) {
@@ -132,7 +159,103 @@ unsafe fn emit_backtrace_by_frames(
132159 // emit anything at all, if the crashing frame is not found for some reason
133160 ip_found = true ;
134161 }
135- writeln ! ( w, "{DD_CRASHTRACK_END_STACKTRACE}" ) ?;
162+ Ok ( ( ) )
163+ }
164+
165+ /// Walk the frame pointer chain from the ucontext's saved registers.
166+ ///
167+ /// After fork(), the child process has a copy-on-write view of the parent's
168+ /// stack memory, so the frame pointer chain from the crashed thread is still
169+ /// readable. This avoids depending on `backtrace::trace_unsynchronized` which
170+ /// uses macOS APIs that don't work in forked-but-not-exec'd children.
171+ ///
172+ /// For each IP we call `dladdr` to resolve the symbol name, symbol address,
173+ /// and containing shared-object path. `dladdr` is safe here because it only
174+ /// reads dyld's internal data structures (no allocation, no Mach IPC).
175+ #[ cfg( target_os = "macos" ) ]
176+ unsafe fn emit_backtrace_from_ucontext (
177+ w : & mut impl Write ,
178+ ucontext : * const ucontext_t ,
179+ ) -> Result < ( ) , EmitterError > {
180+ if ucontext. is_null ( ) {
181+ return Ok ( ( ) ) ;
182+ }
183+ let mcontext = ( * ucontext) . uc_mcontext ;
184+ if mcontext. is_null ( ) {
185+ return Ok ( ( ) ) ;
186+ }
187+
188+ // Get the thread's stack bounds so we only deref frame pointers
189+ // that lie within known stack memory. Both pthread_get_stackaddr_np and
190+ // pthread_get_stacksize_np are async-signal-safe on macOS
191+ let thread = libc:: pthread_self ( ) ;
192+ let stack_top = libc:: pthread_get_stackaddr_np ( thread) as usize ;
193+ let stack_size = libc:: pthread_get_stacksize_np ( thread) ;
194+ let stack_bottom = stack_top. saturating_sub ( stack_size) ;
195+
196+ // Returns true when the range [addr, addr+len) falls within the thread stack
197+ let in_stack_bounds = |addr : usize , len : usize | -> bool {
198+ let end = addr. saturating_add ( len) ;
199+ addr >= stack_bottom && end <= stack_top
200+ } ;
201+
202+ let ss = & ( * mcontext) . __ss ;
203+ #[ cfg( target_arch = "aarch64" ) ]
204+ let ( pc, mut fp) = ( ss. __pc as usize , ss. __fp as usize ) ;
205+ #[ cfg( target_arch = "x86_64" ) ]
206+ let ( pc, mut fp) = ( ss. __rip as usize , ss. __rbp as usize ) ;
207+
208+ emit_frame_with_dladdr ( w, pc) ?;
209+
210+ const MAX_FRAMES : usize = 512 ;
211+ for _ in 0 ..MAX_FRAMES {
212+ if fp == 0 || fp % std:: mem:: align_of :: < usize > ( ) != 0 {
213+ break ;
214+ }
215+ // Each frame record is two pointer-sized words: [saved_fp, return_addr]
216+ // Bail out if the record falls outside the thread stack
217+ if !in_stack_bounds ( fp, 2 * std:: mem:: size_of :: < usize > ( ) ) {
218+ break ;
219+ }
220+ let next_fp = * ( fp as * const usize ) ;
221+ let return_addr = * ( ( fp + std:: mem:: size_of :: < usize > ( ) ) as * const usize ) ;
222+ if return_addr == 0 {
223+ break ;
224+ }
225+ emit_frame_with_dladdr ( w, return_addr) ?;
226+ if next_fp <= fp {
227+ break ;
228+ }
229+ fp = next_fp;
230+ }
231+
232+ Ok ( ( ) )
233+ }
234+
235+ /// Emit a single stack frame, enriched with `dladdr` symbol information.
236+ #[ cfg( target_os = "macos" ) ]
237+ unsafe fn emit_frame_with_dladdr ( w : & mut impl Write , ip : usize ) -> Result < ( ) , EmitterError > {
238+ let mut info: libc:: Dl_info = std:: mem:: zeroed ( ) ;
239+ let resolved = libc:: dladdr ( ip as * const libc:: c_void , & mut info) != 0 ;
240+
241+ write ! ( w, "{{\" ip\" : \" 0x{ip:x}\" " ) ?;
242+
243+ if resolved {
244+ if !info. dli_fbase . is_null ( ) {
245+ write ! ( w, ", \" module_base_address\" : \" {:?}\" " , info. dli_fbase) ?;
246+ }
247+ if !info. dli_saddr . is_null ( ) {
248+ write ! ( w, ", \" symbol_address\" : \" {:?}\" " , info. dli_saddr) ?;
249+ }
250+ if !info. dli_sname . is_null ( ) {
251+ let name = std:: ffi:: CStr :: from_ptr ( info. dli_sname ) ;
252+ if let Ok ( s) = name. to_str ( ) {
253+ write ! ( w, ", \" function\" : \" {s}\" " ) ?;
254+ }
255+ }
256+ }
257+
258+ writeln ! ( w, "}}" ) ?;
136259 w. flush ( ) ?;
137260 Ok ( ( ) )
138261}
@@ -213,7 +336,9 @@ pub(crate) fn emit_crashreport(
213336 // https://doc.rust-lang.org/src/std/backtrace.rs.html#332
214337 // We do this last, so even if it crashes, we still get the other info.
215338 let fault_ip = extract_ip ( ucontext) ;
216- unsafe { emit_backtrace_by_frames ( pipe, config. resolve_frames ( ) , fault_ip) ? } ;
339+ unsafe {
340+ emit_backtrace_by_frames ( pipe, config. resolve_frames ( ) , fault_ip, ucontext) ?
341+ } ;
217342 }
218343 if is_runtime_callback_registered ( ) {
219344 emit_runtime_stack ( pipe) ?;
@@ -501,9 +626,11 @@ mod tests {
501626 } )
502627 } ;
503628 let mut buf = Vec :: new ( ) ;
629+ writeln ! ( buf, "{DD_CRASHTRACK_BEGIN_STACKTRACE}" ) . unwrap ( ) ;
504630 unsafe {
505- emit_backtrace_by_frames ( & mut buf, collection, ip_of_test_fn) . expect ( "to work ;-)" ) ;
631+ emit_backtrace_via_library ( & mut buf, collection, ip_of_test_fn) . expect ( "to work ;-)" ) ;
506632 }
633+ writeln ! ( buf, "{DD_CRASHTRACK_END_STACKTRACE}" ) . unwrap ( ) ;
507634 buf
508635 }
509636
0 commit comments