Skip to content

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

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions