Repository navigation
correctness: a Float64Array element whose bits land in the NaN-box window reads back as its payload integer, and Number.isNaN then reports false #10779
Description
Activity
This reproduces on the class-field path too, and it now blocks a performance fix
The report above used a
Float64Arrayto construct the bit pattern. It is not confined to typed arrays — the same value stored into an ordinary object field, on the path where perry has a consumedPtr<Shape>proof, behaves identically:const f = new Float64Array(1); new Uint32Array(f.buffer).set([0x12345678, 0x7FFE0000]); o.a = f[0]; console.log(typeof o.a, Number.isNaN(o.a), o.a); // node : number true NaN // perry: number false 305419896
That widens the blast radius: any code path that moves a double from binary data into an object field carries it.
It is also now a blocker for #10777
The perf fix for property access is one ~10-line arm in
expr_produces_canonical_raw_f64, admitting aPropertyGetwhose receiver's shape already records the field as numeric. The information exists, is reachable and is already consumed by codegen — the fix is to stop dropping the label.That predicate's contract is not "is a Number"; it is "any NaN it produces must carry a non-tag payload." A
numeric_fieldsslot is user-writable, so this bug sits directly in front of the arm: admitting thePropertyGetwhile a field can hold a tag-window NaN would turn a silent wrong answer into a silently wrong unboxed answer.So fixing this unblocks a measured performance fix as well as closing a correctness hole. It is on the critical path for #10761's operations 1–3.
- added 6 commits that reference this issue
on Sep 20, 2026 ⚠️ The fix for this landed in train 235 with a regression —Buffer.readFloatLEdoes not compile onmainThe canonicaliser from PR #10785 is in
mainas of merge train 235 (v0.5.1614). A follow-up fix is not, and without it any module callingBuffer.readFloatLEorreadFloatBEfails to compile:'%r1' defined with type 'float' but expected 'double'A hard codegen failure, not a wrong value. Fix is in #10789.
canonicalize_lane_f32(nanbox_inline.rs:63) is reached frombuffer_intrinsic.rs:428on theBuffer.readFloat*path and emits anfcmpon anf32lane, whileLlBlock::fcmprenders its operand type asdoubleunconditionally.I opened #10785 without catching this. The reason it got through is worth recording: the read-shape harness had
readDoubleLEand noreadFloat*, and none of the 153 programs across the three suites calls one. The guard that PR added had zero coverage on one of its two arms — untested by construction, not by oversight. The harness now carriesreadFloatLE,readFloatBEandreadDoubleBE; on the pre-fix runtime the first two SIGSEGV.#10789 also closes the last raw float source — NaNs arriving from native code — which is witnessed: before, a C
floatreturn SIGSEGVs on the forgedStringHeader*and a Cdoublereturn prints305419896.- added 4 commits that reference this issue
on Sep 20, 2026
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.