lambdaclass / lambda_ethereum_rust

Lambda Ethereum Rust Execution client
Apache License 2.0
174 stars 21 forks source link

[SECURITY] Attempt to add with overflow in MCOPY opcode #1156

Closed mhoste51 closed 1 day ago

mhoste51 commented 3 days ago

Our team at FuzzingLabs discovered a bug in the op_mcopy function, this bug can lead to a panic when calculating words_copied.

Root cause

    // MCOPY operation
    pub fn op_mcopy(
        &mut self,
        current_call_frame: &mut CallFrame,
    ) -> Result<OpcodeSuccess, VMError> {
        let dest_offset = current_call_frame
            .stack
            .pop()?
            .try_into()
            .unwrap_or(usize::MAX);
        let src_offset: usize = current_call_frame
            .stack
            .pop()?
            .try_into()
            .unwrap_or(usize::MAX);
        let size: usize = current_call_frame
            .stack
            .pop()?
            .try_into()
            .unwrap_or(usize::MAX);

        let words_copied = (size + WORD_SIZE - 1) / WORD_SIZE;

        let memory_byte_size = src_offset
            .checked_add(size)
            .and_then(|src_sum| {
                dest_offset
                    .checked_add(size)
                    .map(|dest_sum| src_sum.max(dest_sum))
            })
            .ok_or(VMError::OverflowInArithmeticOp)?;

        let memory_expansion_cost = current_call_frame.memory.expansion_cost(memory_byte_size)?;
        let gas_cost = gas_cost::MCOPY_STATIC
            + gas_cost::MCOPY_DYNAMIC_BASE * words_copied
            + memory_expansion_cost;

        self.increase_consumed_gas(current_call_frame, gas_cost)?;

        if size > 0 {
            current_call_frame
                .memory
                .copy(src_offset, dest_offset, size)?;
        }

        Ok(OpcodeSuccess::Continue)
    }

let words_copied = (size + WORD_SIZE - 1) / WORD_SIZE; We can controlsizeand give a value equal tousize::MAX. SinceWORD_SIZE = 32, when we attempt to addusize::MAX + 32, we encounter an overflow because words_copied is also ausize.

Step to reproduce

Payload

[90, 90, 90, 94]

GAS
GAS
GAS
MCOPY

Add to test :

#[test]
fn test_overflow_mcopy() {
    let mut vm = new_vm_with_bytecode(Bytes::copy_from_slice(&[90, 90, 90, 94]));
    let mut current_call_frame = vm.call_frames.pop().unwrap();
    vm.execute(&mut current_call_frame);
}

Backtrace

---- tests::test_overflow_mcopy stdout ----
thread 'tests::test_overflow_mcopy' panicked at crates/vm/levm/./src/opcode_handlers/stack_memory_storage_flow.rs:273:29:
attempt to add with overflow
stack backtrace:
   0: rust_begin_unwind
             at /rustc/59e2c01c2217a01546222e4d9ff4e6695ee8a1db/library/std/src/panicking.rs:658:5
   1: core::panicking::panic_fmt
             at /rustc/59e2c01c2217a01546222e4d9ff4e6695ee8a1db/library/core/src/panicking.rs:74:14
   2: core::panicking::panic_const::panic_const_add_overflow
             at /rustc/59e2c01c2217a01546222e4d9ff4e6695ee8a1db/library/core/src/panicking.rs:181:21
   3: ethereum_rust_levm::opcode_handlers::stack_memory_storage_flow::<impl ethereum_rust_levm::vm::VM>::op_mcopy
             at ./src/opcode_handlers/stack_memory_storage_flow.rs:273:29
   4: ethereum_rust_levm::vm::VM::execute
             at ./src/vm.rs:222:34
   5: lib::tests::test_overflow_mcopy
             at ./tests/tests.rs:50:5
   6: lib::tests::test_overflow_mcopy::{{closure}}
             at ./tests/tests.rs:47:25
   7: core::ops::function::FnOnce::call_once
             at /rustc/59e2c01c2217a01546222e4d9ff4e6695ee8a1db/library/core/src/ops/function.rs:250:5
   8: core::ops::function::FnOnce::call_once
             at /rustc/59e2c01c2217a01546222e4d9ff4e6695ee8a1db/library/core/src/ops/function.rs:250:5
ilitteri commented 1 day ago

Thanks a lot for filing this! We've solved it at https://github.com/lambdaclass/lambda_ethereum_rust/issues/1110.

running 1 test
test test_overflow_mcopy ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 134 filtered out; finished in 0.00s