Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/jet/src/builder/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1003,6 +1003,10 @@ fn load_i8<'a>(bctx: &BuildCtx<'a, '_>, ptr: PointerValue<'a>) -> Result<IntValu
Ok(int)
}

/// Truncates an EVM word (i256) to i32.
///
/// This is safe for memory operations because EVM gas costs make >4GB memory
/// economically impossible. See docs/adrs/adr-004.md for full analysis.
fn load_i32<'a>(bctx: &BuildCtx<'a, '_>, ptr: PointerValue<'a>) -> Result<IntValue<'a>, Error> {
let int = load_int(bctx, ptr, bctx.env.types().i32)?;
Ok(int)
Expand Down
3 changes: 3 additions & 0 deletions crates/jet_runtime/src/builtins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,9 @@ enum MemoryExpansionError {
/// Expands memory to accommodate an access at offset with given size.
/// Follows EVM semantics: rounds up to 32-byte boundaries and updates memory_len.
///
/// Uses u32 for offset/size because EVM gas costs make >4GB memory economically
/// impossible. See docs/adrs/adr-004.md for full analysis.
///
/// # Safety
///
/// This function is unsafe because it dereferences the given pointer and may reallocate memory.
Expand Down
1 change: 1 addition & 0 deletions crates/jet_runtime/src/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pub struct Context {
stack: [Word; STACK_SIZE_WORDS as usize],

// Pointer-based memory layout as per ADR-002
// u32 is safe for offsets/sizes, see ADR-004 (EVM gas costs prevent >4GB)
pub(crate) memory_ptr: *mut u8,
pub(crate) memory_len: u32,
pub(crate) memory_cap: u32,
Expand Down
154 changes: 154 additions & 0 deletions docs/adrs/adr-004.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# ADR 004: u32 is Safe for EVM Memory Offsets and Sizes

```
status: accepted
project: jet
area:
- runtime
- memory-model
- security
```

## Context

The EVM operates on 256-bit words for all stack values, including memory offsets and sizes passed to opcodes like `MLOAD`, `MSTORE`, `KECCAK256`, etc. However, in Jet's implementation, we truncate these 256-bit values to 32-bit integers (u32) when performing actual memory operations.

This truncation raises two potential security concerns:

1. **Incorrect Memory Access**: Truncating offset/size from 256-bit to 32-bit could cause operations on the wrong memory region (if the high bits were non-zero, truncation would silently discard them).

2. **Bounds Check Bypass**: When computing `end = offset + size`, overflow from saturating or wrapping arithmetic could allow the sum to pass bounds checks while the actual memory access reads beyond allocated bounds.

## Decision

**We affirm that u32 is safe and sufficient for EVM memory offsets and sizes.**

All memory operations will continue to truncate 256-bit EVM stack values to u32 for:
- Memory offsets
- Memory sizes
- Memory expansion calculations

## Rationale

### Gas Costs Make >4GB Memory Economically Impossible

The EVM charges quadratic gas costs for memory expansion:

```
gas_cost = 3 × words + (words² ÷ 512)
where words = ⌈bytes ÷ 32⌉
```

Calculating actual costs at realistic gas prices ($3500/ETH, 30 Gwei):

| Memory Size | Gas Cost | USD Cost |
|-------------|----------------------|----------------------|
| 1 MB | 2,195,456 | $231 |
| 100 MB | 20,981,350,400 | $2.2 million |
| 1 GB | 2,199,123,918,848 | $231 million |
| 4 GB | 35,184,774,742,016 | $3.7 billion |

**u32::MAX = 4,294,967,295 bytes ≈ 4 GB**

Even at 1 GB, the cost is $231 million. Reaching the 4 GB limit (needed to overflow u32) would cost approximately **$3.7 billion** in gas fees at current market rates.

### Economic Security Boundary

This quadratic cost function creates an **economic security boundary** that makes attacks via large memory allocations infeasible:

- No rational attacker would spend billions of dollars to trigger u32 overflow
- Even if gas prices dropped 1000×, the cost would still be millions of dollars
- The EVM gas limit per block (~30 million gas) is far below what's needed for even 100 MB

### Arithmetic Overflow is Checked

The second concern (bounds check bypass via overflow) is prevented by defense-in-depth:

```rust
// In jet_mem_expand (called before any memory access):
let end_offset = match offset.checked_add(size) {
Some(end) => end,
None => return MemoryExpansionError::ArithmeticOverflow as i8,
};
```

This `checked_add` happens **before** `jet_mem_expand` allocates memory and **before** any builtin (like `jet_ops_keccak256`) accesses memory. If `offset + size` would overflow u32, the expansion fails with an error code that is checked by the caller, which returns `ReturnCode::Invalid`.

```rust
// In jet_ops_keccak256 (defensive check):
let start = offset as usize;
let end = start.saturating_add(size as usize);

if end > memory_len {
return -1;
}
```

The `saturating_add` in the builtin is a **defensive secondary check**, not the primary protection. By the time `jet_ops_keccak256` is called, `jet_mem_expand` has already validated that `offset + size` does not overflow and that memory is expanded to cover the range.

### Implementation Pattern

The safe pattern used throughout Jet's memory operations:

1. **Truncate** 256-bit stack value to u32 (via `load_i32`)
2. **Validate** via `call_mem_expand_checked` which calls `jet_mem_expand`
- Uses `checked_add` to detect overflow
- Returns error code on overflow or allocation failure
- Caller checks return code and branches to error handling
3. **Access** memory via builtins that operate on u32 offsets/sizes
- Builtins include defensive bounds checks
- Only called after successful expansion

This pattern is currently used by these implemented opcodes:
- `MLOAD` / `MSTORE` / `MSTORE8` — read/write 32-byte or 1-byte values
- `KECCAK256` — hash memory range

Additional implemented opcodes that use `load_i32` truncation:
- `RETURNDATACOPY` — copy return data to memory (calls `jet_contract_call_return_data_copy`)
- `RETURN` — return from contract execution with memory data

Future opcodes that must follow this pattern when implemented:
- `CODECOPY` / `CALLDATACOPY` — copy code/calldata to memory
- `CREATE` / `CREATE2` — create contract from memory
- `CALLCODE` / `DELEGATECALL` / `STATICCALL` — additional call variants with memory parameters
- `REVERT` — revert with memory data

## Consequences

### Positive

- **Performance**: u32 arithmetic is faster than u64 on many architectures
- **Memory Efficiency**: Smaller context struct size (u32 vs u64 for `memory_len`, `memory_cap`)
- **Correctness**: Economic security boundary is stronger than any technical checks we could implement
- **Simplicity**: No need for complex 64-bit or arbitrary-precision memory addressing

### Negative

- **Non-obvious Safety**: Developers unfamiliar with EVM gas economics might incorrectly assume u32 truncation is a security issue
- **Future-proofing**: If Ethereum fundamentally changes its gas model (unlikely due to backward compatibility requirements), this assumption could break

### Mitigations

1. **Documentation**: This ADR serves as canonical documentation for why u32 is safe
2. **Comments**: Add references to this ADR at truncation sites
3. **Defensive Checks**: Keep secondary bounds checks in builtins as defense-in-depth
4. **Tests**: Include tests that verify behavior with maximum reasonable memory sizes

## Code Locations

Truncation occurs in these files:

- `crates/jet/src/builder/ops.rs`: `load_i32()` function used by all memory opcodes
- `crates/jet_runtime/src/builtins.rs`: `jet_mem_expand()` signature uses `u32`
- `crates/jet_runtime/src/exec.rs`: `Context` struct fields `memory_len: u32`, `memory_cap: u32`

Key validation functions:
- `crates/jet_runtime/src/builtins.rs`: `jet_mem_expand()` uses `checked_add` for overflow detection
- `crates/jet/src/builder/ops.rs`: `call_mem_expand_checked()` validates return codes and branches on error

## Related

- ADR-001: EVM Word Representation and Runtime Implementation Strategy
- ADR-002: Pointer-Based Memory Representation in exec_ctx
- EVM Yellow Paper: Gas costs for memory expansion
- Ethereum's TANGERINE WHISTLE hard fork: Introduced quadratic memory costs