fix(gc): root globalThis across its own lazy builtin population (#6982) - #6994
Conversation
📝 WalkthroughWalkthroughGlobal builtin population now roots ChangesGlobalThis GC safety
Estimated code review effort: 3 (Moderate) | ~25 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
crates/perry-runtime/src/object/global_this/populate.rs (1)
216-237: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy liftRoot live closure/object pointers across the intervening allocations in
populate_global_this_builtins.
singleton()no longer holds a stale address, but this function still derives and reuses un-rooted sibling pointers across allocation-heavy sections:
- The Storage/WebCrypto path captures
closure_ptr/proto_obj/ctor_valuebefore many string allocations, then passes them toinstall_storage_globals(...)andinstall_global_webcrypto(singleton()).install_storage_globalsinstalls methods/accessors and creates storage objects without rooting the passed pointers.- Global function closures are allocated at line 499, then
name_keyis allocated at line 523 beforefn_valueis created from the same rawclosure_ptrat line 524 and written at line 525.- Namespace objects are allocated at 564, then namespace installers run before
ns_valueis re-derived from rawns_objat 603.install_global_webcrypto(singleton())hands an address intonative_module.rs, whose body allocates the crypto key string before dereferencing that value.Root these with
crate::gc::RuntimeHandleScopeand reload from the handles before the following allocations/usages to avoid dangling references from GC evacuation.🤖 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/object/global_this/populate.rs` around lines 216 - 237, Root all live closure, prototype, constructor, storage, WebCrypto, global-function, and namespace object pointers in populate_global_this_builtins with crate::gc::RuntimeHandleScope before allocation-heavy operations. Reload pointer values from their handles immediately before install_storage_globals, install_global_webcrypto, property writes, and namespace installation; also update install_global_webcrypto to receive and reload a rooted handle before its crypto-key string allocation.Source: Learnings
🤖 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.
Outside diff comments:
In `@crates/perry-runtime/src/object/global_this/populate.rs`:
- Around line 216-237: Root all live closure, prototype, constructor, storage,
WebCrypto, global-function, and namespace object pointers in
populate_global_this_builtins with crate::gc::RuntimeHandleScope before
allocation-heavy operations. Reload pointer values from their handles
immediately before install_storage_globals, install_global_webcrypto, property
writes, and namespace installation; also update install_global_webcrypto to
receive and reload a rooted handle before its crypto-key string allocation.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: ff9c6949-7103-4e29-8bd5-9b5f7fa2c031
📒 Files selected for processing (3)
changelog.d/6994-gc-globalthis-population-rooting.mdcrates/perry-runtime/src/object/global_this/fetch_globals.rscrates/perry-runtime/src/object/global_this/populate.rs
js_get_global_this registers two root slots for the freshly allocated singleton so an evacuating collector rewrites them on a move, then passed the raw pre-GC pointer by value into populate_global_this_builtins, which installs several hundred builtins and allocates on nearly every step. When a copying minor relocated the singleton mid-population, every later js_object_set_field_by_name(singleton, ..) addressed the dead from-space copy, whose bytes had been recycled for freshly relocated objects. Root the singleton in a RuntimeHandleScope and re-read it through the handle at every use; return the value re-read from the registered cache slot. Binding singleton as a closure makes the conversion exhaustive by construction.
bc998a2 to
2415771
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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 `@changelog.d/6994-gc-globalthis-population-rooting.md`:
- Line 1: Remove the ### Fixed heading from the changelog fragment, leaving only
the entry body.
🪄 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: 29ca2d51-79d6-4515-be84-289d63fd4aa9
📒 Files selected for processing (3)
changelog.d/6994-gc-globalthis-population-rooting.mdcrates/perry-runtime/src/object/global_this/fetch_globals.rscrates/perry-runtime/src/object/global_this/populate.rs
🚧 Files skipped from review as they are similar to previous changes (1)
- crates/perry-runtime/src/object/global_this/fetch_globals.rs
| @@ -0,0 +1,44 @@ | |||
| ### Fixed | |||
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Remove the changelog heading.
This fragment must contain only the entry body; ### Fixed is a section header.
Proposed fix
-### Fixed
-📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ### Fixed |
🤖 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 `@changelog.d/6994-gc-globalthis-population-rooting.md` at line 1, Remove the
### Fixed heading from the changelog fragment, leaving only the entry body.
Sources: Coding guidelines, Learnings
Fixes the largest slice of #6982 / #6981.
First: the premise in #6982 was wrong, and the symbolized stacks say so
#6982 recorded that the five crashers faulted on
ldrb w8, [x21, #-0x8]!and concluded this was "a GC-headerobj_typeread ataddr-8— the collector validating a pointer that should never have reached it". It also flagged that as an n=2 hypothesis matched by offset into an unnamed symbol, and asked for a symbols build before any fix. That was the right call.No special build was needed:
post_link.rs::strip_final_binaryskipsstripwhenPERRY_DEBUG_SYMBOLS=1is set. With real symbols, all five:There is not a single GC frame in any of them. No mark, no trace, no copying, no evacuate. The faulting
ldrb [x21, #-0x8]!isjs_array_lengthreading an array header atarr-8— an ordinary mutator read, not collector validation. The collector is not the faulting code; it is the code that moved the object out from under the mutator.The faulting addresses are the tell: their high halves are ASCII —
0x434c4f53is"CLOS",0x004e614eis"NaN"— relocated string payload read where a pointer/descriptor was expected.Root cause
js_get_global_thisregisters two root slots for the freshly allocated singleton (THREAD_GLOBAL_THIS,GLOBAL_THIS_PTR), with a long comment explaining that this exists precisely so an evacuating collector rewrites them on a move. It then hands the raw, pre-GC pointer by value topopulate_global_this_builtins, which installs several hundred builtins and allocates on nearly every step:Those allocations are exactly the 4 579–5 965 objects the copying minor reports. When it relocates the singleton mid-population, the two registered slots are rewritten and the by-value argument is not, so every later
js_object_set_field_by_name(singleton, ..)/set_builtin_property_attrs(singleton as usize, ..)addresses the dead from-space copy whose bytes have been recycled.js_get_global_thisthen returns that stale address too.The GC diagnostics for the cycle immediately before each crash confirm the shape:
compiled_shadowandshadow_rootsentirely zero,native_stack_fallback.decision = "skip_disabled". Nothing kept that pointer alive or current.Same class as #6951/#6972 (a raw reference held across a collection point), one layer further out. Only reachable with the conservative native-stack scan off — production's
Auto -> SkipDisabled— which is #6981's "the scan is load-bearing for correctness": it was pinning the argument register.The fix
Root the singleton in a
RuntimeHandleScopeand re-read it through the handle at every use (root_raw_mut_ptr/get_raw_mut_ptr— the idiomarray/header.rsalready uses), and return the value re-read from the registered cache slot. The two helpers that take the singleton (alias_number_static_to_global_function,alias_typed_array_proto_to_string) had the same window and get the same treatment.singletonis rebound as a closure rather than a value, so the conversion is exhaustive by construction: any use not converted fails to compile.Measurements
macOS arm64, pinned Node 26.5.0, release build from
aa1c15028, evacuating precise-roots armPERRY_GC_HEAP_LIMIT=8 PERRY_GC_INCREMENTAL=0 PERRY_CONSERVATIVE_STACK_SCAN=off, withcopied_objects > 0verified on every run (4 579 / 4 640 / 4 614 / 5 948 / 5 965 before; 9 672 / 15 368 / 15 409 after, on the files that now run to completion).Control arms pin causality — on the unfixed build each of the five passes with the conservative scan on, passes with precise roots and no pressure, and crashes only with both.
repsel_canonical_i32ta_param_numeric_readtypedarray_param_readrepsel_ptr_shape_barriersrepsel_ptr_shape_localsrepsel_proven_this_frozenTypeError: bump is not a function(#6992)Crashes on that arm: 6 -> 2. All 21 corpus files remain byte-identical to Node on the as-shipped configuration — no regression.
One defect or several? (the question #6982 asked)
The five crashes shared one blocking defect, and it was masking at least two more. All five faulted downstream of the same
js_get_global_this_builtin_value -> populate_global_this_builtinsspine, at four different sites, purely depending on where the recycled bytes landed. Removing that shared first hurdle made two independent defects observable:repsel_ptr_shape_localsnow crashes with a completely different 2-frame stack (..._Pt_constructor + 236 -> js_object_set_field_by_name + 3044): a compiled constructor's receiver going stale across the same collection. That is the ctor-receiver rooting class fix(gc): operand temporaries are precise roots — collection methods, constructor args, string-method receiver (#6970, #6969, #6971) #6983 is already addressing, so it is deliberately untouched here.thismethod value is lost across a relocating minor (TypeError: bump is not a function) #6992 —repsel_proven_this_frozenno longer crashes but loses a method value.What I did NOT do, and one thing reviewers should know
No cargo-test-visible unit test. I verified the fix on the real reproducer in both directions, but I did not land a Rust unit test, and I would rather say so than imply coverage that does not exist. The guards needed to force a relocating minor at a controlled point (
CopyingNurseryTestGuard,ConservativeScanDisabledGuard,GcTriggerThresholdTestGuard::make_arena_trigger_due) arepub(super)insidegc::tests, while the functions under test are private toobject::global_this; bridging them is real work and I did not want to ship a test whose teeth I had not verified in both directions.The corpus does not gate this class on PRs — filed as #6993. Measured directly on the unfixed binaries, every PR-gated arm is green:
PR_ARMSisdefault,verify_evac,cons_scan_off,shipped_default, and the evacuating base needsPERRY_GC_INCREMENTAL=0andPERRY_CONSERVATIVE_STACK_SCAN=offtogether — a combination that appears only in--arms all, which runs on push, not onpull_request. I did not simply add the arm toPR_ARMSbecause 13 corpus cells are still red on it and that would block every unrelated PR.Verified on the quiet mini, pinned Node 26.5.0,
cargo fmt --all -- --checkclean.Summary by CodeRabbit
globalThislazy singleton population that could lead to stale pointers and crashes while initializing global built-ins.globalThisreads return the currently valid object after garbage collection moves it.