Skip to content

Commit de3343d

Browse files
committed
ZJIT: Local variables desync after Kernel#eval and friends
Fixes <#970>, which requires `--zjit-disable-hir-opt` to observe, but there should be situations where this happens even with optimizations. Each and every send could land in `eval`, and so have the potential to modify locals of the caller. We don't want to issue explicit reloads after every send, so we hold local values across sends, but in case the send is an `eval` and it modifies locals, this is incorrect. Add `NoEPEscape` to correct for `eval`. Note that this holding of locals in the frame state happens regardless of whether the environment has escaped entering the send, matching that, we always put `NoEPEscape`.
1 parent 1ca2d2d commit de3343d

5 files changed

Lines changed: 1517 additions & 1146 deletions

File tree

zjit/src/codegen_tests.rs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -566,7 +566,7 @@ fn test_setblockparam() {
566566
test { 1 }
567567
");
568568
assert_contains_opcode("test", YARVINSN_setblockparam);
569-
assert_snapshot!(assert_compiles("test { 1 }"), @"3");
569+
assert_snapshot!(assert_compiles_allowing_exits("test { 1 }"), @"3");
570570
}
571571

572572
#[test]
@@ -581,7 +581,7 @@ fn test_setblockparam_nested_block() {
581581
end
582582
test { 1 }
583583
");
584-
assert_snapshot!(assert_compiles("test { 1 }"), @"3");
584+
assert_snapshot!(assert_compiles_allowing_exits("test { 1 }"), @"3");
585585
}
586586

587587
#[test]
@@ -594,7 +594,7 @@ fn test_getblockparamproxy_after_setblockparam() {
594594
test { 1 }
595595
");
596596
assert_contains_opcode("test", YARVINSN_setblockparam);
597-
assert_snapshot!(assert_compiles("test { 1 }"), @"3");
597+
assert_snapshot!(assert_compiles_allowing_exits("test { 1 }"), @"3");
598598
}
599599

600600
#[test]
@@ -6331,3 +6331,23 @@ fn test_getlocal_level_zero_after_setlocal_wc_0() {
63316331
test
63326332
"#), @"2");
63336333
}
6334+
6335+
// Regression test for losing track of where locals are with --zjit-disable-hir-opt due
6336+
// to lack of NoEPEscape patchpoint. Reduced from ruby-spec
6337+
// https://github.com/Shopify/ruby/issues/970
6338+
#[test]
6339+
fn test_eval_with_hir_opt_disable() {
6340+
set_call_threshold(1);
6341+
crate::options::disable_hir_opt();
6342+
assert_snapshot!(inspect(r#"
6343+
def test
6344+
ofor = nil
6345+
n = 0
6346+
eval("n = 2")
6347+
raise unless ofor.nil? # commenting out this line dodges the bug
6348+
n
6349+
end
6350+
test
6351+
test
6352+
"#), @"2");
6353+
}

zjit/src/hir.rs

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4951,8 +4951,12 @@ impl Function {
49514951
self.push_insn(block, Insn::PatchPoint { invariant: Invariant::MethodRedefined { klass: recv_class, method: method_id, cme }, state });
49524952
}
49534953

4954-
/// Side exit back to the state after a block-backed send.
4955-
/// Using the pre-send snapshot would re-execute the send in the interpreter.
4954+
/// Side exit back to the state after a send. Using the pre-send snapshot would re-execute the
4955+
/// send in the interpreter.
4956+
///
4957+
/// Any send could cause the caller's environment to escape, and this patchpoint scopes our
4958+
/// analysis to the cases where the environment remains not escaped across the send. The
4959+
/// patchpoint also protects against local modifications through `Kernel#eval` and friends.
49564960
fn gen_post_send_no_ep_escape_patch_point(&mut self, block: BlockId, state: &FrameState, insn_idx: u32) {
49574961
let iseq = state.iseq;
49584962
let mut reload_state = state.clone();
@@ -8616,12 +8620,18 @@ fn add_iseq_to_hir(
86168620
let send = fun.push_insn(block, Insn::Send { recv, cd, block: None, args, state: exit_id, reason });
86178621
fun.push_insn(block, Insn::Jump(BranchEdge { target: join_block, args: vec![send] }));
86188622
state.stack_push(join_param);
8623+
if !state.locals.is_empty() {
8624+
fun.gen_post_send_no_ep_escape_patch_point(join_block, &state, insn_idx);
8625+
}
86198626
// Continue compilation from the join block at the next instruction.
86208627
block = join_block;
86218628
} else {
86228629
// Maybe monomorphic; handled in type_specialize
86238630
let send = fun.push_insn(block, Insn::Send { recv, cd, block: None, args, state: exit_id, reason: Uncategorized(opcode) });
86248631
state.stack_push(send);
8632+
if !state.locals.is_empty() {
8633+
fun.gen_post_send_no_ep_escape_patch_point(block, &state, insn_idx);
8634+
}
86258635
}
86268636
}
86278637
YARVINSN_send => {
@@ -8652,14 +8662,14 @@ fn add_iseq_to_hir(
86528662
};
86538663
let send = fun.push_insn(block, Insn::Send { recv, cd, block: block_handler, args, state: exit_id, reason: Uncategorized(opcode) });
86548664
state.stack_push(send);
8665+
if !state.locals.is_empty() {
8666+
fun.gen_post_send_no_ep_escape_patch_point(block, &state, insn_idx);
8667+
}
86558668

86568669
if let Some(BlockHandler::BlockIseq(_)) = block_handler {
86578670
// Reload locals that may have been modified by the blockiseq.
86588671
// TODO: Avoid reloading locals that are not referenced by the blockiseq
86598672
// or not used after this. Max thinks we could eventually DCE them.
8660-
if !ep_escaped && !state.locals.is_empty() {
8661-
fun.gen_post_send_no_ep_escape_patch_point(block, &state, insn_idx);
8662-
}
86638673
let mut base: Option<InsnId> = None;
86648674
for local_idx in 0..state.locals.len() {
86658675
let ep_offset = local_idx_to_ep_offset(iseq, local_idx);
@@ -8700,12 +8710,12 @@ fn add_iseq_to_hir(
87008710
let recv = state.stack_pop()?;
87018711
let send_forward = fun.push_insn(block, Insn::SendForward { recv, cd, blockiseq, args, state: exit_id, reason: SendForwardNotSpecialized });
87028712
state.stack_push(send_forward);
8713+
if !state.locals.is_empty() {
8714+
fun.gen_post_send_no_ep_escape_patch_point(block, &state, insn_idx);
8715+
}
87038716

87048717
if !blockiseq.is_null() {
87058718
// Reload locals that may have been modified by the blockiseq.
8706-
if !ep_escaped && !state.locals.is_empty() {
8707-
fun.gen_post_send_no_ep_escape_patch_point(block, &state, insn_idx);
8708-
}
87098719
let mut base: Option<InsnId> = None;
87108720
for local_idx in 0..state.locals.len() {
87118721
let ep_offset = local_idx_to_ep_offset(iseq, local_idx);
@@ -8743,14 +8753,14 @@ fn add_iseq_to_hir(
87438753
let blockiseq: IseqPtr = get_arg(pc, 1).as_ptr();
87448754
let result = fun.push_insn(block, Insn::InvokeSuper { recv, cd, blockiseq, args, state: exit_id, reason: Uncategorized(opcode) });
87458755
state.stack_push(result);
8756+
if !state.locals.is_empty() {
8757+
fun.gen_post_send_no_ep_escape_patch_point(block, &state, insn_idx);
8758+
}
87468759

87478760
if !blockiseq.is_null() {
87488761
// Reload locals that may have been modified by the blockiseq.
87498762
// TODO: Avoid reloading locals that are not referenced by the blockiseq
87508763
// or not used after this. Max thinks we could eventually DCE them.
8751-
if !ep_escaped && !state.locals.is_empty() {
8752-
fun.gen_post_send_no_ep_escape_patch_point(block, &state, insn_idx);
8753-
}
87548764
let mut base: Option<InsnId> = None;
87558765
for local_idx in 0..state.locals.len() {
87568766
let ep_offset = local_idx_to_ep_offset(iseq, local_idx);
@@ -8790,14 +8800,14 @@ fn add_iseq_to_hir(
87908800
let recv = state.stack_pop()?;
87918801
let result = fun.push_insn(block, Insn::InvokeSuperForward { recv, cd, blockiseq, args, state: exit_id, reason: InvokeSuperForwardNotSpecialized });
87928802
state.stack_push(result);
8803+
if !state.locals.is_empty() {
8804+
fun.gen_post_send_no_ep_escape_patch_point(block, &state, insn_idx);
8805+
}
87938806

87948807
if !blockiseq.is_null() {
87958808
// Reload locals that may have been modified by the blockiseq.
87968809
// TODO: Avoid reloading locals that are not referenced by the blockiseq
87978810
// or not used after this. Max thinks we could eventually DCE them.
8798-
if !ep_escaped && !state.locals.is_empty() {
8799-
fun.gen_post_send_no_ep_escape_patch_point(block, &state, insn_idx);
8800-
}
88018811
let mut base: Option<InsnId> = None;
88028812
for local_idx in 0..state.locals.len() {
88038813
let ep_offset = local_idx_to_ep_offset(iseq, local_idx);

0 commit comments

Comments
 (0)