Skip to content

fix(p2p): bound declared contract-class bytecode length before allocating (A-1258)#24213

Merged
PhilWindle merged 2 commits into
merge-train/spartan-v5from
phil/a-1258-datatxvalidator-allocates-attacker-declared-bytecode-length
Jun 22, 2026
Merged

fix(p2p): bound declared contract-class bytecode length before allocating (A-1258)#24213
PhilWindle merged 2 commits into
merge-train/spartan-v5from
phil/a-1258-datatxvalidator-allocates-attacker-declared-bytecode-length

Conversation

@PhilWindle

Copy link
Copy Markdown
Collaborator

Fixes A-1258.

Problem

DataTxValidator decodes contract-class publication logs before proof validation (data validation runs ahead of proof validation on the req/resp, block-proposal, RPC, and gossip paths). The decode trusts the declared public-bytecode length field:

  • bufferFromFields (stdlib/src/abi/buffer.ts) reads the declared byteLength and, when the payload is shorter, does Buffer.alloc(byteLength) — trusting the declared length over the bytes actually present.
  • ContractClassPublishedEvent.fromLog decodes the bytecode via bufferFromFields, and DataTxValidator.#hasCorrectContractClassIds calls fromLog before any class-id / proof rejection.

A contract-class log is a fixed CONTRACT_CLASS_LOG_SIZE_IN_FIELDS array, so the real packed bytecode is capped at ~93 KiB. But the declared length is unbounded: a ~96 KiB log declaring 16 MiB forces a 16 MiB allocation on every node that validates the tx, before the proof check rejects it — a memory-pressure / validation-amplification path.

Fix

  • Add an optional maxByteLength parameter to bufferFromFields; when provided, it throws (before allocating) if the declared length exceeds it. Callers that don't pass it keep the documented blob-reconstruction padding behaviour unchanged.
  • fromLog passes the fixed log's payload capacity (payloadFields × 31), so an over-declared length is rejected before allocation. DataTxValidator already wraps fromLog in try/catch, so the throw becomes a clean pre-proof TX_ERROR_MALFORMED_CONTRACT_CLASS_LOG rejection.

Tests

  • data_validator.test.ts: a log declaring a 16 MiB length is rejected, and Buffer.alloc is never called with the declared size (test time drops from ~1.2s to ~40ms — no large allocation). Run against the pre-fix compiled code, the allocation still happened (red); green after the fix.
  • Existing contract_class_published_event.test.ts "fits a max-size public bytecode" and the buffer.test.ts padding tests confirm legitimate max-size bytecode and the padding contract are unaffected.

…ting (A-1258)

DataTxValidator decodes contract-class publication logs before proof
validation. bufferFromFields trusted the declared byte-length field and
called Buffer.alloc(byteLength) even when it far exceeded the payload
actually present, so a malformed class-publication tx could force
attacker-chosen multi-MiB allocations on every node that validates the
gossiped tx (req/resp, block proposal, RPC, gossip) before the proof
validator rejects it.

Add an optional maxByteLength guard to bufferFromFields (preserving the
documented blob-reconstruction padding for callers that don't pass it);
ContractClassPublishedEvent.fromLog passes the fixed log's payload
capacity, so an over-declared length throws before allocation.
DataTxValidator already wraps fromLog in try/catch, so the throw becomes
a clean pre-proof rejection.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@PhilWindle PhilWindle added the ci-draft Run CI on draft PRs. label Jun 20, 2026
@AztecBot

AztecBot commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

Flakey Tests

🤖 says: This CI run detected 1 tests that failed, but were tolerated due to a .test_patterns.yml entry.

\033FLAKED\033 (8;;http://ci.aztec-labs.com/1811d9d48d727eab�1811d9d48d727eab8;;�): yarn-project/end-to-end/scripts/run_test.sh ha src/composed/ha/e2e_ha_full.parallel.test.ts "should delete old duties via cleanupOldDuties based on DB time, not node time" (40s) (code: 0)

@PhilWindle PhilWindle marked this pull request as ready for review June 22, 2026 11:09
// The fixed-size class log can hold at most this many packed-bytecode bytes (every payload field
// carries 31 bytes). Bound the declared length to it so a malformed log can't declare a multi-MiB
// length backed by a tiny payload and force a large allocation during early (pre-proof) validation.
const maxByteLength = (bytecodeFields.length - 1) * (Fr.SIZE_IN_BYTES - 1);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can byecodeFields end up being an empty buffer?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't be, but I added a guard against it

…258)

Defensive guard from PR review: an empty fields array would destructure
length to undefined and throw on length.toNumber(). A valid bufferAsFields
output always carries the leading byte-length field, so empty encodes no
data — return an empty buffer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@PhilWindle PhilWindle merged commit 400b434 into merge-train/spartan-v5 Jun 22, 2026
12 checks passed
@PhilWindle PhilWindle deleted the phil/a-1258-datatxvalidator-allocates-attacker-declared-bytecode-length branch June 22, 2026 15:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-draft Run CI on draft PRs.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants