Skip to main content

revmc_codegen/tests/
resume_at_call.rs

1// Tests for resume_at persistence in call_with_interpreter (49274dbb).
2//
3// When JIT code encounters a CALL instruction, it suspends execution and
4// returns `InterpreterAction::NewFrame`. After the callee completes, the
5// caller must resume from the instruction *after* the CALL — not from PC=0.
6//
7// Without the fix, `call_with_interpreter` did not persist `ecx.resume_at`
8// back into the interpreter's bytecode PC, causing re-execution from the
9// beginning on every re-entry.
10
11use super::{
12    DEF_ADDR, DEF_CALLER, DEF_CD, DEF_GAS_LIMIT, DEF_SPEC, DEF_VALUE, TestHost,
13    insert_call_outcome_test,
14};
15use crate::{Backend, EvmCompiler};
16use revm_bytecode::opcode as op;
17use revm_interpreter::{
18    CallInput, FrameInput, Gas, InputsImpl, InstructionResult, Interpreter, InterpreterAction,
19    InterpreterResult, SharedMemory, interpreter::ExtBytecode,
20};
21use revm_primitives::{Bytes, U256};
22
23matrix_tests!(call_then_push = |compiler| run_call_then_push(compiler));
24matrix_tests!(call_then_return = |compiler| run_call_then_return(compiler));
25matrix_tests!(call_returndatasize = |compiler| run_call_returndatasize(compiler));
26matrix_tests!(
27    call_pop_push_sload_stack_len = |compiler| run_call_pop_push_sload_stack_len(compiler)
28);
29
30/// Contract: PUSH args → CALL → PUSH1 0x42 → STOP
31///
32/// After the CALL returns, execution must continue with PUSH1 0x42 (not restart
33/// from PUSH args). We verify by checking the stack after the second call to
34/// `call_with_interpreter`.
35fn run_call_then_push<B: Backend>(compiler: &mut EvmCompiler<B>) {
36    #[rustfmt::skip]
37    let bytecode: &[u8] = &[
38        // Set up CALL arguments (7 stack items)
39        op::PUSH1, 0,    // ret length
40        op::PUSH1, 0,    // ret offset
41        op::PUSH1, 0,    // args length
42        op::PUSH1, 0,    // args offset
43        op::PUSH1, 0,    // value = 0
44        op::PUSH1, 0x69, // address
45        op::GAS,         // gas (all remaining)
46        op::CALL,        // suspends here with NewFrame
47        // -- after CALL returns, execution resumes here --
48        op::PUSH1, 0x42, // push marker value
49        op::STOP,
50    ];
51
52    unsafe { compiler.clear() }.unwrap();
53    compiler.inspect_stack(true);
54    let f = unsafe { compiler.jit("resume_call", bytecode, DEF_SPEC) }.unwrap();
55
56    // First call: should suspend at CALL with NewFrame
57    let mut host = TestHost::new();
58    let input = InputsImpl {
59        target_address: DEF_ADDR,
60        bytecode_address: None,
61        caller_address: DEF_CALLER,
62        input: CallInput::Bytes(Bytes::from_static(DEF_CD)),
63        call_value: DEF_VALUE,
64    };
65    let bytecode_obj = revm_bytecode::Bytecode::new_raw(Bytes::copy_from_slice(bytecode));
66    let ext_bytecode = ExtBytecode::new(bytecode_obj);
67    let mut interpreter =
68        Interpreter::new(SharedMemory::new(), ext_bytecode, input, false, DEF_SPEC, DEF_GAS_LIMIT);
69
70    let action = unsafe { f.call_with_interpreter(&mut interpreter, &mut host) };
71
72    // Should get NewFrame(Call(...))
73    let return_memory_offset = match &action {
74        InterpreterAction::NewFrame(FrameInput::Call(call_inputs)) => {
75            Some(call_inputs.return_memory_offset.clone())
76        }
77        other => panic!("expected NewFrame(Call), got {other:?}"),
78    };
79
80    // Simulate the callee completing successfully with no output
81    let call_result = InterpreterResult {
82        result: InstructionResult::Stop,
83        output: Bytes::new(),
84        gas: Gas::new(0),
85    };
86    insert_call_outcome_test(&mut interpreter, call_result, return_memory_offset);
87
88    // Second call: should resume after CALL, execute PUSH1 0x42, STOP
89    let action = unsafe { f.call_with_interpreter(&mut interpreter, &mut host) };
90
91    match &action {
92        InterpreterAction::Return(result) => {
93            assert_eq!(
94                result.result,
95                InstructionResult::Stop,
96                "expected Stop after resume, got {:?}",
97                result.result
98            );
99            // Stack should have: [call_success_indicator(1), 0x42]
100            // The CALL pushed success=1, then PUSH1 0x42
101            assert_eq!(interpreter.stack.len(), 2, "stack should have 2 items");
102            assert_eq!(
103                interpreter.stack.data()[1],
104                U256::from(0x42),
105                "top of stack should be 0x42 (the marker value pushed after CALL)"
106            );
107        }
108        other => panic!("expected Return after resume, got {other:?}"),
109    }
110}
111
112/// Contract: PUSH args → CALL → PUSH1 32 → PUSH0 → RETURN
113///
114/// After the CALL returns, execution should RETURN 32 zero bytes.
115/// Without the resume_at fix, it would re-enter from the beginning and try
116/// to CALL again, never reaching the RETURN.
117fn run_call_then_return<B: Backend>(compiler: &mut EvmCompiler<B>) {
118    #[rustfmt::skip]
119    let bytecode: &[u8] = &[
120        // CALL arguments
121        op::PUSH1, 0,    // ret length
122        op::PUSH1, 0,    // ret offset
123        op::PUSH1, 0,    // args length
124        op::PUSH1, 0,    // args offset
125        op::PUSH1, 0,    // value = 0
126        op::PUSH1, 0x69, // address
127        op::GAS,         // gas
128        op::CALL,        // suspends
129        // -- resume point --
130        op::POP,          // pop call success indicator
131        op::PUSH1, 32,   // return size
132        op::PUSH0,       // return offset
133        op::RETURN,       // should return 32 zero bytes
134    ];
135
136    unsafe { compiler.clear() }.unwrap();
137    compiler.inspect_stack(true);
138    let f = unsafe { compiler.jit("resume_return", bytecode, DEF_SPEC) }.unwrap();
139
140    let mut host = TestHost::new();
141    let input = InputsImpl {
142        target_address: DEF_ADDR,
143        bytecode_address: None,
144        caller_address: DEF_CALLER,
145        input: CallInput::Bytes(Bytes::from_static(DEF_CD)),
146        call_value: DEF_VALUE,
147    };
148    let bytecode_obj = revm_bytecode::Bytecode::new_raw(Bytes::copy_from_slice(bytecode));
149    let ext_bytecode = ExtBytecode::new(bytecode_obj);
150    let mut interpreter =
151        Interpreter::new(SharedMemory::new(), ext_bytecode, input, false, DEF_SPEC, DEF_GAS_LIMIT);
152
153    // First call: suspends at CALL
154    let action = unsafe { f.call_with_interpreter(&mut interpreter, &mut host) };
155    let return_memory_offset = match &action {
156        InterpreterAction::NewFrame(FrameInput::Call(call_inputs)) => {
157            Some(call_inputs.return_memory_offset.clone())
158        }
159        other => panic!("expected NewFrame(Call), got {other:?}"),
160    };
161
162    // Simulate callee returning
163    let call_result = InterpreterResult {
164        result: InstructionResult::Stop,
165        output: Bytes::new(),
166        gas: Gas::new(0),
167    };
168    insert_call_outcome_test(&mut interpreter, call_result, return_memory_offset);
169
170    // Second call: should resume, POP, PUSH1 32, PUSH0, RETURN
171    let action = unsafe { f.call_with_interpreter(&mut interpreter, &mut host) };
172
173    match &action {
174        InterpreterAction::Return(result) => {
175            assert_eq!(result.result, InstructionResult::Return);
176            assert_eq!(result.output.len(), 32, "expected 32-byte return output");
177        }
178        other => panic!("expected Return after resume, got {other:?}"),
179    }
180}
181
182/// Contract: CALL → RETURNDATASIZE → PUSH0 → MSTORE → PUSH1 32 → PUSH0 → RETURN
183///
184/// After the CALL returns with 7 bytes of data, RETURNDATASIZE must reflect
185/// the actual return data length (7). The value is stored to memory and returned.
186///
187/// This verifies that DSE does NOT eliminate RETURNDATASIZE when its output is
188/// live, and that the builtin correctly reads return_data after suspend/resume.
189fn run_call_returndatasize<B: Backend>(compiler: &mut EvmCompiler<B>) {
190    #[rustfmt::skip]
191    let bytecode: &[u8] = &[
192        // CALL arguments
193        op::PUSH1, 0,    // ret length
194        op::PUSH1, 0,    // ret offset
195        op::PUSH1, 0,    // args length
196        op::PUSH1, 0,    // args offset
197        op::PUSH1, 0,    // value = 0
198        op::PUSH1, 0x69, // address
199        op::GAS,         // gas
200        op::CALL,        // suspends
201        // -- resume point --
202        op::POP,              // pop call success
203        op::RETURNDATASIZE,   // push return data length
204        op::PUSH0,            // dest offset
205        op::MSTORE,           // store to memory
206        op::PUSH1, 32,        // return size
207        op::PUSH0,            // return offset
208        op::RETURN,
209    ];
210
211    unsafe { compiler.clear() }.unwrap();
212    compiler.inspect_stack(true);
213    let f = unsafe { compiler.jit("resume_rds", bytecode, DEF_SPEC) }.unwrap();
214
215    let mut host = TestHost::new();
216    let input = InputsImpl {
217        target_address: DEF_ADDR,
218        bytecode_address: None,
219        caller_address: DEF_CALLER,
220        input: CallInput::Bytes(Bytes::from_static(DEF_CD)),
221        call_value: DEF_VALUE,
222    };
223    let bytecode_obj = revm_bytecode::Bytecode::new_raw(Bytes::copy_from_slice(bytecode));
224    let ext_bytecode = ExtBytecode::new(bytecode_obj);
225    let mut interpreter =
226        Interpreter::new(SharedMemory::new(), ext_bytecode, input, false, DEF_SPEC, DEF_GAS_LIMIT);
227
228    // First call: suspends at CALL
229    let action = unsafe { f.call_with_interpreter(&mut interpreter, &mut host) };
230    let return_memory_offset = match &action {
231        InterpreterAction::NewFrame(FrameInput::Call(call_inputs)) => {
232            Some(call_inputs.return_memory_offset.clone())
233        }
234        other => panic!("expected NewFrame(Call), got {other:?}"),
235    };
236
237    // Simulate callee returning 7 bytes of data.
238    let return_data = Bytes::from_static(&[0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0x42]);
239    let call_result = InterpreterResult {
240        result: InstructionResult::Stop,
241        output: return_data,
242        gas: Gas::new(0),
243    };
244    insert_call_outcome_test(&mut interpreter, call_result, return_memory_offset);
245
246    // Second call: resume → POP → RETURNDATASIZE → MSTORE → RETURN
247    let action = unsafe { f.call_with_interpreter(&mut interpreter, &mut host) };
248
249    match &action {
250        InterpreterAction::Return(result) => {
251            assert_eq!(result.result, InstructionResult::Return);
252            assert_eq!(result.output.len(), 32, "expected 32-byte return output");
253            // Memory word should contain RETURNDATASIZE = 7.
254            let value = U256::from_be_slice(&result.output);
255            assert_eq!(
256                value,
257                U256::from(7),
258                "RETURNDATASIZE should be 7 after CALL with 7-byte return data"
259            );
260        }
261        other => panic!("expected Return after resume, got {other:?}"),
262    }
263}
264
265/// Regression test for stale `len.addr` after `POP, PUSH1(noop), SLOAD`.
266///
267/// When a section contains `POP` (stores `len = start-1`), then a noop `PUSH`
268/// (restores `section_len_offset` to 0 without storing), followed by a net-zero
269/// builtin like `SLOAD`, the `len.addr` store was skipped because both
270/// `section_len_offset` and `diff` were 0. This left `len.addr` stale at
271/// `start-1`, causing the next section head to load an incorrect stack length.
272///
273/// The test pushes a marker value (0xBEEF) below the CALL arguments. After
274/// resume, the sequence `POP, PUSH1 5, SLOAD` triggers the bug, then a
275/// `JUMP → JUMPDEST` creates a new section that reloads `len.addr`. With the
276/// bug, the stack length is off by 1, causing the marker to be read from the
277/// wrong position.
278fn run_call_pop_push_sload_stack_len<B: Backend>(compiler: &mut EvmCompiler<B>) {
279    // Trigger: `POP, PUSH1(noop), SLOAD` in the resume section, where SLOAD
280    // is the last non-noop instruction before a JUMPDEST section head.
281    //
282    // POP stores `len.addr = start - 1`. The noop PUSH1 resets
283    // `section_len_offset` to 0. SLOAD (net-zero) then sees `diff == 0 &&
284    // section_len_offset == 0` and skips the `len.addr` store, leaving it
285    // stale at `start - 1`. The JUMPDEST loads the stale value and
286    // misaligns all subsequent stack accesses.
287    //
288    // The JUMPDEST at pc=21 is made a reachable jump target (and therefore a
289    // section head) via a JUMP at pc=36 in unreachable code after RETURN.
290    const JUMPDEST_PC: u8 = 21;
291    #[rustfmt::skip]
292    let bytecode: &[u8] = &[
293        // Push marker below CALL args.
294        op::PUSH2, 0xBE, 0xEF,          // pc=0
295        // CALL arguments (7 stack items).
296        op::PUSH1, 0,                    // pc=3: ret length
297        op::PUSH1, 0,                    // pc=5: ret offset
298        op::PUSH1, 0,                    // pc=7: args length
299        op::PUSH1, 0,                    // pc=9: args offset
300        op::PUSH1, 0,                    // pc=11: value
301        op::PUSH1, 0x69,                 // pc=13: address
302        op::GAS,                         // pc=15: gas
303        op::CALL,                        // pc=16: suspends
304        // -- resume section --
305        op::POP,                         // pc=17: pop call result
306        op::PUSH1, 0x05,                 // pc=18: noop (SLOAD key)
307        op::SLOAD,                       // pc=20: net-0 builtin → falls through
308        // -- new section head (also targeted by JUMP at pc=35) --
309        op::JUMPDEST,                    // pc=21: reloads stale len.addr
310        // Stack should be [marker, sload_result].
311        op::POP,                         // pc=22: pop sload_result
312        // Stack: [marker]
313        op::PUSH0,                       // pc=23: offset = 0
314        op::MSTORE,                      // pc=24: mem[0..32] = marker
315        op::PUSH1, 32,                   // pc=25: return size
316        op::PUSH0,                       // pc=27: return offset
317        op::RETURN,                      // pc=28
318        // Unreachable: a JUMP that targets the JUMPDEST, making it a
319        // reachable jump target in the CFG even though this path is dead.
320        op::JUMPDEST,                    // pc=29: prevents "no valid predecessor" pruning
321        op::PUSH1, 0,                    // pc=30: dummy push (for stack depth ≥ 2)
322        op::PUSH1, 0,                    // pc=32: dummy push
323        op::PUSH1, JUMPDEST_PC,          // pc=34
324        op::JUMP,                        // pc=36: targets pc=21
325    ];
326
327    unsafe { compiler.clear() }.unwrap();
328    compiler.inspect_stack(true);
329    let f = unsafe { compiler.jit("pop_push_sload", bytecode, DEF_SPEC) }.unwrap();
330
331    let mut host = TestHost::new();
332    let input = InputsImpl {
333        target_address: DEF_ADDR,
334        bytecode_address: None,
335        caller_address: DEF_CALLER,
336        input: CallInput::Bytes(Bytes::from_static(DEF_CD)),
337        call_value: DEF_VALUE,
338    };
339    let bytecode_obj = revm_bytecode::Bytecode::new_raw(Bytes::copy_from_slice(bytecode));
340    let ext_bytecode = ExtBytecode::new(bytecode_obj);
341    let mut interpreter =
342        Interpreter::new(SharedMemory::new(), ext_bytecode, input, false, DEF_SPEC, DEF_GAS_LIMIT);
343
344    // First call: suspends at CALL.
345    let action = unsafe { f.call_with_interpreter(&mut interpreter, &mut host) };
346    let return_memory_offset = match &action {
347        InterpreterAction::NewFrame(FrameInput::Call(call_inputs)) => {
348            Some(call_inputs.return_memory_offset.clone())
349        }
350        other => panic!("expected NewFrame(Call), got {other:?}"),
351    };
352
353    // Simulate callee returning successfully.
354    let call_result = InterpreterResult {
355        result: InstructionResult::Stop,
356        output: Bytes::new(),
357        gas: Gas::new(0),
358    };
359    insert_call_outcome_test(&mut interpreter, call_result, return_memory_offset);
360
361    // Second call: resume → POP → PUSH1 → SLOAD → JUMPDEST → POP → MSTORE → RETURN.
362    let action = unsafe { f.call_with_interpreter(&mut interpreter, &mut host) };
363
364    match &action {
365        InterpreterAction::Return(result) => {
366            assert_eq!(
367                result.result,
368                InstructionResult::Return,
369                "expected Return, got {:?}",
370                result.result
371            );
372            assert_eq!(result.output.len(), 32, "expected 32-byte return output");
373            let value = U256::from_be_slice(&result.output);
374            assert_eq!(
375                value,
376                U256::from(0xBEEF),
377                "returned value should be the marker 0xBEEF; \
378                 stale len.addr causes the JUMPDEST section to misalign the stack"
379            );
380        }
381        other => panic!("expected Return after resume, got {other:?}"),
382    }
383}