Skip to content

fix(runtime): zero-fill new Uint8Array(n) — buffer_alloc returns reclaimed dirty memory#6429

Merged
proggeramlug merged 1 commit into
PerryTS:mainfrom
proggeramlug:fix/uint8array-alloc-zero-fill
Jul 15, 2026
Merged

fix(runtime): zero-fill new Uint8Array(n) — buffer_alloc returns reclaimed dirty memory#6429
proggeramlug merged 1 commit into
PerryTS:mainfrom
proggeramlug:fix/uint8array-alloc-zero-fill

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

new Uint8Array(n) can observe uninitialized memory — a spec violation (a length-constructed typed array must be zero-filled), and a genuine memory-disclosure hazard.

Root cause

js_uint8array_alloc set the header and relied on the backing block already being zero. It isn't. buffer_alloc allocates through arena_alloc_gc_old, and the old generation reclaims and re-hands dirty blocks — only pristine mmap pages read as zero.

The sibling paths already know this:

  • js_buffer_alloc (Buffer.alloc) does ptr::write_bytes(data, fill, size).
  • zeroed_array_buffer_storage carries the exact comment: "buffer_alloc does not zero, but ArrayBuffer per ECMAScript spec must observe zero-initialized bytes."

js_uint8array_alloc was the one sibling that forgot. The fix adds the same explicit zero-fill:

let buf = buffer_alloc(length);
unsafe {
    (*buf).length = length;
+   if length > 0 {
+       ptr::write_bytes(buffer_data_mut(buf), 0, length as usize);
+   }
}

Why nobody hit it, and how it surfaced

Latent on macOS (length-constructed arrays land on fresh, zero mmap pages), and latent on current main even on Linux (they happen to land on clean blocks). It surfaced under #6407 — an unrelated net-dispatch PR that also expands BUFFER_PROTOTYPE_METHODS from 11 to ~93 entries, perturbing -O3/thin-LTO code+data layout and moving these allocations onto reclaimed dirty blocks. test_gap_uint8array_source_dispatch then printed garbage, non-deterministic bytes for the boolean (new Uint8Array(true)) and string (new Uint8Array("2")) length cases:

run 1: boolean 5   string 56,21
run 2: boolean 4   string 16,12
run 3: boolean 5   string 0,14
       (node: boolean 0   string 0,0)

Reproduced on a Linux box: 20/20 runs correct with this fix, byte-identical to Node; reverting it fails. This is why #6407's CI was red on conformance-smoke (8) — landing this on main unblocks it on rebase.

On the missing test

I deliberately did not add a gap test. The defect requires the arena to re-hand a specific dirtied block, which neither macOS nor main-on-Linux reproduces on demand — I verified a GC-reuse stress test (fill 0xFF → drop → gc() → realloc → assert zero) passes with and without the fix on macOS. A test that can't fail without the fix isn't a regression guard, it's noise. The fix stands on its own correctness: zero-filling a spec-zero-initialized buffer can never regress a caller, and three sibling allocators already do exactly this.

…y memory

`js_uint8array_alloc` set the header and relied on the backing block already
being zero. It isn't: `buffer_alloc` allocates through `arena_alloc_gc_old`,
and the old generation reclaims and re-hands dirty blocks — only pristine mmap
pages read as zero. So `new Uint8Array(n)` (and the bool / string /
numeric-length sources that funnel through it) could observe stale bytes, which
is a spec violation: a length-constructed typed array is zero-initialized.

The sibling paths already know this. `js_buffer_alloc` (Buffer.alloc) does
`write_bytes(data, fill, size)`, and `zeroed_array_buffer_storage` carries the
exact comment — "buffer_alloc does not zero, but ArrayBuffer per ECMAScript
spec must observe zero-initialized bytes." `js_uint8array_alloc` was the lone
sibling that forgot; this adds the same explicit zero-fill.

Latent on macOS (length-constructed arrays land on fresh, zero mmap pages) and
on current main even on Linux (they happen to land on clean blocks). It surfaced
under an unrelated codegen change (PerryTS#6407 expands BUFFER_PROTOTYPE_METHODS 11->93,
shifting -O3/LTO layout) which moved these allocations onto reclaimed dirty
blocks: test_gap_uint8array_source_dispatch then printed garbage, non-
deterministic bytes for the 'boolean' / 'string' length cases. Reproduced on a
Linux box: 20/20 runs correct with this fix, byte-identical to Node; reverting
it fails.

No new gap test: the defect needs the arena to re-hand a specific dirtied block,
which neither macOS nor main-on-Linux reproduces on demand, so any portable test
would pass with or without the fix (it would not prove it can fail). The fix is
unconditionally correct — zero-filling a spec-zero-initialized buffer can never
regress a caller.
@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Caution

Review failed

An error occurred during the review process. Please try again later.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@proggeramlug
proggeramlug merged commit 39e440c into PerryTS:main Jul 15, 2026
26 checks passed
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.

1 participant