@@ -19,13 +19,18 @@ struct ruby_procs_t {
1919// option is to adjust this number downwards.
2020// NOTE the maximum size stack is this times 33
2121#define FRAMES_PER_WALK_RUBY_STACK 32
22+
23+ // The maximum number of JIT frames to unwind via frame pointers.
24+ // YJIT creates one native frame per JIT entry (not per Ruby method),
25+ // so in practice there is typically only 1 (occasionally 2 for nested entries).
26+ #define MAX_JIT_FP_FRAMES 4
2227// When resolving a CME, we need to traverse environment pointers until we
2328// find IMEMO_MENT. Since we can't do a while loop, we have to bound this
2429// the max encountered in experimentation on a production rails app is 6.
2530// This increases insn for the kernel verifier all code in the ep check "loop"
2631// is M*N for instruction checks, so be extra sensitive about additions there.
2732// If we get ERR_RUBY_READ_CME_MAX_EP regularly, we may need to raise it.
28- #define MAX_EP_CHECKS 6
33+ #define MAX_EP_CHECKS 6
2934
3035// Constants related to reading a method entry
3136// https://github.com/ruby/ruby/blob/523857bfcb0f0cdfd1ed7faa09b9c59a0266e7e2/method.h#L118
@@ -222,8 +227,9 @@ static EBPF_INLINE ErrorCode read_ruby_frame(
222227 // frames will almost certainly be incorrect for Ruby versions < 2.6.
223228 frame_type = RUBY_FRAME_TYPE_CME_CFUNC ;
224229 } else if (record -> rubyUnwindState .jit_detected ) {
225- // If we detected a jit frame and are now in a cfunc, push the c frame
226- // as we can no longer unwind native anymore
230+ // JIT is active but frame pointers are not available, so we cannot unwind
231+ // through JIT frames to get back to native code. Push the cfunc inline
232+ // instead of handing off to the native unwinder.
227233 frame_type = RUBY_FRAME_TYPE_CME_CFUNC ;
228234 } else {
229235 // We save this cfp on in the "Record" entry, and when we start the unwinder
@@ -401,19 +407,62 @@ static EBPF_INLINE ErrorCode walk_ruby_stack(
401407 record -> rubyUnwindState .cfunc_saved_frame = 0 ;
402408 }
403409
410+ // If the CPU PC is in the JIT region, walk the native frame pointer chain through JIT frames.
411+ // This follows the same pattern as the V8 unwinder (v8_tracer.ebpf.c): push each JIT frame,
412+ // then use unwinder_unwind_frame_pointer() to advance PC/SP/FP to the caller.
413+ // YJIT creates one native FP frame per JIT entry, not per Ruby method, so there are
414+ // typically only 1-2 frames to walk.
415+ //
416+ // If frame_pointers_enabled is false (e.g. x86_64 without --yjit-perf), we push a single
417+ // dummy JIT frame and skip FP walking -- the stack will be truncated at the Ruby VM frames
418+ // but won't produce garbage from following an invalid FP chain.
404419 if (
405420 rubyinfo -> jit_start > 0 && record -> state .pc > rubyinfo -> jit_start &&
406421 record -> state .pc < rubyinfo -> jit_end ) {
407- record -> rubyUnwindState .jit_detected = true;
422+ if (rubyinfo -> frame_pointers_enabled ) {
423+ // Walk the native FP chain through JIT frames, pushing each as a JIT frame
424+ // so it can potentially be symbolized via perf maps later.
425+ UNROLL for (int j = 0 ; j < MAX_JIT_FP_FRAMES ; j ++ )
426+ {
427+ ErrorCode jit_error =
428+ push_ruby (& record -> state , trace , RUBY_FRAME_TYPE_JIT , (u64 )record -> state .pc , 0 , 0 );
429+ if (jit_error ) {
430+ return jit_error ;
431+ }
432+
433+ if (!unwinder_unwind_frame_pointer (& record -> state )) {
434+ // FP chain broken, cannot continue
435+ * next_unwinder = PROG_UNWIND_STOP ;
436+ return ERR_OK ;
437+ }
408438
409- // If the first frame is a jit PC, the leaf ruby frame should be the jit "owner"
410- // the cpu PC is also pushed as the address,
411- // as in theory this can be used to symbolize the JIT frame later
412- if (trace -> num_frames == 0 ) {
413- ErrorCode error =
439+ // Check if we've left the JIT region
440+ if (record -> state .pc < rubyinfo -> jit_start || record -> state .pc >= rubyinfo -> jit_end ) {
441+ break ;
442+ }
443+ }
444+ // After walking JIT frames, PC should be in rb_vm_exec or other native code.
445+ // We must resolve the mapping for the new PC so that text_section_id/offset/bias
446+ // are up to date. Without this, the native unwinder would try to use stale mapping
447+ // info from the JIT region and fail with ERR_NATIVE_NO_PID_PAGE_MAPPING.
448+ ErrorCode map_err = get_next_unwinder_after_native_frame (record , next_unwinder );
449+ if (map_err ) {
450+ return map_err ;
451+ }
452+ // The resolved unwinder should be PROG_UNWIND_RUBY (since PC is in rb_vm_exec
453+ // which is in interpreter_offsets) or PROG_UNWIND_NATIVE. Either way, we continue
454+ // with the Ruby VM stack walk below and the mapping state is now correct for when
455+ // we eventually hand off to the native unwinder.
456+ } else {
457+ // No frame pointers available: push a single dummy JIT frame.
458+ // We cannot walk the FP chain so we will not be able to resume native unwinding.
459+ // Mark jit_detected so that cfuncs are pushed inline and end-of-stack uses
460+ // PROG_UNWIND_STOP instead of PROG_UNWIND_NATIVE.
461+ record -> rubyUnwindState .jit_detected = true;
462+ ErrorCode jit_error =
414463 push_ruby (& record -> state , trace , RUBY_FRAME_TYPE_JIT , (u64 )record -> state .pc , 0 , 0 );
415- if (error ) {
416- return error ;
464+ if (jit_error ) {
465+ return jit_error ;
417466 }
418467 }
419468 }
@@ -426,8 +475,9 @@ static EBPF_INLINE ErrorCode walk_ruby_stack(
426475
427476 if (last_stack_frame <= stack_ptr ) {
428477 // We have processed all frames in the Ruby VM and can stop here.
429- // if this process has been JIT'd, the PC is invalid and we cannot resume native unwinding so
430- // we are done
478+ // If we walked through JIT frames via FP, the state is clean and native unwinding
479+ // can continue. If JIT was detected without FP, the PC is still in the JIT region
480+ // and native unwinding would fail, so we stop.
431481 * next_unwinder = record -> rubyUnwindState .jit_detected ? PROG_UNWIND_STOP : PROG_UNWIND_NATIVE ;
432482 goto save_state ;
433483 } else {
0 commit comments