Skip to content

fix(gc): refresh obj before the class-static mirror on both own-data-write arms - #7381

Merged
proggeramlug merged 1 commit into
mainfrom
fix/7341-mirror-static-write-refresh
Aug 4, 2026
Merged

fix(gc): refresh obj before the class-static mirror on both own-data-write arms#7381
proggeramlug merged 1 commit into
mainfrom
fix/7341-mirror-static-write-refresh

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Another of the catches left open after #7373#7376/#7380.

The bug

js_object_set_field_by_name's two own-data-write arms perform the write — an inline slot store, or overflow_set into the overflow map — and then call mirror_class_object_static_write.

Both writes can reach an allocator: overflow_set inserts into a Rust map, and a minor GC triggers on the malloc-count threshold as well as on arena-block allocation. The mirror's first instruction dereferences obj (ldr w8, [x0] at +24), so a collection inside that write leaves the hook reading from-space.

The function already carries a refresh_roots_after_alloc!() macro and calls it after every other allocating step. These two sites were simply missed.

Why it's scoped to two sites and not eight

There are eight mirror_class_object_static_write(obj, key, value) call sites. I patched all eight first — and the macro republishes value from its handle as well as obj, so arms that intentionally rebind value locally get clobbered. Only the two own-data-write arms are load-bearing for the fault, and only they are safe to refresh.

Verification

The fault moves out of mirror_class_object_static_write entirely, into a separate and unrelated catch in js_string_index_getjs_string_char_at which stays open for follow-up. A fault that moves is the signal that the patch is real; a fault that doesn't move by a byte means the value was already dead before you touched it.

Baselines were checked rather than assumed:

  • test_gap_2159_defineproperty_class_prototype and test_gap_6301_event_target_subclass fail identically on pristine main — pre-existing, one is already in known_failures.json
  • perry-runtime --lib fails 3 / 5 / 3 tests across three runs of pristine main (cargo-test: perry-runtime's suite fails a different number of tests on every run #7365 nondeterminism), so the failure count seen with this patch sits inside main's own noise band
  • 58 object/assign/class/field/shape gap tests otherwise unchanged

Summary by CodeRabbit

  • Bug Fixes
    • Fixed an issue that could cause static property writes to use outdated object references after memory allocation.
    • Improved reliability of property updates across both cached and initial write paths.
    • Confirmed existing baseline behavior remains unchanged.

…write arms

js_object_set_field_by_name performs the property write -- an inline slot
store, or overflow_set into the overflow map -- and then calls
mirror_class_object_static_write. Both writes can reach an allocator
(overflow_set inserts into a Rust map, and a minor GC triggers on the
malloc-count threshold as well as on arena blocks), and the mirror's
first instruction dereferences obj. A collection in that write leaves the
hook reading from-space.

The function already has refresh_roots_after_alloc!() and calls it after
every other allocating step; these two sites were missed.

Scoped to these two arms deliberately. The macro republishes value from
its handle as well as obj, so arms that intentionally rebind value
locally must not refresh -- applying it to all eight mirror sites is
wrong for that reason.

Found via test_gap_gc_assign_string_source_rooting under #7341
quarantine. The fault moves out of the mirror entirely and into a
separate catch in js_string_index_get, which stays open.

Baselines verified rather than assumed: the two gap tests that fail
alongside this also fail on pristine main, and perry-runtime --lib fails
3/5/3 across three runs of pristine main (#7365).
@proggeramlug
proggeramlug merged commit 37cd06d into main Aug 4, 2026
21 of 45 checks passed
@proggeramlug
proggeramlug deleted the fix/7341-mirror-static-write-refresh branch August 4, 2026 15:08
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 81b12469-905c-4c10-afb6-894f89a5da9f

📥 Commits

Reviewing files that changed from the base of the PR and between 9198f30 and 62b4803.

📒 Files selected for processing (2)
  • changelog.d/7381-mirror-static-write-refresh.md
  • crates/perry-runtime/src/object/field_set_by_name.rs

📝 Walkthrough

Walkthrough

This PR fixes a bug in js_object_set_field_by_name where object pointers could become stale after garbage collection during property writes. It adds refresh_roots_after_alloc!() calls in two code paths to refresh obj, key, value, and interned_key before calling mirror_class_object_static_write.

Changes

Static-write root refresh

Layer / File(s) Summary
Root refresh before static-property mirroring
crates/perry-runtime/src/object/field_set_by_name.rs, changelog.d/7381-mirror-static-write-refresh.md
Adds refresh_roots_after_alloc!() calls in the transition-cache hit path and the initial-key write path to re-read rooted pointers before calling mirror_class_object_static_write. A changelog entry documents the root cause, the fix, and the discovery method through from-space quarantine testing.

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

Possibly related issues

Possibly related PRs

  • PerryTS/perry#6811: Adds the same refresh_roots_after_alloc!() calls to the same transition-cache and initial-key write paths in js_object_set_field_by_name.
  • PerryTS/perry#6941: Applies the same GC-root refresh strategy to js_object_set_field_by_name, covering the class static-property mirroring paths this PR addresses.
  • PerryTS/perry#7179: Addresses GC safety in the same function and file by ensuring proper field-count publication and barriered setters for traced-field writes.
✨ 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/7341-mirror-static-write-refresh

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.

proggeramlug added a commit that referenced this pull request Aug 4, 2026
…7383)

Completes #7381, which fixed two of eight and scoped itself there on the
theory that refresh_roots_after_alloc!() -- which republishes obj, key,
value and interned_key together -- could clobber an arm that rebinds
value locally.

That theory was wrong. None of the eight arms rebinds any of the four
after its handle is taken, so republishing is a no-op except for the
relocation it repairs. Measured rather than inspected: the full-coverage
build scores 58 pass / 2 fail on the object/assign/class/field/shape gap
set, byte-identical to pristine main, with both failures pre-existing.

With all eight refreshed the fault leaves mirror_class_object_static_write
entirely and surfaces the next catch in the chain at js_jsvalue_equals.

Co-authored-by: Ralph Küpper <ralph@skelpo.com>
proggeramlug added a commit that referenced this pull request Aug 4, 2026
…7401)

* fix(lint): bring field_set_by_name.rs back under the 2000-line cap

#7381 and #7383 took the file from 1990 to 2048 lines, so `lint` -- a
REQUIRED check -- has been red on main, and every merge since has
bypassed it.

The overrun is comment volume, not code: eight refresh_roots_after_alloc!
call sites each carried a multi-line rationale block. The rationale now
sits once at the macro that implements it, which is where it belonged,
and the call sites are bare.

Worth recording how I got this wrong first: I checked the gate against
origin/main, saw identical output, and called it pre-existing. That
baseline already contained the two merges that caused it. The right
comparison is a commit from before the change.

* docs: name the fragment for its real PR (#7401)

---------

Co-authored-by: Ralph Küpper <ralph@skelpo.com>
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