Skip to content

fix(runtime): register the per-thread globalThis cache as a GC root#6103

Merged
proggeramlug merged 1 commit into
PerryTS:mainfrom
proggeramlug:fix/globalthis-cache-gc-root
Jul 8, 2026
Merged

fix(runtime): register the per-thread globalThis cache as a GC root#6103
proggeramlug merged 1 commit into
PerryTS:mainfrom
proggeramlug:fix/globalthis-cache-gc-root

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Problem

js_get_global_this caches this thread's globalThis in a thread-local raw-pointer cell (THREAD_GLOBAL_THIS) but — unlike its sibling js_module_top_this — never registers that cache slot as a GC root.

The authoritative GLOBAL_THIS_PTR static is scanned and relocated by scan_object_cache_roots_mut. So when a copying minor evacuates globalThis, that static and the object's overflow side-table are rekeyed to the moved address — but the thread-local cache still points at the stale from-space copy.

Subsequent js_get_global_this() calls hand back that dead pointer. Reading an overflow-stored builtin such as globalThis.Error then returns undefined, because the overflow entry was rekeyed onto the moved object. Downstream this surfaces as Class extends value is not a constructor in a program that constructs many classes during startup and happens to trigger a collection between two global reads (e.g. class X extends Error {} where globalThis.Error reads undefined).

Fix

Register the THREAD_GLOBAL_THIS cache slot as a mutable global root (via js_gc_register_global_root), exactly as js_module_top_this already does for its own thread-local cache. The collector then rewrites the raw pointer to the forwarding address on every move.

Raw-pointer global-root slots are already handled by mark_global_root_bits / rewrite_value_bits (the same path js_module_top_this relies on), and their rewrite-on-move is covered by test_rewrite_mutable_root_slots_updates_shadow_and_global_roots.

Validation

  • Confirmed against a large esbuild-bundled CLI app whose class-heavy setup triggered a collection between two globalThis reads: before the fix, globalThis.Error read undefined and the app threw Class extends value is not a constructor; after the fix that throw is gone deterministically across repeated runs and the app proceeds further into its pipeline.
  • The rewrite mechanism this fix uses is already unit-tested (test_rewrite_mutable_root_slots_updates_shadow_and_global_roots).

Summary by CodeRabbit

  • Bug Fixes
    • Improved stability when accessing the app’s shared runtime state, reducing the risk of crashes or invalid data after memory management events.
    • Fixed an issue where thread-local cached state could become outdated during cleanup, helping ensure consistent behavior across requests.

`js_get_global_this` caches this thread's `globalThis` in a thread-local
raw-pointer cell (`THREAD_GLOBAL_THIS`) but, unlike its sibling
`js_module_top_this`, never registers that cache slot as a GC root. The
authoritative `GLOBAL_THIS_PTR` static IS scanned and relocated by
`scan_object_cache_roots_mut`, so when a copying minor evacuates
`globalThis` the static and the object's overflow side-table are rekeyed
to the moved address — while the thread-local cache keeps pointing at the
stale from-space copy.

Subsequent `js_get_global_this()` calls then hand back that dead pointer.
Reading an overflow-stored builtin such as `globalThis.Error` returns
`undefined` (its overflow entry was rekeyed onto the moved object), which
surfaces downstream as `Class extends value is not a constructor` in
programs that construct many classes during startup and trigger a
collection between two global reads.

Register the cache slot as a mutable global root (exactly as
`js_module_top_this` already does) so the collector rewrites the raw
pointer to the forwarding address on every move. Raw-pointer global-root
slots are handled by `mark_global_root_bits` / `rewrite_value_bits` and
covered by `test_rewrite_mutable_root_slots_updates_shadow_and_global_roots`.
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3a25b0fe-1f84-41b4-94a8-fbb456cfdefa

📥 Commits

Reviewing files that changed from the base of the PR and between 496620e and 4f91209.

📒 Files selected for processing (1)
  • crates/perry-runtime/src/object/global_this/fetch_globals.rs

📝 Walkthrough

Walkthrough

In js_get_global_this(), the first-time allocation path for the per-thread globalThis now registers the THREAD_GLOBAL_THIS cache slot as a mutable GC global root, ensuring the collector rewrites this cached pointer during copying/evacuating collections instead of leaving it stale.

Changes

GC Root Registration Fix

Layer / File(s) Summary
Register thread-local globalThis cache as GC root
crates/perry-runtime/src/object/global_this/fetch_globals.rs
After allocating and caching new_ptr in THREAD_GLOBAL_THIS, the code now registers the cache slot address with the GC via runtime_write_barrier_root_heap_word and js_gc_register_global_root, so the pointer is updated on collection instead of becoming stale.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Related PRs: None specified.

Suggested labels: bug, gc, runtime

Suggested reviewers: None specified.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately names the main runtime GC fix.
Description check ✅ Passed The PR explains the problem, fix, and validation, though it doesn’t follow the repo’s template sections.
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 unit tests (beta)
  • Create PR with unit tests

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.

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