From 3206244def3e83d934b5af294072f288b2f704ef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 22:50:55 +0000 Subject: [PATCH 1/3] Initial plan From e949c92aae4ead9b89b669e326035b187a917b7f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 22:53:52 +0000 Subject: [PATCH 2/3] docs: add ADR-004 explaining why u32 is safe for EVM memory operations Co-authored-by: tcrypt25519 <212655132+tcrypt25519@users.noreply.github.com> --- crates/jet/src/builder/ops.rs | 4 + crates/jet_runtime/src/builtins.rs | 3 + crates/jet_runtime/src/exec.rs | 1 + docs/adrs/adr-004.md | 148 +++++++++++++++++++++++++++++ 4 files changed, 156 insertions(+) create mode 100644 docs/adrs/adr-004.md diff --git a/crates/jet/src/builder/ops.rs b/crates/jet/src/builder/ops.rs index f3cd54c..04c2f94 100644 --- a/crates/jet/src/builder/ops.rs +++ b/crates/jet/src/builder/ops.rs @@ -1003,6 +1003,10 @@ fn load_i8<'a>(bctx: &BuildCtx<'a, '_>, ptr: PointerValue<'a>) -> Result4GB memory +/// economically impossible. See docs/adrs/adr-004.md for full analysis. fn load_i32<'a>(bctx: &BuildCtx<'a, '_>, ptr: PointerValue<'a>) -> Result, Error> { let int = load_int(bctx, ptr, bctx.env.types().i32)?; Ok(int) diff --git a/crates/jet_runtime/src/builtins.rs b/crates/jet_runtime/src/builtins.rs index 6857f8d..c53e06c 100644 --- a/crates/jet_runtime/src/builtins.rs +++ b/crates/jet_runtime/src/builtins.rs @@ -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. diff --git a/crates/jet_runtime/src/exec.rs b/crates/jet_runtime/src/exec.rs index 8cf84e9..60cb795 100644 --- a/crates/jet_runtime/src/exec.rs +++ b/crates/jet_runtime/src/exec.rs @@ -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, diff --git a/docs/adrs/adr-004.md b/docs/adrs/adr-004.md new file mode 100644 index 0000000..5e1f80f --- /dev/null +++ b/docs/adrs/adr-004.md @@ -0,0 +1,148 @@ +# 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 used by: +- `MLOAD` / `MSTORE` / `MSTORE8` +- `KECCAK256` +- `CODECOPY` / `CALLDATACOPY` / `RETURNDATACOPY` +- `CREATE` / `CREATE2` +- `CALL` / `CALLCODE` / `DELEGATECALL` / `STATICCALL` +- `RETURN` / `REVERT` + +## 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: +- `crates/jet_runtime/src/builtins.rs`: `jet_mem_expand()` line 430: `checked_add` +- `crates/jet/src/builder/ops.rs`: `call_mem_expand_checked()` line 1043: return code checking + +## 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 From 0d0bcd6f2518da351c78d2a506aa48f3379ed4f1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 00:03:53 +0000 Subject: [PATCH 3/3] docs: remove line numbers and clarify implemented opcodes in ADR-004 Co-authored-by: tcrypt25519 <212655132+tcrypt25519@users.noreply.github.com> --- docs/adrs/adr-004.md | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/docs/adrs/adr-004.md b/docs/adrs/adr-004.md index 5e1f80f..206d93b 100644 --- a/docs/adrs/adr-004.md +++ b/docs/adrs/adr-004.md @@ -99,13 +99,19 @@ The safe pattern used throughout Jet's memory operations: - Builtins include defensive bounds checks - Only called after successful expansion -This pattern is used by: -- `MLOAD` / `MSTORE` / `MSTORE8` -- `KECCAK256` -- `CODECOPY` / `CALLDATACOPY` / `RETURNDATACOPY` -- `CREATE` / `CREATE2` -- `CALL` / `CALLCODE` / `DELEGATECALL` / `STATICCALL` -- `RETURN` / `REVERT` +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 @@ -136,9 +142,9 @@ Truncation occurs in these files: - `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: -- `crates/jet_runtime/src/builtins.rs`: `jet_mem_expand()` line 430: `checked_add` -- `crates/jet/src/builder/ops.rs`: `call_mem_expand_checked()` line 1043: return code checking +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