Skip to content

fix(runtime): #5901 — strict write to a frozen symbol-keyed property throws TypeError - #5958

Merged
proggeramlug merged 1 commit into
mainfrom
fix/t262-5901-frozen-symbol-strict
Jul 4, 2026
Merged

fix(runtime): #5901 — strict write to a frozen symbol-keyed property throws TypeError#5958
proggeramlug merged 1 commit into
mainfrom
fix/t262-5901-frozen-symbol-strict

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Summary

A strict-mode obj[sym] = v (Symbol key) onto a frozen object silently no-op'd instead of throwing a TypeError. Fixes Object/freeze/frozen-object-contains-symbol-properties-strict from #5901.

Root cause

Strict-mode obj[sym] = v routes through js_put_value_setordinary_set_with_receiverown_set_descriptor. For a symbol key, own_set_descriptor returned Data { writable: true } unconditionally whenever the property existed — ignoring the receiver's frozen state and any per-symbol writable:false attribute. So the ordinary [[Set]] reported success, and js_put_value_set never threw. (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 present in set_symbol_property:

  • frozen receiver ⇒ non-writable;
  • else consult the per-symbol attrs table (defineProperty(obj, sym, {writable:false})).

ordinary_set_with_receiver then returns false, and js_put_value_set throws under strict mode. A sealed-but-not-frozen object's existing symbol property stays writable (sealed permits value changes), so only genuine non-writable slots are rejected.

Before / after (test262 built-ins/Object)

  • freeze sub-slice: 0 fail (frozen-object-contains-symbol-properties-strict was the sole failure there).

Verified against Node that: a 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 access all behave identically. No behavioral change for non-symbol keys or non-frozen receivers — the change is scoped to the symbol branch of own_set_descriptor.

Validation

cargo fmt --all -- --check clean; scripts/check_file_size.sh clean (proxy.rs 1972, properties.rs 626 lines). Built and tested on an internal Linux box.

Refs #5901.

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling of symbol-keyed object properties during assignment.
    • Existing symbol properties can now correctly behave as read-only when they are not writable, which helps make Proxy and Reflect.set behavior more accurate.
    • Assignment to symbol-keyed properties now better respects strict-mode rules and existing property attributes.

…throws

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.
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a new symbol_property_is_non_writable helper function that checks frozen flags and per-symbol property attributes, re-exports it from the symbol module, and updates proxy.rs's own_set_descriptor to use it for computing writability of symbol-keyed own data properties instead of always returning true.

Changes

Symbol Writability Check

Layer / File(s) Summary
Non-writable check helper
crates/perry-runtime/src/symbol/properties.rs, crates/perry-runtime/src/symbol.rs
Added symbol_property_is_non_writable(obj_f64, sym_f64) that decodes NaN-boxed keys, checks OBJ_FLAG_FROZEN, and falls back to stored PropertyAttrs writability; re-exported it from symbol.rs.
Proxy set descriptor fix
crates/perry-runtime/src/proxy.rs
own_set_descriptor's symbol-keyed branch now returns None for TAG_UNDEFINED values and computes OwnSetDescriptor::Data { writable } via the new helper instead of hardcoding true.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • PerryTS/perry#5882: Both PRs modify own_set_descriptor/[[Set]] decision logic in the same proxy.rs code path.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main runtime fix for strict symbol-keyed writes on frozen objects.
Description check ✅ Passed The description covers the bug, root cause, fix, related issue, and validation, though it does not follow every template heading exactly.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/t262-5901-frozen-symbol-strict

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
crates/perry-runtime/src/symbol/properties.rs (1)

375-393: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider extracting the shared "is receiver frozen" check.

The GC-flag frozen check here (Lines 382-391) duplicates the equivalent block in set_symbol_property (Lines 311-320 in the unchanged context). Both must independently derive obj_key/sym_key, cast to GcHeader, and test OBJ_FLAG_FROZEN. Since this PR exists precisely because two related code paths drifted out of sync (writability logic wasn't mirrored here originally), extracting a shared fn is_heap_receiver_frozen(obj_f64: f64, obj_key: usize) -> bool helper would reduce the chance of the same divergence recurring.

♻️ Suggested extraction
+fn heap_receiver_is_frozen(obj_f64: f64, obj_key: usize) -> bool {
+    if (obj_f64.to_bits() >> 48) == 0x7FFD
+        && obj_key >= 0x10000
+        && crate::object::is_valid_obj_ptr(obj_key as *const u8)
+    {
+        let gc = (obj_key - crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader;
+        return unsafe { (*gc)._reserved } & crate::gc::OBJ_FLAG_FROZEN != 0;
+    }
+    false
+}
+
 pub(crate) fn symbol_property_is_non_writable(obj_f64: f64, sym_f64: f64) -> bool {
     let obj_key = unsafe { obj_key_from_f64(obj_f64) };
     let sym_key = unsafe { sym_key_from_f64(sym_f64) };
     if obj_key == 0 || sym_key == 0 {
         return false;
     }
-    // Only heap receivers carry the GC integrity flag word.
-    if (obj_f64.to_bits() >> 48) == 0x7FFD
-        && obj_key >= 0x10000
-        && crate::object::is_valid_obj_ptr(obj_key as *const u8)
-    {
-        let gc = (obj_key - crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader;
-        let flags = unsafe { (*gc)._reserved };
-        if flags & crate::gc::OBJ_FLAG_FROZEN != 0 {
-            return true;
-        }
-    }
+    if heap_receiver_is_frozen(obj_f64, obj_key) {
+        return true;
+    }
     get_symbol_property_attrs(obj_key, sym_key).is_some_and(|attrs| !attrs.writable())
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/symbol/properties.rs` around lines 375 - 393, The
frozen-receiver GC flag check in symbol_property_is_non_writable is duplicated
from set_symbol_property and can drift again; extract the shared heap-receiver
frozen test into a helper such as is_heap_receiver_frozen and reuse it in both
code paths. Keep the existing obj_key/sym_key derivation in
symbol_property_is_non_writable, but move the GcHeader cast, OBJ_FLAG_FROZEN
read, and heap-validity guard into the shared helper so both writability and
mutability logic stay aligned.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/perry-runtime/src/proxy.rs`:
- Around line 901-913: The symbol-own-property branch in proxy.rs is using
js_object_get_symbol_property() and TAG_UNDEFINED to detect presence, which
incorrectly treats an existing symbol property whose value is undefined as
missing. Update this logic in the same area as OwnSetDescriptor::Data to use the
symbol existence check already used by the setter path, such as
object_symbol_data_property_exists or own_symbol_property, and then derive
writability from that result instead of inspecting the returned value bits.

---

Nitpick comments:
In `@crates/perry-runtime/src/symbol/properties.rs`:
- Around line 375-393: The frozen-receiver GC flag check in
symbol_property_is_non_writable is duplicated from set_symbol_property and can
drift again; extract the shared heap-receiver frozen test into a helper such as
is_heap_receiver_frozen and reuse it in both code paths. Keep the existing
obj_key/sym_key derivation in symbol_property_is_non_writable, but move the
GcHeader cast, OBJ_FLAG_FROZEN read, and heap-validity guard into the shared
helper so both writability and mutability logic stay aligned.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0d09c6a6-16f0-445a-89af-caf5472eabee

📥 Commits

Reviewing files that changed from the base of the PR and between 1839711 and bbc0e7e.

📒 Files selected for processing (3)
  • crates/perry-runtime/src/proxy.rs
  • crates/perry-runtime/src/symbol.rs
  • crates/perry-runtime/src/symbol/properties.rs

Comment on lines 901 to 913
if unsafe { crate::symbol::js_is_symbol(key) } != 0 {
let value = unsafe { crate::symbol::js_object_get_symbol_property(target, key) };
return (value.to_bits() != TAG_UNDEFINED)
.then_some(OwnSetDescriptor::Data { writable: true });
if value.to_bits() == TAG_UNDEFINED {
return None;
}
// An existing symbol-keyed own data property is non-writable when the
// receiver is frozen or its per-symbol attrs say so — so a strict
// `obj[sym] = v` is rejected (throws) rather than silently no-op'd
// (test262 Object/freeze/frozen-object-contains-symbol-properties-strict).
// Mirrors the string-keyed / `set_symbol_property` guards.
let writable = !crate::symbol::symbol_property_is_non_writable(target, key);
return Some(OwnSetDescriptor::Data { writable });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "fn js_object_get_symbol_property" -A 20 crates/perry-runtime/src/symbol
rg -n "fn object_symbol_data_property_exists" -A 10 crates/perry-runtime/src/symbol

Repository: PerryTS/perry

Length of output: 3248


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '240,340p' crates/perry-runtime/src/symbol/get.rs
printf '\n==== proxy.rs ====\n'
sed -n '880,940p' crates/perry-runtime/src/proxy.rs
printf '\n==== properties.rs ====\n'
sed -n '340,390p' crates/perry-runtime/src/symbol/properties.rs

Repository: PerryTS/perry

Length of output: 10316


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "own_set_descriptor\(" crates/perry-runtime/src/proxy.rs crates/perry-runtime/src -g '!target'
printf '\n==== symbol property writes ====\n'
rg -n "store_object_symbol_property_root|object_symbol_data_property_exists|symbol_property_is_non_writable|js_object_get_symbol_property" crates/perry-runtime/src/symbol crates/perry-runtime/src -g '!target'
printf '\n==== assignment path around own_set_descriptor usage ====\n'
sed -n '940,1080p' crates/perry-runtime/src/proxy.rs

Repository: PerryTS/perry

Length of output: 17423


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '==== js_object_get_symbol_property internals ====\\n'
sed -n '1,220p' crates/perry-runtime/src/symbol/get.rs
printf '\\n==== own_set_descriptor call sites ====\\n'
sed -n '1110,1165p' crates/perry-runtime/src/proxy.rs
printf '\\n==== later own_set_descriptor call site ====\\n'
sed -n '1290,1338p' crates/perry-runtime/src/proxy.rs
printf '\\n==== symbol data property tests / docs ====\\n'
rg -n "undefined|TAG_UNDEFINED|object_symbol_data_property_exists|defineProperty\\(obj, sym" crates/perry-runtime/src/symbol crates/perry-runtime/src/object -g '!target'

Repository: PerryTS/perry

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "own_symbol_property|pub use .*object_symbol_data_property_exists|pub use .*own_symbol_property|object_symbol_data_property_exists" crates/perry-runtime/src/symbol.rs crates/perry-runtime/src/symbol crates/perry-runtime/src/proxy.rs

Repository: PerryTS/perry

Length of output: 1895


Use an existence check for symbol own-properties here
js_object_get_symbol_property() can return TAG_UNDEFINED for a property that exists with value undefined, so this branch still conflates “missing” with “present but undefined”. That lets an extensible receiver fall through to the create/new-property path instead of treating the existing symbol data property as read-only. Use the same presence check as the symbol setter path (object_symbol_data_property_exists / own_symbol_property) instead of testing the returned value bits.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/proxy.rs` around lines 901 - 913, The
symbol-own-property branch in proxy.rs is using js_object_get_symbol_property()
and TAG_UNDEFINED to detect presence, which incorrectly treats an existing
symbol property whose value is undefined as missing. Update this logic in the
same area as OwnSetDescriptor::Data to use the symbol existence check already
used by the setter path, such as object_symbol_data_property_exists or
own_symbol_property, and then derive writability from that result instead of
inspecting the returned value bits.

@proggeramlug
proggeramlug merged commit 32aa095 into main Jul 4, 2026
16 of 17 checks passed
@proggeramlug
proggeramlug deleted the fix/t262-5901-frozen-symbol-strict branch July 4, 2026 11:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant