Skip to content

fix(runtime): break Object.prototype.hasOwnProperty self-dispatch recursion (#4276)#4282

Closed
proggeramlug wants to merge 3 commits into
mainfrom
worktree-fix-4276-call-bind-builtin-proto
Closed

fix(runtime): break Object.prototype.hasOwnProperty self-dispatch recursion (#4276)#4282
proggeramlug wants to merge 3 commits into
mainfrom
worktree-fix-4276-call-bind-builtin-proto

Conversation

@proggeramlug
Copy link
Copy Markdown
Contributor

Summary

Fixes #4276. The "uncurry-this" idiom
Function.prototype.call.bind(Object.prototype.hasOwnProperty) returned an empty
[object Object] (typeof "object") instead of invoking the method — but only
when the eventual receiver was a builtin prototype object such as
Object.prototype or Error.prototype / a NativeError.prototype. Plain
objects, Array.prototype, and Math already worked.

This idiom is the backbone of test262's propertyHelper.js
(__hasOwnProperty = Function.prototype.call.bind(Object.prototype.hasOwnProperty)),
so it gated verifyProperty for every descriptor test subjecting
Object.prototype / Error.prototype.

Root cause

Object.prototype (and every Error/NativeError prototype) carries its own
hasOwnProperty as a thunk (object_prototype_has_own_property_thunk) that
reads IMPLICIT_THIS and re-dispatches js_native_call_method(this, "hasOwnProperty"). When the receiver is one of those prototypes, the
dispatcher's own-field scan re-found the hasOwnProperty field, called the thunk
again, and recursed until the call-depth guard (MAX_CALL_METHOD_DEPTH = 512)
bailed and returned the empty NULL_OBJECT_BYTES sentinel — which stringifies as
[object Object].

Plain objects don't carry an own hasOwnProperty, so the field scan missed and
the real "hasOwnProperty" arm computed the answer. That's why only
builtin-prototype receivers regressed, and why the direct .call form (folded by
codegen) was unaffected.

Fix

In the own-field scan of js_native_call_method, skip a field whose stored
closure is the self-redispatching proto thunk and fall through to the real
"hasOwnProperty" arm. A small predicate
(is_self_redispatching_proto_thunk) in global_this.rs identifies the thunk by
func-ptr. Only hasOwnProperty's thunk re-dispatches by name; the sibling
Object.prototype thunks (toString, valueOf, propertyIsEnumerable,
isPrototypeOf, toLocaleString) delegate to dedicated js_object_* helpers
and never recurse, so they're deliberately left untouched.

Verification

  • Repro from the issue now matches Node (true/boolean, no [object Object]).
  • New parity test test-parity/node-suite/globals/uncurry-this-builtin-proto.ts
    — byte-identical with node --experimental-strip-types. Covers
    Object.prototype, Error.prototype, TypeError.prototype,
    RangeError.prototype, the anchor receivers (plain/Array.prototype/Math),
    and the other uncurried Object.prototype methods (guarding against
    over-reach).
  • cargo test --release -p perry-runtime — 977 passed, 0 failed.
  • Confirmed test_gap_3716_uncurry_this's pre-existing line-8 gap
    (typeof call.bind off a reified Function.prototype.call value — a separate
    reification gap) is byte-unchanged by this fix.

Note

The companion fix-error-descriptors branch referenced in the issue (Error
instance + prototype descriptor parity) becomes observable through test262 once
this dispatch fix lands.

@proggeramlug
Copy link
Copy Markdown
Contributor Author

Closing as redundant. #4276 was already fixed by #4279 (merged), which makes the proto thunks call their underlying ops directly — no re-dispatch, so the recursion this PR guards against can't occur on main. This PR's approach (re-introduce the js_native_call_method re-dispatch + add a skip guard) would revert main's simpler fix for no behavioral gain. The superior test coverage from this branch is salvaged in #4293 (test-only, byte-identical to node on main).

@proggeramlug proggeramlug deleted the worktree-fix-4276-call-bind-builtin-proto branch June 3, 2026 19:51
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.

runtime: Function.prototype.call.bind(fn) returns receiver instead of result for builtin-prototype receivers (gates verifyProperty)

1 participant