From a1a4f8e6ea836c2d2e6535b38d8bf61bdc439ff7 Mon Sep 17 00:00:00 2001 From: xermicus Date: Tue, 13 Jan 2026 10:15:31 +0100 Subject: [PATCH] Clip OOB values in call data load and copy (#448) The OOB logic is implemented in the runtime but truncation circumvented it. Closes #263 --------- Signed-off-by: xermicus Signed-off-by: Cyrill Leutwiler --- CHANGELOG.md | 3 ++ crates/integration/codesize.json | 16 +++++----- crates/integration/contracts/MemoryBounds.sol | 31 +++++++++++++++++++ .../llvm-context/src/polkavm/context/mod.rs | 22 +++++++++++++ .../llvm-context/src/polkavm/evm/calldata.rs | 8 ++--- 5 files changed, 68 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c9d030..b3a2984 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ This is a development pre-release. Supported `polkadot-sdk` rev: `unstable2507` +### Fixed +- OOB access in `calldataload` and `calldatacopy` should always produce zero values instead of consuming all gas. + ## v0.6.0 This is a development pre-release. diff --git a/crates/integration/codesize.json b/crates/integration/codesize.json index 4b0a406..9933ef0 100644 --- a/crates/integration/codesize.json +++ b/crates/integration/codesize.json @@ -1,10 +1,10 @@ { - "Baseline": 911, - "Computation": 2337, - "DivisionArithmetics": 14488, - "ERC20": 17189, - "Events": 1672, - "FibonacciIterative": 1454, - "Flipper": 2106, - "SHA1": 7814 + "Baseline": 864, + "Computation": 2323, + "DivisionArithmetics": 14475, + "ERC20": 17229, + "Events": 1663, + "FibonacciIterative": 1421, + "Flipper": 2154, + "SHA1": 7751 } \ No newline at end of file diff --git a/crates/integration/contracts/MemoryBounds.sol b/crates/integration/contracts/MemoryBounds.sol index 3b094bc..5726c04 100644 --- a/crates/integration/contracts/MemoryBounds.sol +++ b/crates/integration/contracts/MemoryBounds.sol @@ -21,12 +21,43 @@ pragma solidity ^0.8.24; "Instantiated": 0 } } + }, + { + "Call": { + "dest": { + "Instantiated": 0 + }, + "data": "489eb33e00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001a300000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "Call": { + "dest": { + "Instantiated": 0 + }, + "data": "db1ff7bb00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001a300000000000000000000000000000000000000000000000000000000000000" + } } ] } */ contract MemoryBounds { + function oobLoad(bytes memory data) public pure returns (uint256, bytes memory) { + uint256 result1; + assembly { + let ptr := mload(add(data, 0x20)) + result1 := calldataload(ptr) + } + return (result1, data); + } + + function oobCopy(bytes memory data) public pure returns (uint256, bytes memory) { + assembly { + calldatacopy(0, 0x3a0000000000000000000000000000000000, 0x20) + } + } + fallback() external { assembly { // Accessing OOB offsets should always work when the length is 0. diff --git a/crates/llvm-context/src/polkavm/context/mod.rs b/crates/llvm-context/src/polkavm/context/mod.rs index 3730227..9d3131a 100644 --- a/crates/llvm-context/src/polkavm/context/mod.rs +++ b/crates/llvm-context/src/polkavm/context/mod.rs @@ -1054,6 +1054,28 @@ impl<'ctx> Context<'ctx> { .into_int_value()) } + /// Clip a memory offset to the maximum value that fits into a register. + pub fn clip_to_xlen( + &self, + value: inkwell::values::IntValue<'ctx>, + ) -> anyhow::Result> { + let clipped = self.xlen_type().const_all_ones(); + let is_overflow = self.builder().build_int_compare( + inkwell::IntPredicate::UGT, + value, + self.builder() + .build_int_z_extend(clipped, self.word_type(), "value_clipped")?, + "is_value_overflow", + )?; + let truncated = + self.builder() + .build_int_truncate(value, self.xlen_type(), "value_truncated")?; + Ok(self + .builder() + .build_select(is_overflow, clipped, truncated, "value")? + .into_int_value()) + } + /// Build a call to PolkaVM `sbrk` for extending the heap from offset by `size`. /// The allocation is aligned to 32 bytes. /// diff --git a/crates/llvm-context/src/polkavm/evm/calldata.rs b/crates/llvm-context/src/polkavm/evm/calldata.rs index 1ac093e..c3bef98 100644 --- a/crates/llvm-context/src/polkavm/evm/calldata.rs +++ b/crates/llvm-context/src/polkavm/evm/calldata.rs @@ -8,7 +8,7 @@ pub fn load<'ctx>( offset: inkwell::values::IntValue<'ctx>, ) -> anyhow::Result> { let output_pointer = context.build_alloca_at_entry(context.word_type(), "call_data_output"); - let offset = context.safe_truncate_int_to_xlen(offset)?; + let offset = context.clip_to_xlen(offset)?; context.build_runtime_call( revive_runtime_api::polkavm_imports::CALL_DATA_LOAD, @@ -40,9 +40,9 @@ pub fn copy<'ctx>( source_offset: inkwell::values::IntValue<'ctx>, size: inkwell::values::IntValue<'ctx>, ) -> anyhow::Result<()> { - let source_offset = context.safe_truncate_int_to_xlen(source_offset)?; - let size = context.safe_truncate_int_to_xlen(size)?; - let destination_offset = context.safe_truncate_int_to_xlen(destination_offset)?; + let source_offset = context.clip_to_xlen(source_offset)?; + let size = context.clip_to_xlen(size)?; + let destination_offset = context.clip_to_xlen(destination_offset)?; let output_pointer = context.build_heap_gep(destination_offset, size)?; context.build_runtime_call(