You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Nine fused js_array_* callback/reduce entry points return garbage values —
not empty, not an error — when the receiver is a Buffer-backed Uint8Array and
codegen could not statically prove the receiver type.
Measured against node v26.5.1 (matches .node-version) on main @ 0a1e78e5f.
Full list
entry point
measured
node
js_array_map
[1.29e-318,1.69e-312,1.27e-313]
[6,2,4]
js_array_filter
[]
[3,2]
js_array_find
undefined
1
js_array_findIndex
-1
1
js_array_some
false
true
js_array_every
false
true
js_array_reduce
9.12e-313
6
js_array_reduce_right
"z|4e-323|1.46e-311|6.48e-319"
"z|2|1|3"
js_array_forEach (1-arg)
visits garbage
3;1;2;
js_array_reduce_right is the widest: it is wrong even for a statically
typedconst u = new Uint8Array([3,1,2]), because dispatch_buffer_method
has no reduceRight arm either, so both dispatch paths land on the generic
helper.
Root cause
Two facts compose.
normalize_array_receiver (array/header.rs:844) is permissive, not
rejecting: for a registered typed array or Buffer it returns the raw
address rather than null (if is_typed_array || is_buffer { return raw_addr … }). So the re-dispatch each of these helpers performs afterwards
IS reached — this is not the fix(gc): install array growth forwarding for low-address arenas #8041 ordering bug.
That re-dispatch asks only lookup_typed_array_kind. Perry's new Uint8Array([…]) is a BufferHeader, not a TypedArrayHeader
(buffer::js_uint8array_new), so it is absent from the typed-array registry
and the probe never answers for it. There is no Buffer arm.
The helper then reads the BufferHeader as an ArrayHeader. Both share the {length: u32, capacity: u32} prefix, so length is correct while the
elements — read as boxed f64 slots at base + 8 + i*8 over a payload that is
one byte per element — are raw bytes reinterpreted. Correct length, garbage
values. It is also an out-of-bounds read of length * 7 bytes past the
buffer's real payload; the values observed differ run to run, which is how you
can tell it is reading uninitialized memory rather than mis-decoding.
This predates #8041 — normalize_array_receiver was not touched by that
commit — so it is a standing gap, not the regression #8090/#8109/#8119/#8120/#8130
have been closing. It is the same registry gap buffer_receiver_as_uint8_typed_array (#8096) documents, in nine more places.
Reachability
Only the non-provable receiver reaches these helpers:
Verified per method with --trace llvm, grepping call sites only
(^\s+(%… = )?(tail )?call) — grepping the whole .ll is useless because every
runtime symbol is declared in every module.
A registry typed array (Int32Array, Float64Array) is correct on every
one of these, because it is in the typed-array registry and the existing
re-dispatch fires. Only the Buffer-backed Uint8Array shape is affected.
Why this is not a one-line fix — the decision needed
The obvious move is buffer_receiver_as_uint8_typed_array(), which #8119 added
for toSorted/toReversed/with. But that helper returns a copy, and its
own doc scopes it to immutable methods. These nine pass the receiver to a user
callback as the 3rd argument (js_typed_array_map does js_closure_call3(callback, v, i, recv), typedarray/iterate.rs:42), so
delegating through a copy would hand the callback a different object than
node does, and a write through it would be silently lost:
u.forEach((v,i,arr)=>{arr[0]=9;});// node mutates u; a copy would not
So one of these has to be chosen deliberately:
(a) a live TypedArrayHeader view over the Buffer's bytes — no copy,
correct 3rd argument. Needs a view type that aliases rather than owns; typed_array_to_array_buffer goes the other direction.
(b) a Buffer arm in each helper that reads elements via js_buffer_get
and passes the original Buffer as the 3rd argument. Nine small edits, no new
machinery, but nine places to keep in step.
(c) extend dispatch_buffer_method with map/filter/forEach/find/ findIndex/some/every/reduce/reduceRight and have the generic
helpers delegate to it, mirroring what js_array_copy_within already does
for copyWithin (array/immutable.rs:370). This also fixes the
statically-typed reduceRight and findLast holes for free, since those are
the same missing arms.
(c) looks strongest: it is the pattern already in the tree, it is one place,
and it closes the static-receiver cases too. But it is a real design call, not
a mechanical reorder, which is why this is an issue rather than part of the #8135 PR.
Testing note for whoever takes this — a vacuous probe to avoid
holder.u.every((x) => x > 0) returns true under both node and perry,
because the garbage values (1.29e-318 &c.) are also > 0. A probe of that
shape reports PASS on the broken path. The discriminating predicate is (x) => x === 3 || x === 1 || x === 2, which measures false. Any every / some receiver test here must use value identity, never a sign or truthiness
test.
Context
Found by the exhaustive Array.prototype receiver sweep that produced #8135.
Full table of all 33 Array.prototype-reachable entry points, with the
funnel-ordering verdict and the measured node diff for each, is in the #8135
PR description.
Summary
Nine fused
js_array_*callback/reduce entry points return garbage values —not empty, not an error — when the receiver is a Buffer-backed
Uint8Arrayandcodegen could not statically prove the receiver type.
Measured against node
v26.5.1(matches.node-version) onmain@0a1e78e5f.Full list
js_array_map[1.29e-318,1.69e-312,1.27e-313][6,2,4]js_array_filter[][3,2]js_array_findundefined1js_array_findIndex-11js_array_somefalsetruejs_array_everyfalsetruejs_array_reduce9.12e-3136js_array_reduce_right"z|4e-323|1.46e-311|6.48e-319""z|2|1|3"js_array_forEach(1-arg)3;1;2;js_array_reduce_rightis the widest: it is wrong even for a staticallytyped
const u = new Uint8Array([3,1,2]), becausedispatch_buffer_methodhas no
reduceRightarm either, so both dispatch paths land on the generichelper.
Root cause
Two facts compose.
normalize_array_receiver(array/header.rs:844) is permissive, notrejecting: for a registered typed array or Buffer it returns the raw
address rather than null (
if is_typed_array || is_buffer { return raw_addr … }). So the re-dispatch each of these helpers performs afterwardsIS reached — this is not the fix(gc): install array growth forwarding for low-address arenas #8041 ordering bug.
That re-dispatch asks only
lookup_typed_array_kind. Perry'snew Uint8Array([…])is aBufferHeader, not aTypedArrayHeader(
buffer::js_uint8array_new), so it is absent from the typed-array registryand the probe never answers for it. There is no Buffer arm.
The helper then reads the
BufferHeaderas anArrayHeader. Both share the{length: u32, capacity: u32}prefix, solengthis correct while theelements — read as boxed f64 slots at
base + 8 + i*8over a payload that isone byte per element — are raw bytes reinterpreted. Correct length, garbage
values. It is also an out-of-bounds read of
length * 7bytes past thebuffer's real payload; the values observed differ run to run, which is how you
can tell it is reading uninitialized memory rather than mis-decoding.
This predates #8041 —
normalize_array_receiverwas not touched by thatcommit — so it is a standing gap, not the regression #8090/#8109/#8119/#8120/#8130
have been closing. It is the same registry gap
buffer_receiver_as_uint8_typed_array(#8096) documents, in nine more places.Reachability
Only the non-provable receiver reaches these helpers:
Verified per method with
--trace llvm, grepping call sites only(
^\s+(%… = )?(tail )?call) — grepping the whole.llis useless because everyruntime symbol is
declared in every module.A registry typed array (
Int32Array,Float64Array) is correct on everyone of these, because it is in the typed-array registry and the existing
re-dispatch fires. Only the Buffer-backed
Uint8Arrayshape is affected.Why this is not a one-line fix — the decision needed
The obvious move is
buffer_receiver_as_uint8_typed_array(), which #8119 addedfor
toSorted/toReversed/with. But that helper returns a copy, and itsown doc scopes it to immutable methods. These nine pass the receiver to a user
callback as the 3rd argument (
js_typed_array_mapdoesjs_closure_call3(callback, v, i, recv),typedarray/iterate.rs:42), sodelegating through a copy would hand the callback a different object than
node does, and a write through it would be silently lost:
So one of these has to be chosen deliberately:
TypedArrayHeaderview over the Buffer's bytes — no copy,correct 3rd argument. Needs a view type that aliases rather than owns;
typed_array_to_array_buffergoes the other direction.js_buffer_getand passes the original Buffer as the 3rd argument. Nine small edits, no new
machinery, but nine places to keep in step.
dispatch_buffer_methodwithmap/filter/forEach/find/findIndex/some/every/reduce/reduceRightand have the generichelpers delegate to it, mirroring what
js_array_copy_withinalready doesfor
copyWithin(array/immutable.rs:370). This also fixes thestatically-typed
reduceRightandfindLastholes for free, since those arethe same missing arms.
(c) looks strongest: it is the pattern already in the tree, it is one place,
and it closes the static-receiver cases too. But it is a real design call, not
a mechanical reorder, which is why this is an issue rather than part of the
#8135 PR.
Testing note for whoever takes this — a vacuous probe to avoid
holder.u.every((x) => x > 0)returnstrueunder both node and perry,because the garbage values (
1.29e-318&c.) are also> 0. A probe of thatshape reports PASS on the broken path. The discriminating predicate is
(x) => x === 3 || x === 1 || x === 2, which measuresfalse. Anyevery/somereceiver test here must use value identity, never a sign or truthinesstest.
Context
Found by the exhaustive
Array.prototypereceiver sweep that produced #8135.Full table of all 33
Array.prototype-reachable entry points, with thefunnel-ordering verdict and the measured node diff for each, is in the #8135
PR description.