diff --git a/crates/perry-runtime/src/object/global_this.rs b/crates/perry-runtime/src/object/global_this.rs index 0d9c621145..e81a61a0d0 100644 --- a/crates/perry-runtime/src/object/global_this.rs +++ b/crates/perry-runtime/src/object/global_this.rs @@ -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, diff --git a/crates/perry-runtime/src/object/native_call_method.rs b/crates/perry-runtime/src/object/native_call_method.rs index 54d654c8ec..5b6b80c984 100644 --- a/crates/perry-runtime/src/object/native_call_method.rs +++ b/crates/perry-runtime/src/object/native_call_method.rs @@ -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 diff --git a/test-parity/node-suite/globals/uncurry-this-builtin-proto.ts b/test-parity/node-suite/globals/uncurry-this-builtin-proto.ts new file mode 100644 index 0000000000..a59570984b --- /dev/null +++ b/test-parity/node-suite/globals/uncurry-this-builtin-proto.ts @@ -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"));