Skip to content

Commit 45cbc41

Browse files
AshinGauRichard1048576claudenekomoto911
authored
fix(security): Security audit, fix security findings in grevm parallel EVM (#99)
follow #97 --------- Co-authored-by: Richard <xunqiu2@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: nekomoto911 <nekomoto911@gmail.com>
1 parent 26b586c commit 45cbc41

9 files changed

Lines changed: 174 additions & 102 deletions

File tree

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ auto_impl = "1.3"
2929
lazy_static = "1.5.0"
3030
dashmap = "6.0"
3131

32+
# logging
33+
tracing = "0.1"
34+
3235
# metrics
3336
metrics = "0.24"
3437
metrics-derive = "0.1"

benches/gigagas.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,9 @@ fn bench_dependent_erc20(c: &mut Criterion, db_latency_us: u64, num_eoa: usize,
424424

425425
fn bench_uniswap(c: &mut Criterion, db_latency_us: u64) {
426426
let block_size = (GIGA_GAS as f64 / uniswap::ESTIMATED_GAS_USED as f64).ceil() as usize;
427-
let mut final_state = HashMap::from([account::mock_miner_account()]);
427+
let mut final_state = HashMap::default();
428+
let miner = account::mock_miner_account();
429+
final_state.insert(miner.0, miner.1);
428430
let mut final_bytecodes = HashMap::default();
429431
let mut final_txs = Vec::<TxEnv>::new();
430432
for _ in 0..block_size {

src/async_commit.rs

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,25 @@ use revm_context::{
66
use revm_primitives::Address;
77

88
use crate::{GrevmError, ParallelState, TxId};
9-
use std::cmp::Ordering;
9+
use std::{cell::UnsafeCell, cmp::Ordering};
10+
11+
/// Only constructible within the commit thread's scope (or sequential fallback).
12+
/// Guarantees exclusive mutable access to ParallelState.
13+
pub(crate) struct CommitGuard<'a, DB: DatabaseRef> {
14+
state: &'a UnsafeCell<ParallelState<DB>>,
15+
}
16+
17+
impl<'a, DB: DatabaseRef> CommitGuard<'a, DB> {
18+
pub(crate) fn new(state: &'a UnsafeCell<ParallelState<DB>>) -> Self {
19+
Self { state }
20+
}
21+
22+
pub(crate) fn state_mut(&mut self) -> &mut ParallelState<DB> {
23+
// SAFETY: CommitGuard requires `&mut self`, ensuring exclusive access
24+
// to the underlying state for the guard's owner.
25+
unsafe { &mut *self.state.get() }
26+
}
27+
}
1028

1129
/// `StateAsyncCommit` asynchronously finalizes transaction states,
1230
/// serving two critical purposes:
@@ -20,35 +38,44 @@ where
2038
{
2139
coinbase: Address,
2240
results: Vec<ExecutionResult>,
23-
state: &'a ParallelState<DB>,
41+
guard: CommitGuard<'a, DB>,
2442
commit_result: Result<(), GrevmError<DB::Error>>,
2543
disable_nonce_check: bool,
2644
}
2745

46+
// SAFETY: StateAsyncCommit is only used within a single commit thread (never shared).
47+
// CommitGuard ensures exclusive mutable access through its `&mut self` requirement.
48+
unsafe impl<DB: DatabaseRef + Send + Sync> Send for StateAsyncCommit<'_, DB> where DB::Error: Send {}
49+
2850
impl<'a, DB> StateAsyncCommit<'a, DB>
2951
where
3052
DB: DatabaseRef,
3153
{
3254
pub(crate) fn new(
3355
coinbase: Address,
34-
state: &'a ParallelState<DB>,
56+
guard: CommitGuard<'a, DB>,
3557
disable_nonce_check: bool,
3658
) -> Self {
37-
Self { coinbase, results: vec![], state, commit_result: Ok(()), disable_nonce_check }
59+
Self { coinbase, results: vec![], guard, commit_result: Ok(()), disable_nonce_check }
3860
}
3961

40-
fn state_mut(&self) -> &mut ParallelState<DB> {
41-
#[allow(invalid_reference_casting)]
42-
unsafe {
43-
&mut *(self.state as *const ParallelState<DB> as *mut ParallelState<DB>)
44-
}
62+
pub(crate) fn state_mut(&mut self) -> &mut ParallelState<DB> {
63+
self.guard.state_mut()
64+
}
65+
66+
/// SAFETY: Shared read access to state is safe because `ParallelState` reads go through
67+
/// `DashMap` (thread-safe) or the immutable database.
68+
fn state_ref(&self) -> &ParallelState<DB> {
69+
// Safe to get a shared reference since we only mutate exclusively when we have `&mut self`
70+
unsafe { &*self.guard.state.get() }
4571
}
4672

4773
pub(crate) fn init(&mut self) -> Result<(), DB::Error> {
4874
// Accesses the coinbase account to ensure proper handling of miner rewards (via
4975
// increment_balances) within ParallelState. This preemptive access guarantees correct state
5076
// synchronization when applying miner rewards during the final commitment phase.
51-
match self.state_mut().basic(self.coinbase) {
77+
let coinbase = self.coinbase;
78+
match self.state_mut().basic(coinbase) {
5279
Ok(_) => Ok(()),
5380
Err(e) => Err(e),
5481
}
@@ -70,7 +97,7 @@ where
7097
// integrity and prevent double-spending attacks.
7198
let ResultAndState { result, state, lazy_reward } = result_and_state;
7299
if !self.disable_nonce_check {
73-
match self.state.basic_ref(tx_env.caller) {
100+
match self.state_ref().basic_ref(tx_env.caller) {
74101
Ok(info) => {
75102
if let Some(info) = info {
76103
let expect = info.nonce;
@@ -108,6 +135,9 @@ where
108135
}
109136
}
110137
self.results.push(result);
138+
if self.commit_result.is_err() {
139+
return;
140+
}
111141
self.state_mut().commit(state);
112142

113143
// In Ethereum, each transaction includes a miner reward, which would introduce write
@@ -117,6 +147,7 @@ where
117147
// correct concurrency - even if subsequent transactions access the miner's account, they
118148
// will read the proper miner state from ParallelState (verified via commit_idx) without
119149
// creating artificial dependencies.
120-
assert!(self.state_mut().increment_balances(vec![(self.coinbase, lazy_reward)]).is_ok());
150+
let coinbase = self.coinbase;
151+
assert!(self.state_mut().increment_balances(vec![(coinbase, lazy_reward)]).is_ok());
121152
}
122153
}

src/hint.rs

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use revm::primitives::{
44
Address, B256, Bytes, TxKind, U256, alloy_primitives::U160, keccak256, ruint::UintTryFrom,
55
};
66
use revm_context::TxEnv;
7-
use std::{cmp::max, sync::Arc};
7+
use std::{cell::UnsafeCell, cmp::max, sync::Arc};
88

99
/// This module provides functionality for parsing and handling execution hints
1010
/// for parallel transaction execution in the context of Ethereum-like blockchains.
@@ -87,21 +87,25 @@ impl RWSet {
8787
/// Struct to hold shared transaction states and provide methods for parsing
8888
/// and handling execution hints for parallel transaction execution.
8989
pub(crate) struct ParallelExecutionHints {
90-
rw_set: Vec<RWSet>,
90+
rw_set: UnsafeCell<Vec<RWSet>>,
9191
txs: Arc<Vec<TxEnv>>,
9292
}
9393

94+
// SAFETY: Each thread in fork_join_util writes to disjoint indices of rw_set.
95+
unsafe impl Sync for ParallelExecutionHints {}
96+
9497
impl ParallelExecutionHints {
9598
pub(crate) fn new(txs: Arc<Vec<TxEnv>>) -> Self {
96-
Self { rw_set: vec![RWSet::new(); txs.len()], txs }
99+
Self { rw_set: UnsafeCell::new(vec![RWSet::new(); txs.len()]), txs }
97100
}
98101

99102
fn generate_dependency(&self) -> TxDependency {
100103
let num_txs = self.txs.len();
101104
let mut last_write_tx: HashMap<LocationAndType, TxId> = HashMap::new();
102105
let mut dependent_tx: Vec<Option<TxId>> = vec![None; num_txs];
103106
let mut affect_txs = vec![HashSet::new(); num_txs];
104-
for (txid, rw_set) in self.rw_set.iter().enumerate() {
107+
let rw_set = unsafe { &*self.rw_set.get() };
108+
for (txid, rw_set) in rw_set.iter().enumerate() {
105109
for location in rw_set.read_set.iter() {
106110
if let Some(&previous) = last_write_tx.get(location) {
107111
let new_dep = dependent_tx[txid].map_or(previous, |dep| max(dep, previous));
@@ -120,8 +124,7 @@ impl ParallelExecutionHints {
120124
let txs = self.txs.clone();
121125
// Utilize fork-join utility to process transactions in parallel
122126
fork_join_util(txs.len(), None, |start_tx, end_tx, _| {
123-
#[allow(invalid_reference_casting)]
124-
let hints = unsafe { &mut *(&self.rw_set as *const Vec<RWSet> as *mut Vec<RWSet>) };
127+
let hints = unsafe { &mut *self.rw_set.get() };
125128
for index in start_tx..end_tx {
126129
let tx_env = &txs[index];
127130
let rw_set = &mut hints[index];
@@ -281,16 +284,25 @@ impl ParallelExecutionHints {
281284
true
282285
}
283286

284-
fn get_contract_type(_contract_address: Address, _data: &Bytes) -> ContractType {
285-
// TODO(gaoxin): Parse the correct contract type to determined how to handle call data
286-
ContractType::ERC20
287+
fn get_contract_type(_contract_address: Address, data: &Bytes) -> ContractType {
288+
// Without bytecode analysis, we use a heuristic: if the function selector matches
289+
// a known ERC20 function, treat it as ERC20. Otherwise, return UNKNOWN to be
290+
// conservative and avoid incorrect dependency hints.
291+
if data.len() >= 4 {
292+
let func_id = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
293+
match ERC20Function::from(func_id) {
294+
ERC20Function::UNKNOWN => ContractType::UNKNOWN,
295+
_ => ContractType::ERC20,
296+
}
297+
} else {
298+
ContractType::UNKNOWN
299+
}
287300
}
288301

289302
fn decode_contract_parameters(data: &Bytes) -> (u32, Vec<B256>) {
290-
let func_id: u32 = 0;
291303
let mut parameters: Vec<B256> = Vec::new();
292304
if data.len() <= 4 {
293-
return (func_id, parameters);
305+
return (0, parameters);
294306
}
295307

296308
let func_id = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);

src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,9 @@ pub fn fork_join_util<'scope, F>(num_elements: usize, num_partitions: Option<usi
179179
where
180180
F: Fn(usize, usize, usize) + Send + Sync + 'scope,
181181
{
182+
if num_elements == 0 {
183+
return;
184+
}
182185
let parallel_cnt = num_partitions.unwrap_or(*CONCURRENT_LEVEL);
183186
let remaining = num_elements % parallel_cnt;
184187
let chunk_size = num_elements / parallel_cnt;

src/scheduler.rs

Lines changed: 35 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
use crate::{
22
AbortReason, CONCURRENT_LEVEL, FALLBACK_SEQUENTIAL, GrevmError, LocationAndType, MemoryEntry,
33
ParallelState, ReadVersion, Task, TransactionResult, TransactionStatus, TxId, TxState,
4-
TxVersion, async_commit::StateAsyncCommit, hint::ParallelExecutionHints, storage::CacheDB,
5-
tx_dependency::TxDependency, utils::ContinuousDetectSet,
4+
TxVersion,
5+
async_commit::{CommitGuard, StateAsyncCommit},
6+
hint::ParallelExecutionHints,
7+
storage::CacheDB,
8+
tx_dependency::TxDependency,
9+
utils::ContinuousDetectSet,
610
};
711
use ahash::{AHashMap as HashMap, AHashSet as HashSet};
812
use alloy_evm::{
@@ -25,6 +29,7 @@ use revm_inspector::NoOpInspector;
2529
use revm_primitives::Address;
2630

2731
use std::{
32+
cell::UnsafeCell,
2833
cmp::max,
2934
collections::BTreeMap,
3035
fmt::Debug,
@@ -36,6 +41,9 @@ use std::{
3641
time::Instant,
3742
};
3843

44+
/// min number of txs to parallel execute.
45+
const MIN_PARALLEL_TXS: usize = 64;
46+
3947
pub(crate) type MVMemory = DashMap<LocationAndType, BTreeMap<TxId, MemoryEntry>>;
4048

4149
#[derive(Metrics)]
@@ -261,7 +269,7 @@ where
261269
env: BlockEnv,
262270
block_size: usize,
263271
txs: Arc<Vec<TxEnv>>,
264-
state: ParallelState<DB>,
272+
state: UnsafeCell<ParallelState<DB>>,
265273
results: Mutex<Vec<ExecutionResult>>,
266274
tx_states: Vec<Mutex<TxState>>,
267275
tx_results: Vec<Mutex<Option<TransactionResult<DB::Error>>>>,
@@ -276,6 +284,12 @@ where
276284
metrics: ExecuteMetricsCollector,
277285
}
278286

287+
// SAFETY: Scheduler is shared across threads via `thread::scope`. The `UnsafeCell<ParallelState>`
288+
// is safe because: (1) only the commit thread mutates it (via StateAsyncCommit), serialized by
289+
// finality ordering, (2) worker threads only read via DatabaseRef (DashMap, thread-safe),
290+
// (3) fallback_sequential() is only called after all threads have joined.
291+
unsafe impl<DB: DatabaseRef + Send + Sync> Sync for Scheduler<DB> where DB::Error: Send + Sync {}
292+
279293
impl<DB> Debug for Scheduler<DB>
280294
where
281295
DB: DatabaseRef,
@@ -315,7 +329,7 @@ where
315329
env,
316330
block_size: num_txs,
317331
txs,
318-
state,
332+
state: UnsafeCell::new(state),
319333
results: Mutex::new(vec![]),
320334
tx_states: (0..num_txs).map(|_| Mutex::new(TxState::default())).collect(),
321335
tx_results: (0..num_txs).map(|_| Mutex::new(None)).collect(),
@@ -375,17 +389,14 @@ where
375389

376390
if (Instant::now() - start).as_millis() > 8_000 {
377391
start = Instant::now();
378-
println!(
379-
"stuck..., block_number: {}, finality_idx: {}, validation_idx: {}, execution_idx: {}",
380-
self.env.number,
381-
self.scheduler_ctx.finality_idx(),
382-
self.scheduler_ctx.validation_idx(),
383-
self.scheduler_ctx.executed_set.continuous_idx(),
392+
tracing::warn!(
393+
target: "grevm::scheduler",
394+
block_number = %self.env.number,
395+
finality_idx = self.scheduler_ctx.finality_idx(),
396+
validation_idx = self.scheduler_ctx.validation_idx(),
397+
execution_idx = self.scheduler_ctx.executed_set.continuous_idx(),
398+
"parallel execution stuck",
384399
);
385-
let status: Vec<(TxId, TransactionStatus)> =
386-
self.tx_states.iter().map(|s| s.lock().status.clone()).enumerate().collect();
387-
println!("transaction status: {:?}", status);
388-
self.tx_dependency.print();
389400
}
390401
}
391402
}
@@ -394,7 +405,7 @@ where
394405
let mut commit_idx = 0;
395406
let mut commiter = commiter.lock();
396407
let async_commit_state =
397-
std::env::var("ASYNC_COMMIT_STATE").map_or(true, |s| s.parse().unwrap());
408+
std::env::var("ASYNC_COMMIT_STATE").map_or(true, |s| s.parse().unwrap_or(true));
398409
while !self.abort.load(Ordering::Acquire) && commit_idx < self.block_size {
399410
while commit_idx < self.scheduler_ctx.finality_idx.load(Ordering::Acquire) {
400411
if async_commit_state {
@@ -420,7 +431,7 @@ where
420431

421432
/// Take `ExecutionResult` and `ParallelState`
422433
pub fn take_result_and_state(self) -> (Vec<ExecutionResult>, ParallelState<DB>) {
423-
(self.results.into_inner(), self.state)
434+
(self.results.into_inner(), self.state.into_inner())
424435
}
425436

426437
/// Paralle execution
@@ -432,16 +443,18 @@ where
432443
self.metrics.total_tx_cnt.store(self.block_size, Ordering::Relaxed);
433444
let concurrency_level = concurrency_level.unwrap_or(
434445
std::env::var("GREVM_CONCURRENT_LEVEL")
435-
.map_or(*CONCURRENT_LEVEL, |s| s.parse().unwrap()),
446+
.map_or(*CONCURRENT_LEVEL, |s| s.parse().unwrap_or(*CONCURRENT_LEVEL)),
436447
);
437-
if *FALLBACK_SEQUENTIAL {
448+
if *FALLBACK_SEQUENTIAL || self.block_size < MIN_PARALLEL_TXS {
438449
return self.fallback_sequential();
439450
}
440451
let commiter = Mutex::new(StateAsyncCommit::new(
441452
self.env.beneficiary,
442-
&self.state,
453+
CommitGuard::new(&self.state),
443454
self.cfg.disable_nonce_check,
444455
));
456+
457+
let state_ref = unsafe { &*self.state.get() };
445458
commiter.lock().init().map_err(|e| GrevmError { txid: 0, error: EVMError::Database(e) })?;
446459
thread::scope(|scope| {
447460
scope.spawn(|| {
@@ -458,7 +471,7 @@ where
458471
let cache_db = CacheDB::new(
459472
self.cfg.spec,
460473
self.env.beneficiary,
461-
&self.state,
474+
state_ref,
462475
&self.mv_memory,
463476
&self.scheduler_ctx.commit_idx,
464477
);
@@ -548,13 +561,6 @@ where
548561
Ok(())
549562
}
550563

551-
fn state_mut(&self) -> &mut ParallelState<DB> {
552-
#[allow(invalid_reference_casting)]
553-
unsafe {
554-
&mut *(&self.state as *const ParallelState<DB> as *mut ParallelState<DB>)
555-
}
556-
}
557-
558564
/// Fallback to sequential execution
559565
pub fn fallback_sequential(&self) -> Result<(), GrevmError<DB::Error>> {
560566
let mut results = self.results.lock();
@@ -564,7 +570,8 @@ where
564570
}
565571

566572
let mut sequential_results = Vec::with_capacity(self.block_size - num_commit);
567-
let state_mut = self.state_mut();
573+
let mut commit_guard = CommitGuard::new(&self.state);
574+
let state_mut = commit_guard.state_mut();
568575
{
569576
let evm = Context::mainnet()
570577
.with_db(state_mut)

0 commit comments

Comments
 (0)