Summary
A malformed (attacker-controlled) L1 commitBatch transaction can panic and crash any node running in layer1 verify mode. The derivation goroutine has no recover(), so a single crafted batch takes the whole node down (DoS). Two distinct panics originate from the same root cause: an unvalidated blockCount in node/derivation/batch_info.go.
Root cause
In ParseBatch (node/derivation/batch_info.go), the live commitBatch / commitBatchWithProof ABI populates LastBlockNumber from L1 calldata (uint64, attacker-controlled) and leaves BlockContexts nil, so execution takes the decompressed-stream else branch:
blockCount = batch.LastBlockNumber - parentBatchBlock // no lower/upper bound
Rollup.sol::_commitBatch does not constrain lastBlockNumber relative to the parent and _getBLSMsgHash is still a return bytes32(0) stub, so signatures don't bind the field — arbitrary values can land on L1.
Mechanism A — blockCount == 0 → nil deref
The underflow guard rejects < but not ==. With lastBlockNumber == parentLastBlockNumber, blockCount == 0:
ParseBatch returns empty blockContexts with a nil error.
derive() loops over the empty slice and returns (nil, nil).
derivation.go:470 then runs lastHeader.Number.Uint64() → nil pointer dereference.
Cheapest trigger: a single batch with lastBlockNumber == parent.
Mechanism B — blockCount * 60 overflow → makeslice panic
A huge blockCount (≳ 3.07e17) makes bcLen := blockCount * 60 wrap around uint64 to a small value, bypassing the len(batchBytes) < bcLen length guard. make([]*BlockContext, int(blockCount)) then attempts the un-wrapped allocation → makeslice: len out of range panic / OOM.
Impact
- DoS: crashes every node in layer1 verify mode that processes the offending L1 log.
- The derivation goroutine (
derivation.go) has no recover(), so the panic kills the process.
Fix (node-side, targeted)
Validate blockCount at source in ParseBatch:
- Reject
blockCount == 0 (a valid batch has ≥1 block) — covers mechanism A across all branches.
- Bound
blockCount by payload length using division (blockCount > len(batchBytes)/60) before the multiply — covers mechanism B (no overflow, and make is bounded by real payload size).
With blockCount validated, the downstream derivation.go:470 deref becomes unreachable, so no separate guard is added there.
Recommended follow-up (root cause, contract-side)
Add require(lastBlockNumber > parentLastBlockNumber) plus a per-batch span cap in Rollup.sol::_commitBatchWithBatchData, so invalid spans can never be committed on L1 in the first place. Tracked separately as the BLS message hash is still a stub.
Summary
A malformed (attacker-controlled) L1
commitBatchtransaction can panic and crash any node running in layer1 verify mode. The derivation goroutine has norecover(), so a single crafted batch takes the whole node down (DoS). Two distinct panics originate from the same root cause: an unvalidatedblockCountinnode/derivation/batch_info.go.Root cause
In
ParseBatch(node/derivation/batch_info.go), the livecommitBatch/commitBatchWithProofABI populatesLastBlockNumberfrom L1 calldata (uint64, attacker-controlled) and leavesBlockContextsnil, so execution takes the decompressed-streamelsebranch:Rollup.sol::_commitBatchdoes not constrainlastBlockNumberrelative to the parent and_getBLSMsgHashis still areturn bytes32(0)stub, so signatures don't bind the field — arbitrary values can land on L1.Mechanism A —
blockCount == 0→ nil derefThe underflow guard rejects
<but not==. WithlastBlockNumber == parentLastBlockNumber,blockCount == 0:ParseBatchreturns emptyblockContextswith a nil error.derive()loops over the empty slice and returns(nil, nil).derivation.go:470then runslastHeader.Number.Uint64()→ nil pointer dereference.Cheapest trigger: a single batch with
lastBlockNumber == parent.Mechanism B —
blockCount * 60overflow → makeslice panicA huge
blockCount(≳ 3.07e17) makesbcLen := blockCount * 60wrap arounduint64to a small value, bypassing thelen(batchBytes) < bcLenlength guard.make([]*BlockContext, int(blockCount))then attempts the un-wrapped allocation →makeslice: len out of rangepanic / OOM.Impact
derivation.go) has norecover(), so the panic kills the process.Fix (node-side, targeted)
Validate
blockCountat source inParseBatch:blockCount == 0(a valid batch has ≥1 block) — covers mechanism A across all branches.blockCountby payload length using division (blockCount > len(batchBytes)/60) before the multiply — covers mechanism B (no overflow, andmakeis bounded by real payload size).With
blockCountvalidated, the downstreamderivation.go:470deref becomes unreachable, so no separate guard is added there.Recommended follow-up (root cause, contract-side)
Add
require(lastBlockNumber > parentLastBlockNumber)plus a per-batch span cap inRollup.sol::_commitBatchWithBatchData, so invalid spans can never be committed on L1 in the first place. Tracked separately as the BLS message hash is still a stub.