Skip to content
Closed
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
23 changes: 11 additions & 12 deletions crates/payload/builder/src/prewarming.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,10 @@ impl BestTransactionsPrewarming {
Provider: StateProviderFactory + Clone + 'static,
{
let pool = executor.prewarming_pool();
let prewarm = ctx.prewarm.clone();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 [SECURITY] Coordinator-thread worker state survives across payload builds

WorkerPool::init and pool.clear() broadcast only to Rayon-owned pool threads, but in_place_scope allows the external calling thread to execute scoped jobs. A job on that caller lazily stores this build's EVM in the global worker TLS; cleanup does not clear it, so a later build on the same coordinator thread can reuse stale parent state/environment. Its replay may then be rejected as invalid, causing an otherwise valid payment transaction to be omitted from the payload.

Recommended Fix:
Initialize and explicitly clear the external scope caller's worker slot for every build, with a barrier before jobs begin, or avoid global worker TLS for external scope participants and pass build-scoped state directly. Add a two-build regression test on the same coordinator thread with distinct contexts and force a scoped task onto that thread.

pool.init::<PrewarmEvmState>(|_| prewarm.evm_for_ctx());

pool.in_place_scope(|scope| {
let prewarm = ctx.prewarm.clone();
scope.spawn(move |_| {
pool.init::<PrewarmEvmState>(|_| prewarm.evm_for_ctx());
});

let advance = |ctx: &mut BestTransactionsPrewarmingContext<Txs, Provider>| {
let Some(tx) = ctx.best_txs.next() else {
let _ = ctx.transactions_tx.send(None);
Expand Down Expand Up @@ -780,20 +777,22 @@ mod tests {
}

#[test]
fn prewarming_does_not_use_shared_worker_state_slot() {
fn prewarming_clears_worker_state() {
let executor = TaskExecutor::test();
let pool = executor.prewarming_pool();
pool.init::<usize>(|existing| existing.map(|value| *value).unwrap_or(1));

let sender = Address::random();
let txs = vec![test_tx(sender, 0)];
let log = Arc::new(Mutex::new(TestLog::default()));
let mut prewarming = prewarming_with_executor(executor.clone(), txs, log);
{
let sender = Address::random();
let txs = vec![test_tx(sender, 0)];
let log = Arc::new(Mutex::new(TestLog::default()));
let mut prewarming = prewarming_with_executor(executor.clone(), txs, log);

assert!(prewarming.next().is_some());
assert!(prewarming.next().is_some());
}

pool.broadcast(pool.current_num_threads(), |worker| {
assert_eq!(*worker.get::<usize>(), 1);
assert_eq!(*worker.get_or_init(|| 2usize), 2);
});
}
}
Loading