Skip to content

fix(typedarray): throw on prototype accessor reads off-instance (#4140)#4174

Merged
proggeramlug merged 1 commit into
mainfrom
fix-4140-ta-proto-getters
Jun 3, 2026
Merged

fix(typedarray): throw on prototype accessor reads off-instance (#4140)#4174
proggeramlug merged 1 commit into
mainfrom
fix-4140-ta-proto-getters

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

Closes the not-typedarray-object family in #4140

PR #4143 fixed the headline BYTES_PER_ELEMENT gap. The one remaining
checklist item it marked "could not reproduce" — not-typedarray-object.js
(×11 concrete types) — was tested wrong (it called the abstract %TypedArray%()
rather than reading the prototype's accessor). It is in fact still failing on
main:

Uint8Array.prototype.buffer       // Node: throws TypeError ; Perry: returned undefined
Uint8Array.prototype.byteLength   // Node: throws TypeError ; Perry: returned undefined
Uint8Array.prototype.byteOffset   // Node: throws TypeError ; Perry: returned undefined
Uint8Array.prototype.length       // Node: throws TypeError ; Perry: returned undefined

The prototype object is an ordinary object with no [[ViewedArrayBuffer]]
slot, so reading these accessors off it must throw TypeError.

Root cause

The four %TypedArray%.prototype getters are installed as reflection-only
builtin accessors that don't flip the ACCESSORS_IN_USE hot-path gate (#2060),
and the per-kind prototypes carry no recorded [[Prototype]] link in the
chain the property getter walks — they reach the shared %TypedArray%.prototype
only via Object.getPrototypeOf's OBJ_FLAG_TYPED_ARRAY_PROTO flag. So a plain
value read off the prototype never reached the inherited accessor and returned
the empty slot.

Fix

In js_object_get_field_by_name, for the four accessor names, resolve the
getter off the shared intrinsic when obj is the intrinsic itself or any object
flagged OBJ_FLAG_TYPED_ARRAY_PROTO. Real instances short-circuit far earlier,
so the receiver here is never a concrete typed array and the getter throws —
matching Node. Guarded by a cheap key filter + heap-pointer / GC_TYPE_OBJECT
check so ordinary reads (and non-pointer obj values threaded through the
generic getter) pay nothing and are never dereferenced.

Verification

  • New fixture typedarray-prototype-accessor-throws.ts — all 11 types, the
    intrinsic, an Object.create(Uint8Array.prototype) derived object, and real
    instances — byte-identical to node --experimental-strip-types.
  • globals node-suite sweep on an isolated build: PASS=40 / FAIL=9, the exact
    same 9 pre-existing failures
    as the unmodified base (no new regressions); the
    new fixture and the fix(typedarray): BYTES_PER_ELEMENT own props on constructor + prototype (#4140) #4143 typedarray-constructor-own-props.ts both pass.
  • cargo test -p perry-runtime green; cargo fmt --check clean.

Out of scope (pre-existing, filed-worthy separately)

  • (arr as any).length = -1 on a regular array segfaults — reproduces on a clean
    main build (both NO_AUTO_OPTIMIZE and auto-optimize), unrelated to this
    change. This is what makes array-length-invalid.ts / structured-clone-transfer.ts
    fail in the sweep above.
  • propertyIsEnumerable on a TA prototype returns [object Object] not a boolean.
  • Function.prototype-style buffer getter .call(realInstance) throws (Perry
    doesn't fully model ArrayBuffer behind a view).

The `%TypedArray%.prototype` `buffer`/`byteLength`/`byteOffset`/`length`
getters are installed as reflection-only builtin accessors that deliberately
don't flip the `ACCESSORS_IN_USE` hot-path gate (#2060). On top of that, the
per-kind prototypes (`Uint8Array.prototype`, …) carry no recorded
`[[Prototype]]` link in the chain the property getter walks — they resolve to
the shared `%TypedArray%.prototype` only through `Object.getPrototypeOf`'s
`OBJ_FLAG_TYPED_ARRAY_PROTO` flag. So a plain value read off the prototype
(`Uint8Array.prototype.buffer`) never reached the inherited accessor and
silently returned the empty slot (`undefined`) instead of Node's `TypeError`
(test262 `built-ins/TypedArrayConstructors/<T>/prototype/not-typedarray-object.js`,
11 types).

Resolve the getter off the shared intrinsic for both the intrinsic itself and
any object flagged `OBJ_FLAG_TYPED_ARRAY_PROTO`. A genuine concrete typed
array short-circuits far earlier in `js_object_get_field_by_name`, so the
receiver here is never a real instance and the getter throws — matching Node.
Guarded by a cheap key filter + heap-pointer/`GC_TYPE_OBJECT` check so ordinary
reads (and non-pointer `obj` values threaded through the generic getter) pay
nothing and are never dereferenced.

New parity fixture `typedarray-prototype-accessor-throws.ts` exercises all 11
typed-array types, the intrinsic, an `Object.create`-derived object, and real
instances; byte-identical to `node --experimental-strip-types`.
@proggeramlug proggeramlug merged commit 516c578 into main Jun 3, 2026
11 checks passed
@proggeramlug proggeramlug deleted the fix-4140-ta-proto-getters branch June 3, 2026 03:09
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