Summary
A Float64Array element whose bit pattern falls inside perry's NaN-box tag window reads back as its payload integer instead of the double it was written as. No error; the value is silently wrong.
const a = new Float64Array(1);
const view = new BigUint64Array(a.buffer);
view[0] = 0x7FF8000012345678n; // a NaN whose payload is 0x12345678
console.log(typeof a[0], Number.isNaN(a[0]), a[0]);
// node : number true NaN
// perry: number false 305419896
305419896 is 0x12345678 — the NaN payload, surfaced as an integer. perry also reports Number.isNaN(a[0]) as false for a value that is a NaN by IEEE-754 definition.
A typed array is raw memory with a defined bit-level interpretation: any 64-bit pattern may legitimately be stored and must read back as the Float64 it denotes. Perry's NaN-boxing uses the NaN space for tagged values, and a genuine f64 NaN with a payload collides with it.
How this arises in ordinary code
It does not need a BigUint64Array to construct. Any of these can put an arbitrary bit pattern into a Float64Array:
- reading binary data — a file, a socket, a WASM memory, a GPU buffer
new Float64Array(someArrayBuffer) over bytes from anywhere
- deserialising a struct written by C, Rust or another language
- signalling NaNs produced by numeric code that encodes information in the payload
In each case the value is silently replaced by its payload integer, and Number.isNaN then agrees that it is not a NaN — so a downstream isNaN check cannot catch it either.
Two related divergences found alongside
Number.isNaN returns false for a NaN in the case above. That is the same root cause surfacing through a different API, but worth stating separately because Number.isNaN is the standard defensive check and it is the one thing a program would use to defend itself here.
A module-global BigInt64Array read throws a TypeError where node returns the value. Minimal case: declare a BigInt64Array at module scope and read an element from inside a function.
Provenance
Found while investigating #10777, on base — no change of ours is involved, and all three reproduce on origin/main plus only unrelated open PRs. Compared against node v26.8.1 on Linux x86-64.
I am filing the first two together because they share a root cause; the BigInt64Array one is separate and may well be unrelated, but it was found in the same sweep and would otherwise go unrecorded.
Summary
A
Float64Arrayelement whose bit pattern falls inside perry's NaN-box tag window reads back as its payload integer instead of the double it was written as. No error; the value is silently wrong.305419896is0x12345678— the NaN payload, surfaced as an integer. perry also reportsNumber.isNaN(a[0])as false for a value that is a NaN by IEEE-754 definition.A typed array is raw memory with a defined bit-level interpretation: any 64-bit pattern may legitimately be stored and must read back as the
Float64it denotes. Perry's NaN-boxing uses the NaN space for tagged values, and a genuinef64NaN with a payload collides with it.How this arises in ordinary code
It does not need a
BigUint64Arrayto construct. Any of these can put an arbitrary bit pattern into aFloat64Array:new Float64Array(someArrayBuffer)over bytes from anywhereIn each case the value is silently replaced by its payload integer, and
Number.isNaNthen agrees that it is not a NaN — so a downstreamisNaNcheck cannot catch it either.Two related divergences found alongside
Number.isNaNreturnsfalsefor a NaN in the case above. That is the same root cause surfacing through a different API, but worth stating separately becauseNumber.isNaNis the standard defensive check and it is the one thing a program would use to defend itself here.A module-global
BigInt64Arrayread throws aTypeErrorwhere node returns the value. Minimal case: declare aBigInt64Arrayat module scope and read an element from inside a function.Provenance
Found while investigating #10777, on base — no change of ours is involved, and all three reproduce on
origin/mainplus only unrelated open PRs. Compared against node v26.8.1 on Linux x86-64.I am filing the first two together because they share a root cause; the
BigInt64Arrayone is separate and may well be unrelated, but it was found in the same sweep and would otherwise go unrecorded.