Skip to content

Commit e56a177

Browse files
committed
ZJIT: Keep surviving JIT frame across longjmp
A tag established inside a live ZJIT frame can catch a longjmp without unwinding that native frame. Record that fact on rb_vm_tag and materialize only frames above the target, preserving its JITFrame link and keeping JITFrame saves at a single store.
1 parent 3a2434c commit e56a177

5 files changed

Lines changed: 85 additions & 8 deletions

File tree

eval_intern.h

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,14 @@ extern int select_large_fdset(int, fd_set *, fd_set *, fd_set *, struct timeval
106106
EC_SAVE_TAG_CFP(_tag, _ec); \
107107
rb_vm_tag_jmpbuf_init(&_tag.buf); \
108108

109-
// Remember the CFP as of EC_PUSH_TAG so that ZJIT can materialize frames
110-
// only up to longjmp's target CFP. When a C method does longjmp inside it,
111-
// the target CFP may not be equal to the VM_FRAME_FLAG_FINISH frame.
109+
// Remember the CFP and whether it was already running in ZJIT as of
110+
// EC_PUSH_TAG. When a C method does longjmp inside it, the target CFP may not
111+
// be equal to the VM_FRAME_FLAG_FINISH frame. If its ZJIT frame predates this
112+
// tag, its native stack survives the jump and should not be materialized.
112113
#if USE_ZJIT
113-
# define EC_SAVE_TAG_CFP(_tag, _ec) _tag.cfp = _ec->cfp
114+
# define EC_SAVE_TAG_CFP(_tag, _ec) \
115+
_tag.cfp = _ec->cfp; \
116+
_tag.zjit_frame_active = CFP_ZJIT_FRAME_P(_ec->cfp)
114117
#else
115118
# define EC_SAVE_TAG_CFP(_tag, _ec)
116119
#endif
@@ -166,7 +169,7 @@ rb_ec_tag_jump(const rb_execution_context_t *ec, enum ruby_tag_type st)
166169
{
167170
RUBY_ASSERT(st > TAG_NONE && st <= TAG_FATAL, ": Invalid tag jump: %d", (int)st);
168171
#if USE_ZJIT
169-
rb_zjit_materialize_frames(ec, ec->cfp);
172+
rb_zjit_materialize_frames_for_longjmp(ec, ec->cfp);
170173
#endif
171174
ec->tag->state = st;
172175
ruby_longjmp(RB_VM_TAG_JMPBUF_GET(ec->tag->buf), 1);

vm.c

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2884,14 +2884,15 @@ vm_exec_loop(rb_execution_context_t *ec, enum ruby_tag_type state,
28842884

28852885
#if USE_ZJIT
28862886
// Materialize JITFrame-enabled CFP into interpreter-compatible CFP
2887-
void
2888-
rb_zjit_materialize_frames(const rb_execution_context_t *ec, rb_control_frame_t *cfp)
2887+
static void
2888+
zjit_materialize_frames(const rb_execution_context_t *ec, rb_control_frame_t *cfp, bool materialize_target)
28892889
{
28902890
if (!rb_zjit_enabled_p) return;
28912891
const rb_control_frame_t *end_cfp = ec->tag->cfp;
28922892
VM_ASSERT(cfp <= end_cfp);
28932893

28942894
while (true) {
2895+
if (cfp == end_cfp && !materialize_target) break;
28952896
if (CFP_ZJIT_FRAME_P(cfp)) {
28962897
const zjit_jit_frame_t *jit_frame = CFP_ZJIT_FRAME(cfp);
28972898
cfp->pc = jit_frame->pc;
@@ -2931,6 +2932,20 @@ rb_zjit_materialize_frames(const rb_execution_context_t *ec, rb_control_frame_t
29312932
cfp = RUBY_VM_PREVIOUS_CONTROL_FRAME(cfp);
29322933
}
29332934
}
2935+
2936+
void
2937+
rb_zjit_materialize_frames(const rb_execution_context_t *ec, rb_control_frame_t *cfp)
2938+
{
2939+
zjit_materialize_frames(ec, cfp, true);
2940+
}
2941+
2942+
void
2943+
rb_zjit_materialize_frames_for_longjmp(const rb_execution_context_t *ec, rb_control_frame_t *cfp)
2944+
{
2945+
// A ZJIT frame active before the tag's setjmp is below it on the native
2946+
// stack and survives longjmp. Materialize only the frames unwound above it.
2947+
zjit_materialize_frames(ec, cfp, !ec->tag->zjit_frame_active);
2948+
}
29342949
#endif
29352950

29362951
static inline VALUE

vm_core.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1029,6 +1029,8 @@ struct rb_vm_tag {
10291029
#if USE_ZJIT
10301030
// ec->cfp as of EC_PUSH_TAG, which is saved for materializing JITFrame.
10311031
rb_control_frame_t *cfp;
1032+
// Whether cfp had a ZJIT frame before this tag's setjmp was established.
1033+
bool zjit_frame_active;
10321034
#endif
10331035
};
10341036

zjit.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ void rb_zjit_invalidate_no_singleton_class(VALUE klass);
9292
void rb_zjit_invalidate_root_box(void);
9393
void rb_zjit_jit_frame_update_references(zjit_jit_frame_t *jit_frame);
9494
void rb_zjit_materialize_frames(const rb_execution_context_t *ec, rb_control_frame_t *cfp);
95+
void rb_zjit_materialize_frames_for_longjmp(const rb_execution_context_t *ec, rb_control_frame_t *cfp);
9596
size_t rb_zjit_hash_new_size(void);
9697
bool rb_zjit_class_allocate_instance_fastpath(VALUE klass, size_t *size_out, shape_id_t *shape_id_out);
9798
bool rb_zjit_str_resurrect_fastpath(VALUE str, bool chilled, size_t *size_out, VALUE *flags_out, long *len_out, size_t *byte_size_out);
@@ -136,6 +137,7 @@ static inline void rb_zjit_invalidate_no_singleton_class(VALUE klass) {}
136137
static inline void rb_zjit_invalidate_root_box(void) {}
137138
static inline void rb_zjit_jit_frame_update_references(zjit_jit_frame_t *jit_frame) {}
138139
static inline void rb_zjit_materialize_frames(const rb_execution_context_t *ec, rb_control_frame_t *cfp) {}
140+
static inline void rb_zjit_materialize_frames_for_longjmp(const rb_execution_context_t *ec, rb_control_frame_t *cfp) {}
139141
static inline const zjit_jit_frame_t *CFP_ZJIT_FRAME(const rb_control_frame_t *cfp) { return NULL; }
140142
#endif // #if USE_ZJIT
141143

zjit/src/codegen_tests.rs

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use crate::backend::lir::Assembler;
66
use crate::codegen::max_iseq_versions;
77
use crate::cruby::*;
88
use crate::hir::{Insn, iseq_to_hir};
9-
use crate::options::{get_option, rb_zjit_prepare_options, set_call_threshold, set_inline_threshold};
9+
use crate::options::{get_option, rb_zjit_prepare_options, set_call_threshold, set_inline_threshold, set_max_versions};
1010
use crate::payload::IseqVersion;
1111
use crate::hir::tests::hir_build_tests::assert_contains_opcode;
1212
use crate::payload::*;
@@ -7198,6 +7198,61 @@ fn test_regression_gc_stress_with_lazy_block_code() {
71987198
"#), @":ok");
71997199
}
72007200

7201+
// Hash recursion uses catch/throw internally. The target frame remains in JIT
7202+
// code after the caught throw, so longjmp must not materialize and detach it
7203+
// before a callee side exit uses its updated PC and stack map.
7204+
#[test]
7205+
fn test_keep_jit_frame_for_caught_jump() {
7206+
rb_zjit_prepare_options();
7207+
let old_call_threshold = unsafe { crate::options::rb_zjit_call_threshold };
7208+
let old_inline_threshold = get_option!(inline_threshold);
7209+
let old_max_versions = get_option!(max_versions);
7210+
set_call_threshold(1);
7211+
set_inline_threshold(0);
7212+
set_max_versions(2);
7213+
let result = inspect(r#"
7214+
module KeepJITFrameAssertions
7215+
def assert_receiver(*)
7216+
raise unless is_a?(KeepJITFrameBase)
7217+
end
7218+
end
7219+
7220+
class KeepJITFrameBase
7221+
include KeepJITFrameAssertions
7222+
7223+
def hash_class = Hash
7224+
7225+
def test
7226+
hash = hash_class[]
7227+
recursive = [hash]
7228+
hash[:x] = recursive
7229+
object = Object.new
7230+
lookup = { hash => object }
7231+
7232+
[recursive, [hash]].each do |key|
7233+
key = { x: key }
7234+
assert_receiver(object, lookup[key], -> { key.inspect })
7235+
end
7236+
end
7237+
end
7238+
7239+
class KeepJITFrameHash < Hash
7240+
end
7241+
7242+
class KeepJITFrameSubclass < KeepJITFrameBase
7243+
def hash_class = KeepJITFrameHash
7244+
end
7245+
7246+
KeepJITFrameBase.new.test
7247+
KeepJITFrameSubclass.new.test
7248+
:ok
7249+
"#);
7250+
set_max_versions(old_max_versions);
7251+
set_inline_threshold(old_inline_threshold);
7252+
set_call_threshold(old_call_threshold);
7253+
assert_snapshot!(result, @":ok");
7254+
}
7255+
72017256
#[test]
72027257
fn test_float_arithmetic() {
72037258
set_call_threshold(1);

0 commit comments

Comments
 (0)