//! Indexed field get + accessor/prototype-property helpers. //! Pure relocation out of field_get_set.rs (issue #1103 split). use super::*; /// Get a field from an object by index /// /// #1129/#1136: the small-pointer guard below previously used a 16 MB /// floor (0x1000000), which rejected legitimate iOS-device heap /// pointers from libsystem_malloc — `splitDeepLink()` returning /// `{ segments }` and the caller destructuring `const { segments } = …` /// silently produced `undefined`. The real liveness check is the /// downstream `is_valid_obj_ptr` / `obj_type` validation; this gate /// only needs to keep the small-handle range and null/guard pages /// out before unsafe deref. 64 KB matches the bar used elsewhere in /// this module (e.g. `js_object_get_field_ic_miss`). #[no_mangle] pub extern "C" fn js_object_get_field(obj: *const ObjectHeader, field_index: u32) -> JSValue { let obj = { let b = obj as u64; let t = b >> 48; if t >= 0x7FF8 { if t == 0x7FFC || (b & 0x0000_FFFF_FFFF_FFFF) == 0 || (b & 0x0000_FFFF_FFFF_FFFF) < 0x10000 { return JSValue::undefined(); } (b & 0x0000_FFFF_FFFF_FFFF) as *const ObjectHeader } else { obj } }; if obj.is_null() || (obj as usize) < 0x10000 { return JSValue::undefined(); } unsafe { // Bounds check: check inline fields first, then overflow map let fc = crate::object::object_live_slot_count(obj); object_field_at_with_live(obj, field_index, fc) } } /// [`js_object_get_field`]'s body against a live inline-slot bound the caller /// already resolved (#8122): the by-name lookup tail and the parameter guard /// resolve the receiver's descriptor once per call and read every field they /// need against that bound, instead of paying `object_live_slot_count`'s /// shape-table probe per field. /// /// # Safety /// `obj` must be a live, non-null `GC_TYPE_OBJECT` and `live` its published /// live inline-slot bound. #[inline] pub(crate) unsafe fn object_field_at_with_live( obj: *const ObjectHeader, field_index: u32, live: u32, ) -> JSValue { if field_index >= live { // Check overflow map for fields that didn't fit in inline storage return match overflow_get(obj as usize, field_index as usize) { Some(bits) => JSValue::from_bits(bits), None => JSValue::undefined(), }; } // The published shape bound already proves this slot is inline. JSON // objects can legitimately have more than 10,000 slots (#10175); their // total field count is not a reason to reject an in-bounds read. let fields_ptr = (obj as *const u8).add(std::mem::size_of::()) as *const JSValue; let val = *fields_ptr.add(field_index as usize); // Guard: null POINTER_TAG (0x7FFD_0000_0000_0000) is never legitimate — replace with undefined if val.bits() == 0x7FFD_0000_0000_0000 { eprintln!( "[NULL_PTR_FIELD_GET] obj={:p} field_index={} class_id={} field_count={}", obj, field_index, (*obj).class_id, live ); return JSValue::undefined(); } val } pub(crate) unsafe fn own_data_field_by_name( obj: *const ObjectHeader, key: *const crate::StringHeader, ) -> Option { if key.is_null() { return None; } if obj.is_null() || !is_valid_obj_ptr(obj as *const u8) { return None; } let obj_gc = (obj as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; if (*obj_gc).obj_type != crate::gc::GC_TYPE_OBJECT { return None; } let keys_view = crate::object::object_keys(obj); let keys = keys_view.arr(); let keys_ptr = keys as usize; if keys.is_null() || (keys_ptr as u64) >> 48 != 0 || keys_ptr < 0x10000 { return None; } let keys_gc = (keys as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; if (*keys_gc).obj_type != crate::gc::GC_TYPE_ARRAY { return None; } let key_count = keys_view.count() as usize; if key_count > 65536 { return None; } let alloc_limit = std::cmp::max( crate::object::object_live_slot_count(obj), crate::object::INLINE_SLOT_FLOOR as u32, ) as usize; // #6759: shape-index + raw dense-slot scan, replacing the per-element // `js_array_get` + `js_string_key_matches` walk. This is the READ path's // copy of the scan that #8936 killed on the [[Set]]/delete side — an // isolated overwrite-loop profile still showed `js_array_get_f64` at 23.5% // self time, and the caller graph attributed it here. The shared helper // preserves #1781's SSO-key acceptance (its byte resolver is SSO-aware). if let Some(islot) = crate::object::keys_find_slot_by_key_ptr(keys, key_count as u32, key) { let i = islot as usize; { if i < alloc_limit { return Some(js_object_get_field(obj, i as u32)); } return Some(match overflow_get(obj as usize, i) { Some(bits) => JSValue::from_bits(bits), None => JSValue::undefined(), }); } } None } /// #2856 synthetic method reads + #9019 own-field shadowing for a Map/Set /// iterator receiver's property GET (extracted from /// `get_field_by_name_tail.rs`, which sits at the file-size cap). /// /// Ordinary [[Get]] order: an OWN property — user code can store one past /// the reserved floor since #9019 — shadows every synthetic method. This is /// also what makes user data properties on iterators readable at all: the /// old arm returned `undefined` for every non-`next` key without consulting /// own fields, so a stored value was write-only. `@@iterator` remains the only /// synthetic bound method: ordinary collection iterators do not have the /// generator-only `return`/`throw` methods (#9086). `next`, `return`, and /// `throw` deliberately resolve through the caller's generic scans (`None`), /// so the prototype chain remains authoritative; any other key is absent /// (`Some(undefined)`). pub(crate) unsafe fn map_set_iterator_property( obj: *const ObjectHeader, key: *const crate::StringHeader, ) -> Option { if let Some(v) = own_data_field_by_name(obj, key) { return Some(v); } let key_ptr = (key as *const u8).add(std::mem::size_of::()); let key_len = (*key).byte_len as usize; let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); let bind_name: Option<&'static [u8]> = match key_bytes { b"@@iterator" => Some(b"@@iterator"), _ => None, }; if let Some(name) = bind_name { let this_f64 = f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); let result = super::super::js_class_method_bind(this_f64, name.as_ptr(), name.len()); return Some(JSValue::from_bits(result.to_bits())); } if matches!(key_bytes, b"next" | b"return" | b"throw") { return None; } Some(JSValue::undefined()) } crate::perry_thread_local! { static OBJECT_PROTOTYPE_LOOKUP_DEPTH: std::cell::Cell = const { std::cell::Cell::new(0) }; } struct ObjectPrototypeLookupGuard; impl Drop for ObjectPrototypeLookupGuard { fn drop(&mut self) { OBJECT_PROTOTYPE_LOOKUP_DEPTH.with(|depth| { depth.set(depth.get().saturating_sub(1)); }); } } fn object_prototype_lookup_guard() -> Option { OBJECT_PROTOTYPE_LOOKUP_DEPTH.with(|depth| { if depth.get() != 0 { None } else { depth.set(1); Some(ObjectPrototypeLookupGuard) } }) } unsafe fn default_object_prototype_property_value( receiver_addr: usize, key: *const crate::StringHeader, ) -> Option { let _guard = object_prototype_lookup_guard()?; // The address cache deliberately avoids constructing globalThis on array // index-write hot paths. A real inherited Get/HasProperty miss cannot use // that shortcut: an unmaterialized Object.prototype still has its methods. // Bootstrap may allocate and collect, so keep both inputs rooted until the // intrinsic address has been resolved. The existing hot path stays intact // once this thread has a realm global. if !super::super::global_this_is_materialized() { let scope = crate::gc::RuntimeHandleScope::new(); let receiver_h = scope.root_nanbox_f64(crate::value::js_nanbox_pointer(receiver_addr as i64)); let key_h = scope.root_nanbox_f64(crate::value::nanbox_string_key(key)); super::super::js_get_global_this(); let proto_addr = crate::array::object_prototype_addr(); if proto_addr == 0 { return None; } let receiver_addr = crate::value::js_nanbox_get_pointer(receiver_h.get_nanbox_f64()) as usize; let key = crate::value::js_nanbox_get_pointer(key_h.get_nanbox_f64()) as *const crate::StringHeader; return prototype_property_value_with_guard(proto_addr, receiver_addr, key); } let proto_addr = crate::array::object_prototype_addr(); if proto_addr == 0 { return None; } prototype_property_value_with_guard(proto_addr, receiver_addr, key) } /// Read an inherited property while the caller holds /// [`ObjectPrototypeLookupGuard`]. Keeping guard acquisition outside this /// helper lets Error-family lookup resolve a lazy builtin prototype without a /// recursive ordinary-object fallback. unsafe fn prototype_property_value_with_guard( proto_addr: usize, receiver_addr: usize, key: *const crate::StringHeader, ) -> Option { // #7498: THIS IS THE FRAME `PERRY_GC_PROTECT_FROMSPACE=1` FAULTS IN on the // `[...obj.arr]` path — a 56-byte from-space `GC_TYPE_STRING`, i.e. `key`. // Both arguments are GC-managed and both are live across the call below // before their first use: the recursive `js_object_get_field_by_name` on // `Object.prototype` can run an accessor, which is user code, and user code // allocates. A copying minor there moves the key string and the receiver and // rewrites only the slots it can see; a bare argument is not one. // (#7795 removed the two resolution calls that used to allocate here as // well — the rooting is still required for the prototype read itself.) // // Root all three before the first of those calls and read each back at its // point of use. NaN-boxed handles only, so this module adds no bare // `get_raw_*_ptr` to `scripts/raw_handle_debt.py`. let scope = crate::gc::RuntimeHandleScope::new(); let proto_h = scope.root_nanbox_f64(crate::value::js_nanbox_pointer(proto_addr as i64)); let key_h = scope.root_nanbox_f64(crate::value::nanbox_string_key(key)); let receiver_h = scope.root_nanbox_f64(crate::value::js_nanbox_pointer(receiver_addr as i64)); let proto_ptr = || crate::value::js_nanbox_get_pointer(proto_h.get_nanbox_f64()) as *mut ObjectHeader; let key = || { crate::value::js_nanbox_get_pointer(key_h.get_nanbox_f64()) as *const crate::StringHeader }; let receiver_addr = || crate::value::js_nanbox_get_pointer(receiver_h.get_nanbox_f64()) as usize; // #7795: resolve `Object.prototype` from the memoized, GC-healed cache // instead of re-running `globalThis.Object` (which interns an `"Object"` // key string) plus a `closure_get_dynamic_prop("prototype")` on EVERY // ordinary-object property MISS. `object_prototype_addr` performs exactly // this resolution, caches only a successful one, heals the address through // the forwarding chain, and is itself a registered GC root // (`scan_prototype_addr_cache_roots_mut`) — the array index-read fast path // already depends on it. `Object.prototype` is non-writable and // non-configurable per spec, so the memo cannot go stale. if proto_ptr() as usize == receiver_addr() { return None; } let receiver = crate::value::js_nanbox_pointer(receiver_addr() as i64); let previous_this = super::super::js_implicit_this_set(receiver); // The DISPLACED `this` and the displaced accessor receiver both ride // through `js_object_get_field_by_name` — which can run a getter — before // being republished. Rooting the ACCESSOR_RECEIVER_OVERRIDE cell (#7231) // protects the armed value, not these saved ones; that residual is what // these two handles close. let previous_this_h = scope.root_nanbox_f64(previous_this); let prev_override = accessor_receiver_override_begin(receiver); let prev_override_h = prev_override.map(|v| scope.root_nanbox_f64(v)); let property = js_object_get_field_by_name(proto_ptr(), key()); accessor_receiver_override_end(prev_override_h.map(|h| h.get_nanbox_f64())); super::super::js_implicit_this_set(previous_this_h.get_nanbox_f64()); if property.is_undefined() { None } else { Some(property) } } pub(crate) unsafe fn ordinary_object_prototype_property_value( obj: *const ObjectHeader, key: *const crate::StringHeader, ) -> Option { if obj.is_null() || key.is_null() { return None; } let gc = gc_header_for(obj); if (*gc).obj_type != crate::gc::GC_TYPE_OBJECT { return None; } // #10827: this asked only whether the RECEIVER was born without a // prototype. A receiver whose chain was ended higher up — an instance of a // class whose `prototype` was `setPrototypeOf(..., null)`, or one pointed // at an `Object.create(null)` — reached `Object.prototype` anyway and // answered `toString` from it while `"toString" in o` said false. if super::super::prototype_chain::prototype_chain_ends_in_explicit_null(obj as usize) { return None; } if super::super::prototype_chain::object_static_prototype(obj as usize).is_some() { return None; } let class_id = (*obj).class_id; // Declared ES class instances have a registered class id, but still end // their implicit prototype chain at Object.prototype. Their class // methods/accessors have already been consulted before this fallback, so // a miss must remain eligible for Object.prototype (including user-added // properties). Keep excluding unregistered native/synthetic class ids: // those object kinds resolve their own intrinsic prototype chains. if class_id != 0 && !is_anon_shape_class_id(class_id) { if !super::super::class_registry::is_class_id_registered(class_id) { return None; } if super::super::extends_builtin_error(class_id) { // Error subclasses end at an Error-family prototype before // Object.prototype. Hold the recursion guard while resolving the // lazy builtin: constructor/prototype lookup itself may miss an // ordinary property, and must not re-enter this same fallback. let scope = crate::gc::RuntimeHandleScope::new(); let receiver_h = scope.root_nanbox_f64(crate::value::js_nanbox_pointer(obj as usize as i64)); let key_h = scope.root_nanbox_f64(crate::value::nanbox_string_key(key)); let _guard = object_prototype_lookup_guard()?; let prototype_name = super::super::builtin_error_prototype_name(class_id); let prototype = super::super::builtin_prototype_value(prototype_name); let prototype_value = JSValue::from_bits(prototype.to_bits()); if prototype_value.is_pointer() { let prototype_addr = prototype_value.as_pointer::() as usize; if prototype_addr != 0 { let receiver_addr = crate::value::js_nanbox_get_pointer(receiver_h.get_nanbox_f64()) as usize; let key = crate::value::js_nanbox_get_pointer(key_h.get_nanbox_f64()) as *const crate::StringHeader; if let Some(value) = prototype_property_value_with_guard(prototype_addr, receiver_addr, key) { return Some(value); } } } // The guard drops with this branch; let the ordinary final // fallback consult Object.prototype on a genuine Error miss. } // #9362: Object.setPrototypeOf(C.prototype, parentPrototype) changes // the inherited surface of C instances. util.inherits(C, Parent) uses // exactly that operation after materializing C.prototype. Class // instance lookup normally uses the vtable and then jumps straight to // Object.prototype, so a user-selected parent on the declaration // prototype was skipped. Walk the materialized declaration prototype // on a vtable miss, preserving the instance as the accessor receiver. let decl_proto = super::super::class_decl_prototype_object(class_id); if !decl_proto.is_null() && super::super::prototype_chain::object_has_user_prototype_override( decl_proto as usize, ) { let _guard = object_prototype_lookup_guard()?; if let Some(value) = prototype_property_value_with_guard(decl_proto as usize, obj as usize, key) { return Some(value); } } } default_object_prototype_property_value(obj as usize, key) } crate::perry_thread_local! { /// Receiver to bind when an accessor getter is reached by walking a /// prototype chain. `js_object_get_field_by_name(proto, key)` re-derives the /// accessor receiver from its `obj` argument — which is the PROTOTYPE during /// an inherited read, not the original instance. `resolve_inherited_field` /// stashes the real receiver here for the duration of the walk; the getter /// invocation consumes it so `this` is the instance, matching the spec's /// `[[Get]](P, Receiver)`. (object-literal getters on a `Object.create` /// prototype — e.g. @hono/node-server's request prototype reading /// `this[incomingKey].method`.) /// /// **This is a GC root, and must stay one (#7231).** The stashed receiver /// is a NaN-boxed heap value that stays armed for the whole prototype /// walk, and a walk can reach a Proxy `get` trap — arbitrary user code /// that allocates. Nothing else refers to it while it sits here, so /// without `scan_accessor_receiver_override_root_mut` the getter is /// invoked with a `this` naming from-space. /// /// RESIDUAL, same shape as `CURRENT_NEW_TARGET`: the displaced value that /// `accessor_receiver_override_begin` returns rides a bare Rust local /// through the walk and is republished by `_end`. Rooting the cell /// protects the ARMED value, not the saved one. static ACCESSOR_RECEIVER_OVERRIDE: std::cell::Cell> = const { std::cell::Cell::new(None) }; } /// Root + rewrite the in-flight inherited-accessor receiver. pub(crate) fn scan_accessor_receiver_override_root_mut( visitor: &mut crate::gc::RuntimeRootVisitor<'_>, ) { ACCESSOR_RECEIVER_OVERRIDE.with(|cell| { if let Some(mut value) = cell.get() { if visitor.visit_nanbox_f64_slot(&mut value) { cell.set(Some(value)); } } }); } pub(crate) fn accessor_receiver_override_begin(receiver: f64) -> Option { ACCESSOR_RECEIVER_OVERRIDE.with(|c| { // Keep the OUTERMOST receiver across multi-hop prototype walks. let to_set = c.get().or(Some(receiver)); c.replace(to_set) }) } /// Consume the original receiver before entering user code through a Proxy, /// just as invoke_accessor_getter does before entering a getter body. pub(crate) fn accessor_receiver_override_take() -> Option { ACCESSOR_RECEIVER_OVERRIDE.with(|c| c.take()) } pub(crate) fn accessor_receiver_override_end(prev: Option) { ACCESSOR_RECEIVER_OVERRIDE.with(|c| c.set(prev)); } /// `this` to pass to a class getter (vtable `getters`) found while resolving a /// property. When the getter was reached by walking a prototype chain, `obj` is /// the PROTOTYPE the getter lives on — bind the original instance stashed by /// `resolve_inherited_field` instead. Take() consumes it so the getter body /// runs with a clean override. pub(crate) unsafe fn class_getter_this(obj: *const ObjectHeader) -> f64 { ACCESSOR_RECEIVER_OVERRIDE .with(|c| c.take()) .unwrap_or_else(|| f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits())) } /// Run the class vtable getter `getter_ptr` found while resolving a property /// of `obj`, with `this` from [`class_getter_this`] and the body isolated from /// the enclosing inherited-property resolution (#11201). pub(crate) unsafe fn call_class_getter(getter_ptr: usize, obj: *const ObjectHeader) -> f64 { let this_f64 = class_getter_this(obj); let _boundary = crate::object::prototype_chain::UserCodeResolutionBoundary::enter(); let f: extern "C" fn(f64) -> f64 = std::mem::transmute(getter_ptr); f(this_f64) } pub(crate) unsafe fn invoke_accessor_getter(get_bits: u64, receiver: f64) -> JSValue { let closure = (get_bits & crate::value::POINTER_MASK) as *const crate::closure::ClosureHeader; if closure.is_null() { return JSValue::undefined(); } // Consume any inherited-receiver override: the getter's `this` must be the // original instance, not the prototype the accessor lives on. Take() clears // it so the getter BODY runs with a fresh override (a nested inherited read // inside the getter gets its own). let eff_receiver = ACCESSOR_RECEIVER_OVERRIDE .with(|c| c.take()) .unwrap_or(receiver); // OrdinaryCallBindThis: a primitive receiver (accessor inherited from // Number.prototype / Object.prototype etc.) is boxed ONCE up front for a // sloppy getter; a strict getter observes the raw primitive. // // #9417: every value below is GC-managed and lives across an allocation. // `coerce_call_this` boxes a primitive receiver, `clone_closure_rebind_this` // allocates a fresh `ClosureHeader`, and `js_closure_call0` runs USER CODE — // any of the three can drive an evacuating young-gen minor. A bare Rust // local is exactly the slot the collector cannot see or rewrite (#7249, // #7498), and the receiver here is not merely read afterwards: it is // PUBLISHED into the GC-rooted `IMPLICIT_THIS` cell, and `prev` is written // back into that same cell after the getter body has had a full turn to // allocate. Either store installs a pre-collection address as some frame's // `this`, after which every ordinary property read off it answers // `undefined` (`js_object_get_own_field_or_undef` fails its // `obj_type == GC_TYPE_OBJECT` check on the retired cell and returns // TAG_UNDEFINED rather than faulting) — the silent-wrong-answer shape // claude-code reported as `Cannot read properties of undefined (reading // 'def')` on its unauthenticated path. // // Root all four and re-read each at its point of use. let scope = crate::gc::RuntimeHandleScope::new(); let get_h = scope.root_nanbox_u64(get_bits); let recv_h = scope.root_nanbox_f64(eff_receiver); let coerced = crate::closure::coerce_call_this( f64::from_bits(get_h.get_nanbox_u64()), recv_h.get_nanbox_f64(), ); let recv_h = scope.root_nanbox_f64(coerced); let call_bits = crate::closure::clone_closure_rebind_this(get_h.get_nanbox_u64(), recv_h.get_nanbox_f64()); let call_h = scope.root_nanbox_u64(call_bits); if (call_h.get_nanbox_u64() & crate::value::POINTER_MASK) == 0 { return JSValue::undefined(); } let prev_h = scope.root_nanbox_f64(super::super::js_implicit_this_set(recv_h.get_nanbox_f64())); let closure = (call_h.get_nanbox_u64() & crate::value::POINTER_MASK) as *const crate::closure::ClosureHeader; let result_f64 = { let _boundary = crate::object::prototype_chain::UserCodeResolutionBoundary::enter(); crate::closure::js_closure_call0(closure) }; super::super::js_implicit_this_set(prev_h.get_nanbox_f64()); JSValue::from_bits(result_f64.to_bits()) } /// Setter analog of [`invoke_accessor_getter`]: rebinds `this` to the /// receiver and invokes the setter closure with the assigned value. pub(crate) unsafe fn invoke_accessor_setter(set_bits: u64, receiver: f64, value: f64) { let closure = (set_bits & crate::value::POINTER_MASK) as *const crate::closure::ClosureHeader; if closure.is_null() { return; } // Strict/sloppy receiver coercion — see invoke_accessor_getter. // #9417: same rooting contract as `invoke_accessor_getter` — plus `value`, // which is a bare local across the closure-clone allocation and is then // handed to user code as the assigned value. let scope = crate::gc::RuntimeHandleScope::new(); let set_h = scope.root_nanbox_u64(set_bits); let recv_h = scope.root_nanbox_f64(receiver); let value_h = scope.root_nanbox_f64(value); let coerced = crate::closure::coerce_call_this( f64::from_bits(set_h.get_nanbox_u64()), recv_h.get_nanbox_f64(), ); let recv_h = scope.root_nanbox_f64(coerced); let call_bits = crate::closure::clone_closure_rebind_this(set_h.get_nanbox_u64(), recv_h.get_nanbox_f64()); let call_h = scope.root_nanbox_u64(call_bits); if (call_h.get_nanbox_u64() & crate::value::POINTER_MASK) == 0 { return; } let prev_h = scope.root_nanbox_f64(super::super::js_implicit_this_set(recv_h.get_nanbox_f64())); let closure = (call_h.get_nanbox_u64() & crate::value::POINTER_MASK) as *const crate::closure::ClosureHeader; let _ = crate::closure::js_closure_call1(closure, value_h.get_nanbox_f64()); super::super::js_implicit_this_set(prev_h.get_nanbox_f64()); } /// Invoke an accessor owned by a descriptor-marked object before its empty /// backing slot is read. Gate-neutral builtin installs deliberately leave the /// process-wide `ACCESSORS_IN_USE` flag clear, but stamp their owner with /// `OBJ_FLAG_HAS_DESCRIPTORS`; the caller checks that bit before entering this /// helper, so ordinary object reads pay only the already-loaded header-bit /// test. This also makes direct reads of builtin prototype accessors preserve /// their real behavior (`Set.prototype.size` throws, `RegExp.prototype.source` /// returns `"(?:)"`, and so on) once startup is descriptor-gate-free. pub(crate) unsafe fn builtin_reflection_accessor_read( obj: *const ObjectHeader, key_bytes: &[u8], ) -> Option { let name = std::str::from_utf8(key_bytes).ok()?; let acc = get_accessor_descriptor(obj as usize, name)?; if acc.get == 0 { return Some(JSValue::undefined()); } let receiver = crate::value::js_nanbox_pointer(obj as i64); Some(invoke_accessor_getter(acc.get, receiver)) } /// True when `addr` is the shared `%TypedArray%.prototype` intrinsic or one of /// the per-kind typed-array prototypes (`Int8Array.prototype`, …). These objects /// host the `%TypedArray%.prototype` methods/getters but are NOT themselves /// typed arrays, so a method invoked directly on them (e.g. /// `Int8Array.prototype.entries()`) must fail `ValidateTypedArray` and throw a /// `TypeError`. Mirrors the per-kind/intrinsic detection in /// the builtin-accessor read path. pub(crate) unsafe fn is_typed_array_prototype(addr: usize) -> bool { if addr == 0 || (addr as u64) >> 48 != 0 || !super::super::is_valid_obj_ptr(addr as *const u8) { return false; } let intrinsic_proto = super::super::TYPED_ARRAY_INTRINSIC_PROTO_PTR.load(std::sync::atomic::Ordering::Relaxed); if intrinsic_proto != 0 && addr as i64 == intrinsic_proto { return true; } // Per-kind protos are plain `GC_TYPE_OBJECT`s carrying the proto flag in the // shared `_reserved` word; gate the flag read on the object type so a // regular array whose `_reserved` happens to collide isn't misclassified. let gc = (addr as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; (*gc).obj_type == crate::gc::GC_TYPE_OBJECT && ((*gc)._reserved & crate::gc::OBJ_FLAG_TYPED_ARRAY_PROTO) != 0 } pub(crate) unsafe fn primitive_object_prototype_accessor( name: &str, receiver: f64, ) -> Option { if !crate::state::state().descriptors.accessors_in_use.get() { return None; } let object_ctor = super::super::js_get_global_this_builtin_value(b"Object".as_ptr(), 6); let ctor_value = JSValue::from_bits(object_ctor.to_bits()); if !ctor_value.is_pointer() { return None; } let ctor_ptr = ctor_value.as_pointer::() as usize; let proto = crate::closure::closure_get_dynamic_prop(ctor_ptr, "prototype"); let proto_value = JSValue::from_bits(proto.to_bits()); if !proto_value.is_pointer() { return None; } let proto_ptr = proto_value.as_pointer::() as usize; let acc = get_accessor_descriptor(proto_ptr, name)?; if acc.get == 0 { return Some(JSValue::undefined()); } Some(invoke_accessor_getter(acc.get, receiver)) } unsafe fn bind_closure_value_to_receiver(value: JSValue, receiver: f64) -> JSValue { let bits = value.bits(); if (bits & crate::value::TAG_MASK) != crate::value::POINTER_TAG { return value; } let ptr = (bits & crate::value::POINTER_MASK) as usize; if !crate::closure::is_closure_ptr(ptr) { return value; } JSValue::from_bits(crate::closure::clone_closure_rebind_this(bits, receiver)) } pub(crate) unsafe fn primitive_builtin_prototype_property( builtin_name: &[u8], key: *const crate::StringHeader, receiver: f64, ) -> Option { if key.is_null() { return None; } let ctor = js_get_global_this_builtin_value(builtin_name.as_ptr(), builtin_name.len()); let ctor_value = JSValue::from_bits(ctor.to_bits()); if !ctor_value.is_pointer() { return None; } let ctor_ptr = ctor_value.as_pointer::() as usize; let proto = crate::closure::closure_get_dynamic_prop(ctor_ptr, "prototype"); let proto_value = JSValue::from_bits(proto.to_bits()); if !proto_value.is_pointer() { return None; } let proto_ptr = proto_value.as_pointer::(); if proto_ptr.is_null() { return None; } // An ACCESSOR installed on the builtin prototype // (`Object.defineProperty(Number.prototype, "x", { get(){…} })`) must run // with the ORIGINAL primitive receiver — boxed/raw per getter strictness // inside `invoke_accessor_getter` — not the prototype object the accessor // happens to live on (which a plain field read below would hand it). if crate::state::state().descriptors.accessors_in_use.get() { if let Some(name) = crate::string::header_str_checked(key) { if let Some(acc) = get_accessor_descriptor(proto_ptr as usize, name) { if acc.get == 0 { return Some(JSValue::undefined()); } return Some(invoke_accessor_getter(acc.get, receiver)); } } } // #10482: the direct-accessor short-circuit just above only covers an // accessor installed ON `proto_ptr` itself (`Number.prototype`). A key // inherited from FURTHER up the chain — `Object.prototype.__proto__`, // now a real accessor — resolves through this generic fallback instead, // which recurses into `js_object_get_field_by_name(proto_ptr, key)`. // That recursive walk finds the accessor on the ancestor and invokes it, // but with no override in place it binds `this` to whichever prototype // object the walk was probing (`Number.prototype`) rather than the // original primitive `receiver` — so `(5).__proto__` was answering // `Object.getPrototypeOf(Number.prototype)` (`Object.prototype`) instead // of `Object.getPrototypeOf(5)` (`Number.prototype`). Stash the real // receiver in the same thread-local override // `resolve_inherited_field_from_prototype` uses for the identical // problem one level up, so `invoke_accessor_getter` (reached from // inside the recursive call) picks it up via `ACCESSOR_RECEIVER_OVERRIDE` // instead of the prototype object it was handed. let prev_override = accessor_receiver_override_begin(receiver); let value = js_object_get_field_by_name(proto_ptr, key); accessor_receiver_override_end(prev_override); if value.is_undefined() { return None; } Some(bind_closure_value_to_receiver(value, receiver)) } /// The Boolean/BigInt property-read lane carries a NaN-boxed primitive rather /// than a raw number. A BigInt and the property key can move while resolving /// the builtin prototype, so keep both rooted across every lookup and re-read /// their addresses before the recursive prototype walk. pub(crate) unsafe fn primitive_tagged_prototype_property( builtin_name: &[u8], key: *const crate::StringHeader, receiver: f64, ) -> Option { if key.is_null() { return None; } let scope = crate::gc::RuntimeHandleScope::new(); let key_h = scope.root_nanbox_f64(crate::value::nanbox_string_key(key)); let receiver_h = scope.root_nanbox_f64(receiver); let ctor_h = scope.root_nanbox_f64(js_get_global_this_builtin_value( builtin_name.as_ptr(), builtin_name.len(), )); let ctor_value = JSValue::from_bits(ctor_h.get_nanbox_u64()); if !ctor_value.is_pointer() { return None; } let ctor_ptr = ctor_value.as_pointer::() as usize; let proto_h = scope.root_nanbox_f64(crate::closure::closure_get_dynamic_prop( ctor_ptr, "prototype", )); let proto_value = JSValue::from_bits(proto_h.get_nanbox_u64()); if !proto_value.is_pointer() { return None; } if crate::state::state().descriptors.accessors_in_use.get() { let key_ptr = JSValue::from_bits(key_h.get_nanbox_u64()).as_string_ptr(); if let Some(name) = crate::string::header_str_checked(key_ptr) { let proto_ptr = JSValue::from_bits(proto_h.get_nanbox_u64()).as_pointer::(); if let Some(acc) = get_accessor_descriptor(proto_ptr as usize, name) { if acc.get == 0 { return Some(JSValue::undefined()); } return Some(invoke_accessor_getter(acc.get, receiver_h.get_nanbox_f64())); } } } let prev_override = accessor_receiver_override_begin(receiver_h.get_nanbox_f64()); let prev_h = prev_override.map(|value| scope.root_nanbox_f64(value)); let proto_ptr = JSValue::from_bits(proto_h.get_nanbox_u64()).as_pointer::(); let key_ptr = JSValue::from_bits(key_h.get_nanbox_u64()).as_string_ptr(); let value = js_object_get_field_by_name(proto_ptr, key_ptr); accessor_receiver_override_end(prev_h.map(|handle| handle.get_nanbox_f64())); if value.is_undefined() { return None; } let value_h = scope.root_nanbox_u64(value.bits()); Some(bind_closure_value_to_receiver( JSValue::from_bits(value_h.get_nanbox_u64()), receiver_h.get_nanbox_f64(), )) } pub(crate) unsafe fn string_index_value( str_value: f64, key: *const crate::StringHeader, ) -> Option { if key.is_null() { return None; } let str_ptr = crate::value::js_get_string_pointer_unified(str_value) as *const crate::StringHeader; if str_ptr.is_null() { return None; } let key_value = JSValue::string_ptr(key as *mut crate::StringHeader); let value = crate::string::js_string_index_get(str_ptr, f64::from_bits(key_value.bits())); let js_value = JSValue::from_bits(value.to_bits()); if js_value.is_undefined() { None } else { Some(js_value) } } /// Resolve an inherited `Array.prototype` property for an Array-subclass /// instance after its own fields and class-declared methods have missed. /// An explicit per-instance prototype replaces the ordinary class chain and /// therefore suppresses this implicit fallback. pub(crate) unsafe fn array_subclass_prototype_field( obj: *const ObjectHeader, key: *const crate::StringHeader, ) -> Option { if obj.is_null() || key.is_null() || super::super::prototype_chain::object_static_prototype(obj as usize).is_some() || !crate::array::is_array_subclass_class_id((*obj).class_id) { return None; } let name = crate::string::header_str_checked(key)?; // `array_prototype_property_value` copies `name` before its first // allocation and roots the receiver across the prototype lookup. array_prototype_property_value(name, obj as usize) } pub(crate) unsafe fn array_prototype_property_value( name: &str, receiver_addr: usize, ) -> Option { // #7498 — THE FAULT `PERRY_GC_PROTECT_FROMSPACE=1` REPORTS FOR // `[...obj.arr]`, measured with lldb: `EXC_BAD_ACCESS` on the `ldrsb` of // the UTF-8 scan inside `js_string_from_bytes` below, reading a 56-byte // retired-from-space `GC_TYPE_STRING`. // // `name` is not an owned string. `get_field_by_name_object_tail` slices it // straight out of the key `StringHeader`'s payload // (`slice::from_raw_parts(key_ptr, key_len)`), so it is a BORROW OF THE GC // HEAP — and a borrow is exactly the thing the collector cannot see or // rewrite. Every call below allocates: `js_get_global_this_builtin_value` // interns `"Array"`, `closure_get_dynamic_prop` can run an accessor, and // `js_string_from_bytes` reads its SOURCE bytes *after* its own // `string_storage_alloc`. Any one of those can move the key out from under // `name`. // // A `RuntimeHandleScope` cannot fix this: rooting the key would keep the // object alive and rewrite the slot, but `name`'s pointer is a `&str`, not // a slot. The only sound shape is to stop borrowing the heap — see // [`HeapKeyBytes`]. let name_copy = super::HeapKeyBytes::copy_of(name.as_bytes()); let name: &str = std::str::from_utf8_unchecked(name_copy.as_bytes()); // #9192: an explicit `Object.setPrototypeOf(arr, p)` REPLACES the implicit // `Array.prototype` chain this function otherwise hardcodes. Before the fix // a retargeted array both failed to inherit `p`'s named properties AND kept // inheriting `Array.prototype`'s — `Object.setPrototypeOf(a, {foo:1})` left // `a.foo` undefined while `typeof a.map` stayed `"function"` (node: `1` and // `"undefined"`). `null` inherits nothing at all. // // The two callers that pass a NON-array receiver here // (`array_subclass_prototype_field` and the `fill` fallback in // `native_call_method`) already require the absence of a recorded // prototype, so this branch is reachable only for a retargeted array. if let Some(proto_bits) = super::super::prototype_chain::object_static_prototype(receiver_addr) { if proto_bits == crate::value::TAG_NULL { return None; } let scope = crate::gc::RuntimeHandleScope::new(); let receiver_h = scope.root_nanbox_f64(crate::value::js_nanbox_pointer(receiver_addr as i64)); let proto_h = scope.root_heap_word_u64(proto_bits); let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); if key.is_null() { return None; } let key_h = scope.root_nanbox_f64(crate::value::nanbox_string_key(key)); let receiver_addr = crate::value::js_nanbox_get_pointer(receiver_h.get_nanbox_f64()) as usize; let key = crate::value::js_nanbox_get_pointer(key_h.get_nanbox_f64()) as *const crate::StringHeader; return super::super::prototype_chain::resolve_inherited_field_from_prototype( receiver_addr, proto_h.get_heap_word_u64(), key, ); } let scope = crate::gc::RuntimeHandleScope::new(); let receiver_h = scope.root_nanbox_f64(crate::value::js_nanbox_pointer(receiver_addr as i64)); let ctor = super::super::js_get_global_this_builtin_value(b"Array".as_ptr(), 5); let ctor_value = JSValue::from_bits(ctor.to_bits()); if !ctor_value.is_pointer() { return None; } let ctor_ptr = ctor_value.as_pointer::() as usize; let proto = crate::closure::closure_get_dynamic_prop(ctor_ptr, "prototype"); let proto_value = JSValue::from_bits(proto.to_bits()); if !proto_value.is_pointer() { return None; } // #7498: the receiver is rooted before the allocating global lookup above; // `Array.prototype` and the fresh key are rooted before the calls below, // which can collect (`js_object_get_field_by_name` runs getters and // `default_object_prototype_property_value` interns another key). let proto_h = scope.root_nanbox_f64(proto); let key_h = scope.root_nanbox_f64(crate::value::nanbox_string_key( crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32), )); let proto_ptr = || crate::value::js_nanbox_get_pointer(proto_h.get_nanbox_f64()) as usize; let receiver_addr = || crate::value::js_nanbox_get_pointer(receiver_h.get_nanbox_f64()) as usize; let key = || { crate::value::js_nanbox_get_pointer(key_h.get_nanbox_f64()) as *const crate::StringHeader }; if let Some(v) = own_data_field_by_name(proto_ptr() as *const ObjectHeader, key()) { return Some(v); } if let Some(v) = crate::array::array_named_property_get_by_name( proto_ptr() as *const crate::array::ArrayHeader, name, ) { return Some(JSValue::from_bits(v.to_bits())); } if proto_ptr() == receiver_addr() { return default_object_prototype_property_value(receiver_addr(), key()); } let receiver = crate::value::js_nanbox_pointer(receiver_addr() as i64); let prev_override = accessor_receiver_override_begin(receiver); let prev_override_h = prev_override.map(|v| scope.root_nanbox_f64(v)); let v = js_object_get_field_by_name(proto_ptr() as *const ObjectHeader, key()); accessor_receiver_override_end(prev_override_h.map(|h| h.get_nanbox_f64())); if v.is_undefined() { default_object_prototype_property_value(receiver_addr(), key()) } else { Some(v) } }