Repository: lambdaclass/ethrex (Rust)

Issue:
[SECURITY] Non-Compliance : Unnecessary Memory Allocation in LOGX Opcode Implementation
Our team at [FuzzingLabs](https://github.com/FuzzingLabs) has identified a non-compliance issue in the current implementation of the [LOGX](https://github.com/lambdaclass/ethrex/blob/main/crates/vm/levm/src/opcode_handlers/logging.rs#L16) opcode. Specifically, during the execution of the `LOGX` opcode (e.g., `LOG0`), **levm** unnecessarily allocates memory even when the size of the data to be logged is zero. This behavior results in an incorrect update to the `MSIZE` opcode value and deviates from expected EVM behavior as seen in other implementations such as **revm** or **geth**.

## Root cause

```rust
pub fn op_log(
        &mut self,
        current_call_frame: &mut CallFrame,
        op: Opcode,
    ) -> Result<OpcodeSuccess, VMError> {
        if current_call_frame.is_static {
            return Err(VMError::OpcodeNotAllowedInStaticContext);
        }

        let number_of_topics = (u8::from(op))
            .checked_sub(u8::from(Opcode::LOG0))
            .ok_or(VMError::InvalidOpcode)?;

        let offset: usize = current_call_frame
            .stack
            .pop()?
            .try_into()
            .map_err(|_| VMError::VeryLargeNumber)?;
        let size = current_call_frame
            .stack
            .pop()?
            .try_into()
            .map_err(|_| VMError::VeryLargeNumber)?;
        let mut topics = Vec::new();
        for _ in 0..number_of_topics {
            let topic = current_call_frame.stack.pop()?;
            let mut topic_bytes = [0u8; 32];
            topic.to_big_endian(&mut topic_bytes);
            topics.push(H256::from_slice(&topic_bytes));
        }

        let gas_cost = gas_cost::log(current_call_frame, size, offset, number_of_topics)
            .map_err(VMError::OutOfGas)?;

        self.increase_consumed_gas(current_call_frame, gas_cost)?;

        let data = current_call_frame.memory.load_range(offset, size)?;
        let log = Log {
            address: current_call_frame.msg_sender, // Should change the addr if we are on a Call/Create transaction (Call should be the contract we are calling, Create should be the original caller)
            topics,
            data: Bytes::from(data),
        };
        current_call_frame.logs.push(log);

        Ok(OpcodeSuccess::Continue)
    }
```

The issue lies in the `op_log` function, where no check is performed for `size = 0` before attempting to allocate memory. Specifically:

```rust
let data = current_call_frame.memory.load_range(offset, size)?;
```

This line attempts to load data from memory even when `size = 0`, triggering unnecessary memory allocation at the specified offset.

## Step to reproduce

**Payload**

```rust
[95, 97, 89, 0, 160, 89]

PUSH0
PUSH2 0x89
LOG0
MSIZE
```

Add to [test](https://github.com/lambdaclass/ethrex/blob/main/crates/vm/levm/tests/edge_case_tests.rs) :

```rust
#[test]
fn test_non_compliance_log() {
    let mut vm = new_vm_with_bytecode(Bytes::copy_from_slice(&[95, 97, 89, 0, 160, 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::zero());
}
```

## Backtrace

```rust
thread 'test_non_compliance_log' panicked at crates/vm/levm/tests/edge_case_tests.rs:18:5:
assertion `left == right` failed
  left: 22784
 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_log
             at ./tests/edge_case_tests.rs:18:5
   5: edge_case_tests::test_non_compliance_log::{{closure}}
             at ./tests/edge_case_tests.rs:12:29
   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
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

failures:
    test_non_compliance_log

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 22 filtered out; finished in 0.12s
```


Discussion:

Patch:
diff --git a/crates/vm/levm/src/memory.rs b/crates/vm/levm/src/memory.rs
index 995990ca82b..0baf3ba2162 100644
--- a/crates/vm/levm/src/memory.rs
+++ b/crates/vm/levm/src/memory.rs
@@ -66,6 +66,10 @@ impl Memory {
     }
 
     pub fn load_range(&mut self, offset: usize, size: usize) -> Result<Vec<u8>, VMError> {
+        if size == 0 {
+            return Ok(Vec::new());
+        }
+
         let size_to_load = offset.checked_add(size).ok_or(VMError::Internal(
             InternalError::ArithmeticOperationOverflow,
         ))?;


Test patch:
diff --git a/crates/vm/levm/tests/edge_case_tests.rs b/crates/vm/levm/tests/edge_case_tests.rs
index 50a5bdade66..57198efc18c 100644
--- a/crates/vm/levm/tests/edge_case_tests.rs
+++ b/crates/vm/levm/tests/edge_case_tests.rs
@@ -251,3 +251,14 @@ fn test_non_compliance_extcodecopy_size_and_destoffset() {
         &U256::from(64)
     );
 }
+
+#[test]
+fn test_non_compliance_log() {
+    let mut vm = new_vm_with_bytecode(Bytes::copy_from_slice(&[95, 97, 89, 0, 160, 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::zero()
+    );
+}