For the complete documentation index, see llms.txt. This page is also available as Markdown.

76497 bc high detached task semaphore permit leak in consensus layer rpc processor starves engineactor router and halts block production

Submitted on May 4th 2026 at 17:19:01 UTC by @InfiniteSec for Audit Comp | Base Azul

  • Report ID: #76497

  • Report Type: Blockchain/DLT

  • Report severity: High

  • Target: https://github.com/base/base/tree/v0.8.0-rc.28

  • Impacts:

    • Temporary freezing of network transactions by delaying one block by 500% or more of the average block time of the preceding 24 hours beyond standard difficulty adjustments

Description

Brief/Intro

The consensus-layer EngineRpcProcessor spawns detached tokio tasks that hold semaphore permits for their entire lifetime, even after the upstream HTTP connection has been closed. An attacker can send 1200 optimism_outputAtBlock requests to the unauthenticated consensus RPC port 9545, exhausting all 16 semaphore permits with detached tasks and filling the 1024-capacity rpc_tx channel. The EngineActor blocks on rpc_tx.send().await, starving the SequencerActor's BuildRequest. The end-to-end PoC measures block production dropping from 0.50 blocks/s to 0.05 blocks/s: only 1 block produced in 20 seconds (expected 10), a delay ratio of 1000%, far exceeding the Immunefi High threshold of 500%.

Vulnerability Details

The vulnerability is in the semaphore acquisition and detached task spawning logic within EngineRpcProcessor::start(). The processor uses a semaphore with a capacity of 16 to limit concurrent queries:

// crates/consensus/service/src/actors/engine/rpc_request_processor.rs
const MAX_CONCURRENT_ENGINE_RPC_QUERIES: usize = 16;
let semaphore = Arc::new(Semaphore::new(MAX_CONCURRENT_ENGINE_RPC_QUERIES));

In the main loop, each request received from rpc_rx acquires a semaphore permit, then spawns a detached task to handle the request:

The code comment explicitly states the tasks are "intentionally detached." When the upstream HTTP connection times out or is closed, the detached task is not cancelled; it continues running until the EL query completes. The permit lifetime is bound to the task, not the connection.

optimism_outputAtBlock is the slowest public RPC method on port 9545. It performs two EL RPC calls in query.rs: client.l2_block_by_label(block) and client.get_proof(...), both awaited without any timeout:

The EL RPC client is constructed without configuring a request timeout. Under EL load or network latency, these queries can take several seconds or longer.

The complete causal chain of the attack is as follows. The attacker sends a flood of concurrent optimism_outputAtBlock requests through port 9545 (default 0.0.0.0:9545, no JWT, no authentication). Each request flows through the jsonrpsee server into RollupRpc::output_at_block, then through QueuedEngineRpcClient into the shared engine_actor_request_tx channel (capacity 1024). The EngineActor's single-threaded routing loop receives requests from inbound_request_rx and, for RpcRequest variants, forwards them via rpc_tx.send(*rpc_req).await to the processor channel (capacity 1024). The processor acquires a semaphore permit and spawns a detached task to execute the EL query. After 16 tasks exhaust all permits, the processor loop blocks, stopping consumption from rpc_rx. Subsequent requests fill the rpc_tx channel (1024 capacity), after which the EngineActor blocks at rpc_tx.send().await. At this point the EngineActor cannot process any request type: BuildRequest, ProcessSafeL2SignalRequest, ProcessDelegatedForkchoiceUpdateRequest, ProcessFinalizedL2BlockNumberRequest, ProcessUnsafeL2BlockRequest, and ResetRequest are all starved. The SequencerActor sends BuildRequest through the same engine_actor_request_tx channel; these requests queue in the channel but cannot be consumed by the EngineActor, halting block production.

The HTTP-layer TimeoutLayer(60s) only closes the HTTP connection; it does not cancel the detached tokio tasks. ConcurrencyLimitLayer(1024) allows 1024 concurrent requests into the system. LoadShedLayer only rejects requests when the concurrency limit is reached. These defenses are ineffective against requests that have already entered the processing pipeline.

Impact Details

This vulnerability falls under the Blockchain/DLT category, mapping to Immunefi v2.3 High severity item 2: "Temporary freezing of network transactions by delaying one block by 500% or more of the average block time of the preceding 24 hours."

The end-to-end PoC precisely measures the attack impact. Before the attack, the SequencerSimulator produced blocks at the expected 2-second cadence (5 blocks in 10 seconds, rate = 0.50 blocks/s). After 1200 optimism_outputAtBlock HTTP requests were sent, only 1 block was produced in 20 seconds (expected 10), with a block rate of 0.05 blocks/s and a delay ratio of 1000%, far exceeding the 500% threshold defined in Immunefi High severity. The attacker does not need to maintain connections, as detached tasks hold permits until EL queries complete. By periodically sending new request batches, the attacker can sustain the starvation state continuously.

References

  • Detached task spawning (permit lifetime bound to task, not connection): https://github.com/base/base/blob/v0.8.0-rc.24/crates/consensus/service/src/actors/engine/rpc_request_processor.rs#L92-L102

  • Semaphore capacity definition (16 permits): https://github.com/base/base/blob/v0.8.0-rc.24/crates/consensus/service/src/actors/engine/rpc_request_processor.rs#L69

  • acquire_owned blocking point: https://github.com/base/base/blob/v0.8.0-rc.24/crates/consensus/service/src/actors/engine/rpc_request_processor.rs#L87-L90

  • EngineActor single-threaded routing loop: https://github.com/base/base/blob/v0.8.0-rc.24/crates/consensus/service/src/actors/engine/actor.rs#L114-L168

  • rpc_tx.send().await blocking EngineActor: https://github.com/base/base/blob/v0.8.0-rc.24/crates/consensus/service/src/actors/engine/actor.rs#L134-L139

  • rpc_tx channel capacity 1024: https://github.com/base/base/blob/v0.8.0-rc.24/crates/consensus/service/src/actors/engine/actor.rs#L58

  • engine_actor_request_tx shared channel: https://github.com/base/base/blob/v0.8.0-rc.24/crates/consensus/service/src/service/node.rs#L373

  • QueuedEngineRpcClient uses shared channel: https://github.com/base/base/blob/v0.8.0-rc.24/crates/consensus/service/src/service/node.rs#L538

  • OutputAtBlock EL queries without timeout: https://github.com/base/base/blob/v0.8.0-rc.24/crates/consensus/engine/src/query.rs#L88-L134

  • HTTP middleware (TimeoutLayer does not cancel detached tasks): https://github.com/base/base/blob/v0.8.0-rc.24/crates/consensus/service/src/actors/rpc/actor.rs#L72-L74

  • RollupRpc unconditional registration: https://github.com/base/base/blob/v0.8.0-rc.24/crates/consensus/service/src/actors/rpc/actor.rs#L126-L131

  • Default RPC port configuration (0.0.0.0:9545): https://github.com/base/base/blob/v0.8.0-rc.24/crates/client/cli/src/rpc.rs#L25-L28

https://gist.github.com/link-infsec/c1075791b5daac5786e55940c0564b05

Proof of Concept

1

Setup

Place the following test file at crates/consensus/service/tests/actors/poc_rpc_flood_e2e.rs. Add to the mod.rs in the same directory:

No additional dependency changes are needed; all dependencies are already in the base-consensus-node dev-dependencies.

2

Run command

3

Execution output

The test launches a real EngineActor (production actor.rs:57-169 code path with tokio::select! routing loop and rpc_tx channel creation), a real RpcActor (jsonrpsee HTTP server with the full HTTP middleware stack: TimeoutLayer(60s) + ConcurrencyLimitLayer(1024) + LoadShedLayer, registering RollupNodeApiServer's optimism_outputAtBlock method), and bridges them through QueuedEngineRpcClient to the same shared engine_actor_request_tx channel. The SequencerSimulator sends BuildRequest every 2 seconds through the same channel, precisely mimicking production SequencerActor's block production flow.

Phase 1 verifies normal block production (5 blocks in 10 seconds, rate=0.50 blocks/s). Phase 2 sends 1200 optimism_outputAtBlock requests via HTTP client to the RpcActor, traversing the complete production path. Phase 3 measures the block production rate for 20 seconds after the attack: 1 block/20s (rate=0.05), delay ratio 1000%, far exceeding the Immunefi High threshold of 500%.

Was this helpful?