Repository: lambdaclass/ethrex (Rust)

Issue:
[SECURITY] Non-Compliance in Memory Allocation for KECCAK256
Our team at [FuzzingLabs](https://github.com/FuzzingLabs) identified a non-compliance issue in the [KECCAK256](https://github.com/lambdaclass/ethrex/blob/5bbe595ff9e3900b2d84f075d3b19672a10854b9/crates/vm/levm/src/opcode_handlers/keccak.rs#L14) implementation. During the execution of the this opcode, LEVM incorrectly allocates memory even when the `size` parameter is zero. This behavior deviates from expected EVM standards as implemented in widely adopted EVMs like REVM and Geth. Allocating memory in this scenario is unnecessary and results in an inflated memory size (`MSIZE`) and unnecessary gas consumption.

## Root cause

```rust
    pub fn op_keccak256(
        &mut self,
        current_call_frame: &mut CallFrame,
    ) -> Result<OpcodeSuccess, VMError> {
        let offset: usize = current_call_frame
            .stack
            .pop()?
            .try_into()
            .map_err(|_| VMError::VeryLargeNumber)?;
        let size: usize = current_call_frame
            .stack
            .pop()?
            .try_into()
            .map_err(|_| VMError::VeryLargeNumber)?;

        let gas_cost =
            gas_cost::keccak256(current_call_frame, size, offset).map_err(VMError::OutOfGas)?;

        self.increase_consumed_gas(current_call_frame, gas_cost)?;

        let value_bytes = current_call_frame.memory.load_range(offset, size)?;

        let mut hasher = Keccak256::new();
        hasher.update(value_bytes);
        let result = hasher.finalize();
        current_call_frame
            .stack
            .push(U256::from_big_endian(&result))?;

        Ok(OpcodeSuccess::Continue)
    }
```

```rust
let value_bytes = current_call_frame.memory.load_range(offset, size)?;
```

- Even when `size == 0`, the memory is unnecessarily extended up to the `offset`.
- This leads to unnecessary memory allocation.

For example:

- Input: `offset = 1`, `size = 0`.
- Expected memory after `KECCAK256`: No changes, `MSIZE` should return `0`.
- Actual memory after `KECCAK256`: Memory is extended to 32 bytes, and `MSIZE` incorrectly returns `32`.

### Recommendations

To align LEVM with expected EVM behavior:

1. Modify the `op_keccak256` implementation to check if `size = 0` before performing memory allocation.
2. Ensure that memory operations are skipped entirely when `size = 0`, and return the Keccak256 hash of an empty byte array.

## Step to reproduce

**Payload**

```rust
[88, 88, 32, 89]
PC
PC
KECCAK256
MSIZE
```

Add to [test](https://github.com/lambdaclass/ethrex/blob/5bbe595ff9e3900b2d84f075d3b19672a10854b9/crates/vm/levm/tests/edge_case_tests.rs) :

```rust
#[test]
fn test_non_compliance_keccak256() {
    let mut vm = new_vm_with_bytecode(Bytes::copy_from_slice(&[88, 88, 32, 89]))
    .unwrap();
    let mut current_call_frame = vm.call_frames.pop().unwrap();
    vm.execute(&mut current_call_frame);
    assert_eq!(current_call_frame.stack.stack[0], U256::from_str("0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470").unwrap());
    assert_eq!(current_call_frame.stack.stack[1], U256::zero());
}
```

# Backtrace

```rust
thread 'test_non_compliance_keccak256' panicked at crates/vm/levm/tests/edge_case_tests.rs:14:5:
assertion `left == right` failed
  left: 32
 right: 0
stack backtrace:
   0: rust_begin_unwind
             at /rustc/8adb4b30f40e6fbd21dc1ba26c3301c7eeb6de3c/library/std/src/panicking.rs:665:5
   1: core::panicking::panic_fmt
             at /rustc/8adb4b30f40e6fbd21dc1ba26c3301c7eeb6de3c/library/core/src/panicking.rs:76:14
   2: core::panicking::assert_failed_inner
   3: core::panicking::assert_failed
             at /home/mhoste/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/panicking.rs:373:5
   4: edge_case_tests::test_non_compliance_keccak256
             at ./tests/edge_case_tests.rs:14:5
   5: edge_case_tests::test_non_compliance_keccak256::{{closure}}
             at ./tests/edge_case_tests.rs:9:35
   6: core::ops::function::FnOnce::call_once
             at /home/mhoste/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5
   7: core::ops::function::FnOnce::call_once
             at /rustc/8adb4b30f40e6fbd21dc1ba26c3301c7eeb6de3c/library/core/src/ops/function.rs:250:5
```


Discussion:

Patch:
diff --git a/crates/vm/levm/src/opcode_handlers/keccak.rs b/crates/vm/levm/src/opcode_handlers/keccak.rs
index 77a11c4df3f..29cd2872a30 100644
--- a/crates/vm/levm/src/opcode_handlers/keccak.rs
+++ b/crates/vm/levm/src/opcode_handlers/keccak.rs
@@ -31,14 +31,17 @@ impl VM {
 
         self.increase_consumed_gas(current_call_frame, gas_cost)?;
 
-        let value_bytes = current_call_frame.memory.load_range(offset, size)?;
+        let value_bytes = if size == 0 {
+            vec![]
+        } else {
+            current_call_frame.memory.load_range(offset, size)?
+        };
 
         let mut hasher = Keccak256::new();
         hasher.update(value_bytes);
-        let result = hasher.finalize();
         current_call_frame
             .stack
-            .push(U256::from_big_endian(&result))?;
+            .push(U256::from_big_endian(&hasher.finalize()))?;
 
         Ok(OpcodeSuccess::Continue)
     }


Test patch:
diff --git a/crates/vm/levm/tests/edge_case_tests.rs b/crates/vm/levm/tests/edge_case_tests.rs
index 33845f3bdfd..13e8bd867a8 100644
--- a/crates/vm/levm/tests/edge_case_tests.rs
+++ b/crates/vm/levm/tests/edge_case_tests.rs
@@ -1,3 +1,5 @@
+use std::str::FromStr;
+
 use bytes::Bytes;
 use ethrex_core::U256;
 use ethrex_levm::{
@@ -103,6 +105,22 @@ fn test_is_negative() {
     vm.execute(&mut current_call_frame);
 }
 
+#[test]
+fn test_non_compliance_keccak256() {
+    let mut vm = new_vm_with_bytecode(Bytes::copy_from_slice(&[88, 88, 32, 89])).unwrap();
+    let mut current_call_frame = vm.call_frames.pop().unwrap();
+    vm.execute(&mut current_call_frame);
+    assert_eq!(
+        *current_call_frame.stack.stack.first().unwrap(),
+        U256::from_str("0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470")
+            .unwrap()
+    );
+    assert_eq!(
+        *current_call_frame.stack.stack.get(1).unwrap(),
+        U256::zero()
+    );
+}
+
 #[test]
 fn test_sdiv_zero_dividend_and_negative_divisor() {
     let mut vm = new_vm_with_bytecode(Bytes::copy_from_slice(&[