For the complete documentation index, see llms.txt. This page is also available as Markdown.
75488 bc low off by one in batcher default frame size causes permanent livelock in submit pending halting l2 finalization and freezing all pending withdrawals
Submitted on Apr 29th 2026 at 12:41:02 UTC by @GiuseppeDeLaZara for Audit Comp | Base Azul
Network not being able to confirm new transactions (total network shutdown)
Description
Brief/Intro
The Base batcher's default max_frame_size (130,044) doesn't account for the 1-byte DERIVATION_VERSION_0 prefix added by BlobEncoder::encode_packed(). This causes every full frame to overflow BLOB_MAX_DATA_SIZE by 1 byte. The failed frame is requeued and retried in a tight synchronous loop with zero async yield points — submit_pending() never returns, permanently blocking the batcher driver.
Vulnerability Details
Three components interact to create this livelock:
1. Frame sizing (channel_out.rs:140-168)
output_frame(max_size=130044) produces frames with data.len() = 130021:
encode_packed() prepends 1 byte that output_frame doesn't account for:
3. Infinite retry in submit_pending() (core/submissions.rs:52-169)
On DataTooLarge, the code requeues the frame and continues:
requeue() rewinds the channel cursor (encoder.rs:594-618), so next_submission() returns the same immutable Arc<Frame>. The semaphore permit was just dropped so it's immediately re-acquirable. The entire error path is synchronous — the tokio task never yields.
Why this is the default behavior
config.rs:97-98: max_frame_size = 130044 (the default)
cli.rs:227: max_frame_size = target_frame_size (no separate flag)
Any channel with compressed output ≥ 130,021 bytes triggers it
At Base mainnet throughput, channels fill well past this on every cycle
The developers know the correct math
Their own test helper blob_filling_submission() at driver.rs:441:
But the production config doesn't apply this subtraction.
Impact Details
The batcher is the sole DA submitter for the L2. When submit_pending() livelocks:
No batch data reaches L1 → derivation pipeline stalls
TEE/ZK provers have no data to prove → no proposals finalized
All pending L2→L1 withdrawals are frozen (require finalized state)
Restarting with defaults reproduces immediately — not a transient failure
This is "Network not being able to confirm new transactions (total network shutdown)" per the scope definition.
Funds at risk: All assets pending withdrawal through the L2 bridge are frozen for the duration of the outage. On Base mainnet this includes all ETH and ERC-20 tokens in the bridge contracts and any in-flight withdrawal messages.
crates/batcher/core/src/driver.rs:157 — Driver blocked at submit_pending().await
crates/batcher/core/src/driver.rs:441 — Test helper with correct math
Proof of Concept
This PoC calls the real production SubmissionQueue::submit_pending() from base-batcher-core and proves it enters a permanent infinite loop. It is not a unit test — it's a standalone binary that exercises the actual production code path.
The PoC uses a LivelockPipeline that models the real FrameEncoder's requeue behavior: calling requeue() makes the frame available again via next_submission(), just like the cursor rewind in production.
What it demonstrates:
submit_pending() spawned with a production-sized frame (130021 bytes) never returns
After 2 seconds, the iteration counter shows hundreds of thousands of synchronous retries
The TxManager is never called — encode_packed fails every time before reaching send_async
The same test with the fixed size (130020 bytes) completes immediately
How to run
No network access, no environment variables, no external services.
Dependencies
base/base repository at tag v0.8.0-rc.28
Rust toolchain (same as the workspace requires)
No external services or mainnet forking needed (this is an offchain Rust component, not a smart contract)