diff --git a/crates/perry-runtime/src/object/array_object_ops.rs b/crates/perry-runtime/src/object/array_object_ops.rs index aeb27265d0..7525b0cb73 100644 --- a/crates/perry-runtime/src/object/array_object_ops.rs +++ b/crates/perry-runtime/src/object/array_object_ops.rs @@ -13,6 +13,51 @@ unsafe fn is_array_object(obj: *const ObjectHeader) -> bool { (*gc_header).obj_type == crate::gc::GC_TYPE_ARRAY } +/// Apply `Object.freeze` / `Object.seal` to an array's OWN index + named data +/// properties. The generic `mark_all_keys` walks `(*obj).keys_array`, but an +/// array's indices live in the dense element store and its named props in the +/// `ARRAY_NAMED_PROPS` side table — neither appears in `keys_array` — so +/// freeze/seal historically missed them, leaving a frozen array's elements +/// writable/configurable. Returns `true` when `obj` is an array (handled here), +/// `false` otherwise so the caller can fall back to the ordinary key walk. +pub(crate) unsafe fn mark_all_array_props( + obj: *mut ObjectHeader, + drop_writable: bool, + drop_configurable: bool, +) -> bool { + if !is_array_object(obj) { + return false; + } + // Any explicit per-index/named attribute override makes the raw numeric + // fast paths ineligible — gate them on the descriptor flag so the recorded + // non-writable/non-configurable attrs are actually honored on read/write. + { + let gc = gc_header_for(obj); + (*gc)._reserved |= crate::gc::OBJ_FLAG_ARRAY_DESCRIPTORS; + } + let arr = obj as *const crate::array::ArrayHeader; + let addr = obj as usize; + let mut apply = |key: String| { + let mut attrs = + super::get_property_attrs(addr, &key).unwrap_or(PropertyAttrs::new(true, true, true)); + if drop_writable { + attrs.bits &= !PropertyAttrs::WRITABLE; + } + if drop_configurable { + attrs.bits &= !PropertyAttrs::CONFIGURABLE; + } + super::set_property_attrs(addr, key, attrs); + }; + let len = (*arr).length; + for i in 0..len { + apply(i.to_string()); + } + for name in crate::array::array_named_property_names(arr, false) { + apply(name); + } + true +} + pub(crate) unsafe fn array_property_is_enumerable( obj: *mut ObjectHeader, key_str: *const crate::StringHeader, @@ -308,14 +353,29 @@ pub(crate) unsafe fn define_array_property( let desc_has_get = super::desc_has_field(descriptor_value, b"get"); let desc_has_set = super::desc_has_field(descriptor_value, b"set"); if desc_has_get || desc_has_set { - // Non-configurable existing index can't switch to an accessor. + // ValidateAndApplyPropertyDescriptor for an existing non-configurable + // index: reject the data→accessor switch AND a change to a + // non-configurable accessor's `get`/`set` (or a forbidden + // enumerable/configurable change). The historical check only + // rejected the data→accessor case, so redefining a non-configurable + // accessor index with a different setter silently succeeded. if exists { let cur = super::get_property_attrs(obj as usize, key_name) .unwrap_or_else(|| PropertyAttrs::new(true, true, true)); - let already_accessor = - super::get_accessor_descriptor(obj as usize, key_name).is_some(); - if !cur.configurable() && !already_accessor { - return Some(false); + if !cur.configurable() { + let cur_accessor = super::get_accessor_descriptor(obj as usize, key_name); + let cur_value = if cur_accessor.is_none() { + crate::array::js_array_get_f64(arr, index) + } else { + f64::from_bits(crate::value::TAG_UNDEFINED) + }; + super::validate_nonconfigurable_redefine( + key_name, + cur, + cur_accessor, + cur_value, + descriptor_value, + ); } } let get_field = js_object_get_field_by_name(desc_ptr as *const ObjectHeader, get_key); @@ -502,6 +562,34 @@ pub(crate) unsafe fn define_array_property( return Some(true); } + // ValidateAndApplyPropertyDescriptor for an EXISTING non-configurable named + // (non-index) own property on an array. The index path above performs this + // check; the named path historically did not, so a redefine of a + // non-configurable `arr.prop` (data or accessor) silently succeeded instead + // of throwing a TypeError. + { + let cur_accessor = super::get_accessor_descriptor(obj as usize, key_name); + let cur_attrs = super::get_property_attrs(obj as usize, key_name); + if cur_attrs.is_some() || cur_accessor.is_some() { + let attrs = cur_attrs.unwrap_or_else(|| PropertyAttrs::new(true, true, true)); + if !attrs.configurable() { + let cur_value = if cur_accessor.is_none() { + crate::array::array_named_property_get_by_name(arr, key_name) + .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)) + } else { + f64::from_bits(crate::value::TAG_UNDEFINED) + }; + super::validate_nonconfigurable_redefine( + key_name, + attrs, + cur_accessor, + cur_value, + descriptor_value, + ); + } + } + } + // Named (non-index) accessor on an array target: store get/set in the // side table, exactly like the index path above. Without this, a // `defineProperty(arr, "prop", {get,set})` silently stored `undefined` diff --git a/crates/perry-runtime/src/object/exotic_expando.rs b/crates/perry-runtime/src/object/exotic_expando.rs index 4267773987..b622b787f5 100644 --- a/crates/perry-runtime/src/object/exotic_expando.rs +++ b/crates/perry-runtime/src/object/exotic_expando.rs @@ -305,6 +305,15 @@ pub(crate) fn exotic_has_own_property(kind: ExoticKind, addr: usize, name: &str) || (super::descriptors_in_use() && super::get_accessor_descriptor(addr, name).is_some()) } +/// Default enumerability for an exotic instance's own key when no explicit +/// attribute entry exists. A built-in slot that the spec defines as +/// non-enumerable (Error's `message`/`stack`) stays non-enumerable even after a +/// plain `err.message = x` write (which is a `[[Set]]` — it never changes +/// attributes); a user-added expando (`err.foo = 1`) defaults to enumerable. +pub(crate) fn exotic_default_enumerable(kind: ExoticKind, name: &str) -> bool { + !(kind == ExoticKind::Error && matches!(name, "message" | "stack")) +} + /// Own expando string keys: data props in insertion order, then /// accessor-only keys. Optionally filtered to enumerable ones. pub(crate) fn exotic_own_keys(kind: ExoticKind, addr: usize, enumerable_only: bool) -> Vec { @@ -320,7 +329,7 @@ pub(crate) fn exotic_own_keys(kind: ExoticKind, addr: usize, enumerable_only: bo keys.retain(|k| { super::get_property_attrs(addr, k) .map(|a| a.enumerable()) - .unwrap_or(true) + .unwrap_or_else(|| exotic_default_enumerable(kind, k)) }); } keys diff --git a/crates/perry-runtime/src/object/object_ops.rs b/crates/perry-runtime/src/object/object_ops.rs index f3c6d9cbc0..cf37dc4d2b 100644 --- a/crates/perry-runtime/src/object/object_ops.rs +++ b/crates/perry-runtime/src/object/object_ops.rs @@ -1034,7 +1034,9 @@ pub extern "C" fn js_object_property_is_enumerable(obj_value: f64, key_value: f6 } let enumerable = super::get_property_attrs(addr, key_name) .map(|a| a.enumerable()) - .unwrap_or(true); + .unwrap_or_else(|| { + super::exotic_expando::exotic_default_enumerable(kind, key_name) + }); return f64::from_bits(if enumerable { TAG_TRUE } else { TAG_FALSE }); } diff --git a/crates/perry-runtime/src/object/object_ops_frozen.rs b/crates/perry-runtime/src/object/object_ops_frozen.rs index 62356b84a7..5e4ba82ffe 100644 --- a/crates/perry-runtime/src/object/object_ops_frozen.rs +++ b/crates/perry-runtime/src/object/object_ops_frozen.rs @@ -189,6 +189,16 @@ pub extern "C" fn js_object_freeze(obj_value: f64) -> f64 { } return obj_value; } + // Arrays store indices densely and named props in a side table — + // neither is in `keys_array`, so handle them explicitly. + if super::mark_all_array_props( + obj, /*drop_writable=*/ true, /*drop_configurable=*/ true, + ) { + mark_all_symbol_keys( + obj, /*drop_writable=*/ true, /*drop_configurable=*/ true, + ); + return obj_value; + } // Drop writable + configurable for every existing key. mark_all_keys( obj, /*drop_writable=*/ true, false, /*drop_configurable=*/ true, @@ -277,6 +287,15 @@ pub extern "C" fn js_object_seal(obj_value: f64) -> f64 { } return obj_value; } + // Arrays: indices + named props live outside `keys_array`. + if super::mark_all_array_props( + obj, /*drop_writable=*/ false, /*drop_configurable=*/ true, + ) { + mark_all_symbol_keys( + obj, /*drop_writable=*/ false, /*drop_configurable=*/ true, + ); + return obj_value; + } // Drop configurable for every existing key (but leave writable intact). mark_all_keys( obj, /*drop_writable=*/ false, false, /*drop_configurable=*/ true, diff --git a/crates/perry-runtime/src/value/dynamic_object.rs b/crates/perry-runtime/src/value/dynamic_object.rs index 0986bb360a..b3caa128d7 100644 --- a/crates/perry-runtime/src/value/dynamic_object.rs +++ b/crates/perry-runtime/src/value/dynamic_object.rs @@ -371,6 +371,23 @@ pub unsafe extern "C" fn js_dynamic_object_get_property( // Handle Error objects specially if object_type == crate::error::OBJECT_TYPE_ERROR { + // An own expando / accessor property (installed via defineProperty, or a + // reassigned `message`/`stack`) lives in the exotic side tables and wins + // over the builtin slot. The compiled member-get path consults these, + // but this lower-level dynamic getter — used by + // `Object.defineProperties` to read each descriptor off the properties + // bag — historically dropped straight to `undefined` for any key other + // than the five native slots, so an accessor/data expando on an Error + // read as `undefined` (and `defineProperties(obj, errObj)` then threw + // "Property description must be an object: undefined"). + if let Some(v) = crate::object::exotic_expando::exotic_get_own_property( + ptr as usize, + crate::object::exotic_expando::ExoticKind::Error, + property_name, + obj_value, + ) { + return v; + } let error_ptr = ptr as *mut crate::error::ErrorHeader; match property_name { "message" => {