Skip to content

Commit

Permalink
fix(levm): prevent unsafe arithmetic (#1095)
Browse files Browse the repository at this point in the history
**Motivation**

The idea is to prevent arithmetic overflows and underflows in the levm
code.

**Description**

Design desitions:
- For the gas increment I used `checked_add` and added a
`ConsumedGasOverflow` error.
- For depth used `checked_add` and the `StackOverflow` error
- In the `VM::execute` method I decided to do not raise an error,
because the previous behavior was to do not raise errors, so used the
saturating functions
- For opcode operations used `InvalidOpcode` error

Closes #1075

---------

Co-authored-by: JereSalo <[email protected]>
Co-authored-by: Juani Medone <[email protected]>
Co-authored-by: Javier Chatruc <[email protected]>
Co-authored-by: Ivan Litteri <[email protected]>
Co-authored-by: ilitteri <[email protected]>
Co-authored-by: Jeremías Salomón <[email protected]>
  • Loading branch information
7 people authored Nov 14, 2024
1 parent 15cf7e2 commit 108163a
Show file tree
Hide file tree
Showing 24 changed files with 1,340 additions and 633 deletions.
5 changes: 5 additions & 0 deletions crates/vm/levm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,10 @@ as_conversions = "deny"
unwrap_used = "deny"
expect_used = "deny"

arithmetic_side_effects = "deny"
overflow_check_conditional = "warn"
manual_saturating_arithmetic = "warn"


[lib]
path = "./src/lib.rs"
16 changes: 12 additions & 4 deletions crates/vm/levm/src/account.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
use crate::{
constants::EMPTY_CODE_HASH,
errors::{InternalError, VMError},
};
use bytes::Bytes;
use ethereum_rust_core::{H256, U256};
use keccak_hash::keccak;
use std::collections::HashMap;

use crate::{constants::EMPTY_CODE_HASH, errors::VMError};

#[derive(Clone, Default, Debug, PartialEq, Eq)]
pub struct AccountInfo {
pub balance: U256,
Expand Down Expand Up @@ -88,7 +90,13 @@ impl Account {
self
}

pub fn increment_nonce(&mut self) {
self.info.nonce += 1;
// TODO: Replace nonce increments with this or cache's analog (currently does not have senders)
pub fn increment_nonce(&mut self) -> Result<(), VMError> {
self.info.nonce = self
.info
.nonce
.checked_add(1)
.ok_or(VMError::Internal(InternalError::NonceOverflowed))?;
Ok(())
}
}
25 changes: 17 additions & 8 deletions crates/vm/levm/src/call_frame.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
use crate::{constants::STACK_LIMIT, errors::VMError, memory::Memory, opcodes::Opcode};
use crate::{
constants::STACK_LIMIT,
errors::{InternalError, VMError},
memory::Memory,
opcodes::Opcode,
};
use bytes::Bytes;
use ethereum_rust_core::{types::Log, Address, U256};
use std::collections::HashMap;
Expand Down Expand Up @@ -114,18 +119,22 @@ impl CallFrame {
}
}

pub fn next_opcode(&mut self) -> Option<Opcode> {
pub fn next_opcode(&mut self) -> Result<Option<Opcode>, VMError> {
let opcode = self.opcode_at(self.pc);
self.increment_pc();
opcode
self.increment_pc()?;
Ok(opcode)
}

pub fn increment_pc_by(&mut self, count: usize) {
self.pc += count;
pub fn increment_pc_by(&mut self, count: usize) -> Result<(), VMError> {
self.pc = self
.pc
.checked_add(count)
.ok_or(VMError::Internal(InternalError::PCOverflowed))?;
Ok(())
}

pub fn increment_pc(&mut self) {
self.increment_pc_by(1);
pub fn increment_pc(&mut self) -> Result<(), VMError> {
self.increment_pc_by(1)
}

pub fn pc(&self) -> usize {
Expand Down
106 changes: 2 additions & 104 deletions crates/vm/levm/src/constants.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use crate::errors::{InternalError, VMError};
use ethereum_rust_core::{H256, U256};

pub const SUCCESS_FOR_CALL: i32 = 1;
Expand All @@ -8,101 +7,6 @@ pub const SUCCESS_FOR_RETURN: i32 = 1;
pub const REVERT_FOR_CREATE: i32 = 0;
pub const WORD_SIZE: usize = 32;

/// Contains the gas costs of the EVM instructions (in wei)
pub mod gas_cost {
use ethereum_rust_core::U256;

pub const ADD: U256 = U256([3, 0, 0, 0]);
pub const MUL: U256 = U256([5, 0, 0, 0]);
pub const SUB: U256 = U256([3, 0, 0, 0]);
pub const DIV: U256 = U256([5, 0, 0, 0]);
pub const SDIV: U256 = U256([5, 0, 0, 0]);
pub const MOD: U256 = U256([5, 0, 0, 0]);
pub const SMOD: U256 = U256([5, 0, 0, 0]);
pub const ADDMOD: U256 = U256([8, 0, 0, 0]);
pub const MULMOD: U256 = U256([8, 0, 0, 0]);
pub const EXP_STATIC: U256 = U256([10, 0, 0, 0]);
pub const EXP_DYNAMIC_BASE: U256 = U256([50, 0, 0, 0]);
pub const SIGNEXTEND: U256 = U256([5, 0, 0, 0]);
pub const LT: U256 = U256([3, 0, 0, 0]);
pub const GT: U256 = U256([3, 0, 0, 0]);
pub const SLT: U256 = U256([3, 0, 0, 0]);
pub const SGT: U256 = U256([3, 0, 0, 0]);
pub const EQ: U256 = U256([3, 0, 0, 0]);
pub const ISZERO: U256 = U256([3, 0, 0, 0]);
pub const AND: U256 = U256([3, 0, 0, 0]);
pub const OR: U256 = U256([3, 0, 0, 0]);
pub const XOR: U256 = U256([3, 0, 0, 0]);
pub const NOT: U256 = U256([3, 0, 0, 0]);
pub const BYTE: U256 = U256([3, 0, 0, 0]);
pub const SHL: U256 = U256([3, 0, 0, 0]);
pub const SHR: U256 = U256([3, 0, 0, 0]);
pub const SAR: U256 = U256([3, 0, 0, 0]);
pub const KECCAK25_STATIC: U256 = U256([30, 0, 0, 0]);
pub const KECCAK25_DYNAMIC_BASE: U256 = U256([6, 0, 0, 0]);
pub const CALLDATALOAD: U256 = U256([3, 0, 0, 0]);
pub const CALLDATASIZE: U256 = U256([2, 0, 0, 0]);
pub const CALLDATACOPY_STATIC: U256 = U256([3, 0, 0, 0]);
pub const CALLDATACOPY_DYNAMIC_BASE: U256 = U256([3, 0, 0, 0]);
pub const RETURNDATASIZE: U256 = U256([2, 0, 0, 0]);
pub const RETURNDATACOPY_STATIC: U256 = U256([3, 0, 0, 0]);
pub const RETURNDATACOPY_DYNAMIC_BASE: U256 = U256([3, 0, 0, 0]);
pub const ADDRESS: U256 = U256([2, 0, 0, 0]);
pub const ORIGIN: U256 = U256([2, 0, 0, 0]);
pub const CALLER: U256 = U256([2, 0, 0, 0]);
pub const BLOCKHASH: U256 = U256([20, 0, 0, 0]);
pub const COINBASE: U256 = U256([2, 0, 0, 0]);
pub const TIMESTAMP: U256 = U256([2, 0, 0, 0]);
pub const NUMBER: U256 = U256([2, 0, 0, 0]);
pub const PREVRANDAO: U256 = U256([2, 0, 0, 0]);
pub const GASLIMIT: U256 = U256([2, 0, 0, 0]);
pub const CHAINID: U256 = U256([2, 0, 0, 0]);
pub const SELFBALANCE: U256 = U256([5, 0, 0, 0]);
pub const BASEFEE: U256 = U256([2, 0, 0, 0]);
pub const BLOBHASH: U256 = U256([3, 0, 0, 0]);
pub const BLOBBASEFEE: U256 = U256([2, 0, 0, 0]);
pub const POP: U256 = U256([2, 0, 0, 0]);
pub const MLOAD_STATIC: U256 = U256([3, 0, 0, 0]);
pub const MSTORE_STATIC: U256 = U256([3, 0, 0, 0]);
pub const MSTORE8_STATIC: U256 = U256([3, 0, 0, 0]);
pub const JUMP: U256 = U256([8, 0, 0, 0]);
pub const JUMPI: U256 = U256([10, 0, 0, 0]);
pub const PC: U256 = U256([2, 0, 0, 0]);
pub const MSIZE: U256 = U256([2, 0, 0, 0]);
pub const GAS: U256 = U256([2, 0, 0, 0]);
pub const JUMPDEST: U256 = U256([1, 0, 0, 0]);
pub const TLOAD: U256 = U256([100, 0, 0, 0]);
pub const TSTORE: U256 = U256([100, 0, 0, 0]);
pub const MCOPY_STATIC: U256 = U256([3, 0, 0, 0]);
pub const MCOPY_DYNAMIC_BASE: U256 = U256([3, 0, 0, 0]);
pub const PUSH0: U256 = U256([2, 0, 0, 0]);
pub const PUSHN: U256 = U256([3, 0, 0, 0]);
pub const DUPN: U256 = U256([3, 0, 0, 0]);
pub const SWAPN: U256 = U256([3, 0, 0, 0]);
pub const LOGN_STATIC: U256 = U256([375, 0, 0, 0]);
pub const LOGN_DYNAMIC_BASE: U256 = U256([375, 0, 0, 0]);
pub const LOGN_DYNAMIC_BYTE_BASE: U256 = U256([8, 0, 0, 0]);
pub const CALLVALUE: U256 = U256([2, 0, 0, 0]);
pub const CODESIZE: U256 = U256([2, 0, 0, 0]);
pub const CODECOPY_STATIC: U256 = U256([3, 0, 0, 0]);
pub const CODECOPY_DYNAMIC_BASE: U256 = U256([3, 0, 0, 0]);
pub const GASPRICE: U256 = U256([2, 0, 0, 0]);
pub const EXTCODECOPY_DYNAMIC_BASE: U256 = U256([3, 0, 0, 0]);
pub const SELFDESTRUCT_STATIC: U256 = U256([5000, 0, 0, 0]);
pub const SELFDESTRUCT_DYNAMIC: U256 = U256([25000, 0, 0, 0]);
pub const COLD_ADDRESS_ACCESS_COST: U256 = U256([2600, 0, 0, 0]);
}

// Costs in gas for call opcodes (in wei)
pub mod call_opcode {
use ethereum_rust_core::U256;

pub const WARM_ADDRESS_ACCESS_COST: U256 = U256([100, 0, 0, 0]);
pub const COLD_ADDRESS_ACCESS_COST: U256 = U256([2600, 0, 0, 0]);
pub const NON_ZERO_VALUE_COST: U256 = U256([9000, 0, 0, 0]);
pub const BASIC_FALLBACK_FUNCTION_STIPEND: U256 = U256([2300, 0, 0, 0]);
pub const VALUE_TO_EMPTY_ACCOUNT_COST: U256 = U256([25000, 0, 0, 0]);
}
pub const STACK_LIMIT: usize = 1024;

pub const GAS_REFUND_DENOMINATOR: u64 = 5;
Expand All @@ -122,14 +26,6 @@ pub const MAX_CREATE_CODE_SIZE: usize = 2 * MAX_CODE_SIZE;

pub const INVALID_CONTRACT_PREFIX: u8 = 0xef;

// Costs in gas for init word and init code (in wei)
pub const INIT_WORD_COST: u64 = 2;

pub fn init_code_cost(init_code_length: usize) -> Result<u64, VMError> {
let length_u64 = u64::try_from(init_code_length)
.map_err(|_| VMError::Internal(InternalError::ConversionError))?;
Ok(INIT_WORD_COST * (length_u64 + 31) / 32)
}
pub mod create_opcode {
use ethereum_rust_core::U256;

Expand All @@ -148,6 +44,8 @@ pub const BLOB_BASE_FEE_UPDATE_FRACTION: U256 = U256([3338477, 0, 0, 0]);

// Storage constants
pub const COLD_STORAGE_ACCESS_COST: U256 = U256([2100, 0, 0, 0]);
pub const WARM_ADDRESS_ACCESS_COST: U256 = U256([100, 0, 0, 0]);
pub const BALANCE_COLD_ADDRESS_ACCESS_COST: U256 = U256([2600, 0, 0, 0]);

// Block constants
pub const LAST_AVAILABLE_BLOCK_LIMIT: U256 = U256([256, 0, 0, 0]);
Expand Down
10 changes: 8 additions & 2 deletions crates/vm/levm/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,16 @@ impl Cache {
Ok(())
}

pub fn increment_account_nonce(&mut self, address: &Address) {
// TODO: Replace nonce increments with this (currently does not have senders)
pub fn increment_account_nonce(&mut self, address: &Address) -> Result<(), VMError> {
if let Some(account) = self.accounts.get_mut(address) {
account.info.nonce += 1;
account.info.nonce = account
.info
.nonce
.checked_add(1)
.ok_or(VMError::Internal(InternalError::NonceOverflowed))?;
}
Ok(())
}

pub fn is_account_cached(&self, address: &Address) -> bool {
Expand Down
115 changes: 69 additions & 46 deletions crates/vm/levm/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,84 +2,113 @@ use crate::account::Account;
use bytes::Bytes;
use ethereum_rust_core::{types::Log, Address};
use std::collections::HashMap;
use thiserror;

/// Errors that halt the program
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum VMError {
#[error("Stack underflow")]
#[error("Stack Underflow")]
StackUnderflow,
#[error("Stack overflow")]
#[error("Stack Overflow")]
StackOverflow,
#[error("Invalid jump")]
#[error("Invalid Jump")]
InvalidJump,
#[error("Opcode not allowed in static context")]
#[error("Opcode Not Allowed In Static Context")]
OpcodeNotAllowedInStaticContext,
#[error("Opcode not found")]
#[error("Opcode Not Found")]
OpcodeNotFound,
#[error("Invalid bytecode")]
#[error("Invalid Bytecode")]
InvalidBytecode,
#[error("Out of gas")]
OutOfGas,
#[error("Very large number")]
#[error("Very Large Number")]
VeryLargeNumber,
#[error("Overflow in arithmetic operation")]
OverflowInArithmeticOp,
#[error("Fatal error")]
#[error("Fatal Error")]
FatalError,
#[error("Invalid transaction")]
#[error("Invalid Transaction")]
InvalidTransaction,
#[error("Revert opcode")]
#[error("Revert Opcode")]
RevertOpcode,
#[error("Invalid opcode")]
#[error("Invalid Opcode")]
InvalidOpcode,
#[error("Missing blob hashes")]
#[error("Missing Blob Hashes")]
MissingBlobHashes,
#[error("Blob hash index out of bounds")]
#[error("Blob Hash Index Out Of Bounds")]
BlobHashIndexOutOfBounds,
#[error("Sender account does not exist")]
#[error("Sender Account Does Not Exist")]
SenderAccountDoesNotExist,
#[error("Address does not match an account")]
#[error("Address Does Not Match An Account")]
AddressDoesNotMatchAnAccount,
#[error("Sender account should not have bytecode")]
#[error("Sender Account Should Not Have Bytecode")]
SenderAccountShouldNotHaveBytecode,
#[error("Sender balance should contain transfer value")]
#[error("Sender Balance Should Contain Transfer Value")]
SenderBalanceShouldContainTransferValue,
#[error("Gas price is lower than base fee")]
#[error("Gas Price Is Lower Than Base Fee")]
GasPriceIsLowerThanBaseFee,
#[error("Address already occupied")]
#[error("Address Already Occupied")]
AddressAlreadyOccupied,
#[error("Contract output too big")]
#[error("Contract Output Too Big")]
ContractOutputTooBig,
#[error("Invalid initial byte")]
#[error("Invalid Initial Byte")]
InvalidInitialByte,
#[error("Nonce overflow")]
NonceOverflow,
#[error("Memory load out of bounds")]
#[error("Memory Load Out Of Bounds")]
MemoryLoadOutOfBounds,
#[error("Memory store out of bounds")]
#[error("Memory Store Out Of Bounds")]
MemoryStoreOutOfBounds,
#[error("Gas limit price product overflow")]
GasLimitPriceProductOverflow,
#[error("Balance Overflow")]
BalanceOverflow,
#[error("Balance Underflow")]
BalanceUnderflow,
#[error("Gas refunds underflow")]
GasRefundsUnderflow,
#[error("Gas refunds overflow")]
GasRefundsOverflow,
// OutOfGas
#[error("Out Of Gas")]
OutOfGas(#[from] OutOfGasError),
// Internal
#[error("Internal error: {0}")]
Internal(#[from] InternalError),
#[error("Data size overflow")]
DataSizeOverflow,
#[error("Gas cost overflow")]
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
pub enum OutOfGasError {
#[error("Gas Cost Overflow")]
GasCostOverflow,
#[error("Offset overflow")]
OffsetOverflow,
#[error("Creation cost is too high")]
#[error("Gas Used Overflow")]
GasUsedOverflow,
#[error("Creation Cost Is Too High")]
CreationCostIsTooHigh,
#[error("Max gas limit exceeded")]
#[error("Consumed Gas Overflow")]
ConsumedGasOverflow,
#[error("Max Gas Limit Exceeded")]
MaxGasLimitExceeded,
#[error("Gas refunds underflow")]
GasRefundsUnderflow,
#[error("Gas refunds overflow")]
GasRefundsOverflow,
#[error("Arithmetic operation divided by zero in gas calculation")]
ArithmeticOperationDividedByZero,
}

#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum InternalError {
#[error("Overflowed when incrementing nonce")]
NonceOverflowed,
#[error("Underflowed when incrementing nonce")]
NonceUnderflowed,
#[error("Overflowed when incrementing program counter")]
PCOverflowed,
#[error("Underflowed when decrementing program counter")]
PCUnderflowed,
#[error("Arithmetic operation overflowed")]
ArithmeticOperationOverflow,
#[error("Arithmetic operation underflowed")]
ArithmeticOperationUnderflow,
#[error("Arithmetic operation divided by zero")]
ArithmeticOperationDividedByZero,
#[error("Accound should have been cached")]
AccountShouldHaveBeenCached,
#[error("Tried to convert one type to another")]
ConversionError,
#[error("Division error")]
DivisionError,
#[error("Tried to access last call frame but found none")]
CouldNotAccessLastCallframe, // Last callframe before execution is the same as the first, but after execution the last callframe is actually the initial CF
#[error("Tried to read from empty code")]
Expand All @@ -98,12 +127,6 @@ pub enum InternalError {
ExcessBlobGasShouldNotBeNone,
#[error("Error in utils file")]
UtilsError,
#[error("Accound should have been cached")]
AccountShouldHaveBeenCached,
#[error("Tried to convert one type to another")]
ConversionError,
#[error("Division error")]
DivisionError,
}

#[derive(Debug, Clone)]
Expand Down
Loading

0 comments on commit 108163a

Please sign in to comment.