@@ -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.
18541856fn 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
18671870fn 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) {
19071923fn 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.
0 commit comments