Skip to content

fix: zero backing bytes outside copied ArrayBuffer views - #18

Merged
skvelymake merged 1 commit into
mainfrom
fix/sanitize-copied-array-buffers
Oct 27, 2026
Merged

skvelymake merged 1 commit into
mainfrom
fix/sanitize-copied-array-buffers

Conversation

@skvelymake

@skvelymake skvelymake commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Overview

Copying an ArrayBuffer view across an isolate boundary copied its whole backing buffer, so a small pooled Node Buffer exposed the rest of the 8 KB pool through .buffer. That pool can hold data from unrelated requests. This change zeroes every byte that no copied view covers before the result reaches the receiver, closing the heap disclosure reported in VMB-542 at the isolated-vm layer.

Risk Estimate

  • ⚠️ Big/complex change
  • ⚠️ Big splash zone
  • ⚠️ High stakes if errors occur

Changes

  • After deserialization (ExternalCopy, implicit copies, callback/closure argument vectors), audit the reconstructed graph without running JavaScript, then zero bytes outside the union of view ranges. Offsets, buffer size and aliasing between views are preserved.
  • Including the backing ArrayBuffer explicitly anywhere in the graph keeps all of its bytes. SharedArrayBuffers are never touched, and neither is reusable ExternalCopy storage.
  • Live objects inserted via derefInto() are opaque leaves, never audited or modified. They're held as v8::Global because ValueDeserializer::ReadValue closes its own handle scope.
  • Plain records are recognized by intrinsic Object.prototype identity, so receiver prototype pollution can't make copies fail. Proxies, accessors and unknown object types are rejected, and the audit is capped at 1,048,576 edges/roots.
  • With transferIn, external storage is consumed only after sanitization succeeds (CopyIntoPrivate + Consume), so a failed audit leaves the ExternalCopy intact.
  • Breaking: code that relied on reading the full backing buffer of a copied view now sees zeros. The README and CHANGELOG document the new contract.
  • Tests: new tests/array-buffer-sanitization.js covers aliasing/offsets, explicit buffer inclusion, every view type, cycles, SAB, reusable vs transferred storage, memory-limit charging, implicit copy paths in both directions, multi-argument vectors, prototype pollution, derefInto(), and a transferIn audit failure. tests/array-buffer-copy.js is updated for the zeroed bytes. Full suite: 85 passed.

Project Link

Jira issue

Copying a view also copied its whole backing ArrayBuffer, so small pooled
Node Buffers leaked the rest of the pool into the receiving isolate (and
vice versa on implicit copies).

After reconstruction, audit the copied graph without running JavaScript
and zero bytes outside the union of the views' ranges, unless the backing
ArrayBuffer itself is part of the graph. SharedArrayBuffers are untouched.

- Live objects inserted via derefInto() are opaque leaves and are never
  audited or modified (held as Globals: ReadValue closes its own scope).
- Plain records are recognised by intrinsic prototype identity, so
  receiver prototype pollution cannot make copies fail.
- transferIn consumes external storage only after sanitization succeeds,
  so a failed audit no longer destroys the ExternalCopy.
- Audit work is bounded to 1,048,576 edges/roots.
@skvelymake
skvelymake requested a review from a team as a code owner October 20, 2026 08:49

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM 👍

Great Job

@skvelymake
skvelymake merged commit d83ab67 into main Oct 27, 2026
17 of 20 checks passed
@skvelymake
skvelymake deleted the fix/sanitize-copied-array-buffers branch October 27, 2026 11:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants