Skip to content

Walk YJIT JIT frames via frame pointers for full stack unwinding#26

Draft
dalehamel wants to merge 5 commits intoruby-jit-upstreamfrom
jit-fp-unwinding
Draft

Walk YJIT JIT frames via frame pointers for full stack unwinding#26
dalehamel wants to merge 5 commits intoruby-jit-upstreamfrom
jit-fp-unwinding

Conversation

@dalehamel
Copy link
Copy Markdown
Member

Screenshot 2026-02-10 at 3 41 54 PM

Replace the jit_detected flag approach with V8-style frame pointer unwinding through Ruby JIT frames. When YJIT emits frame pointers (always on arm64, with --yjit-perf on x86_64), the Ruby eBPF unwinder walks the native FP chain through JIT frames, pushes each as a RUBY_FRAME_TYPE_JIT frame, then resolves the post-JIT mapping so native unwinding can continue below the Ruby VM stack.

Three bugs fixed along the way:

  • parseMappings discarded prctl-labeled [anon:...] mappings, so the YJIT JIT region was never visible to interpreter handlers.
  • IsAnonymous() returned false for labeled anonymous mappings, so SynchronizeMappings would have skipped them even if parsing preserved them.
  • After walking JIT frames via FP, get_next_unwinder_after_native_frame must be called to re-resolve the mapping for the new PC, otherwise the native unwinder fails with stale text_section_id/offset.

@dalehamel dalehamel force-pushed the jit-fp-unwinding branch 3 times, most recently from 015b62c to 3ac1562 Compare February 10, 2026 22:58
@dalehamel dalehamel force-pushed the jit-fp-unwinding branch 2 times, most recently from 2b462cc to aabba13 Compare February 11, 2026 17:12
Replace the jit_detected flag approach with V8-style frame pointer
unwinding through Ruby JIT frames. When YJIT emits frame pointers
(always on arm64, with --yjit-perf on x86_64), the Ruby eBPF unwinder
walks the native FP chain through JIT frames, pushes each as a
RUBY_FRAME_TYPE_JIT frame, then resolves the post-JIT mapping so native
unwinding can continue below the Ruby VM stack.

When frame pointers are not available, the original behavior is
preserved: a single dummy JIT frame is pushed, cfuncs are pushed inline,
and native unwinding is stopped at the end of the Ruby stack.

Also fixes parseMappings discarding prctl-labeled [anon:...] mappings,
which prevented the YJIT JIT region from being visible to interpreter
handlers.
…tion

Restructure the eBPF JIT frame pointer walk: instead of a separate loop
before the main CFP walk, advance the native FP chain by one frame per
main loop iteration. This keeps the native unwind state in lockstep with
the Ruby VM stack, supporting both YJIT (1 JIT frame, exits after first
iteration) and ZJIT (1 JIT frame per iseq, 1:1 with CFPs).

Fix JIT region detection in SynchronizeMappings to scan all mappings
(including non-executable ---p reservations) for the prctl-labeled JIT
region, then only register LPM prefixes for executable pages. This
ensures jit_start/jit_end cover the full reserved address range even
when the r-xp committed pages don't carry the label.

Also fix IsAnonymous() to recognize [anon:...] labeled mappings, and
remove debug log spam from parseMappings.
Comment thread interpreter/ruby/ruby.go
for _, prefix := range prefixes {
_, exists := r.prefixes[prefix]
if !exists {
err := ebpf.UpdatePidInterpreterMapping(pid, prefix, support.ProgUnwindRuby, 0, 0)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as with otel repo, we probably need to call DeletePidInterpreterMapping at some point

dalehamel added a commit that referenced this pull request Apr 7, 2026
- Restore findJITRegion() from base (open-telemetry#1102) which was incorrectly removed
  during rebase - PR #26 now reuses the shared function instead of inline code
- Remove redundant inline first-pass JIT detection (now handled by findJITRegion)
- Detach cleanup is inherited from the updated base (open-telemetry#1102)
dalehamel added a commit that referenced this pull request Apr 7, 2026
- Restore findJITRegion() from base (open-telemetry#1102) which was incorrectly removed
  during rebase - PR #26 now reuses the shared function instead of inline code
- Remove redundant inline first-pass JIT detection (now handled by findJITRegion)
- Detach cleanup is inherited from the updated base (open-telemetry#1102)
dalehamel added a commit that referenced this pull request Apr 7, 2026
- Restore findJITRegion() from base (open-telemetry#1102) which was incorrectly removed
  during rebase - PR #26 now reuses the shared function instead of inline code
- Remove redundant inline first-pass JIT detection (now handled by findJITRegion)
- Detach cleanup is inherited from the updated base (open-telemetry#1102)
dalehamel added a commit that referenced this pull request Apr 8, 2026
- Restore findJITRegion() from base (open-telemetry#1102) which was incorrectly removed
  during rebase - PR #26 now reuses the shared function instead of inline code
- Remove redundant inline first-pass JIT detection (now handled by findJITRegion)
- Detach cleanup is inherited from the updated base (open-telemetry#1102)
dalehamel added a commit that referenced this pull request Apr 8, 2026
- Restore findJITRegion() from base (open-telemetry#1102) which was incorrectly removed
  during rebase - PR #26 now reuses the shared function instead of inline code
- Remove redundant inline first-pass JIT detection (now handled by findJITRegion)
- Detach cleanup is inherited from the updated base (open-telemetry#1102)
dalehamel added a commit that referenced this pull request Apr 8, 2026
- Restore findJITRegion() from base (open-telemetry#1102) which was incorrectly removed
  during rebase - PR #26 now reuses the shared function instead of inline code
- Remove redundant inline first-pass JIT detection (now handled by findJITRegion)
- Detach cleanup is inherited from the updated base (open-telemetry#1102)
dalehamel added a commit that referenced this pull request Apr 8, 2026
- Restore findJITRegion() from base (open-telemetry#1102) which was incorrectly removed
  during rebase - PR #26 now reuses the shared function instead of inline code
- Remove redundant inline first-pass JIT detection (now handled by findJITRegion)
- Detach cleanup is inherited from the updated base (open-telemetry#1102)
@dalehamel dalehamel force-pushed the ruby-jit-upstream branch 2 times, most recently from 5831ed4 to e151da4 Compare April 8, 2026 14:42
dalehamel added a commit that referenced this pull request Apr 8, 2026
- Restore findJITRegion() from base (open-telemetry#1102) which was incorrectly removed
  during rebase - PR #26 now reuses the shared function instead of inline code
- Remove redundant inline first-pass JIT detection (now handled by findJITRegion)
- Detach cleanup is inherited from the updated base (open-telemetry#1102)
dalehamel added a commit that referenced this pull request Apr 8, 2026
- Restore findJITRegion() from base (open-telemetry#1102) which was incorrectly removed
  during rebase - PR #26 now reuses the shared function instead of inline code
- Remove redundant inline first-pass JIT detection (now handled by findJITRegion)
- Detach cleanup is inherited from the updated base (open-telemetry#1102)
dalehamel added a commit that referenced this pull request Apr 8, 2026
- Restore findJITRegion() from base (open-telemetry#1102) which was incorrectly removed
  during rebase - PR #26 now reuses the shared function instead of inline code
- Remove redundant inline first-pass JIT detection (now handled by findJITRegion)
- Detach cleanup is inherited from the updated base (open-telemetry#1102)
@dalehamel
Copy link
Copy Markdown
Member Author

FYI #36 will replace this branch

@dalehamel dalehamel force-pushed the ruby-jit-upstream branch from 0c685ac to edbf402 Compare April 9, 2026 13:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants