//! `js_object_get_field_by_name` inline-cache hot path: leading receiver //! guards. The object-deref tail lives in `get_field_by_name_tail.rs`. //! Pure relocation out of field_get_set.rs (issue #1103 split). use super::*; mod class_wtf8; mod shape_inherited; use crate::codegen_abi::LayoutWord; use class_wtf8::class_value_get_wtf8_field; /// Wall 10 — read a property a framework attached to a native registry handle /// via `Object.setPrototypeOf(handle, proto)` (Express's `res`/`req`). The link /// is keyed by the handle id in the owner property bag. Returns `None` (so the /// caller yields `undefined`) when no recorded prototype carries the key. /// Cheap when no prototype was recorded (the common handle case). fn handle_proto_inherited_field( handle_id: usize, key: *const crate::StringHeader, ) -> Option { crate::object::prototype_chain::object_static_prototype(handle_id)?; let v = crate::object::prototype_chain::resolve_inherited_field(handle_id, key)?; if v.bits() == crate::value::TAG_UNDEFINED { None } else { Some(v) } } /// #2846 Proxy-receiver forwarding for a generic property read. Split out and /// `#[inline(never)]` on purpose: `js_proxy_is_proxy` (via `lookup`) and /// `js_proxy_get` (via `RuntimeHandleScope::new`) each resolve their own /// `thread_local!` (the proxy registry, the transient-handle root stack). /// Both accessors are pure address computations from LLVM's point of view — /// `readnone`, no observable side effect — so once this code was inlined into /// `js_object_get_field_by_name` the optimizer hoisted BOTH out of the /// `is_proxy_cell` guard above them and ran them unconditionally on every /// call, proxy receiver or not. Measured on an `o[k]` loop over a two-property /// plain object (never a Proxy): two `_tlv_get_addr` calls sitting directly in /// `js_object_get_field_by_name`'s prologue, 9.2% of the whole access. /// `#[inline(never)]` keeps the optimizer from seeing inside this function at /// the call site, so it cannot hoist anything out of it; `is_proxy_cell` /// itself stays inline in the caller since it touches no thread-local. #[cold] #[inline(never)] fn proxy_receiver_get(raw_addr: u64, key: *const crate::StringHeader) -> Option { let boxed = f64::from_bits(crate::value::POINTER_TAG | (raw_addr & 0x0000_FFFF_FFFF_FFFF)); if crate::proxy_hooks::proxy_is_proxy(boxed) == 0 { return None; } let key_f64 = f64::from_bits(crate::value::js_nanbox_string(key as i64).to_bits()); let v = crate::proxy_hooks::proxy_get(boxed, key_f64); Some(JSValue::from_bits(v.to_bits())) } #[no_mangle] pub extern "C" fn js_object_get_field_by_name( obj: *const ObjectHeader, key: *const crate::StringHeader, ) -> JSValue { let receiver_bits = obj as u64; let receiver = JSValue::from_bits(receiver_bits); if receiver.is_bool() || receiver.is_bigint() { if !key.is_null() { unsafe { let primitive = f64::from_bits(receiver_bits); let builtin = if receiver.is_bool() { b"Boolean".as_slice() } else { b"BigInt".as_slice() }; if let Some(value) = primitive_tagged_prototype_property(builtin, key, primitive) { return value; } } } return JSValue::undefined(); } if !key.is_null() { // The probe classifies the raw ABI word itself, after its name test: // a native handle id (`headers[key]`) reaches here before the handle // dispatch below. if let Some(value) = unsafe { crate::typedarray_named::get(obj as u64, key) } { return JSValue::from_bits(value.to_bits()); } } if let Some(value) = unsafe { super::super::native_get::try_settled_get_by_name(obj, key) } { return value; } get_field_by_name_past_data_probe(obj, key) } #[inline(never)] pub(crate) fn class_value_get_field( obj: *const ObjectHeader, key: *const crate::StringHeader, bits: u64, class_id: u32, ) -> JSValue { // A class function's own bag and actual constructor parent are the // complete public [[Get]]. Per-evaluation object redirects below still // choose their own receiver before entering this same lookup. if bits >> 48 != 0x7FFE && !super::super::class_registry::CLASS_OBJECT_EVER .load(std::sync::atomic::Ordering::Relaxed) { if let Some(value) = unsafe { crate::object::class_value::constructor_property_get( (bits & crate::value::POINTER_MASK) as usize, key, class_id, ) } { return value; } } let class_value = crate::object::class_value::boxed_class_word(bits); let is_prototype_ref = super::super::class_prototype_ref_id(class_value).is_some(); unsafe { let name_ptr = (key as *const u8).add(std::mem::size_of::()); let name_len = (*key).byte_len as usize; let name_bytes = std::slice::from_raw_parts(name_ptr, name_len); let name = std::str::from_utf8(name_bytes).unwrap_or(""); if name.is_empty() && !name_bytes.is_empty() { let name_bytes = name_bytes.to_vec(); return class_value_get_wtf8_field( class_value, class_id, is_prototype_ref, key, &name_bytes, ); } // v0.5.752: class_ref.constructor synthesizes back to the // same class ref so drizzle's // `Object.getPrototypeOf(value).constructor === Class` chain // collapses correctly (with v0.5.751's getPrototypeOf // returning the class ref for instance receivers). Refs // #420 / #618 followup. if is_prototype_ref && name == "constructor" && class_id != 0 && class_has_own_method(class_id, name) { let value = crate::object::class_method_slot_value(class_id, name) .map(f64::from_bits) .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)); return JSValue::from_bits(value.to_bits()); } if name == "constructor" && is_prototype_ref && class_id != 0 && is_class_id_registered(class_id) { let value = if is_prototype_ref { super::super::class_constructor_ref_value(class_id) } else { class_value }; return JSValue::from_bits(value.to_bits()); } if name == "prototype" && class_id != 0 && is_class_id_registered(class_id) && !is_prototype_ref { let value = super::super::class_registry::class_decl_prototype_value(class_id); if value.to_bits() == crate::value::TAG_UNDEFINED { let value = super::super::class_prototype_ref_value(class_id); return JSValue::from_bits(value.to_bits()); } return JSValue::from_bits(value.to_bits()); } // A capture-carrying class declaration is materialized as a // heap class object (`ClassExprFresh`). References that were // lowered before the declaration's runtime binding existed // (the class constructor itself and earlier helper closures) // still carry the template `ClassRef`. Runtime additions such // as `Object.defineProperty(C, "OPEN", { value: 1 })` live on // the materialized object, so consulting only the template // tables makes `C.OPEN` undefined in those bodies even though // the same expression at the declaration site reads `1`. // // `CLASS_OBJECT_VALUES` is already the runtime identity used // by `instance.constructor`. Read that same current // evaluation first, preserving its own-property and pinned // static-parent semantics; a miss continues through the // ordinary ClassRef registry path below. if !is_prototype_ref { if let Some(class_object) = super::super::class_registry::class_object_value_for_cid(class_id) { let class_object = JSValue::from_bits(class_object.to_bits()); if class_object.is_pointer() { let class_object = class_object.as_pointer::(); if !class_object.is_null() && class_object as usize != obj as usize { let value = js_object_get_field_by_name(class_object, key); if !value.is_undefined() { return value; } } } } } if !is_prototype_ref { if let Some(value) = crate::object::class_value::constructor_property_get( (bits & crate::value::POINTER_MASK) as usize, key, class_id, ) { return value; } } // Instance (prototype) methods must only resolve when reading // off the prototype ref (`C.prototype.m`), NOT off the class ref // itself (`C.m`). In JS a class object does not expose its // prototype methods as static members: `class C { m(){} }` has // `C.m === undefined` (the method lives on `C.prototype`). The // earlier unconditional lookup leaked instance methods onto the // class ref, so `C.m` returned a (mis-bound) function. This // broke NestJS interceptor/guard/pipe resolution: its // `getInterceptorInstance` duck-types `!!metatype.intercept` to // decide "is this a class or an already-built instance"; a // truthy `Class.intercept` made it treat the CLASS as the // instance, so `intercept()` ran with a broken receiver and // returned `{}`, which rxjs `innerFrom` then rejected. Real // static methods are resolved below via // `lookup_static_method_in_chain`. if is_prototype_ref && class_id != 0 && class_has_own_method(class_id, name) { let value = crate::object::class_method_slot_value(class_id, name) .map(f64::from_bits) .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)); return JSValue::from_bits(value.to_bits()); } if is_prototype_ref { // Class accessors are properties of the class prototype // chain (charter step 3); `this` is the prototype ref. if let Some((v, _)) = super::super::class_registry::class_chain_getter_value(class_id, name, || { class_value }) { return v; } return JSValue::undefined(); } // Empty-string is a legal static member key (`static get ''()`); // the `!name.is_empty()` guard below skips it, so resolve a // static accessor named "" here (Test262 accessor-name-static // literal-string-empty). if name.is_empty() { if let Some(v) = super::super::class_registry::class_static_accessor_getter_value( class_id, name, class_value, ) { return JSValue::from_bits(v.to_bits()); } } if !name.is_empty() { if super::super::class_registry::class_static_key_deleted(class_id, name) { // Not an own property any more: the read continues on the // class's [[Prototype]]. return crate::object::class_value::class_prototype_get(class_id, key, class_value); } let result = crate::object::class_value::class_static_get(class_id, name); if let Some(v) = result { return JSValue::from_bits(v.to_bits()); } // Static DATA fields are INHERITED by subclasses, exactly like // static methods: `class D {}; D.kind = "x"; class G extends D {}` // makes `G.kind === "x"` (the class-object proto chain // `G.__proto__ === D` carries statics). The own-field read above // only consulted `class_id`; walk the parent class_id chain here // so an inherited static field (or runtime `Parent.x = …` // assignment — both live in CLASS_DYNAMIC_PROPS) resolves. Static // METHODS are handled by `lookup_static_method_in_chain` below; // this covers the data-field case that was returning `undefined` // (Auth.js sets `SignInError.kind = "signIn"` and reads it off a // `CredentialsSignin` subclass to pick the sign-in vs error page). // // #6530: `name` is an OWN property of every constructor — a // subclass never inherits its parent's `.name` (spec: // ClassDefinitionEvaluation installs it per class). Skip the // chain walk so the #2059 own-name synthesis below answers // with THIS class's registered name instead of an ancestor's. if !matches!(name, "name" | "length") { // Walk the class-object proto chain for an inherited static // DATA field. At EACH level the class's pinned // per-evaluation parent OBJECT is consulted BEFORE the // parent's registry props (`CLASS_DYNAMIC_PROPS`). // // #6552: a subclass of a class-EXPRESSION value evaluated // more than once (`function make(a){return class{static // ast=a}}`, then `class Number$ extends make(x) {}` / // `class Widget$ extends make(y) {}`) records THIS // evaluation's parent object as its static prototype // (`class_prototype_object`, #1788), but the parent's // `CLASS_DYNAMIC_PROPS` are keyed by the class-expression // TEMPLATE id — shared, last-wins across every evaluation. // Reading the registry entry for such a parent collapses // sibling subclasses to the LAST `make(...)` (effect Schema: // `Number$.ast`/`Widget$.ast` both read the last parent's // `ast`). The pinned object carries this evaluation's own // edge, so it is authoritative; the registry read remains // the fallback for a plain declaration parent (#6443: // Auth.js `SignInError.kind`), which has no pinned object. let mut child = class_id; let mut depth = 0usize; while depth < 32 { // The constructor's own `[[Prototype]]`, set by // `Object.setPrototypeOf(Ctor, obj)`. Checked first: // it is the nearest static-side link, and unlike // `class_prototype_object` it is never on an // instance's chain. let static_proto = super::super::class_registry::class_static_prototype(child); if !static_proto.is_null() { // #10911: this walk re-enters with the PARENT as // the object. It was written when effect's `ast` // was a static DATA field (see above), where the // object doesn't matter; effect now makes `ast` a // static GETTER, and a getter found this way ran // with `this` === the parent class. Stash the // class the read started from so the accessor // binds it (spec OrdinaryGet threads Receiver) -- // the same device `resolve_proto_chain_field_inner` // uses for instance getters. let prev = crate::object::field_get_set::accessor_receiver_override_begin( class_value, ); let v = js_object_get_field_by_name(static_proto as *const _, key); crate::object::field_get_set::accessor_receiver_override_end(prev); if !v.is_undefined() { return v; } } let proto = super::super::class_registry::class_prototype_object(child); if !proto.is_null() { let prev = crate::object::field_get_set::accessor_receiver_override_begin( class_value, ); let v = js_object_get_field_by_name(proto as *const _, key); crate::object::field_get_set::accessor_receiver_override_end(prev); // Return a value present on the pinned object even // when it is `null` — a static explicitly set to // `null` on THIS evaluation is authoritative and // must not fall through to the last-wins registry // entry (a sibling evaluation's value). Only // `undefined` means "absent here", which continues // the walk to the parent's registry props / a higher // ancestor. if !v.is_undefined() { return v; } } let p = match get_parent_class_id(child) { Some(p) if p != 0 && p != child => p, _ => break, }; // A key deleted on THIS ancestor is not provided by it, // but a higher ancestor may still define it — `delete // Mid.foo` must let `Sub.foo` inherit `Base.foo`, not // resolve to undefined. Skip the registry read for the // deleted level and keep walking up. if !super::super::class_registry::class_static_key_deleted(p, name) { let inherited = crate::object::class_value::class_static_get(p, name); if let Some(v) = inherited { return JSValue::from_bits(v.to_bits()); } } child = p; depth += 1; } } if super::super::class_registry::lookup_static_method_in_chain(class_id, name).is_some() { // The declaration's own function object, on whichever class // declares it (`Q.a === P.a`). if let Some(v) = crate::object::class_value::inherited_static_method_value(class_id, name) { return JSValue::from_bits(v.to_bits()); } let heap_name = { let layout = std::alloc::Layout::from_size_align(name_len.max(1), 1).unwrap(); let ptr = std::alloc::alloc(layout); std::ptr::copy_nonoverlapping(name_ptr, ptr, name_len); ptr }; let result = js_class_method_bind(class_value, heap_name, name_len); return JSValue::from_bits(result.to_bits()); } // `class X extends Promise` — a value read of an inherited // builtin static (`X.resolve`, `X.all`, …) resolves to the // reified Promise static (so `X.resolve.bind(X)` works). Only // fires when no user static shadowed it above. if super::super::promise_parent_in_chain(class_id) && super::super::promise_static_function_spec(name).is_some() { let v = super::super::js_promise_static_function_value(name_ptr, name_len); if v.to_bits() != crate::value::TAG_UNDEFINED { return JSValue::from_bits(v.to_bits()); } } if let Some(v) = super::super::class_registry::class_static_accessor_getter_value( class_id, name, class_value, ) { return JSValue::from_bits(v.to_bits()); } // #1788: a subclass of a class-expression value // (`class Sub extends make("A") {}`) inherits the parent // class OBJECT's OWN per-evaluation static fields. The // parent object was recorded as `class_id`'s static // prototype at `extends` time; walk that chain (also // covering multi-level `class Leaf extends Mid {}`). // #6530: except `name` — an own property of every // constructor, never inherited; without the guard a // subclass of a per-evaluation class object reported its // BASE's synthesized `.name` (bundled zod: // `z.string().constructor.name` gave "ZodType"). if !matches!(name, "name" | "length") { if let Some(v) = super::super::class_registry::resolve_proto_chain_field(class_id, key) { if !v.is_undefined() && !v.is_null() { return v; } } } // #36 / #321: the subclass extends a FUNCTION value // (`class Svc extends Context.Tag(id)<...>() {}`). Read the // named static off the parent closure — its OWN props // (`Svc.key` → "Svc") plus, via the closure getter, its // static prototype (`Svc._op` → "Tag" on TagProto). // #10210: the edge is keyed by the class that directly // `extends `, which may be an ANCESTOR of this // class (`class Flags extends ConfigTag {}` where // `ConfigTag extends Context.Service()(id)`), so walk the // parent chain like `super()` dispatch does. // `name`/`length` are own properties of every constructor (#6530), // never inherited from a function-valued parent. if let Some(closure_ptr) = (!matches!(name, "name" | "length")) .then(|| super::super::class_registry::parent_closure_in_chain(class_id)) .flatten() { let v = crate::closure::closure_get_dynamic_prop(closure_ptr, name); let vb = JSValue::from_bits(v.to_bits()); if !vb.is_undefined() && !vb.is_null() { return vb; } } // #2059: the constructor's built-in `name` own property — // the class name. Checked last so an explicit static // `name` member (method/field, handled above) still wins. // This is what `assert.throws` reads via // `thrown.constructor.name` to label the thrown error. if name == "name" && class_id != 0 && !super::super::class_registry::class_static_key_deleted(class_id, name) { if let Some(cname) = super::super::class_registry::class_name_for_id(class_id) { let s = crate::string::js_string_from_bytes(cname.as_ptr(), cname.len() as u32); return JSValue::from_bits(crate::js_nanbox_string(s as i64).to_bits()); } } if name == "length" && class_id != 0 && !is_prototype_ref && !super::super::class_registry::class_static_key_deleted(class_id, name) { if let Some(length) = super::super::class_registry::class_length_for_id(class_id) { return JSValue::number(length as f64); } } // A class constructor is also a Function object. Reify // inherited Function.prototype methods for value reads // (`const bind = C.bind`) just as the closure path does; // the captured ClassRef is accepted by native method // dispatch and by `js_function_bind`. if !is_prototype_ref { if let Some(method) = super::reified_function_method_name(name) { let value = crate::closure::reify_function_method_value(class_value, method); return JSValue::from_bits(value.to_bits()); } } // No own static / inherited entry resolved the name. A class // constructor is a function, so a bare read of `.caller` or // `.arguments` hits the poison-pill %ThrowTypeError% accessor // on `Function.prototype` — strict-mode throws (Perry only // compiles strict code). Placed last so any own static field, // accessor, or `defineProperty`-installed data prop of that // name takes precedence. Prototype-refs (`C.prototype`) are // plain objects and are excluded. if !is_prototype_ref && matches!(name, "caller" | "arguments") { crate::fs::validate::throw_type_error_with_code( "Restricted function property access", "ERR_INVALID_ARG_TYPE", ); } // #11492: a constructor's chain ends at %Function.prototype%, // so a user method or expando installed there // (`Function.prototype.myHelper = fn`) is readable through // `C.myHelper` exactly as through a closure. if !is_prototype_ref { if let Some(v) = crate::closure::function_prototype_inherited_get(0, name, class_value) { return JSValue::from_bits(v.to_bits()); } } } // The built-in constructor object's `constructor` value is // inherited from Function.prototype. It is therefore only the // fallback after own computed fields, static methods, and // static accessors of the same name have had a chance to win. if name == "constructor" && class_id != 0 && is_class_id_registered(class_id) { let constructor = super::super::js_get_global_this_builtin_value( b"Function".as_ptr(), b"Function".len(), ); return JSValue::from_bits(constructor.to_bits()); } } return JSValue::undefined(); } #[cfg(test)] mod primitive_proto_accessor_tests_10648 { use super::*; #[test] fn boolean_and_bigint_inherit_object_proto_getter() { let scope = crate::gc::RuntimeHandleScope::new(); let key = crate::string::js_string_from_bytes(b"__proto__".as_ptr(), 9); let key_h = scope.root_nanbox_f64(crate::value::nanbox_string_key(key)); let receivers = [ ( "true", "Boolean", scope.root_nanbox_f64(f64::from_bits(crate::value::TAG_TRUE)), ), ( "false", "Boolean", scope.root_nanbox_f64(f64::from_bits(crate::value::TAG_FALSE)), ), ( "positive bigint", "BigInt", scope.root_nanbox_f64(crate::value::js_nanbox_bigint( crate::bigint_hooks::js_bigint_from_i64(1) as i64, )), ), ( "negative bigint", "BigInt", scope.root_nanbox_f64(crate::value::js_nanbox_bigint( crate::bigint_hooks::js_bigint_from_i64(-1) as i64, )), ), ]; for (label, name, receiver) in receivers { let expected = scope.root_nanbox_f64(crate::object::builtin_prototype_value(name)); let key_ptr = crate::value::JSValue::from_bits(key_h.get_nanbox_u64()).as_string_ptr(); let actual = js_object_get_field_by_name( receiver.get_nanbox_u64() as *const ObjectHeader, key_ptr, ); assert_eq!( actual.bits(), expected.get_nanbox_u64(), "{label} must inherit Object.prototype.__proto__" ); } } } /// The generic read after a site's own-key and holder-shape probes declined. pub(crate) fn get_field_by_name_after_site_miss( obj: *const ObjectHeader, key: *const crate::StringHeader, ) -> JSValue { if let Some(value) = unsafe { super::super::native_get::try_settled_get_by_name(obj, key) } { return value; } get_field_by_name_past_data_probe(obj, key) } #[cfg(test)] pub(super) fn test_get_past_data_probe( obj: *const ObjectHeader, key: *const crate::StringHeader, ) -> JSValue { get_field_by_name_past_data_probe(obj, key) } /// The generic read after `try_settled_get_by_name` has already declined. fn get_field_by_name_past_data_probe( obj: *const ObjectHeader, key: *const crate::StringHeader, ) -> JSValue { // Guard hoisted to the call site: an ordinary key is rejected on a length // compare and one byte here, so the overwhelmingly common property read // makes no call into the private-member path at all. if !super::cannot_be_private_member_name(key) { if let Some(value) = super::private_member_get_by_name(obj, key) { return JSValue::from_bits(value.to_bits()); } } // An elements-backed Array-subclass instance answers its indices and // `length` from its store; an absent index falls through to the ordinary // lookup, which reaches the prototype chain (the shape has no index keys). if let Some((_, elements)) = unsafe { crate::array::subclass_elements::backed(obj as usize) } { if let Some(elements_key) = unsafe { crate::array::subclass_elements::key_of_header(key) } { if let Some(value) = unsafe { crate::array::subclass_elements::get_by_key(elements, elements_key) } { return JSValue::from_bits(value.to_bits()); } } } // #7341: the `.size` arm below calls two helpers that allocate, and every // arm AFTER it dereferences `obj` again. Shadow the parameter so that arm // can republish the post-collection address instead of leaving from-space // in scope for the rest of the function. let mut obj = obj; // #5972: a null key reaches here when the property-key expression didn't // yield a usable string handle — e.g. `js_get_string_pointer_unified` // returned 0 for a NaN/number key that fell through its coercion branches. // Several arms below deref `(*key).byte_len` without a null check, so a // null key would SIGSEGV at offset 4 (KERN_INVALID_ADDRESS at 0x4). Per JS // semantics such a lookup simply misses → undefined. Same defensive shape // as the #2128 invalid-key guard further down. Every in-runtime caller // passes an interned non-null key, so this only affects the codegen // computed-access path. if key.is_null() { return JSValue::undefined(); } // `process.env` is a live OS-backed exotic object. Direct reads are // codegen-specialized, but an alias (`const env = process.env; env.X`) // reaches this generic path. On Windows the OS lookup also supplies the // required case-insensitivity (`env.Path === env.PATH`). if let Some(value) = crate::process::process_env_get_field(obj, key) { return value; } // #2846: the receiver may be a Proxy value that arrived through a generic // property read (e.g. `rec.proxy.a` where `rec = Proxy.revocable(...)`). // Proxies are encoded as small fake pointers; deref-ing one as an // ObjectHeader would read unmapped memory. Route to the proxy get dispatch, // which forwards to the target (or throws on a revoked proxy) — matching // Node. `js_proxy_is_proxy` validates the value is a *registered* proxy so a // real heap object whose address happens to be small isn't misrouted. { // Proxy ids live in the proxy id band; `js_proxy_is_proxy` confirms // it is a *registered* proxy before we route to the proxy getter. // // #6699: the receiver arrives in two encodings. A generic property read // passes the raw small proxy-id pointer (`top16 == 0`). The class-field // IC-miss fallback (`js_object_get_field_by_name_f64`, reached when the // typed-`this` field guard rejects an off-shape receiver) instead // forwards the *full NaN-box* value with the `0x7FFD` heap-pointer tag // still set — exactly the tagged encoding the FAST LANE below already // strips. A tagged proxy value (`0x7FFD_0000_000F_xxxx`) is not itself // in the proxy id band, so the un-normalized band test missed it and the // read fell through to `undefined` instead of the get trap. This bit a // typed-`this` field read whose `this` is a Proxy — e.g. pi's TUI theme // is a `new Proxy({}, …)` whose `get` trap forwards to the real Theme; // `theme.fg()` runs with `this === proxy`, and `this.fgColors` must hit // the trap. Normalize the tag first so both encodings route identically. let addr = obj as u64; let raw_addr = if (addr >> 48) == 0x7FFD { addr & 0x0000_FFFF_FFFF_FFFF } else { addr }; if crate::value::addr_class::is_proxy_cell(raw_addr as usize) && !key.is_null() { if let Some(value) = proxy_receiver_get(raw_addr, key) { return value; } } } // Megamorphic read stub. Primed below once the lane has proved this // receiver ordinary, so a hit only has to re-prove the properties that can // change: heap-object type, not forwarded, no blocking flags, a real class // id, and the receiver's CURRENT shape token. The token pins the exact key // set and order, so a match means the cached slot still names this key; a // stale entry misses rather than resolving to the wrong property. That // guard is `read_stub::stub_receiver_token`, the same function the prime // runs (#10768). The slot word carries the inline/overflow verdict from // prime time, so a hit fetches no bound. // // Sits after the process.env and Proxy arms above, which have their own // semantics and must keep them, and before the lane's guard chain plus the // read-plan probe — which is what a hit is here to skip. The plan's epoch // is bumped by the collector at loop-poll cadence, so on a steady read loop // it is repeatedly cold and falls through to a shape-index hash lookup. unsafe { if let Some(key_bits) = super::super::read_stub::read_stub_key_bits(key) { if let Some(v) = super::super::read_stub::read_stub_lookup(obj, key_bits) { return JSValue::from_bits(v.to_bits()); } } } // FAST LANE (store-plan-cache follow-up): resolve an OWN data field on a // provably-plain arena class instance with no rooting scope, no // exotic-registry probes, and no key hashing. Every gate proves a property // the skipped slow-path checks would have tested: // - band/tag checks: not a proxy (above) / handle / stream encoding; // - `classify_heap_generation != Unknown`: the address is inside a // registered arena page, so its GcHeader is real — and no malloc-backed // exotic (BufferHeader / TypedArrayHeader / DateCell / RegExpHeader / // Temporal cell, all mi- or gc-malloc'd) can classify as arena; // - `GC_TYPE_OBJECT`: not a closure / array / error / Map / Set; // - `class_id != 0` (and not the native-module id): not an arguments // object (allocated with class 0), URL-shape object, builtin prototype // host, or plain literal — those keep their existing paths; // - `OBJ_FLAG_HAS_DESCRIPTORS` clear: no own accessor can shadow the // slot (an own data property shadows inherited accessors per [[Get]]); // `OBJ_FLAG_TYPED_ARRAY_PROTO` clear: not the per-kind TypedArray // prototype host (its reflection accessors have empty backing fields). // The (keys_array, interned key) → index mapping comes from the // epoch-guarded read-plan cache (flushed on GC, descriptor / prototype / // vtable mutations, and property deletes); a lane-local bounded scan // populates it. An absent own key falls through — prototype and getter // resolution stay on the existing path. unsafe { let bits = obj as u64; let top16 = bits >> 48; let raw = if top16 == 0x7FFD { (bits & 0x0000_FFFF_FFFF_FFFF) as usize } else if top16 == 0 { bits as usize } else { 0 }; if raw >= crate::gc::GC_HEADER_SIZE + 0x1000 && !crate::value::addr_class::is_small_handle(raw) && crate::value::addr_class::is_above_handle_band(key as usize) { let key_gc = (key as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; // A POINTER_TAG receiver is honest, and its arena flag excludes // the malloc-backed exotics; a raw word needs the page ownership // proof before its header is read. if (*key_gc).gc_flags & crate::gc::GC_FLAG_INTERNED != 0 && if top16 == 0x7FFD { (*((raw - crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader)).gc_flags & crate::gc::GC_FLAG_ARENA != 0 } else { crate::arena::classify_heap_generation(raw) != crate::arena::HeapGeneration::Unknown } { let gc_hdr = (raw as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; const LANE_BLOCKING: u16 = crate::gc::OBJ_FLAG_HAS_DESCRIPTORS | crate::gc::OBJ_FLAG_TYPED_ARRAY_PROTO; if crate::gc_type_is_object!((*gc_hdr).obj_type) && (*gc_hdr)._reserved & LANE_BLOCKING == 0 { let o = raw as *const ObjectHeader; let class_id = (*o).class_id; if class_id != 0 && class_id != super::super::native_module::NATIVE_MODULE_CLASS_ID { let keys_view = crate::object::object_keys(o); let keys = keys_view.arr(); if !keys.is_null() && ((keys as u64) >> 48) == 0 && crate::value::addr_class::is_above_handle_band(keys as usize) { let alloc_limit = crate::object::object_inline_capacity(o) as usize; if let Some(idx) = super::super::prop_plan::read_plan_lookup( LayoutWord((*o).shape_word()), keys as usize, key as usize, keys_view.count(), ) { let idx = keys_view.storage_slot(idx); if let Some(value) = fast_lane_own_read(o, key, raw, idx, alloc_limit) { return value; } } 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 { let key_count = keys_view.count() as usize; if key_count <= 4096 { // #8936/#8950's shared resolver: the shape's // hash index answers in O(1), with the raw // dense-slot scan as its own fallback. The // open-coded `keys_array_slot` + // `js_string_key_matches` walk this replaces // was the read-plan cache's MISS path, so it // ran in full — up to `key_count` string // compares — every time the epoch-guarded // plan was flushed (each GC, and every // descriptor / prototype / delete mutation). // On a 500-key receiver that put // `js_string_key_matches` at 9.6% self time // in a computed-key read loop, second only to // this function itself. if let Some(i) = crate::object::keys_find_property_slot_by_key_ptr( keys_view, key_count as u32, key, ) { super::super::prop_plan::read_plan_record( LayoutWord((*o).shape_word()), keys as usize, key as usize, i, ); let i = keys_view.storage_slot(i); if let Some(value) = fast_lane_own_read(o, key, raw, i, alloc_limit) { return value; } } } } } } } } } } // A receiver that LOOKS like a bare heap pointer (top 16 bits clear) but does // not land in the platform heap range is a MIS-decoded primitive, not an // object. The common case is a `number` whose raw f64 bits alias a sub-heap // address: a dynamic `arr[i]` read (`js_dyn_index_get`) returns the element's // JSValue bits, codegen forwards them straight to the object field-read ABI // on the type-erased path, and a denormal such as `0x0000_0090_8000_0201` // (~620 GB) arrives here as `obj`. It clears the `>> 48 == 0` check and sits // ABOVE the 1 MB handle band, so the `is_above_handle_band`-only guards on the // special-case reads below (and the `own_key_present` / `js_object_get_class_id` // ObjectHeader derefs they call) passed it straight through — and the read // dereferenced it as a GcHeader → KERN_INVALID_ADDRESS (real macOS allocations // sit at ~3–5 TB, never 620 GB). Pair the band check with `is_valid_obj_ptr` // (the canonical heap-range predicate) and treat a non-heap receiver as a // property miss: reading any data property off a primitive is `undefined`, and // the primitive-prototype methods are resolved on the by-name f64 wrapper's own // path, which never reaches this pointer dereference. if !key.is_null() && ((obj as u64) >> 48) == 0 && crate::value::addr_class::is_above_handle_band(obj as usize) && !crate::value::addr_class::is_valid_obj_ptr(obj as *const u8) { return JSValue::undefined(); } // `class X extends Map | Set` instance — `.size` reads the hidden backing // collection's size. A subclass CAN still define an own `size` (class field // or `Object.defineProperty`), so check own-property precedence first and // only fall back to the backing size when no own key exists. Other backed // reads (`.has`/`.get`/… as METHODS) route through `js_native_call_method`. // Guarded by `is_above_handle_band` (like the class-object probe below) so a // native handle id in [0x10000, HANDLE_BAND_MAX) never reaches `own_key_present`'s // ObjectHeader deref; real subclass instances are ordinary heap objects // above the band. if !key.is_null() && ((obj as u64) >> 48) == 0 && crate::value::addr_class::is_above_handle_band(obj as usize) { unsafe { let name_ptr = (key as *const u8).add(std::mem::size_of::()); let name_len = (*key).byte_len as usize; // #7341: `own_key_present` allocates too, and it runs INSIDE the // original condition — so the scope has to open before that call, // not inside the body. The fault without this is the GC-type probe // at +560, immediately after `bl own_key_present` returns. // // Test the key FIRST and open the scope only for `.size`. Opening it // ahead of the key test would put a `RuntimeHandleScope` on every // property read that reaches this block, which is the one cost this // arm must not have. let is_size_key = std::slice::from_raw_parts(name_ptr, name_len) == b"size"; if is_size_key { let size_arm_scope = crate::gc::RuntimeHandleScope::new(); let size_arm_obj = size_arm_scope.root_raw_const_ptr(obj as *const u8); // #7341 layer 3: `across_const` runs the allocating call and hands // back the POST-collection address in one step, so the pre-call // pointer is never bound and cannot be reached for by mistake. // This is the shape every fix in the sweep converged on. let (has_own_size, fresh) = size_arm_obj.across_const::(|| { super::super::own_key_present(obj as *mut ObjectHeader, key) }); obj = fresh; if !has_own_size { // A subclass may also OVERRIDE `size` on its prototype // (`class M extends Map { get size() { return 42 } }`). Such an // inherited getter lives in the class vtable, not as an own key, // so check the class chain first and fall through to the normal // class/prototype resolution when it shadows the backing size. let class_id = super::super::js_object_get_class_id(obj); // #7341: BOTH helpers below allocate — `class_instance_has_method` // builds a `String` for its cache probe, and `subclass_backing_of` // calls `js_string_from_bytes` to materialise its constant // `BACKING_KEY` on every call. Either can drive an evacuating // minor, and `obj` is a bare local held across them, so on the // `None` fall-through every later arm reads from-space. The fault // is the GC-type probe at `js_object_get_field_by_name + 664` // (`ldurb w8, [x22, #-0x8]`), reached after the plausibility // checks pass because a retired from-space address still looks // like a heap pointer. `test_gap_field_lane_semantics` and // `test_gap_put_value_plan_cache` under from-space quarantine. // // Scoped to this arm, which is gated on the key being exactly // `"size"` — this is not the general property-read fast lane and // costs nothing on it. let obj_h = &size_arm_obj; let has_inherited_size = class_id != 0 && super::super::native_module::class_instance_has_method(class_id, "size"); if !has_inherited_size { let obj_now = obj_h.get_raw_const_ptr::(); let boxed = f64::from_bits(JSValue::pointer(obj_now as *const u8).bits()); match crate::object::map_set_subclass::subclass_backing_of(boxed) { Some(crate::object::map_set_subclass::CollectionBacking::Map(m)) => { return JSValue::number(crate::map::js_map_size(m) as f64); } Some(crate::object::map_set_subclass::CollectionBacking::Set(s)) => { return JSValue::number(crate::set::js_set_size(s) as f64); } None => {} } } } // #7341: republish before falling through — the helpers above // may have moved it, and every arm below dereferences `obj`. // One republish at the end of the arm covers both branches; // #7385 also wrote one inside the `if`, which this supersedes // (rustc flagged it as assigned-never-read). obj = size_arm_obj.get_raw_const_ptr::(); } } } if let Some(value) = unsafe { shape_inherited::get(obj, key) } { return value; } // WeakMap / WeakSet instance — a VALUE read of the collection methods // (`w.add`, `wm.set`, `typeof w.has`; react-server-dom's chunk-preload // dedup does `u.add.bind(u, a)` — #5989) must resolve the brand-checking // prototype thunk. Method CALLS dispatch via js_native_call_method's weak // arms, but this by-name read path had no equivalent, so the read yielded // `undefined` and the subsequent `.bind` threw. Own keys keep precedence // (fresh instances only carry the `__perry_wk_entries` sentinel). if !key.is_null() && ((obj as u64) >> 48) == 0 && crate::value::addr_class::is_above_handle_band(obj as usize) { unsafe { let boxed = f64::from_bits(JSValue::pointer(obj as *const u8).bits()); if let Some(cid) = crate::object::weak_class_id_from_receiver(boxed) { let name_ptr = (key as *const u8).add(std::mem::size_of::()); let name_len = (*key).byte_len as usize; let name = std::slice::from_raw_parts(name_ptr, name_len); let (builtin, known) = if cid == crate::weakref::CLASS_ID_WEAKMAP { ( "WeakMap", matches!(name, b"set" | b"get" | b"has" | b"delete"), ) } else { ("WeakSet", matches!(name, b"add" | b"has" | b"delete")) }; if known && !super::super::own_key_present(obj as *mut ObjectHeader, key) { if let Ok(method_name) = std::str::from_utf8(name) { if let Some(v) = super::super::collection_proto_thunks::collection_proto_method_value( builtin, method_name, ) { return JSValue::from_bits(v.to_bits()); } } } } } } // #7947: the same VALUE read for a `WeakRef` / `FinalizationRegistry` // instance — `typeof wr.deref`, `const d = wr.deref`, `wr.deref.bind(wr)`. // These wrappers had no prototype thunks at all before #7947, so every such // read answered `undefined` (and `.bind` threw "Bind must be called on a // function"). Own keys keep precedence — fresh instances carry only the // `__perry_wr_target` / `__perry_fr_*` sentinels. if !key.is_null() && ((obj as u64) >> 48) == 0 && crate::value::addr_class::is_above_handle_band(obj as usize) { unsafe { let boxed = f64::from_bits(JSValue::pointer(obj as *const u8).bits()); if let Some(cid) = crate::object::weak_wrapper_class_id(boxed) { let name_ptr = (key as *const u8).add(std::mem::size_of::()); let name_len = (*key).byte_len as usize; let name = std::slice::from_raw_parts(name_ptr, name_len); if let Ok(method_name) = std::str::from_utf8(name) { if !super::super::own_key_present(obj as *mut ObjectHeader, key) { if let Some(v) = super::super::weakref_proto_thunks::weakref_proto_method_value_for( cid, method_name, ) { return JSValue::from_bits(v.to_bits()); } } } } } } // `class X extends Promise` instance — a value read of `then`/`catch`/ // `finally` (`p.then` / `typeof p.finally`, and codegen's `p.finally(cb)` // which reads the property first) must resolve the reified Promise prototype // method. The generic prototype walk does not surface these builtin // `Promise.prototype` methods for a subclass instance, so hook them here when // no own key shadows them. The method thunks unwrap the backing cell from // their `this` receiver (see `promise_prototype_receiver`). if !key.is_null() && ((obj as u64) >> 48) == 0 && crate::value::addr_class::is_above_handle_band(obj as usize) { unsafe { let name_ptr = (key as *const u8).add(std::mem::size_of::()); let name_len = (*key).byte_len as usize; let name = std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len)).unwrap_or(""); if matches!(name, "then" | "catch" | "finally") && !super::super::own_key_present(obj as *mut ObjectHeader, key) { let boxed = f64::from_bits(JSValue::pointer(obj as *const u8).bits()); if crate::promise::subclass_backing_promise(boxed).is_some() { if let Some(m) = crate::promise::promise_proto_method(name) { return JSValue::from_bits(m.to_bits()); } } } } } // One existing header/shape classification supplies both the class // read and the exotic gate. Native keeps the original inline typed read; // an ordinary shape skips every exotic probe. let read_kind = super::exotic_named_read::named_read_receiver_kind(obj); use super::exotic_named_read::NamedReadKind; if matches!(read_kind, NamedReadKind::Native | NamedReadKind::Other) { #[cfg(test)] super::exotic_named_read::note_probe(0, obj); if let Some(addr) = crate::typedarray_props::typed_array_addr_from_value(f64::from_bits(obj as u64)) { if !key.is_null() { unsafe { let key_ptr = crate::string::string_data(key); let key_len = (*key).byte_len as usize; let key_bytes = std::slice::from_raw_parts(key_ptr, key_len); let ta = addr as *const crate::typedarray_hook_slots::TypedArrayHeader; if let Some(value) = crypto_key_property_value(addr, key_bytes) { return value; } if let Some(value) = crate::typedarray_props::typed_array_get_own_property_value(ta, key) { return JSValue::from_bits(value.to_bits()); } if let Some(kind) = crate::typedarray_hook_slots::lookup_typed_array_kind(addr) { let elem_size = crate::typedarray_hook_slots::elem_size_for_kind(kind); match key_bytes { b"length" => { let len = crate::typedarray_hook_slots::js_typed_array_length(ta); return JSValue::number(len as f64); } b"byteLength" => { let len = crate::typedarray_hook_slots::js_typed_array_length(ta); return JSValue::number((len as usize * elem_size) as f64); } b"buffer" => { let buf = crate::typedarray_view::js_typed_array_backing_buffer(ta); if buf.is_null() { return JSValue::undefined(); } return JSValue::from_bits( crate::value::js_nanbox_pointer(buf as i64).to_bits(), ); } b"byteOffset" => { return JSValue::number( crate::typedarray_view::js_typed_array_byte_offset(ta) as f64, ); } b"BYTES_PER_ELEMENT" => return JSValue::number(elem_size as f64), // `(new Int8Array(…)).constructor === Int8Array`. The // instance never carries an own `constructor`; it is // inherited from the per-kind prototype. Resolve it to // the global per-kind constructor value so identity holds // (matches the buffer branch below and the `Number` // auto-box path). Custom-prototype views (set via the // `Reflect.construct` newTarget path) record their own // prototype and resolve `.constructor` through that // chain instead — handled before this native fallback. b"constructor" => { // A custom-`[[Prototype]]` view (Reflect.construct // with a newTarget whose `.prototype` is an object) // inherits `.constructor` through that prototype // chain, NOT from the per-kind constructor. if let Some(proto_bits) = super::super::prototype_chain::object_static_prototype(addr) { if proto_bits != crate::value::TAG_NULL { if let Some(inherited) = super::super::prototype_chain::resolve_inherited_field( addr, key, ) { return inherited; } } } // A user patch on the per-kind prototype // (`Object.defineProperty(TA.prototype, // "constructor", { get })` or a data overwrite) // shadows the intrinsic — run the getter with // `this` = the view (observable; test262 // speciesctor-get-ctor-inherited reads // `result.constructor` and counts calls). if let Some(v) = crate::typedarray_hook_slots::species::prototype_constructor_patch( kind, addr, ) { return JSValue::from_bits(v.to_bits()); } let name = crate::typedarray_hook_slots::name_for_kind(kind); let ctor = super::super::js_get_global_this_builtin_value( name.as_ptr(), name.len(), ); return JSValue::from_bits(ctor.to_bits()); } _ => {} } // A typed array reached through an `any`-typed value uses // this generic property getter instead of codegen's typed // method path. Resolve inherited methods/accessors through // the instance's actual prototype, preserving custom // `Reflect.construct` prototypes before the intrinsic // per-kind prototype fallback. Without this, // structured-cloned views had the right brand and bytes // but reads such as `cloned.join` returned `undefined`. match super::super::prototype_chain::object_static_prototype(addr) { Some(crate::value::TAG_NULL) => {} Some(_) => { if let Some(inherited) = super::super::prototype_chain::resolve_inherited_field( addr, key, ) { return inherited; } } None => { let proto = crate::object::builtin_prototype_value( crate::typedarray_hook_slots::name_for_kind(kind), ); if let Some(inherited) = super::super::prototype_chain:: resolve_inherited_field_from_prototype( addr, proto.to_bits(), key, ) { return inherited; } } } } else { let buf = addr as *const crate::buffer::BufferHeader; match key_bytes { // #8149: `length` is a `%TypedArray%` slot. An // `ArrayBuffer` / `SharedArrayBuffer` / `DataView` // exposes only `byteLength`, so node answers // `undefined` for `dv.length` / `ab.length`. b"length" if crate::buffer::is_non_indexed_buffer_view(addr) => { return JSValue::undefined(); } b"length" | b"byteLength" => { return JSValue::number(crate::buffer::js_buffer_length(buf) as f64); } b"buffer" | b"parent" => { let alias = crate::buffer::buffer_backing_array_buffer(addr); return JSValue::from_bits( crate::value::js_nanbox_pointer(alias as i64).to_bits(), ); } b"byteOffset" | b"offset" => { let offset = crate::buffer::buffer_byte_offset(addr); return JSValue::number(offset as f64); } b"BYTES_PER_ELEMENT" => return JSValue::number(1.0), b"constructor" => { // An ArrayBuffer / SharedArrayBuffer cell answers // with ITS constructor — only the Uint8Array // (Buffer-backed view) representation reports // `Uint8Array` (`ta.buffer.constructor === // ArrayBuffer`, test262 ctors/buffer-arg/ // typedarray-backed-by-sharedarraybuffer). let name: &[u8] = if crate::buffer::is_shared_array_buffer(addr) { b"SharedArrayBuffer" } else if crate::buffer::is_any_array_buffer(addr) { b"ArrayBuffer" } else { b"Uint8Array" }; let ctor = super::super::js_get_global_this_builtin_value( name.as_ptr(), name.len(), ); return JSValue::from_bits(ctor.to_bits()); } _ => {} } // Buffer-backed Uint8Arrays share the intrinsic // Uint8Array prototype. Like the TypedArrayHeader branch // above, this is required when the receiver's static type // has been erased (for example by structured clone). match super::super::prototype_chain::object_static_prototype(addr) { Some(crate::value::TAG_NULL) => {} Some(_) => { if let Some(inherited) = super::super::prototype_chain::resolve_inherited_field( addr, key, ) { return inherited; } } None => { let proto = crate::object::builtin_prototype_value("Uint8Array"); if let Some(inherited) = super::super::prototype_chain:: resolve_inherited_field_from_prototype( addr, proto.to_bits(), key, ) { return inherited; } } } } } } // #4363 regression fix: a secret-key Uint8Array (KeyObject backing // buffer) exposes `type` / `symmetricKeySize` / `asymmetricKey*` // through the KeyObject metadata block later in this function. The // typed-array own-property fallback must not shadow those with // `undefined` — fall through for a secret-key buffer so the metadata // block resolves them. Plain typed arrays keep the `undefined` result. if !crate::buffer::is_secret_key(addr) { return JSValue::undefined(); } } } else if read_kind == NamedReadKind::Class { // #6438: precedence for a per-evaluation class object is // own -> THIS object's pinned parent -> generic tail. // // The generic tail folds the own lookup together with a class_id-keyed // prototype-chain walk (`resolve_proto_chain_field_with_receiver`). That // chain goes through the TEMPLATE's parent edge, which is last-wins // across evaluations, so for a factory called twice it answers with the // SIBLING's inherited value and never reports undefined — which would // silently pre-empt the pinned walk below. effect: // // make(ast) -> class SchemaClass { static ast = ast } // makeTypeLiteralClass(..) -> class TypeLiteralClass extends make(ast) {…} // // `Struct(a)` then `Struct(b)` left `structA.ast === astB`, because // `structA.ast` resolved through TypeLiteralClass's TEMPLATE parent edge // (last registered = b's SchemaClass) instead of structA's own parent. // Check the object's OWN fields first, then ITS pinned parent, and only // then fall through to the tail. unsafe { if !key.is_null() { let name_ptr = (key as *const u8).add(std::mem::size_of::()); let name_len = (*key).byte_len as usize; let want = std::slice::from_raw_parts(name_ptr, name_len); // An own accessor (`Object.defineProperty(C, k, { get })`) // answers before the pinned parent; the generic tail runs it // with this read's receiver. if crate::object::key_attrs::object_key_is_accessor(obj, want) { return get_field_by_name_object_tail(obj, key); } if let Some(v) = crate::object::class_registry::class_object_own_field_bytes(obj, want) { return JSValue::from_bits(v.to_bits()); } // #6530: `name` and `prototype` are OWN properties of every // constructor — never inherited through the parent edge. Skip // the pinned-parent recursion for them so the generic tail // below synthesizes both from THIS object's class_id (the // recursion otherwise answered with the BASE class's `.name` // for every capture-carrying subclass — bundled zod's // identity collapse to "ZodType"). if want != b"name" && want != b"prototype" { if let Some(parent) = crate::object::class_registry::class_object_pinned_parent(obj) { let pbits = parent.to_bits(); if (pbits >> 48) == 0x7FFD { let praw = (pbits & 0x0000_FFFF_FFFF_FFFF) as *mut ObjectHeader; if praw as usize != obj as usize && crate::value::addr_class::is_above_handle_band(praw as usize) && crate::object::is_valid_obj_ptr(praw as *const u8) { // #10911: the recursion re-enters with the // PARENT evaluation as the object, so a static // getter found there would bind `this` to the // parent (`Sub.tag` ran with `this === Base`). // Stash the class the read started from -- // `begin` keeps the OUTERMOST one across a // multi-level walk -- exactly as the ClassRef // static-prototype walk below does. let scope = crate::gc::RuntimeHandleScope::new(); let receiver = f64::from_bits( crate::value::js_nanbox_pointer(obj as i64).to_bits(), ); let prev = crate::object::field_get_set::accessor_receiver_override_begin( receiver, ) .map(|value| scope.root_nanbox_f64(value)); let v = js_object_get_field_by_name(praw, key); crate::object::field_get_set::accessor_receiver_override_end( prev.map(|handle| handle.get_nanbox_f64()), ); if !v.is_undefined() { return v; } } } } } } } let own = get_field_by_name_object_tail(obj, key); if !own.is_undefined() { return own; } unsafe { // Re-box the raw class-object pointer as a POINTER-tagged JS value // so `js_class_method_bind` (which expects a value, like the // class-ref path) binds the static method to the right receiver. let class_value = f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); let class_id = super::super::js_object_get_class_id(obj); if class_id != 0 { let name_ptr = (key as *const u8).add(std::mem::size_of::()); let name_len = (*key).byte_len as usize; let name_bytes = std::slice::from_raw_parts(name_ptr, name_len); let name = std::str::from_utf8(name_bytes).ok(); if !name_bytes.is_empty() && !super::super::class_registry::class_static_key_deleted_bytes( class_id, name_bytes, ) { // A class object owns every static method its template // declares (`define_class_object_own_properties`), and so // does each earlier evaluation it inherits from, so a // declaration of a per-evaluation class found here was // DELETED from its object: it must not come back. Only a // method of a shared (never-evaluated-to-an-object) class // is served from the registry. if super::super::class_registry::lookup_static_method_owner_bytes( class_id, name_bytes, ) .is_some_and(|(owner, _)| { let object_owned = super::super::class_registry::class_object_value_for_cid(owner) .is_some() || owner == class_id; !object_owned }) { let heap_name = { let layout = std::alloc::Layout::from_size_align(name_len.max(1), 1).unwrap(); let ptr = std::alloc::alloc(layout); std::ptr::copy_nonoverlapping(name_ptr, ptr, name_len); ptr }; let result = js_class_method_bind(class_value, heap_name, name_len); return JSValue::from_bits(result.to_bits()); } if let Some(v) = super::super::class_registry::class_static_accessor_getter_value_bytes( class_id, name_bytes, class_value, ) { return JSValue::from_bits(v.to_bits()); } // Fresh nested Promise subclasses inherit the same reified // statics as ClassRef receivers (`P.all`, `P.resolve`, ...). if let Some(name) = name.filter(|name| !name.is_empty()) { if super::super::promise_parent_in_chain(class_id) && super::super::promise_static_function_spec(name).is_some() { let value = super::super::js_promise_static_function_value(name_ptr, name_len); if value.to_bits() != crate::value::TAG_UNDEFINED { return JSValue::from_bits(value.to_bits()); } } } } } } return own; } // #2128: a plain JS number value (a finite double or canonical NaN — // anything `JSValue::is_number` returns true for *minus* the raw-I64 // pointer convention where top16 == 0) reaches this generic property-get // when codegen lacks static type info — e.g. drizzle's // `buildQueryFromSourceParams` mapping a chunk that happens to be a // bound-param number (`1` row-id, `31` age). Without this guard the // receiver's f64 bits get bit-cast to a pointer and the first downstream // helper that reads a GC header (`is_registered_set` here, `(*obj).field_*` // elsewhere) derefs unmapped memory and SIGSEGVs. Spec: property access // on a primitive number returns undefined for unknown keys (we don't // auto-box to Number.prototype here; that's handled by the method-dispatch // path, not this property-getter slow path). Heap pointers stored as raw // I64 (module-level objects) have top16 == 0 and are preserved by this // check. { let bits = obj as u64; let top16 = bits >> 48; // Two shapes of primitive-number receiver reach this generic slow // path: (a) a finite double whose top16 is neither a NaN-box tag // nor zero — most numbers (1.0 has top16 0x3FF0, -3.14 has // 0xC008...), and (b) the f64 +0.0 whose full bit pattern is // `0` — distinguishable from a raw heap pointer because real // ObjectHeader allocations live above 0x10000 and from null / // undefined because both are NaN-boxed with top16 == 0x7FFC. let is_primitive_number = JSValue::from_bits(bits).is_number() && (top16 != 0 || bits == 0); if is_primitive_number { // #2138: auto-box the primitive number for the inherited // `.constructor` read so `n.constructor === Number` (and the // duck-type `value.constructor.name === "Number"` lodash/date-fns // use to discriminate primitives). Read the actual intrinsic // prototype, whose shape owns its constructor property. A replaced // global binding does not change that prototype. Other unknown // keys still return undefined per #2128 (was SIGSEGV pre-#2128). if !key.is_null() { unsafe { 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); if let Ok(name) = std::str::from_utf8(key_bytes) { if let Some(v) = primitive_object_prototype_accessor(name, f64::from_bits(bits)) { return v; } } if let Some(v) = primitive_builtin_prototype_property(key, f64::from_bits(bits)) { return v; } if let Some(bound) = bind_primitive_proto_method_static(f64::from_bits(bits), key_bytes) { return bound; } } } return JSValue::undefined(); } } // A named read on a primitive string uses the same own/prototype lookup // as a computed read, including SSO receivers and function identity. if crate::value::JSValue::from_bits(obj as u64).is_any_string() { return JSValue::from_bits( crate::string::js_string_index_get_boxed( f64::from_bits(obj as u64), crate::value::js_nanbox_string(key as i64), ) .to_bits(), ); } // Native module registry handles can arrive here either as raw small // integers or as POINTER_TAG-boxed small integers. Route them before any // GC-header probes such as Date/Promise checks. { let bits = obj as u64; let top16 = bits >> 48; let raw = if top16 == 0 { bits as usize } else if top16 == (crate::value::HANDLE_TAG >> 48) { (bits & 0x0000_FFFF_FFFF_FFFF) as usize } else { 0 }; if crate::value::addr_class::is_small_handle(raw) { if !key.is_null() { unsafe { 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); if key_bytes == b"constructor" { return JSValue::from_bits(crate::object::null_stub_value().to_bits()); } if let Some(dispatch) = handle_property_dispatch() { let bits = dispatch(raw as i64, key_ptr, key_len); // Wall 10 — fall back to a `setPrototypeOf(handle, proto)` // member (Express's augmented `res`/`req`) when the native // dispatch doesn't know the key. See // `handle_proto_inherited_field`. if bits.to_bits() == crate::value::TAG_UNDEFINED { if let Some(v) = handle_proto_inherited_field(raw, key) { return v; } } return JSValue::from_bits(bits.to_bits()); } if let Some(v) = handle_proto_inherited_field(raw, key) { return v; } } } return JSValue::undefined(); } } if matches!(read_kind, NamedReadKind::Native | NamedReadKind::Other) { #[cfg(test)] super::exotic_named_read::note_probe(1, obj); // #2089: a `Date` is a NaN-boxed pointer to an 8-byte `DateCell`. A // generic property read on it (`date.constructor`, `date[k]`, a method // read as a value) must NOT fall through to the object-deref path below — // the cell is far smaller than an `ObjectHeader`, so reading its // `keys_array`/field slots would deref unmapped memory. Resolve the few // meaningful reads here and return `undefined` for everything else // (matching property reads on the old value-type Date). `obj` may arrive // NaN-boxed (top16 == 0x7FFD) or as a raw-I64 pointer (top16 == 0). { let bits = obj as u64; let top16 = bits >> 48; let addr = if top16 == 0x7FFD { (bits & 0x0000_FFFF_FFFF_FFFF) as usize } else if top16 == 0 { bits as usize } else { 0 }; if addr != 0 && crate::date::is_date_cell_addr(addr) { if !key.is_null() { unsafe { 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); // User expando / defineProperty'd own properties first. if let Ok(name) = std::str::from_utf8(key_bytes) { let receiver = f64::from_bits( crate::value::JSValue::pointer(addr as *const u8).bits(), ); if let Some(v) = super::super::exotic_expando::exotic_get_own_property( addr, super::super::exotic_expando::ExoticKind::Date, name, receiver, ) { return JSValue::from_bits(v.to_bits()); } } // Everything else (`constructor`, a Date method read as // a *value*: `const f = d.getTime`, `typeof d.toISOString`, // `d.toJSON === Date.prototype.toJSON`) is a [[Get]] on the // cell's own [[Prototype]]: `Date.prototype` for a plain // Date, `D.prototype` (then `Date.prototype`) for an // instance of `class D extends Date`. The `d.method()` // call form is handled by codegen's fast path and never // reaches here. Unknown keys still return undefined. let receiver = f64::from_bits( crate::value::JSValue::pointer(addr as *const u8).bits(), ); let proto = crate::date_hook_slots::date_cell_prototype(receiver); let pv = JSValue::from_bits(proto.to_bits()); if pv.is_pointer() { let proto_ptr = pv.as_pointer::(); if !proto_ptr.is_null() { let m = js_object_get_field_by_name(proto_ptr, key); if !m.is_undefined() { return JSValue::from_bits(m.bits()); } } } } } return JSValue::undefined(); } } // Temporal cell (#4686): like Date, a `Temporal.*` value is a NaN-boxed // pointer to a small cell that must NOT fall through to the object-deref // path. Resolve its getters (`duration.years`, `plainDate.month`, …) here // and return `undefined` for anything else (a Temporal method read as a // bare value is rare; the `value.method()` call form is handled in // `js_native_call_method`). `obj` may be NaN-boxed (top16 0x7FFD) or a // raw-I64 pointer (top16 0). { let bits = obj as u64; let top16 = bits >> 48; let addr = if top16 == 0x7FFD { (bits & 0x0000_FFFF_FFFF_FFFF) as usize } else if top16 == 0 { bits as usize } else { 0 }; if addr != 0 && crate::temporal::is_temporal_cell_addr(addr) { if !key.is_null() { unsafe { 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 name = String::from_utf8_lossy(key_bytes); let boxed = f64::from_bits(JSValue::pointer(addr as *const u8).bits()); // A user-defined own expando property (`Object.defineProperty` // / plain assignment) shadows the built-in prototype getters, // per OrdinaryGet walking own properties before the prototype. if let Some(v) = super::super::exotic_expando::exotic_get_own_property( addr, super::super::exotic_expando::ExoticKind::Temporal, &name, boxed, ) { return JSValue::from_bits(v.to_bits()); } if let Some(v) = crate::temporal::hooked::get_property(boxed, &name) { return JSValue::from_bits(v.to_bits()); } // A prototype METHOD read as a value (`d.abs`, not `d.abs()`): // return a bound method that re-dispatches through // `js_native_call_method`. Needed because codegen lowers a // spread/dynamic call `d[m](...args)` to a property read + apply, // so the read must yield a callable. Only bind genuine method // names so an unknown property still reads as `undefined`. (#5587) if crate::temporal::hooked::has_method(boxed, &name) { let heap_name = { let layout = std::alloc::Layout::from_size_align(key_bytes.len().max(1), 1) .unwrap(); let ptr = std::alloc::alloc(layout); std::ptr::copy_nonoverlapping( key_bytes.as_ptr(), ptr, key_bytes.len(), ); ptr }; let bound = js_class_method_bind(boxed, heap_name, key_bytes.len()); return JSValue::from_bits(bound.to_bits()); } } } return JSValue::undefined(); } } } // Issue #818 (Effect class-instance pattern): a V8 handle (JS_HANDLE_TAG // = 0x7FFB) reaches here when codegen routes a generic `PropertyGet` // through this slow path — e.g. `Effect.succeed(42).value` where the // call return was a JS handle but the HIR `js_transform` pass didn't // rewrite the consumer-side `.value` into `JsGetProperty` (because the // call lowered as a `StaticMethodCall`, not as a `JsCallMethod`). The // method-call counterpart in `js_call_method` already routes // JS_HANDLE_TAG values to V8 via JS_HANDLE_CALL_METHOD; do the same // here via JS_HANDLE_OBJECT_GET_PROPERTY so subsequent property reads // on a returned class instance reach the live V8 object instead of // falling to the small-handle dispatch (which only knows about // Fastify/axios/sqlite, not generic V8 handles). { let bits = obj as u64; if (bits >> 48) == 0x7FFB && !key.is_null() { let func_ptr = crate::value::JS_HANDLE_OBJECT_GET_PROPERTY .load(std::sync::atomic::Ordering::SeqCst); if !func_ptr.is_null() { let func: unsafe extern "C" fn(f64, *const i8, usize) -> f64 = unsafe { std::mem::transmute(func_ptr) }; unsafe { let key_ptr = (key as *const u8).add(std::mem::size_of::()); let key_len = (*key).byte_len as usize; let result = func(f64::from_bits(bits), key_ptr as *const i8, key_len); return JSValue::from_bits(result.to_bits()); } } return JSValue::undefined(); } } // Issue #618-followup: read INT32-tagged class ref's dynamic property // from the side-table (mirror of the set-side intercept). For drizzle's // `SQL.Aliased` lookup pattern. { let bits = obj as u64; if let (Some(class_id), false) = ( crate::object::class_value::legacy_class_ptr_word(bits), key.is_null(), ) { return class_value_get_field(obj, key, bits, class_id); } } // #1545: Promise `then`/`catch`/`finally` value-reads return a bound // function so `typeof p.then === "function"`, `const f = p.then`, and // passing `p.then` as a deferred callback all work. (The call form // `p.then(cb)` is lowered directly to `js_promise_then` by codegen.) // `obj` arrives NaN-boxed POINTER-tagged here; mask to the raw promise // pointer and confirm via the GC header before treating it as a promise. { let bits = obj as u64; let top16 = bits >> 48; // Callers reach this helper with either a NaN-boxed POINTER-tagged // value (0x7FFD, e.g. the `_f64` wrapper) or an already-masked raw // heap pointer (top16 == 0, e.g. the PIC miss handler), so accept both. let raw = if top16 == 0x7FFD { (bits & 0x0000_FFFF_FFFF_FFFF) as usize } else if top16 == 0 { bits as usize } else { 0 }; // Native-module registry handles live in the handle band and can also // be POINTER_TAG-boxed; do not walk back to a GcHeader for those. if crate::value::addr_class::is_plausible_heap_addr(raw) && !key.is_null() { { unsafe { let gc_header = (raw - crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader; // Buffers / typed arrays are `std::alloc`-backed and carry // NO GcHeader, so the byte at `raw - 8` is unrelated memory // that can read as `GC_TYPE_PROMISE` (5) by coincidence on // an IC-miss read. Exclude them before acting — otherwise a // genuine buffer metadata read would early-return undefined. if (*gc_header).obj_type == crate::gc::GC_TYPE_PROMISE && !crate::buffer::is_registered_buffer(raw) && crate::typedarray_hook_slots::lookup_typed_array_kind(raw).is_none() { let name_ptr = (key as *const u8).add(std::mem::size_of::()); let name_len = (*key).byte_len as usize; let name_bytes = std::slice::from_raw_parts(name_ptr, name_len); let prop = std::str::from_utf8_unchecked(name_bytes); // #5142: a user-attached own expando (`p.status = …`, // `Object.assign(p, …)`) wins over the inherited // prototype method. @tanstack/query-core's // `pendingThenable()` stores `status`/`value` on the // promise and gates its retryer on `thenable.status`; // without this the read came back `undefined`, // `isResolved()` was permanently true, and the fetch // never resolved. if let Some(v) = super::super::exotic_expando::exotic_get_own_property( raw, super::super::exotic_expando::ExoticKind::Promise, prop, f64::from_bits(obj as u64), ) { return JSValue::from_bits(v.to_bits()); } if matches!(name_bytes, b"then" | b"catch" | b"finally") { if let Some(v) = crate::promise::js_promise_bound_method( raw as *mut crate::promise::Promise, prop, ) { return JSValue::from_bits(v.to_bits()); } } // `promise.constructor` is the global `Promise` // (inherited from `Promise.prototype.constructor`). Any // own expando (`p.constructor = X`) already returned via // `exotic_get_own_property` above. execa // (`(async () => {})().constructor.prototype`) reads it // to capture the native promise prototype — without this // arm it fell through to `undefined` and // `.prototype` threw `Cannot read properties of // undefined`. if name_bytes == b"constructor" { let v = crate::object::js_get_global_this_builtin_value( b"Promise".as_ptr(), 7, ); return JSValue::from_bits(v.to_bits()); } // A Promise is a `GC_TYPE_PROMISE` cell, not an // `ObjectHeader`; never fall through to the field/vtable // path below (it would reinterpret the promise's bytes). return JSValue::from_bits(crate::value::TAG_UNDEFINED); } } } } } // #2058: a raw, unboxed finite f64 NUMBER receiver (e.g. `(5).toString`, // or `n.isPrototypeOf` where `n: number`) reaches here with its float // bits intact — numbers are NOT NaN-boxed in Perry, so `5.0` arrives as // 0x4014_0000_0000_0000. That is neither a NaN-box tag (top16 >= 0x7FF8) // nor a masked heap pointer (those have top16 == 0), so the generic // pointer logic below would dereference the float bits as an // `ObjectHeader` → SIGSEGV. Detect the primitive number first: return a // bound-method closure for the inherited Number/Object prototype methods // (so `typeof n.toString === "function"` holds and the value is // callable), and `undefined` for any other key (matching property reads // on primitives). Date timestamps and Web-Stream handles are raw f64 too, // but both are special-cased above, so they never reach this branch. { let bits = obj as u64; let f = f64::from_bits(bits); // A Date is now a NaN-boxed `DateCell` pointer (non-finite bit // pattern), intercepted earlier in this function, so it never reaches // this finite-number branch. if !key.is_null() && f.is_finite() && (bits >> 48) != 0 { unsafe { let name_ptr = (key as *const u8).add(std::mem::size_of::()); let name_len = (*key).byte_len as usize; let name_bytes = std::slice::from_raw_parts(name_ptr, name_len); if let Ok(name) = std::str::from_utf8(name_bytes) { if let Some(v) = primitive_object_prototype_accessor(name, f) { return v; } } if let Some(v) = primitive_builtin_prototype_property(key, f) { return v; } if let Some(bound) = bind_primitive_proto_method_static(f, name_bytes) { return bound; } } return JSValue::undefined(); } } super::get_field_by_name_tail::get_field_by_name_object_tail(obj, key) } #[cfg(test)] mod null_key_guard_5972 { use super::*; /// #5972 part 2: a null key (produced when a NaN/number property-key /// coerces to no usable string handle) must miss → `undefined`, never /// SIGSEGV by dereferencing `(*key).byte_len` at offset 4. #[test] fn null_key_returns_undefined_not_segfault() { { let obj = crate::object::js_object_alloc(0, 0); let key = crate::string::js_string_from_bytes(b"present".as_ptr(), 7); crate::object::js_object_set_field_by_name(obj, key, 42.0); // Sanity: the real key resolves. let hit = js_object_get_field_by_name(obj, key); assert_eq!(f64::from_bits(hit.bits()), 42.0); // The regression: a null key must not crash and must read undefined. let miss = js_object_get_field_by_name(obj, std::ptr::null()); assert_eq!( miss.bits(), crate::value::TAG_UNDEFINED, "null key should miss → undefined" ); } } } #[cfg(test)] mod wtf8_class_object_forwarding_tests { use super::*; #[test] fn template_class_ref_forwards_runtime_wtf8_static_properties() { let _global = crate::gc::global_side_table_test_lock(); unsafe { const CID: u32 = 62_447; crate::object::js_register_class_id(CID); crate::object::js_register_class_name(CID, b"LateStatic".as_ptr(), 10); let scope = crate::gc::RuntimeHandleScope::new(); let template = scope.root_nanbox_f64(crate::object::class_value::js_class_value(CID as i32)); let class = scope.root_raw_mut_ptr(crate::object::js_object_alloc(CID, 0)); class .with_mut_ptr::(|p| crate::object::js_object_mark_class(p as i64)); for bytes in [&b"ascii"[..], &b"\xed\xa0\x80"[..], &b"\xed\xb0\x80"[..]] { let key = scope.root_string_ptr(crate::string::js_string_from_bytes( bytes.as_ptr(), bytes.len() as u32, )); key.with_const_ptr::(|key| { class.with_mut_ptr::(|p| { crate::object::js_object_set_field_by_name(p, key, 42.0) }); class.with_const_ptr::(|p| { assert_eq!( js_object_get_field_by_name(p, key).as_number(), 42.0, "heap class own property {bytes:?}" ); }); let template_ptr = crate::value::js_nanbox_get_pointer(template.get_nanbox_f64()) as *const ObjectHeader; assert_eq!( js_object_get_field_by_name(template_ptr, key).as_number(), 42.0, "template class ref must forward key {bytes:?}" ); }); } } } } /// The fast lane's own data read of storage slot `slot`: primes the read stub /// and answers the value, or `None` when the slot holds the hole (an absent /// optional key, `key_view`), so the generic read continues to the /// prototype. Out of line so the lane's caller keeps its frame. #[inline(never)] unsafe fn fast_lane_own_read( o: *const ObjectHeader, key: *const crate::StringHeader, raw: usize, slot: u32, alloc_limit: usize, ) -> Option { let inline = (slot as usize) < alloc_limit; let value = if inline { super::accessors::js_object_get_field(o, slot) } else { match super::super::overflow_get(raw, slot as usize) { Some(b) => JSValue::from_bits(b), None => JSValue::undefined(), } }; if value.bits() == crate::value::TAG_HOLE { return None; } prime_read_stub(o, key, slot, inline); Some(value) } /// Record `(shape token, key content) -> slot` for the megamorphic read stub. /// /// Only called from inside the fast lane, i.e. once the receiver has already /// been proved an ordinary shaped heap object with a resolvable own slot, so /// the stub never learns an entry for a receiver whose reads have other /// semantics. The lane's proof does not include "not forwarded"; the stub's /// own guard, which `read_stub_prime` shares with the probe, does (#10768). /// Keys that cannot be represented as content bits are skipped by /// `read_stub_key_bits`. #[inline] fn prime_read_stub( obj: *const ObjectHeader, key: *const crate::StringHeader, slot: u32, inline: bool, ) { let slot = if inline { slot } else { slot | crate::proxy::IC_SLOT_OVERFLOW_BIT }; if let Some(key_bits) = super::super::read_stub::read_stub_key_bits(key) { unsafe { super::super::read_stub::read_stub_prime(obj, key_bits, slot) }; } }