Skip to content

Commit 32aa095

Browse files
proggeramlugRalph Küpper
andauthored
fix(runtime): #5901 — strict write to a frozen symbol-keyed property throws (#5958)
A strict-mode `obj[sym] = v` where `sym` is a Symbol routes through `js_put_value_set` → `ordinary_set_with_receiver` → `own_set_descriptor`. For a symbol key that helper returned `Data { writable: true }` unconditionally whenever the property existed, ignoring the receiver's frozen state and any per-symbol `writable:false` attribute. So `Object.freeze(obj); obj[sym] = 2` silently no-op'd instead of throwing the required TypeError (test262 Object/freeze/frozen-object-contains-symbol-properties-strict) — the string-keyed path already reported these correctly. Fix: `own_set_descriptor` now reports a symbol-keyed data property's real writability via a new `symbol::symbol_property_is_non_writable` query, which mirrors the frozen / per-symbol-attr rejection already in `set_symbol_property` (frozen receiver ⇒ non-writable; else consult the per-symbol attrs table). `ordinary_set_with_receiver` then returns false and `js_put_value_set` throws under strict mode. test262 built-ins/Object/freeze: slice now 0 fail. Verified against Node: normal symbol overwrite, a `defineProperty(obj, sym, {writable:false})` strict write (throws), a sealed-but-not-frozen object's existing symbol (still writable), and `Symbol.iterator` all behave correctly. Refs #5901. Co-authored-by: Ralph Küpper <ralph@skelpo.com>
1 parent 1839711 commit 32aa095

3 files changed

Lines changed: 38 additions & 3 deletions

File tree

crates/perry-runtime/src/proxy.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -900,8 +900,16 @@ fn own_set_descriptor(target: f64, key: f64) -> Option<OwnSetDescriptor> {
900900

901901
if unsafe { crate::symbol::js_is_symbol(key) } != 0 {
902902
let value = unsafe { crate::symbol::js_object_get_symbol_property(target, key) };
903-
return (value.to_bits() != TAG_UNDEFINED)
904-
.then_some(OwnSetDescriptor::Data { writable: true });
903+
if value.to_bits() == TAG_UNDEFINED {
904+
return None;
905+
}
906+
// An existing symbol-keyed own data property is non-writable when the
907+
// receiver is frozen or its per-symbol attrs say so — so a strict
908+
// `obj[sym] = v` is rejected (throws) rather than silently no-op'd
909+
// (test262 Object/freeze/frozen-object-contains-symbol-properties-strict).
910+
// Mirrors the string-keyed / `set_symbol_property` guards.
911+
let writable = !crate::symbol::symbol_property_is_non_writable(target, key);
912+
return Some(OwnSetDescriptor::Data { writable });
905913
}
906914

907915
let obj_ptr = extract_pointer(target.to_bits()) as usize;

crates/perry-runtime/src/symbol.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ pub(crate) use properties::{
4141
get_symbol_property_attrs, inspect_custom_symbol_ptr, js_object_define_symbol_accessor,
4242
js_object_delete_symbol_property, js_object_has_own_symbol_property,
4343
reflect_symbol_getter_closure_bits, set_symbol_property_attrs, symbol_accessor_descriptor_bits,
44-
symbol_property_is_enumerable, symbol_property_root_bits,
44+
symbol_property_is_enumerable, symbol_property_is_non_writable, symbol_property_root_bits,
4545
};
4646
pub use properties::{
4747
class_static_symbol_lookup, js_class_register_static_symbol, js_object_has_own_symbol,

crates/perry-runtime/src/symbol/properties.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,33 @@ fn object_symbol_data_property_exists(obj_key: usize, sym_key: usize) -> bool {
365365
})
366366
}
367367

368+
/// True when an existing symbol-keyed own data property is non-writable — the
369+
/// receiver was frozen (`Object.freeze`), or the per-symbol attrs recorded via
370+
/// `Object.defineProperty(obj, sym, {writable:false})` say so. Mirrors the
371+
/// frozen / non-writable rejection in `set_symbol_property`, but as a query so
372+
/// the ordinary-`[[Set]]` walk (`own_set_descriptor`) can report the slot as
373+
/// read-only and let a strict write throw. `obj_f64` / `sym_f64` are the same
374+
/// NaN-boxed values passed to the symbol setters.
375+
pub(crate) fn symbol_property_is_non_writable(obj_f64: f64, sym_f64: f64) -> bool {
376+
let obj_key = unsafe { obj_key_from_f64(obj_f64) };
377+
let sym_key = unsafe { sym_key_from_f64(sym_f64) };
378+
if obj_key == 0 || sym_key == 0 {
379+
return false;
380+
}
381+
// Only heap receivers carry the GC integrity flag word.
382+
if (obj_f64.to_bits() >> 48) == 0x7FFD
383+
&& obj_key >= 0x10000
384+
&& crate::object::is_valid_obj_ptr(obj_key as *const u8)
385+
{
386+
let gc = (obj_key - crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader;
387+
let flags = unsafe { (*gc)._reserved };
388+
if flags & crate::gc::OBJ_FLAG_FROZEN != 0 {
389+
return true;
390+
}
391+
}
392+
get_symbol_property_attrs(obj_key, sym_key).is_some_and(|attrs| !attrs.writable())
393+
}
394+
368395
/// `obj[sym] = value` where `sym` is a Symbol. Stores into the side table.
369396
/// Returns the value (NaN-boxed) for chained assignment semantics.
370397
#[no_mangle]

0 commit comments

Comments
 (0)