Skip to content

Commit 223e32b

Browse files
author
Ralph
committed
fix(runtime): #5844 — Proxy trap dispatch fixes across getOwnPropertyDescriptor, has, set, and setPrototypeOf
Fixes 12 of 33 test262 built-ins/Proxy failures, zero regressions (verified against built-ins/Proxy, built-ins/Object, built-ins/Reflect, language/expressions/in — 219/219/3132/101/15 pass respectively). Root causes, all variations on "a Proxy is a small registered id, not a real heap pointer, and several generic object-machinery code paths either mis-treated it as one or never checked for it at all": - js_reflect_get_own_property_descriptor: the missing-trap fallback read the target's descriptor via the raw ObjectHeader path even when the target was itself a Proxy; now recurses through the Reflect entry point so a chain of trap-less proxies forwards correctly. - ordinary_has_property (the `in` operator's prototype-chain walk) and its array-fast-path counterpart didn't recognize a Proxy sitting in the recorded `[[Prototype]]` chain, silently returning false (or reading garbage) instead of dispatching the proxy's `has` trap. - ordinary_set_with_receiver's prototype-chain walk had the same gap on the write side, and `js_proxy_set`/`Reflect.set` didn't thread a distinct `receiver` through to the trap when a Proxy was reached via a prototype hop rather than as the direct assignment target. - Object.create(proto) and Object.setPrototypeOf(obj, proto) rejected a Proxy `proto` argument outright (their "is this an object" checks don't recognize the small registered id), and Object.create had no path to record a Proxy prototype at all — it can't be modeled via the synthetic-class-id machinery used for real prototype objects, so route it through the same static-prototype side table setPrototypeOf uses. - Object.setPrototypeOf's cycle-detection walk called `[[GetPrototypeOf]]` unconditionally while probing the candidate prototype's chain, including on a Proxy — invoking its trap as an unrelated side effect of cycle-safety bookkeeping (OrdinarySetPrototypeOf step 7.b.ii.1 says to stop the walk there instead). Also fixed, found while debugging the above with a fresh (Proxy-free) repro: the cycle-detection loop's Floyd's tortoise-and-hare walk only guarded `tortoise` against an already-null position before advancing; `hare` (which steps twice per iteration) had no equivalent guard, so a 2-hop-to-null prototype chain re-advanced an already-null `hare` and threw "Cannot convert undefined or null to object" on a plain, entirely ordinary `Object.setPrototypeOf({}, {foo: 1})`. Refs #5844.
1 parent 4def199 commit 223e32b

6 files changed

Lines changed: 158 additions & 42 deletions

File tree

crates/perry-runtime/src/object/descriptors.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1368,6 +1368,7 @@ pub extern "C" fn js_object_create_with_props(proto_value: f64, props_value: f64
13681368
let proto_jv = crate::value::JSValue::from_bits(proto_value.to_bits());
13691369
let proto_is_symbol = unsafe { crate::symbol::js_is_symbol(proto_value) != 0 };
13701370
let proto_ok = proto_jv.is_null()
1371+
|| crate::proxy::js_proxy_is_proxy(proto_value) != 0
13711372
|| (!proto_is_symbol
13721373
&& (unsafe { value_is_object_like(proto_value) }
13731374
|| super::class_ref_id(proto_value).is_some()));

crates/perry-runtime/src/object/field_get_set/has_property.rs

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,18 @@ pub extern "C" fn js_object_has_property(obj: f64, key: f64) -> f64 {
295295
// / `arr.hasOwnProperty(i)` stay correct after `arr.length = N`.
296296
let arr = crate::array::clean_arr_ptr(obj_ptr as *const crate::array::ArrayHeader);
297297
let length = (*arr).length;
298+
// A Proxy installed as the array's `[[Prototype]]`
299+
// (`Object.setPrototypeOf(arr, proxy)`) — `array_spec_has_index`
300+
// only recognizes a *real array* custom prototype, so a Proxy
301+
// hop is silently treated as absent. Recover it here so the
302+
// idx/string-key misses below can fall back to the proxy's
303+
// `[[HasProperty]]` instead of a bare `false` (ECMA-262 10.1.7.1
304+
// step 5).
305+
let proxy_proto =
306+
super::super::prototype_chain::object_static_prototype(obj_ptr as usize)
307+
.filter(|&b| (b >> 48) == 0x7FFD)
308+
.map(f64::from_bits)
309+
.filter(|&v| crate::proxy::js_proxy_is_proxy(v) != 0);
298310
// Numeric key: extract the index. Accept both NaN-boxed i32
299311
// and plain f64 (e.g. literal `1`) provided it's a
300312
// non-negative integer in range.
@@ -329,6 +341,24 @@ pub extern "C" fn js_object_has_property(obj: f64, key: f64) -> f64 {
329341
if crate::array::object_prototype_has_index_prop(idx) {
330342
return nanbox_true;
331343
}
344+
if let Some(proxy) = proxy_proto {
345+
let idx_str = idx.to_string();
346+
let key_ptr = crate::string::js_string_from_bytes(
347+
idx_str.as_ptr(),
348+
idx_str.len() as u32,
349+
);
350+
let key_val = f64::from_bits(
351+
crate::value::js_nanbox_string(key_ptr as i64).to_bits(),
352+
);
353+
return if crate::value::js_is_truthy(crate::proxy::js_proxy_has(
354+
proxy, key_val,
355+
)) != 0
356+
{
357+
nanbox_true
358+
} else {
359+
nanbox_false
360+
};
361+
}
332362
return nanbox_false;
333363
}
334364
if key_val.is_any_string() {
@@ -353,12 +383,21 @@ pub extern "C" fn js_object_has_property(obj: f64, key: f64) -> f64 {
353383
{
354384
return nanbox_true;
355385
}
356-
return nanbox_false;
357-
}
358-
if array_prototype_property_value(key_name, obj_ptr as usize).is_some()
386+
} else if array_prototype_property_value(key_name, obj_ptr as usize)
387+
.is_some()
359388
{
360389
return nanbox_true;
361390
}
391+
if let Some(proxy) = proxy_proto {
392+
return if crate::value::js_is_truthy(crate::proxy::js_proxy_has(
393+
proxy, key,
394+
)) != 0
395+
{
396+
nanbox_true
397+
} else {
398+
nanbox_false
399+
};
400+
}
362401
}
363402
}
364403
}
@@ -497,6 +536,23 @@ unsafe fn ordinary_has_property(
497536
Some(b) if b == TAG_NULL => return false,
498537
Some(b) => {
499538
let top16 = b >> 48;
539+
// A Proxy prototype hop (ECMA-262 10.1.7.1 step 5: `Return ?
540+
// parent.[[HasProperty]](P)`) — the small registered proxy id is
541+
// NOT a real heap pointer, so continuing the raw-pointer walk
542+
// below would misread garbage (or crash). Dispatch through the
543+
// proxy's own `[[HasProperty]]` (trap, or its trap-less forward
544+
// through further proxy targets / the eventual real target) and
545+
// use its boolean result directly — that call already resolves
546+
// the rest of the chain.
547+
if top16 == 0x7FFD {
548+
let proto_val = f64::from_bits(b);
549+
if crate::proxy::js_proxy_is_proxy(proto_val) != 0 {
550+
let key_val =
551+
f64::from_bits(crate::value::js_nanbox_string(key as i64).to_bits());
552+
let result = crate::proxy::js_proxy_has(proto_val, key_val);
553+
return crate::value::js_is_truthy(result) != 0;
554+
}
555+
}
500556
let p = if top16 == 0x7FFD {
501557
(b & crate::value::POINTER_MASK) as usize
502558
} else if top16 == 0 && b > 0x10000 {

crates/perry-runtime/src/object/object_ops/define_properties.rs

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ pub extern "C" fn js_object_set_prototype_of(obj_value: f64, proto: f64) -> f64
167167
let proto_is_null = proto_bits == TAG_NULL;
168168
let proto_is_symbol = unsafe { crate::symbol::js_is_symbol(proto) != 0 };
169169
let proto_ok = proto_is_null
170+
|| crate::proxy::js_proxy_is_proxy(proto) != 0
170171
|| (!proto_is_symbol
171172
&& (unsafe { value_is_object_like(proto) }
172173
|| super::super::class_ref_id(proto).is_some()));
@@ -208,6 +209,17 @@ pub extern "C" fn js_object_set_prototype_of(obj_value: f64, proto: f64) -> f64
208209
const TAG_NULL_U64: u64 = 0x7FFC_0000_0000_0002;
209210
let advance = |bits: u64| -> u64 {
210211
let val = f64::from_bits(bits);
212+
// OrdinarySetPrototypeOf step 7.b.ii.1: if `p`'s [[GetPrototypeOf]]
213+
// is not the ordinary internal method (a Proxy's is exotic — it may
214+
// run arbitrary trap code), the walk stops here without invoking it.
215+
// Without this guard the cycle-detection walk called the target's
216+
// `getPrototypeOf` trap as a side effect of unrelated cycle-safety
217+
// bookkeeping (test262 has/call-in-prototype-index.js,
218+
// set/call-parameters-prototype-index.js observe a `getPrototypeOf`
219+
// trap the test handler never installs).
220+
if crate::proxy::js_proxy_is_proxy(val) != 0 {
221+
return TAG_NULL_U64;
222+
}
211223
let next = js_object_get_prototype_of(val);
212224
let nb = next.to_bits();
213225
if nb == TAG_NULL_U64 {
@@ -227,13 +239,20 @@ pub extern "C" fn js_object_set_prototype_of(obj_value: f64, proto: f64) -> f64
227239
if tortoise == TAG_NULL_U64 {
228240
break;
229241
}
230-
// Advance tortoise one step, hare two steps. Guard the second
231-
// advance: if the first step lands on null, calling advance(null)
232-
// would invoke js_object_get_prototype_of(null) which throws
233-
// "Cannot convert undefined or null to object" (test262
234-
// setPrototypeOf/success.js — plain object proto chain ends at null).
242+
// Advance tortoise one step, hare two steps. Guard every advance
243+
// against an already-null position: calling `advance(null)` would
244+
// invoke `js_object_get_prototype_of(null)`, which throws "Cannot
245+
// convert undefined or null to object" (test262
246+
// setPrototypeOf/success.js — plain object proto chain ends at
247+
// null). `hare` reaches null one loop iteration before `tortoise`
248+
// does (it steps twice per iteration) and, unlike `tortoise`, has
249+
// no top-of-loop check — so a 2-hop-to-null chain (`proto` → some
250+
// object → `Object.prototype` → null) re-advanced an
251+
// already-`TAG_NULL_U64` `hare` on the next iteration.
235252
tortoise = advance(tortoise);
236-
hare = {
253+
hare = if hare == TAG_NULL_U64 {
254+
TAG_NULL_U64
255+
} else {
237256
let h1 = advance(hare);
238257
if h1 == TAG_NULL_U64 {
239258
TAG_NULL_U64

crates/perry-runtime/src/object/object_ops/prototype.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,27 @@ pub extern "C" fn js_object_create(proto_value: f64) -> f64 {
5151
// Set/Map/Regex source Perry can't model as a prototype) falls back
5252
// to the original behavior: a plain prototype-less object.
5353
const POINTER_TAG: u64 = 0x7FFD_0000_0000_0000;
54+
55+
// `Object.create(proxy)` — a Proxy is a small registered id, not a real
56+
// heap pointer, so the synthetic-class-id modeling below (which stores a
57+
// REAL prototype pointer) can't represent it, and the `is_valid_obj_ptr`
58+
// check would reject it outright (falling back to a plain, prototype-less
59+
// object — wrong: reads/writes/`in` on the result must still route through
60+
// the proxy). Record it in the SAME observable `[[Prototype]]` side table
61+
// `Object.setPrototypeOf` uses instead: a plain (class_id 0, non-null-proto)
62+
// object whose prototype hop the generic chain walks (`ordinary_has_property`,
63+
// `own_set_descriptor`'s `prototype_of_for_set`, field-get) already resolve
64+
// through the proxy's traps. (test262 has/call-in-prototype.js,
65+
// has/call-object-create.js, set/call-parameters-prototype.js.)
66+
if crate::proxy::js_proxy_is_proxy(proto_value) != 0 {
67+
let obj = js_object_alloc(0, 0);
68+
crate::object::prototype_chain::object_set_static_prototype(
69+
obj as usize,
70+
proto_value.to_bits(),
71+
);
72+
return f64::from_bits((obj as u64) | POINTER_TAG);
73+
}
74+
5475
let mut class_id: u32 = 0;
5576
let proto_bits = proto_value.to_bits();
5677
if (proto_bits & 0xFFFF_0000_0000_0000) == POINTER_TAG {

crates/perry-runtime/src/proxy.rs

Lines changed: 43 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -711,6 +711,21 @@ fn target_get(target: f64, key: f64) -> f64 {
711711
/// forward to the target directly.
712712
#[no_mangle]
713713
pub extern "C" fn js_proxy_set(proxy_boxed: f64, key: f64, value: f64) -> f64 {
714+
proxy_set_with_receiver(proxy_boxed, key, value, proxy_boxed)
715+
}
716+
717+
/// Proxy `[[Set]]` (ECMA-262 §10.5.9) with an explicit `Receiver`, distinct
718+
/// from `proxy_boxed` itself. Reached when a Proxy sits partway up another
719+
/// object's `[[Prototype]]` chain: `OrdinarySetWithOwnDescriptor` forwards to
720+
/// `parent.[[Set]](P, V, Receiver)` with the ORIGINAL receiver, not `parent`
721+
/// (test262 set/call-parameters-prototype.js — `Object.create(proxy).prop = v`
722+
/// must call the trap with the heir object as `receiver`, not the proxy).
723+
pub(crate) fn proxy_set_with_receiver(
724+
proxy_boxed: f64,
725+
key: f64,
726+
value: f64,
727+
receiver: f64,
728+
) -> f64 {
714729
let id = match lookup(proxy_boxed) {
715730
Some(id) => id,
716731
None => return f64::from_bits(TAG_FALSE),
@@ -740,14 +755,15 @@ pub extern "C" fn js_proxy_set(proxy_boxed: f64, key: f64, value: f64) -> f64 {
740755
let target_h = scope.root_nanbox_f64(target);
741756
let key_h = scope.root_nanbox_f64(key);
742757
let value_h = scope.root_nanbox_f64(value);
758+
let receiver_h = scope.root_nanbox_f64(receiver);
743759
let trap_result = call_trap(
744760
handler,
745761
trap,
746762
&[
747763
target_h.get_nanbox_f64(),
748764
key_h.get_nanbox_f64(),
749765
value_h.get_nanbox_f64(),
750-
proxy_boxed,
766+
receiver_h.get_nanbox_f64(),
751767
],
752768
);
753769
// A falsy trap result means the assignment failed; no invariant check.
@@ -765,22 +781,21 @@ pub extern "C" fn js_proxy_set(proxy_boxed: f64, key: f64, value: f64) -> f64 {
765781
// itself a Proxy, recurse through the proxy dispatch (its own trap or
766782
// target) rather than `ordinary_set`, which would deref the fake pointer.
767783
if lookup(target).is_some() {
768-
return js_proxy_set(target, key, value);
784+
return proxy_set_with_receiver(target, key, value, receiver);
769785
}
770-
reflect_ordinary_set(target, key, value)
771-
}
772-
773-
/// Perform an ordinary (non-proxy) `[[Set]]` and report success as a NaN-boxed
774-
/// boolean, without throwing on a non-writable / non-extensible target the way
775-
/// strict-mode assignment does (#2756 / #615). Returns `false` when the write
776-
/// cannot be applied.
777-
fn reflect_ordinary_set_property_key(target: f64, property_key: f64, value: f64) -> f64 {
778-
nanbox_bool(ordinary_set_with_receiver(
779-
target,
780-
property_key,
781-
value,
782-
target,
783-
))
786+
let scope = crate::gc::RuntimeHandleScope::new();
787+
let target_handle = scope.root_nanbox_f64(target);
788+
let key_handle = scope.root_nanbox_f64(key);
789+
let value_handle = scope.root_nanbox_f64(value);
790+
let receiver_handle = scope.root_nanbox_f64(receiver);
791+
let property_key_handle = scope
792+
.root_nanbox_f64(unsafe { crate::object::js_to_property_key(key_handle.get_nanbox_f64()) });
793+
reflect_ordinary_set_with_receiver(
794+
target_handle.get_nanbox_f64(),
795+
property_key_handle.get_nanbox_f64(),
796+
value_handle.get_nanbox_f64(),
797+
receiver_handle.get_nanbox_f64(),
798+
)
784799
}
785800

786801
/// `Reflect.set` with an explicit receiver: OrdinarySet(target, P, V,
@@ -799,20 +814,6 @@ pub(crate) fn reflect_ordinary_set_with_receiver(
799814
))
800815
}
801816

802-
fn reflect_ordinary_set(target: f64, key: f64, value: f64) -> f64 {
803-
let scope = crate::gc::RuntimeHandleScope::new();
804-
let target_handle = scope.root_nanbox_f64(target);
805-
let key_handle = scope.root_nanbox_f64(key);
806-
let value_handle = scope.root_nanbox_f64(value);
807-
let property_key_handle = scope
808-
.root_nanbox_f64(unsafe { crate::object::js_to_property_key(key_handle.get_nanbox_f64()) });
809-
reflect_ordinary_set_property_key(
810-
target_handle.get_nanbox_f64(),
811-
property_key_handle.get_nanbox_f64(),
812-
value_handle.get_nanbox_f64(),
813-
)
814-
}
815-
816817
fn target_set(target: f64, key: f64, value: f64) {
817818
let property_key = unsafe { crate::object::js_to_property_key(key) };
818819
if unsafe { crate::symbol::js_is_symbol(property_key) } != 0 {
@@ -1331,6 +1332,18 @@ fn ordinary_set_with_receiver(target: f64, key: f64, value: f64, receiver: f64)
13311332

13321333
let mut current = target;
13331334
for _ in 0..64 {
1335+
// A Proxy hop in the prototype chain (`OrdinarySetWithOwnDescriptor`
1336+
// step 2.a-b: `parent = O.[[GetPrototypeOf]](); return
1337+
// parent.[[Set]](P, V, Receiver)`) — dispatch the FULL `[[Set]]`
1338+
// algorithm (trap, or its own trap-less forward) on `current` with the
1339+
// ORIGINAL `receiver`, rather than continuing the manual own-descriptor
1340+
// walk below, which would misread the small proxy id as a heap pointer
1341+
// (test262 set/call-parameters-prototype.js).
1342+
if lookup(current).is_some() {
1343+
return crate::value::js_is_truthy(proxy_set_with_receiver(
1344+
current, key, value, receiver,
1345+
)) != 0;
1346+
}
13341347
// Integer-Indexed exotic [[Set]] (§10.4.5.5): a typed array in the
13351348
// chain intercepts a canonical numeric index key — the prototype
13361349
// chain is NEVER consulted for it. `SameValue(O, Receiver)` writes

crates/perry-runtime/src/proxy/reflect.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use super::{
22
closure_from, coerce_trap_bool, extract_pointer, handler_trap, is_callable_function,
3-
js_closure_call0, js_closure_call2, js_proxy_delete, js_proxy_get, js_proxy_has, js_proxy_set,
4-
lookup, nanbox_bool, reflect_non_object_typeerror, reflect_ordinary_delete_property_key,
3+
js_closure_call0, js_closure_call2, js_proxy_delete, js_proxy_get, js_proxy_has, lookup,
4+
nanbox_bool, reflect_non_object_typeerror, reflect_ordinary_delete_property_key,
55
reflect_ordinary_set_with_receiver, reflect_value_is_object, revoked_return,
66
target_get_property_key, throw_type_error, PROXIES, TAG_NULL, TAG_TRUE, TAG_UNDEFINED,
77
};
@@ -107,7 +107,7 @@ pub extern "C" fn js_reflect_set(target: f64, key: f64, value: f64, receiver: f6
107107
}
108108
};
109109
if lookup(target).is_some() {
110-
return js_proxy_set(target, property_key, value);
110+
return super::proxy_set_with_receiver(target, property_key, value, receiver);
111111
}
112112
reflect_ordinary_set_with_receiver(target, property_key, value, receiver)
113113
}
@@ -348,6 +348,12 @@ pub extern "C" fn js_reflect_get_own_property_descriptor(target: f64, key: f64)
348348
let trap = handler_trap(handler, "getOwnPropertyDescriptor");
349349
let trap_bits = trap.to_bits();
350350
if trap_bits == TAG_UNDEFINED || trap_bits == TAG_NULL {
351+
// No trap — forward to the target's [[GetOwnProperty]]. When the target
352+
// is itself a Proxy, recurse through the Reflect entry point rather than
353+
// the ordinary object path, which would deref the fake proxy pointer.
354+
if lookup(inner).is_some() {
355+
return js_reflect_get_own_property_descriptor(inner, property_key);
356+
}
351357
return crate::object::js_object_get_own_property_descriptor(inner, property_key);
352358
}
353359
if !is_callable_function(trap) {

0 commit comments

Comments
 (0)