|
| 1 | +# Security Audit Report — grevm (Round 3) |
| 2 | + |
| 3 | +**Date:** 2026-03-05 |
| 4 | +**Scope:** All Rust source in `src/` (~3000 LOC) |
| 5 | +**Language:** Rust | **Framework:** revm v29.0.1-gravity fork, Block-STM |
| 6 | +**Repository:** `Galxe/grevm` (branch: `security-audit-fixes`) |
| 7 | +**Prior Audits:** [Round 1 — 2026-02-26](./2026-02-26-security-audit-report.md) (19 findings), [Round 2 — 2026-03-02](./2026-03-02-security-audit-round2-report.md) (10 findings) |
| 8 | + |
| 9 | +--- |
| 10 | + |
| 11 | +## Summary |
| 12 | + |
| 13 | +| Severity | Count | |
| 14 | +|----------|-------| |
| 15 | +| HIGH | 2 | |
| 16 | +| MEDIUM | 4 | |
| 17 | +| LOW | 3 | |
| 18 | +| INFO | 2 | |
| 19 | +| **Total** | **11** | |
| 20 | + |
| 21 | +> [!NOTE] |
| 22 | +> This Round 3 audit was performed against the codebase **after** Round 1 and Round 2 fixes were applied. |
| 23 | +> It includes verification of prior fix completeness and a cross-module review with gravity-reth, gravity-sdk, and gravity-aptos. |
| 24 | +
|
| 25 | +--- |
| 26 | + |
| 27 | +## HIGH Severity (2) |
| 28 | + |
| 29 | +### GREVM-R3-001: TOCTOU in `async_finality` — Lock-Drop-Reacquire Still Exploitable |
| 30 | + |
| 31 | +**File:** `grevm/src/scheduler.rs:355-374` |
| 32 | + |
| 33 | +The `async_finality` loop acquires the lock at line 358 to check `status == Unconfirmed`, then **drops the lock** (temporary `MutexGuard`). It performs timestamp checks (lines 361-371) without holding any lock, then **re-acquires** at line 372 and unconditionally sets `Finality`. |
| 34 | + |
| 35 | +The Round 1 reviewer (GREVM-004) stated logical timestamps prevent exploitation. However, the `lower_ts` mechanism has a gap: `reset_validation_idx(txid+1)` sets `lower_ts[txid+1]`, NOT `lower_ts[txid]`. So for `finality_idx = txid`, the timestamp guard does not fire for conflicts detected AT that txid. |
| 36 | + |
| 37 | +**Race sequence:** |
| 38 | +1. Finality thread: reads `Unconfirmed` at line 358, drops lock |
| 39 | +2. Worker: validates tx, finds conflict, sets `Conflict`, calls `reset_validation_idx(txid+1)` — updates `lower_ts[txid+1]` |
| 40 | +3. Finality thread: checks `lower_ts[txid]` (unchanged!), passes, re-acquires lock, overwrites to `Finality` |
| 41 | + |
| 42 | +**Impact:** Transaction committed as final with stale execution results — consensus-breaking state corruption. |
| 43 | + |
| 44 | +**Fix:** Re-check status after re-acquiring the lock: |
| 45 | +```rust |
| 46 | +let mut tx_state = self.tx_states[finality_idx].lock(); |
| 47 | +if tx_state.status != TransactionStatus::Unconfirmed { |
| 48 | + break; |
| 49 | +} |
| 50 | +tx_state.status = TransactionStatus::Finality; |
| 51 | +``` |
| 52 | + |
| 53 | +**Prior:** GREVM-004 (Round 1). **Still exploitable.** |
| 54 | + |
| 55 | +--- |
| 56 | + |
| 57 | +### GREVM-R3-002: CommitGuard Does Not Prevent Simultaneous Mutable Aliases |
| 58 | + |
| 59 | +**File:** `grevm/src/async_commit.rs:13-27`, `scheduler.rs:456,580` |
| 60 | + |
| 61 | +`CommitGuard::new()` takes `&UnsafeCell<ParallelState<DB>>` (shared ref). Two `CommitGuard` instances can coexist — one at line 456 (`parallel_execute`) and one at line 580 (`fallback_sequential`). While the current code ensures temporal separation (scope joins before fallback), the type system provides no enforcement. |
| 62 | + |
| 63 | +Additionally, `state_ref()` (line 68-71) creates `&ParallelState` while `state_mut()` creates `&mut ParallelState` from the same `UnsafeCell`, within the same `commit()` method — technically UB under Rust's aliasing rules even though the references don't overlap temporally within the method. |
| 64 | + |
| 65 | +**Impact:** The CommitGuard pattern fails to provide the compile-time safety GREVM-R2-001 was designed to deliver. Future refactors could silently introduce UB. |
| 66 | + |
| 67 | +**Prior:** GREVM-R2-001 (Round 2). **Incomplete fix.** |
| 68 | + |
| 69 | +--- |
| 70 | + |
| 71 | +## MEDIUM Severity (4) |
| 72 | + |
| 73 | +### GREVM-R3-003: `nonce + 1` Overflow in Nonce Assertion |
| 74 | + |
| 75 | +**File:** `grevm/src/async_commit.rs:105` |
| 76 | + |
| 77 | +`assert_eq!(change.info.nonce, expect + 1)` — if `expect` is `u64::MAX`, `expect + 1` wraps to 0 in release mode (overflow-checks disabled). The assertion would compare against 0 instead of the intended value. |
| 78 | + |
| 79 | +### GREVM-R3-004: Write Set Not Cleaned on EVM Error Re-execution |
| 80 | + |
| 81 | +**File:** `grevm/src/scheduler.rs:719-739` |
| 82 | + |
| 83 | +When a tx encounters an EVM error, the old write_set from a previous successful incarnation is preserved and stored back. The stale `mv_memory` entries remain. During re-execution, `write_new_locations` flag may be incorrectly computed, potentially skipping necessary revalidation of dependent transactions. |
| 84 | + |
| 85 | +### GREVM-R3-005: `key_tx` Dependency Race with Concurrent `commit` |
| 86 | + |
| 87 | +**File:** `grevm/src/tx_dependency.rs:112-123` |
| 88 | + |
| 89 | +Race between `key_tx(txid)` and `commit(txid-1)` can cause unnecessary double-scheduling. Benign in current code but creates latent performance degradation under pathological workloads. |
| 90 | + |
| 91 | +### GREVM-R3-006: `update_mv_memory` Coinbase Skip Hides User ETH Transfers |
| 92 | + |
| 93 | +**File:** `grevm/src/storage.rs:208-210` |
| 94 | + |
| 95 | +All coinbase changes are skipped in `update_mv_memory`, including **user-initiated ETH transfers** to the coinbase address. If tx A sends ETH to coinbase and tx B reads the coinbase balance, the change is invisible in MVMemory. The `accurate_origin` flag partially mitigates this but has a gap: when `commit_idx == current_tx.txid` and a preceding executed-but-uncommitted tx sent ETH to coinbase. |
| 96 | + |
| 97 | +**Impact:** Potential stale coinbase balance reads causing incorrect execution that validation may not catch. |
| 98 | + |
| 99 | +--- |
| 100 | + |
| 101 | +## LOW Severity (3) |
| 102 | + |
| 103 | +### GREVM-R3-007: ERC20 Transfer Uses Slot 0, TransferFrom Uses Slot 1 for Balances |
| 104 | + |
| 105 | +**File:** `grevm/src/hint.rs:236-253 vs 255-280` |
| 106 | + |
| 107 | +Inconsistent storage slot assumptions between Transfer (slot 0) and TransferFrom (slot 1) for balance mappings. Performance-only impact since validation catches all conflicts. |
| 108 | + |
| 109 | +### GREVM-R3-008: `results.push(result)` Before Error Check |
| 110 | + |
| 111 | +**File:** `grevm/src/async_commit.rs:136-139` |
| 112 | + |
| 113 | +The execution result is pushed to `self.results` even when the nonce check fails. The results vector contains an entry for a transaction whose state was never committed. |
| 114 | + |
| 115 | +**Prior:** GREVM-R2-002. **Partially fixed.** |
| 116 | + |
| 117 | +### GREVM-R3-009: `lazy_reward` Applied After Error Not Guarded at Method Entry |
| 118 | + |
| 119 | +**File:** `grevm/src/async_commit.rs:141-151` |
| 120 | + |
| 121 | +The `commit()` method does not check `self.commit_result` at entry. If a caller fails to check between calls, state corruption could compound. Mitigated by caller's check in `async_commit()`. |
| 122 | + |
| 123 | +--- |
| 124 | + |
| 125 | +## INFO (2) |
| 126 | + |
| 127 | +### GREVM-R3-010: Block-STM Validation Invariant Confirmed ✓ |
| 128 | + |
| 129 | +The Block-STM validation loop correctly detects all read-write conflicts and triggers re-execution. The OCC-based approach ensures eventual consistency even when hints are incorrect. |
| 130 | + |
| 131 | +### GREVM-R3-011: DAG-based Scheduling Correctly Parallelizes Independent Transactions ✓ |
| 132 | + |
| 133 | +The hint-based DAG construction and partition assignment produce correct dependency ordering. Mis-hints only affect performance, not correctness, due to the validation fallback. |
| 134 | + |
| 135 | +--- |
| 136 | + |
| 137 | +## Fix Verification from Prior Rounds |
| 138 | + |
| 139 | +| Prior Finding | Current Status | |
| 140 | +|---------------|----------------| |
| 141 | +| GREVM-004 (TOCTOU in async_finality) | **Still exploitable** — see GREVM-R3-001 | |
| 142 | +| GREVM-006 (CAS loop in next_validation_idx) | **Not implemented** — fix checklist incorrectly marks as done | |
| 143 | +| GREVM-R2-001 (CommitGuard) | **Incomplete** — see GREVM-R3-002 | |
| 144 | +| GREVM-R2-002 (Early return on nonce) | **Partially done** — results.push before error check | |
| 145 | +| GREVM-R2-003 (empty partition guard) | **Partially done** — parallel_cnt not capped | |
| 146 | +| All other fixes | Correctly applied | |
| 147 | + |
| 148 | +--- |
| 149 | + |
| 150 | +## Cumulative Statistics (Rounds 1-3) |
| 151 | + |
| 152 | +| Severity | R1 | R2 | R3 | Total | |
| 153 | +|----------|----|----|-----|-------| |
| 154 | +| CRITICAL | 1 | 0 | 0 | 1 | |
| 155 | +| HIGH | 5 | 2 | 2 | 9 | |
| 156 | +| MEDIUM | 5 | 3 | 4 | 12 | |
| 157 | +| LOW | 5 | 3 | 3 | 11 | |
| 158 | +| INFO | 3 | 2 | 2 | 7 | |
| 159 | +| **Total** | **19** | **10** | **11** | **40** | |
0 commit comments