@@ -20,10 +20,6 @@ struct ruby_procs_t {
2020// NOTE: the maximum size stack is FRAMES_PER_WALK_RUBY_STACK * calls to tail_call().
2121#define FRAMES_PER_WALK_RUBY_STACK 32
2222
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
2723// When resolving a CME, we need to traverse environment pointers until we
2824// find IMEMO_MENT. Since we can't do a while loop, we have to bound this
2925// the max encountered in experimentation on a production rails app is 6.
@@ -457,40 +453,25 @@ static EBPF_INLINE ErrorCode walk_ruby_stack(
457453 }
458454
459455 // Detect if the CPU PC is in the JIT region.
460- // When frame pointers are available, walk the native FP chain through JIT frames.
461- // When not available, push a single dummy JIT frame and skip FP walking.
456+ // When frame pointers are available, we keep the native unwind state in sync with
457+ // the Ruby VM stack by advancing the FP chain by one frame per loop iteration.
458+ // This handles both YJIT (1 JIT frame, exits after first iteration) and ZJIT
459+ // (1 JIT frame per iseq, 1:1 with CFPs, stays in sync throughout the walk).
460+ //
461+ // When frame pointers are not available, we push a single dummy JIT frame and
462+ // set jit_detected to suppress native unwinding.
462463 bool in_jit = rubyinfo -> jit_start > 0 && record -> state .pc >= rubyinfo -> jit_start &&
463464 record -> state .pc < rubyinfo -> jit_end ;
464465
465466 if (in_jit ) {
466467 if (rubyinfo -> frame_pointers_enabled ) {
467- // Walk the native FP chain through JIT frames, pushing each as a JIT frame
468- // so it can potentially be symbolized via perf maps later.
469- UNROLL for (int j = 0 ; j < MAX_JIT_FP_FRAMES ; j ++ )
470- {
471- ErrorCode jit_error =
472- push_ruby (& record -> state , trace , RUBY_FRAME_TYPE_JIT , (u64 )record -> state .pc , 0 , 0 );
473- if (jit_error ) {
474- return jit_error ;
475- }
476-
477- if (!unwinder_unwind_frame_pointer (& record -> state )) {
478- // FP chain broken, cannot continue
479- * next_unwinder = PROG_UNWIND_STOP ;
480- return ERR_OK ;
481- }
482-
483- // Check if we've left the JIT region
484- if (record -> state .pc < rubyinfo -> jit_start || record -> state .pc >= rubyinfo -> jit_end ) {
485- break ;
486- }
487- }
488- // After walking JIT frames, PC should be in rb_vm_exec or other native code.
489- // Resolve the mapping for the new PC so text_section_id/offset/bias are correct.
490- ErrorCode map_err = get_next_unwinder_after_native_frame (record , next_unwinder );
491- if (map_err ) {
492- return map_err ;
468+ // Push a leaf JIT frame with the raw machine PC for perf-map symbolization.
469+ ErrorCode jit_error =
470+ push_ruby (& record -> state , trace , RUBY_FRAME_TYPE_JIT , (u64 )record -> state .pc , 0 , 0 );
471+ if (jit_error ) {
472+ return jit_error ;
493473 }
474+
494475 } else {
495476 // No frame pointers available: push a single dummy JIT frame.
496477 // Mark jit_detected so that cfuncs are pushed inline and end-of-stack uses
@@ -501,18 +482,38 @@ static EBPF_INLINE ErrorCode walk_ruby_stack(
501482 if (jit_error ) {
502483 return jit_error ;
503484 }
485+ in_jit = false;
504486 }
505487 }
506488
507489 for (u32 i = 0 ; i < FRAMES_PER_WALK_RUBY_STACK ; ++ i ) {
490+ // Keep the native unwind state in sync: if the native PC is still in the JIT
491+ // region, advance it by one frame pointer to match the Ruby VM stack pop.
492+ // For YJIT this exits JIT on the first iteration. For ZJIT this pops one JIT
493+ // native frame per CFP, keeping the two stacks in lockstep.
494+ if (in_jit ) {
495+ if (!unwinder_unwind_frame_pointer (& record -> state )) {
496+ * next_unwinder = PROG_UNWIND_STOP ;
497+ return ERR_OK ;
498+ }
499+ if (record -> state .pc < rubyinfo -> jit_start || record -> state .pc >= rubyinfo -> jit_end ) {
500+ // Exited the JIT region. Resolve the mapping for the post-JIT PC so that
501+ // text_section_id/offset/bias are correct for native unwinding later.
502+ in_jit = false;
503+ ErrorCode map_err = get_next_unwinder_after_native_frame (record , next_unwinder );
504+ if (map_err ) {
505+ return map_err ;
506+ }
507+ }
508+ }
509+
508510 error = read_ruby_frame (record , rubyinfo , stack_ptr , next_unwinder );
509511 if (error != ERR_OK )
510512 return error ;
511513
512514 if (last_stack_frame <= stack_ptr ) {
513515 // We have processed all frames in the Ruby VM and can stop here.
514- // If we walked through JIT frames via FP, the state is clean and native unwinding
515- // can continue. If JIT was detected without FP, the PC is still in the JIT region
516+ // If JIT was detected without FP, the PC is still in the JIT region
516517 // and native unwinding would fail, so we stop.
517518 * next_unwinder = record -> rubyUnwindState .jit_detected ? PROG_UNWIND_STOP : PROG_UNWIND_NATIVE ;
518519 goto save_state ;
0 commit comments