Skip to content

fix: align VM with EVM stack semantics and harden decode/cache#692

Closed
Trustdev-eth wants to merge 1 commit into
Jon-Becker:mainfrom
Trustdev-eth:fix/vm-decode-cache-robustness
Closed

fix: align VM with EVM stack semantics and harden decode/cache#692
Trustdev-eth wants to merge 1 commit into
Jon-Becker:mainfrom
Trustdev-eth:fix/vm-decode-cache-robustness

Conversation

@Trustdev-eth
Copy link
Copy Markdown

  • JUMPI pops condition then counter; JUMP/JUMPI reject out-of-range targets without slicing past bytecode.

  • Restore correct stack order for CALL family, RETURN/REVERT, and memory copy opcodes (incl. gas on destination expansion where applicable).

  • SIGNEXTEND skips work when byte index > 31; EXP gas uses ceil(bit_len / 8) for nonzero exponents.

  • MCOPY uses EIP-5656 stack order and max(source, dest) expansion for dynamic gas.

  • Stack peek/peek_no longer fabricates values; insufficient depth surfaces as stack underflow in step.

  • Symbolic executor uses JUMPI inputs[0]/[1] for condition vs jump target and swaps branch exploration PCs.

  • Decode rejects calldata shorter than four bytes and replaces panics with errors on raw heuristic failure.

  • Cache rejects path-like keys; safe hex decode and portable delete via std::fs; docs clarify expiry is absolute unix ts.

  • Use std::fs for delete_path in heimdall-common on Windows-compatible removal.

BREAKING CHANGE: Stack::peek returns Option; Stack::peek_n returns Option<Vec<_>>.

Tests: update jump/JUMPI bytecode and assertions for corrected control flow.

What changed? Why?

Notes to reviewers

How has it been tested?

- JUMPI pops condition then counter; JUMP/JUMPI reject out-of-range targets without slicing past bytecode.

- Restore correct stack order for CALL family, RETURN/REVERT, and memory copy opcodes (incl. gas on destination expansion where applicable).

- SIGNEXTEND skips work when byte index > 31; EXP gas uses ceil(bit_len / 8) for nonzero exponents.

- MCOPY uses EIP-5656 stack order and max(source, dest) expansion for dynamic gas.

- Stack peek/peek_no longer fabricates values; insufficient depth surfaces as stack underflow in step.

- Symbolic executor uses JUMPI inputs[0]/[1] for condition vs jump target and swaps branch exploration PCs.

- Decode rejects calldata shorter than four bytes and replaces panics with errors on raw heuristic failure.

- Cache rejects path-like keys; safe hex decode and portable delete via std::fs; docs clarify expiry is absolute unix ts.

- Use std::fs for delete_path in heimdall-common on Windows-compatible removal.

BREAKING CHANGE: Stack::peek returns Option<StackFrame>; Stack::peek_n returns Option<Vec<_>>.

Tests: update jump/JUMPI bytecode and assertions for corrected control flow.
Co-authored-by: Cursor <cursoragent@cursor.com>
@Trustdev-eth Trustdev-eth requested a review from Jon-Becker as a code owner May 6, 2026 13:41
Copy link
Copy Markdown
Owner

@Jon-Becker Jon-Becker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are these changes necessary? please fill out the PR description.

how have you validated that the evm changes do not break decompilation and or other vm tests? there seems to be a lack of test coverage. generally, if we change the way a function works we should add tests and ensure old tests continue to pass.

please see CI checks as well.

thank you for contributing!

Comment thread crates/cache/src/lib.rs

let binary_vec = decode_hex(&binary_string)
.map_err(|e| Error::Generic(format!("failed to decode hex: {e:?}")))?;
let binary_vec = decode_hex(&binary_string)?;
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why did we remove the verbose error string?

Comment thread crates/cache/src/util.rs
/// Decode a hex string into a bytearray
pub(crate) fn decode_hex(s: &str) -> Result<Vec<u8>, ParseIntError> {
(0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16)).collect()
pub(crate) fn decode_hex(s: &str) -> Result<Vec<u8>, Error> {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this change necessary?


let potential_type =
to_type(potential_types.first().expect("potential types is empty"));
let potential_type = if let Some(t) = potential_types.first() {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could use more idiomatic rust here and use potential_types.first().unwrap_or("bytes32")

pub fn signextend(vm: &mut VM, operation: WrappedOpcode) -> Result<()> {
let x = vm.stack.pop()?.value;
let b = vm.stack.pop()?.value;
let byte_idx = vm.stack.pop()?.value;
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please update test expectations

/// CALL - Message-call into an account
pub fn call(vm: &mut VM, operation: WrappedOpcode) -> Result<()> {
// Stack (top first): retSize, retOffset, argsSize, argsOffset, value, address, gas
let _ret_size = vm.stack.pop()?.value;
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this change necessary?

pub fn callcode(vm: &mut VM, operation: WrappedOpcode) -> Result<()> {
let _ret_size = vm.stack.pop()?.value;
let _ret_offset = vm.stack.pop()?.value;
let _args_size = vm.stack.pop()?.value;
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this change necessary?

pub fn delegatecall(vm: &mut VM, operation: WrappedOpcode) -> Result<()> {
// Stack (top first): retSize, retOffset, argsSize, argsOffset, address, gas
let _ret_size = vm.stack.pop()?.value;
let _ret_offset = vm.stack.pop()?.value;
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this change necessary?

@Jon-Becker Jon-Becker closed this May 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants