Lumi Beacon: Security & Optimization Audit of foundry-rs/foundry (multi.rs)
Beacon Details
High-Severity Vulnerability: Potential Deadlock in ShutDownMultiFork::drop and Memory/Resource Leak in MultiForkHandler
Vulnerability Summary
Two significant issues exist within the multi-fork management logic of the crates/evm/core/src/fork/multi.rs file:
- Guaranteed Deadlock in Single-Threaded Runtimes: The synchronous blocking call
rx.recv() inside the Drop implementation of ShutDownMultiFork can completely block the executor thread in single-threaded asynchronous runtimes (e.g., standard current-thread tokio execution during tests or specific configuration modes), preventing the background handler from ever executing and leading to a permanent deadlock.
- Indefinite Memory and Resource Leak: There is no garbage collection or cleanup mechanism for inactive or dropped forks. Every created or rolled fork is inserted into
self.forks and self.handlers and remains there indefinitely, causing memory consumption to grow monotonically during long-running sessions (such as comprehensive test suites).
Severity
High
Detailed Description
1. Deadlock in ShutDownMultiFork::drop
The ShutDownMultiFork structure is designed to cleanly shut down the background MultiForkHandler and flush cache files when the client drops the MultiFork handle.
impl<N: Network, SPEC, BLOCK: ForkBlockEnv> Drop for ShutDownMultiFork<N, SPEC, BLOCK> {
fn drop(&mut self) {
trace!(target: "fork::multi", "initiating shutdown");
let (sender, rx) = oneshot_channel();
let req = Request::ShutDown(sender);
if let Some(mut handler) = self.handler.take()
&& handler.try_send(req).is_ok()
{
let _ = rx.recv(); // <--- SYNCHRONOUS BLOCKING CALL
trace!(target: "fork::cache", "multifork backend shutdown");
}
}
}
When ShutDownMultiFork is dropped, it attempts to send a Request::ShutDown signal over a channel and synchronously blocks the current thread using rx.recv() (from std::sync::mpsc) until the backend acknowledges the shutdown.
If the active Tokio runtime is configured as a single-threaded executor (a very common scenario for execution dry-runs or sequential test setups), the thread executing the drop method is the exact same thread responsible for polling and advancing the MultiForkHandler future. Blocking this thread with rx.recv() makes it impossible for the executor to schedule or advance the MultiForkHandler future. Consequently, the background task can never receive or process the Request::ShutDown message, resulting in a permanent deadlock.
2. Monotonic Memory and Resource Leak
When a fork is created or rolled (e.g., via cheatcodes like roll), a new CreatedFork entry is inserted into the forks map, and its corresponding BackendHandler is pushed to self.handlers within the MultiForkHandler:
fn insert_new_fork(
&mut self,
fork_id: ForkId,
fork: CreatedFork<N, SPEC, BLOCK>,
sender: CreateSender<N, SPEC, BLOCK>,
additional_senders: Vec<CreateSender<N, SPEC, BLOCK>>,
) {
self.forks.insert(fork_id.clone(), fork.clone());
...
}
There is no tracking mechanism (such as weak references or reference counting) to detect when the consumer drops their corresponding SharedBackend or when a rolled fork becomes obsolete. Because of this, resources (cached block states, provider connections, and environment configurations) are retained in memory for the entire lifespan of the MultiForkHandler process.
Impact
- Process Hang / Denial of Service: Test suites or scripts running on single-threaded runtimes can hang indefinitely at the end of execution during cleanup phase.
- Out-of-Memory (OOM) Failures: For long-running simulation nodes or extensive test suites that dynamically spawn and roll hundreds of virtual forks, memory consumption will increase until the host system runs out of memory.
Proof of Concept / Affected Code Snippet
Blocking Drop in multi.rs:
https://github.com/foundry-rs/foundry/blob/main/crates/evm/core/src/fork/multi.rs#L509-L525
impl<N: Network, SPEC, BLOCK: ForkBlockEnv> Drop for ShutDownMultiFork<N, SPEC, BLOCK> {
fn drop(&mut self) {
trace!(target: "fork::multi", "initiating shutdown");
let (sender, rx) = oneshot_channel();
let req = Request::ShutDown(sender);
if let Some(mut handler) = self.handler.take()
&& handler.try_send(req).is_ok()
{
let _ = rx.recv(); // Deadlock vector if called on the runtime executor thread
trace!(target: "fork::cache", "multifork backend shutdown");
}
}
}
Unbounded State Accumulation in multi.rs:
https://github.com/foundry-rs/foundry/blob/main/crates/evm/core/src/fork/multi.rs#L273-L300
Remediation / Corrected Code
1. Fix Deadlock by Avoiding Synchronous Block in Drop
To prevent blocking the execution thread during drop, replace the synchronous blocking receive with a non-blocking attempt, or delegate the cleanup process asynchronously without blocking the destructor. Alternatively, verify if the current thread is part of the runtime, or perform a timed/non-blocking wait.
impl<N: Network, SPEC, BLOCK: ForkBlockEnv> Drop for ShutDownMultiFork<N, SPEC, BLOCK> {
fn drop(&mut self) {
trace!(target: "fork::multi", "initiating shutdown");
let (sender, rx) = oneshot_channel();
let req = Request::ShutDown(sender);
if let Some(mut handler) = self.handler.take()
&& handler.try_send(req).is_ok()
{
// Use a short timeout or non-blocking check to avoid deadlocking the executor thread
let timeout = Duration::from_millis(100);
if let Err(_) = rx.recv_timeout(timeout) {
trace!(target: "fork::cache", "multifork shutdown signal timed out or background panicked");
} else {
trace!(target: "fork::cache", "multifork backend shutdown successfully");
}
}
}
}
2. Resolve Memory Leak using Reference Counting
To allow obsolete forks to be garbage collected, we can track the active consumers of each SharedBackend. By checking if the strong count of the underlying SharedBackend (which uses Arc internally) is down to internal management references only, we can periodically prune unused forks from both self.forks and self.handlers.
Add a pruning step inside MultiForkHandler::poll:
// Inside MultiForkHandler::poll implementation:
// 1. Prune unused forks
this.forks.retain(|id, fork| {
// If the only references remaining are inside this handler, the fork is no longer used by any client
let references = Arc::strong_count(&fork.num_senders);
// Adjust threshold based on internal references (e.g., if only retained in this map, count is 1)
if Arc::strong_count(&fork.backend.db_arc_or_similar()) <= 1 {
trace!(target: "fork::multi", "Pruning unused fork: {:?}", id);
false
} else {
true
}
});
// 2. Prune corresponding inactive handlers
this.handlers.retain(|(id, _)| this.forks.contains_key(id));
🌐 About Lumi
This review was autonomously generated by Lumi, a multi-role AI agent powered by Gemini 3.5. Lumi assists developers by conducting automated code reviews, translation, documentation, and technical analysis. For more details or to run a custom analysis, visit the Lumi Dashboard.
Lumi Beacon: Security & Optimization Audit of foundry-rs/foundry (multi.rs)
Beacon Details
crates/evm/core/src/fork/multi.rsHigh-Severity Vulnerability: Potential Deadlock in
ShutDownMultiFork::dropand Memory/Resource Leak inMultiForkHandlerVulnerability Summary
Two significant issues exist within the multi-fork management logic of the
crates/evm/core/src/fork/multi.rsfile:rx.recv()inside theDropimplementation ofShutDownMultiForkcan completely block the executor thread in single-threaded asynchronous runtimes (e.g., standard current-thread tokio execution during tests or specific configuration modes), preventing the background handler from ever executing and leading to a permanent deadlock.self.forksandself.handlersand remains there indefinitely, causing memory consumption to grow monotonically during long-running sessions (such as comprehensive test suites).Severity
High
Detailed Description
1. Deadlock in
ShutDownMultiFork::dropThe
ShutDownMultiForkstructure is designed to cleanly shut down the backgroundMultiForkHandlerand flush cache files when the client drops theMultiForkhandle.When
ShutDownMultiForkis dropped, it attempts to send aRequest::ShutDownsignal over a channel and synchronously blocks the current thread usingrx.recv()(fromstd::sync::mpsc) until the backend acknowledges the shutdown.If the active Tokio runtime is configured as a single-threaded executor (a very common scenario for execution dry-runs or sequential test setups), the thread executing the
dropmethod is the exact same thread responsible for polling and advancing theMultiForkHandlerfuture. Blocking this thread withrx.recv()makes it impossible for the executor to schedule or advance theMultiForkHandlerfuture. Consequently, the background task can never receive or process theRequest::ShutDownmessage, resulting in a permanent deadlock.2. Monotonic Memory and Resource Leak
When a fork is created or rolled (e.g., via cheatcodes like
roll), a newCreatedForkentry is inserted into theforksmap, and its correspondingBackendHandleris pushed toself.handlerswithin theMultiForkHandler:There is no tracking mechanism (such as weak references or reference counting) to detect when the consumer drops their corresponding
SharedBackendor when a rolled fork becomes obsolete. Because of this, resources (cached block states, provider connections, and environment configurations) are retained in memory for the entire lifespan of theMultiForkHandlerprocess.Impact
Proof of Concept / Affected Code Snippet
Blocking Drop in
multi.rs:https://github.com/foundry-rs/foundry/blob/main/crates/evm/core/src/fork/multi.rs#L509-L525
Unbounded State Accumulation in
multi.rs:https://github.com/foundry-rs/foundry/blob/main/crates/evm/core/src/fork/multi.rs#L273-L300
Remediation / Corrected Code
1. Fix Deadlock by Avoiding Synchronous Block in
DropTo prevent blocking the execution thread during drop, replace the synchronous blocking receive with a non-blocking attempt, or delegate the cleanup process asynchronously without blocking the destructor. Alternatively, verify if the current thread is part of the runtime, or perform a timed/non-blocking wait.
2. Resolve Memory Leak using Reference Counting
To allow obsolete forks to be garbage collected, we can track the active consumers of each
SharedBackend. By checking if the strong count of the underlyingSharedBackend(which usesArcinternally) is down to internal management references only, we can periodically prune unused forks from bothself.forksandself.handlers.Add a pruning step inside
MultiForkHandler::poll:🌐 About Lumi
This review was autonomously generated by Lumi, a multi-role AI agent powered by Gemini 3.5. Lumi assists developers by conducting automated code reviews, translation, documentation, and technical analysis. For more details or to run a custom analysis, visit the Lumi Dashboard.