Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions crates/perry-runtime/src/object/global_this.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1048,6 +1048,22 @@ extern "C" fn object_prototype_has_own_property_thunk(
}
}

/// #4276: `object_prototype_has_own_property_thunk` re-dispatches through
/// `js_native_call_method(this, "hasOwnProperty")`. When the receiver is a
/// builtin prototype object (`Object.prototype`, `Error.prototype`, every
/// `NativeError.prototype`, …) that carries this thunk as an OWN field, the
/// dispatcher's own-field scan would re-find the field and call the thunk
/// again, recursing until the call-depth guard bails and returns the empty
/// `NULL_OBJECT_BYTES` sentinel — surfacing as `[object Object]`. The field
/// scan in `js_native_call_method` consults this predicate to skip the
/// self-dispatching thunk and fall through to the real `"hasOwnProperty"`
/// arm instead. Exposed by the "uncurry-this" idiom
/// `Function.prototype.call.bind(Object.prototype.hasOwnProperty)` applied to a
/// builtin-prototype receiver (test262's `propertyHelper.js` `verifyProperty`).
pub(crate) fn is_self_redispatching_proto_thunk(func_ptr: *const u8) -> bool {
func_ptr == object_prototype_has_own_property_thunk as *const u8
}

extern "C" fn object_prototype_property_is_enumerable_thunk(
_closure: *const crate::closure::ClosureHeader,
key: f64,
Expand Down
27 changes: 27 additions & 0 deletions crates/perry-runtime/src/object/native_call_method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2577,6 +2577,33 @@ pub unsafe extern "C" fn js_native_call_method(
let key_val = crate::array::js_array_get(keys, i as u32);
if crate::string::js_string_key_matches_bytes(key_val, method_bytes) {
let field_val = js_object_get_field(obj as *mut _, i as u32);
// #4276: a builtin prototype object
// (`Object.prototype`, `Error.prototype`, every
// `NativeError.prototype`) carries its own
// `hasOwnProperty` as a thunk that just re-calls
// `js_native_call_method(this, "hasOwnProperty")`.
// Invoking it here would re-enter this same field
// scan, re-find the field, and recurse until the
// call-depth guard bails — returning the empty
// `NULL_OBJECT_BYTES` sentinel (`[object Object]`).
// Skip such self-dispatching proto thunks so the
// real `"hasOwnProperty"` arm below computes the
// answer. Reached via the uncurry-this idiom
// `Function.prototype.call.bind(
// Object.prototype.hasOwnProperty)` applied to a
// builtin-prototype receiver (test262
// `verifyProperty`).
let field_ptr = crate::value::js_nanbox_get_pointer(f64::from_bits(
field_val.bits(),
)) as usize;
if crate::closure::is_closure_ptr(field_ptr)
&& super::global_this::is_self_redispatching_proto_thunk(
(*(field_ptr as *const crate::closure::ClosureHeader))
.func_ptr,
)
{
break;
}
// Always try the field as a callable —
// `js_native_call_value` validates
// CLOSURE_MAGIC internally and safely
Expand Down
56 changes: 56 additions & 0 deletions test-parity/node-suite/globals/uncurry-this-builtin-proto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// #4276 — the "uncurry-this" idiom
// `Function.prototype.call.bind(Object.prototype.hasOwnProperty)` returned the
// wrong value (an empty `[object Object]`) instead of invoking the method, but
// ONLY when the eventual receiver was a builtin prototype object such as
// `Object.prototype` or `Error.prototype`. Plain objects, `Array.prototype`,
// and `Math` already worked. Root cause: those prototypes carry their own
// `hasOwnProperty` as a thunk that re-dispatches `js_native_call_method(this,
// "hasOwnProperty")`; the dispatcher's own-field scan re-found the field and
// recursed until the call-depth guard bailed, returning the empty-object
// sentinel.
//
// This idiom is the backbone of test262's `propertyHelper.js` `verifyProperty`,
// so it gated every descriptor test subjecting `Object.prototype` /
// `Error.prototype`.

const hasOwn = Function.prototype.call.bind(Object.prototype.hasOwnProperty);

// Plain object + Array.prototype + Math already worked; keep them as anchors.
console.log("plain own", hasOwn({ a: 1 }, "a"));
console.log("plain missing", hasOwn({ a: 1 }, "b"));
console.log("Array.prototype push", hasOwn(Array.prototype, "push"));
console.log("Math abs", hasOwn(Math, "abs"));

// The regressing receivers: builtin prototype objects.
console.log("Object.prototype toString", hasOwn(Object.prototype, "toString"));
console.log("Object.prototype hasOwnProperty", hasOwn(Object.prototype, "hasOwnProperty"));
console.log("Object.prototype missing", hasOwn(Object.prototype, "nope"));
console.log("Error.prototype message", hasOwn(Error.prototype, "message"));
console.log("Error.prototype name", hasOwn(Error.prototype, "name"));
console.log("Error.prototype toString", hasOwn(Error.prototype, "toString"));
console.log("Error.prototype missing", hasOwn(Error.prototype, "zzz"));
console.log("TypeError.prototype name", hasOwn(TypeError.prototype, "name"));
console.log("RangeError.prototype constructor", hasOwn(RangeError.prototype, "constructor"));

// typeof of the result must be "boolean", not "object" (the bug returned the
// empty-object sentinel).
const r = hasOwn(Object.prototype, "toString");
console.log("typeof result", typeof r);

// The same uncurry indirection over the OTHER Object.prototype methods must
// keep working (these never recursed, but guard against the fix over-reaching).
const toStr = Function.prototype.call.bind(Object.prototype.toString);
console.log("uncurry toString array", toStr([]));
console.log("uncurry toString plain", toStr({}));

const isEnum = Function.prototype.call.bind(Object.prototype.propertyIsEnumerable);
console.log("uncurry pie own", isEnum({ a: 1 }, "a"));
console.log("uncurry pie proto", isEnum(Object.prototype, "toString"));

const isProto = Function.prototype.call.bind(Object.prototype.isPrototypeOf);
console.log("uncurry isPrototypeOf", isProto(Object.prototype, {}));

// The direct `.call` form (already worked) must remain correct.
console.log("direct call", Object.prototype.hasOwnProperty.call(Object.prototype, "toString"));
console.log("method form", ({ a: 1 }).hasOwnProperty("a"));
console.log("method form missing", ({ a: 1 }).hasOwnProperty("toString"));