From 6acc8fadeab905b07e2cdee05fb1069bea04f355 Mon Sep 17 00:00:00 2001 From: cl507523 Date: Tue, 14 Apr 2026 11:47:43 +0000 Subject: [PATCH] fix(evm): enforce EIP-211 bounds in Rust RETURNDATACOPY Reject RETURNDATACOPY when offset+size exceeds return-data length (or overflows) to match interpreter semantics, and add an OOB regression case for invalid memory access. Made-with: Cursor --- rust_crate/src/evm/host_functions/control.rs | 91 ++++++++++++++++---- tests/evm_asm/returndatacopy_oob.easm | 28 ++++++ tests/evm_asm/returndatacopy_oob.expected | 8 ++ 3 files changed, 111 insertions(+), 16 deletions(-) create mode 100644 tests/evm_asm/returndatacopy_oob.easm create mode 100644 tests/evm_asm/returndatacopy_oob.expected diff --git a/rust_crate/src/evm/host_functions/control.rs b/rust_crate/src/evm/host_functions/control.rs index 205ee34c6..b36efbed8 100644 --- a/rust_crate/src/evm/host_functions/control.rs +++ b/rust_crate/src/evm/host_functions/control.rs @@ -147,6 +147,34 @@ where return_data_size } +fn validate_returndata_copy_bounds( + return_data_len: usize, + data_offset: u32, + length: u32, +) -> HostFunctionResult<(usize, usize)> { + let data_offset_usize = data_offset as usize; + let length_usize = length as usize; + let data_end = data_offset_usize.checked_add(length_usize).ok_or_else(|| { + crate::evm::error::out_of_bounds_error_with_function( + data_offset, + length, + "return_data_copy: return data offset overflow", + "return_data_copy", + ) + })?; + + if data_end > return_data_len { + return Err(crate::evm::error::out_of_bounds_error_with_function( + data_offset, + length, + "return_data_copy: return data out of bounds", + "return_data_copy", + )); + } + + Ok((data_offset_usize, data_end)) +} + /// Copy return data from the last call to memory /// Copies the return data from the last external call to the specified memory location /// @@ -184,26 +212,57 @@ where // Get the return data from the evmhost let return_data = evmhost.return_data_copy(); - let data_offset_usize = data_offset as usize; + let data_offset_u32 = data_offset as u32; + let (data_offset_usize, data_end) = + validate_returndata_copy_bounds(return_data.len(), data_offset_u32, length_u32)?; - // Prepare buffer for copying - let mut buffer = vec![0u8; length_u32 as usize]; + // Write the buffer to memory + memory.write_bytes(result_offset_u32, &return_data[data_offset_usize..data_end])?; - // Copy from return data with bounds checking - let available_bytes = if data_offset_usize < return_data.len() { - return_data.len() - data_offset_usize - } else { - 0 - }; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::validate_returndata_copy_bounds; - let copy_len = std::cmp::min(available_bytes, length_u32 as usize); - if copy_len > 0 { - buffer[..copy_len] - .copy_from_slice(&return_data[data_offset_usize..data_offset_usize + copy_len]); + #[test] + fn test_validate_returndata_copy_bounds_exact_end_ok() { + let (start, end) = + validate_returndata_copy_bounds(4, 2, 2).expect("bounds should be valid"); + assert_eq!((start, end), (2, 4)); } - // Write the buffer to memory - memory.write_bytes(result_offset_u32, &buffer)?; + #[test] + fn test_validate_returndata_copy_bounds_oob_rejected() { + let err = validate_returndata_copy_bounds(4, 3, 2).expect_err("must fail"); + assert_eq!(err.category(), "memory"); + assert!( + err.message().contains("out of bounds"), + "unexpected error: {}", + err + ); + } - Ok(()) + #[test] + fn test_validate_returndata_copy_bounds_overflow_rejected() { + let err = validate_returndata_copy_bounds(4, u32::MAX, 2).expect_err("must fail"); + assert_eq!(err.category(), "memory"); + assert!( + err.message().contains("offset overflow"), + "unexpected error: {}", + err + ); + } + + #[test] + fn test_validate_returndata_copy_bounds_zero_length_past_end_rejected() { + let err = validate_returndata_copy_bounds(4, 5, 0).expect_err("must fail"); + assert_eq!(err.category(), "memory"); + assert!( + err.message().contains("out of bounds"), + "unexpected error: {}", + err + ); + } } diff --git a/tests/evm_asm/returndatacopy_oob.easm b/tests/evm_asm/returndatacopy_oob.easm new file mode 100644 index 000000000..33b6c3d84 --- /dev/null +++ b/tests/evm_asm/returndatacopy_oob.easm @@ -0,0 +1,28 @@ +// RETURNDATACOPY out-of-bounds must fail per EIP-211. +// Build a 1-byte return buffer via identity precompile (0x04), +// then request offset=1, size=1 (1 + 1 > 1) to trigger failure. + +// input[0] = 0x42 +PUSH1 0x42 +PUSH1 0x00 +MSTORE8 + +// CALL identity precompile with 1-byte input and 32-byte output area. +// Stack order for CALL: gas, addr, value, argsOffset, argsSize, retOffset, retSize +PUSH1 0x20 +PUSH1 0x80 +PUSH1 0x01 +PUSH1 0x00 +PUSH1 0x00 +PUSH1 0x04 +PUSH4 0x100000 +CALL +POP + +// RETURNDATACOPY(dst=0, offset=1, size=1) -> out-of-bounds +PUSH1 0x01 +PUSH1 0x01 +PUSH1 0x00 +RETURNDATACOPY + +STOP diff --git a/tests/evm_asm/returndatacopy_oob.expected b/tests/evm_asm/returndatacopy_oob.expected new file mode 100644 index 000000000..d3727d4fc --- /dev/null +++ b/tests/evm_asm/returndatacopy_oob.expected @@ -0,0 +1,8 @@ +status: invalid memory access +error_code: 9 +stack: [] +memory: '' +storage: {} +transient_storage: {} +return: '' +events: []