Skip to content

derivation: malformed L1 commitBatch panics layer1-verify nodes (blockCount==0 nil deref + blockCount*60 overflow) #994

@curryxbo

Description

@curryxbo

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:

  1. Reject blockCount == 0 (a valid batch has ≥1 block) — covers mechanism A across all branches.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions