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