Skip to content

INT32-tagged number is bit-identical to a class-ref for the same value: a crafted NaN whose payload equals a REGISTERED class id is indistinguishable from a real class reference #10601

Description

@proggeramlug

Found while verifying a CodeRabbit finding on PR #10592 (class_ref_id validation for the
instanceof dynamic-RHS classification, crates/perry-runtime/src/object/instanceof.rs). That fix
is correct and landed, but exposed a deeper issue this ticket is for.

Background

Perry's NaN-boxing reserves the tag band 0x7FFE_xxxx_xxxx_xxxx (top 16 bits) for INT32-tagged
class references — see class_constructor_ref_value/class_ref_id in
crates/perry-runtime/src/object/native_module/class_ref_values.rs. A JS-visible number can carry
the exact same bit pattern: any signaling-NaN payload is a legal IEEE-754 double, and user code can
construct one directly via DataView.

Reproduction (direct, no array/loop involved)

const dv = new DataView(new ArrayBuffer(8));
dv.setUint32(0, 0x7ffe0000, false);
dv.setUint32(4, 5, false);
const crafted: any = dv.getFloat64(0, false);
console.log(typeof crafted, Number.isNaN(crafted));

Expected (Node 26.5.1)

number true

Actual (Perry, this build)

number false

typeof is correct, but Number.isNaN is wrong — Perry does not recognize this exact bit pattern as
NaN. This is consistent with the tag band being reserved for an internal representation (class refs)
rather than treated as a generic double.

A second, harder-to-pin-down symptom: context-dependent instanceof misclassification

With the class_ref_id validation fix from #10592 applied, the same crafted value used directly as an
instanceof RHS correctly throws (Right-hand side of 'instanceof' is not an object), matching Node.
But inside test-files/test_gap_10479_instanceof_value_kinds.ts's full badRhs loop (a [string, any][] array literal with 7 entries, this crafted value as the 7th, iterated with for...of over two
nested loops), the SAME construction misbehaves:

sso3 instanceof craftedNaN: true        (Node/expected: throws TypeError)
newA instanceof craftedNaN: false       (Node/expected: throws TypeError)
null instanceof craftedNaN: false       (Node/expected: throws TypeError)

This does not reproduce in a minimal 2-entry version of the same array/loop shape ([["{}", {}], ["craftedNaN", crafted]] misbehaves correctly / matches Node) — only the full ~200-line test file
context triggers it. That rules out a pure "array storage corrupts the NaN payload" theory (a minimal
array-storage repro round-trips the value fine) and points at something in the broader file — likely a
whole-program codegen specialization or inlining decision that changes how the dynamic
instanceof/class_ref_id classification resolves R once enough distinct call shapes of x instanceof R exist in one compilation unit. Not root-caused further; flagging with the two data points
above (isolated 2-entry case: correct; full-file 7-entry case: wrong) for whoever picks this up.

Suspected area

crates/perry-runtime/src/object/instanceof.rs (js_instanceof_dynamic's RHS classification) and/or
perry-codegen's handling of dynamic instanceof call sites when a function/module has many distinct
shapes of it — possibly PERRY_NO_AUTO_OPTIMIZE-sensitive (not checked either way this session).
Also worth checking whether Number.isNaN/general double round-tripping through DataView has a
broader gap for any payload in the 0x7FF8-0x7FFF band, independent of instanceof.

Impact

Low in practice — requires deliberately constructing a NaN payload via DataView to overlap Perry's
internal tag space, which is not something ordinary JS code does. Not currently known to break a
specific npm package.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    package-auditFound by the 2026 package audit: compiling real npm packages from source instead of native bindings

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions