Skip to content

Commit d9f0b5a

Browse files
k0kubunrwstauner
andauthored
ZJIT: Set cfp->sp on leaf calls with GC (ruby#15137)
Co-authored-by: Randy Stauner <randy@r4s6.net>
1 parent 6e6f5d3 commit d9f0b5a

3 files changed

Lines changed: 28 additions & 7 deletions

File tree

test/ruby/test_zjit.rb

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3165,8 +3165,6 @@ def test(define)
31653165
end
31663166

31673167
def test_regression_cfp_sp_set_correctly_before_leaf_gc_call
3168-
omit 'reproduction for known, unresolved ZJIT bug'
3169-
31703168
assert_compiles ':ok', %q{
31713169
def check(l, r)
31723170
return 1 unless l

zjit/src/codegen.rs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1851,6 +1851,8 @@ fn gen_incr_send_fallback_counter(asm: &mut Assembler, reason: SendFallbackReaso
18511851
///
18521852
/// Unlike YJIT, we don't need to save the stack slots to protect them from GC
18531853
/// because the backend spills all live registers onto the C stack on CCall.
1854+
/// However, to avoid marking uninitialized stack slots, this also updates SP,
1855+
/// which may have cfp->sp for a past frame or a past non-leaf call.
18541856
fn gen_prepare_call_with_gc(asm: &mut Assembler, state: &FrameState, leaf: bool) {
18551857
let opcode: usize = state.get_opcode().try_into().unwrap();
18561858
let next_pc: *const VALUE = unsafe { state.pc.offset(insn_len(opcode) as isize) };
@@ -1859,13 +1861,27 @@ fn gen_prepare_call_with_gc(asm: &mut Assembler, state: &FrameState, leaf: bool)
18591861
asm_comment!(asm, "save PC to CFP");
18601862
asm.mov(Opnd::mem(64, CFP, RUBY_OFFSET_CFP_PC), Opnd::const_ptr(next_pc));
18611863

1864+
gen_save_sp(asm, state.stack_size());
18621865
if leaf {
18631866
asm.expect_leaf_ccall(state.stack_size());
18641867
}
18651868
}
18661869

18671870
fn gen_prepare_leaf_call_with_gc(asm: &mut Assembler, state: &FrameState) {
1868-
gen_prepare_call_with_gc(asm, state, true);
1871+
// In gen_prepare_call_with_gc(), we update cfp->sp for leaf calls too.
1872+
//
1873+
// Here, cfp->sp may be pointing to either of the following:
1874+
// 1. cfp->sp for a past frame, which gen_push_frame() skips to initialize
1875+
// 2. cfp->sp set by gen_prepare_non_leaf_call() for the current frame
1876+
//
1877+
// When (1), to avoid marking dead objects, we need to set cfp->sp for the current frame.
1878+
// When (2), setting cfp->sp at gen_push_frame() and not updating cfp->sp here could lead to
1879+
// keeping objects longer than it should, so we set cfp->sp at every call of this function.
1880+
//
1881+
// We use state.without_stack() to pass stack_size=0 to gen_save_sp() because we don't write
1882+
// VM stack slots on leaf calls, which leaves those stack slots uninitialized. ZJIT keeps
1883+
// live objects on the C stack, so they are protected from GC properly.
1884+
gen_prepare_call_with_gc(asm, &state.without_stack(), true);
18691885
}
18701886

18711887
/// Save the current SP on the CFP
@@ -1907,11 +1923,11 @@ fn gen_spill_stack(jit: &JITState, asm: &mut Assembler, state: &FrameState) {
19071923
fn gen_prepare_non_leaf_call(jit: &JITState, asm: &mut Assembler, state: &FrameState) {
19081924
// TODO: Lazily materialize caller frames when needed
19091925
// Save PC for backtraces and allocation tracing
1926+
// and SP to avoid marking uninitialized stack slots
19101927
gen_prepare_call_with_gc(asm, state, false);
19111928

1912-
// Save SP and spill the virtual stack in case it raises an exception
1929+
// Spill the virtual stack in case it raises an exception
19131930
// and the interpreter uses the stack for handling the exception
1914-
gen_save_sp(asm, state.stack().len());
19151931
gen_spill_stack(jit, asm, state);
19161932

19171933
// Spill locals in case the method looks at caller Bindings
@@ -1958,8 +1974,8 @@ fn gen_push_frame(asm: &mut Assembler, argc: usize, state: &FrameState, frame: C
19581974
asm_comment!(asm, "push callee control frame");
19591975

19601976
if let Some(iseq) = frame.iseq {
1961-
// cfp_opnd(RUBY_OFFSET_CFP_PC): written by the callee frame on side-exits or non-leaf calls
1962-
// cfp_opnd(RUBY_OFFSET_CFP_SP): written by the callee frame on side-exits or non-leaf calls
1977+
// cfp_opnd(RUBY_OFFSET_CFP_PC): written by the callee frame on side-exits, non-leaf calls, or calls with GC
1978+
// cfp_opnd(RUBY_OFFSET_CFP_SP): written by the callee frame on side-exits, non-leaf calls, or calls with GC
19631979
asm.mov(cfp_opnd(RUBY_OFFSET_CFP_ISEQ), VALUE::from(iseq).into());
19641980
} else {
19651981
// C frames don't have a PC and ISEQ in normal operation.

zjit/src/hir.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4153,6 +4153,13 @@ impl FrameState {
41534153
state.locals.clear();
41544154
state
41554155
}
4156+
4157+
/// Return itself without stack. Used by leaf calls with GC to reset SP to the base pointer.
4158+
pub fn without_stack(&self) -> Self {
4159+
let mut state = self.clone();
4160+
state.stack.clear();
4161+
state
4162+
}
41564163
}
41574164

41584165
/// Print adaptor for [`FrameState`]. See [`PtrPrintMap`].

0 commit comments

Comments
 (0)