fix(gc): initialize keys_array before clone_with_extra's collecting allocation (#7683) - #7727
Conversation
…nches (#7683) The slot was left holding whatever the reused hole contained until set_object_keys_array at the end of the function -- but js_array_alloc runs in between, and it can collect. The collector reads that slot as a child edge via object::gc_keys_array_slot. Claude-Session: https://claude.ai/code/session_01Y1QZ5wUP9gRSwpiweT4Wix
…7683) The runtime version of this test passed with the fix deleted -- the window is inside the function and a fresh block is zeroed. The invariant is only decidable in the source, so check it there. Claude-Session: https://claude.ai/code/session_01Y1QZ5wUP9gRSwpiweT4Wix
528beb4 to
7c65e77
Compare
📝 WalkthroughWalkthroughThe clone allocation paths now initialize ChangesClone keys array GC safety
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 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 |
Merging as v0.5.1423
The bytes are not zero: My first test was vacuous, and I'm recording that rather than quietly replacing itI wrote the runtime version first: force a collection into the window ( Reproducing it in-suite therefore needs a specific swept-hole layout and a collection landing in a few-instruction window. That is exactly why the fix is a by-construction initialisation rather than a guard, and why the guard asserts the invariant where it is decidable — the source, in the style of And one more self-inflicted lesson worth leaving in the file: the first version of that source check matched the phrase CreditThe root cause came from an agent's static analysis after a 150-run soak found nothing — and it was right to call that soak inconclusive rather than clean: at a 1-in-102 rate, seeing zero crashes in 150 runs has a ~23% probability by luck, so N≈300–450 would be needed to conclude anything. Fixing the mechanism it found beats soaking harder. Gates 21/21 (now including |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
crates/perry-runtime/src/object/alloc.rs (1)
757-766: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDuplicate GC_STORE_AUDIT annotation line.
Line 765 repeats the same "GC_STORE_AUDIT(INIT): freshly allocated clone starts with no keys-array edge" statement that lines 757-758 already give in full. Line 807 repeats lines 799-800 the same way. Remove the short duplicate line in each branch and keep only the full explanation.
Also applies to: 799-808
🤖 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/alloc.rs` around lines 757 - 766, Remove the duplicate short GC_STORE_AUDIT annotation immediately before (*new_ptr).keys_array = ptr::null_mut() in both affected allocator branches, including the branch around the corresponding second initialization. Preserve the full explanatory comment above each assignment and leave the initialization logic unchanged.
🤖 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.
Nitpick comments:
In `@crates/perry-runtime/src/object/alloc.rs`:
- Around line 757-766: Remove the duplicate short GC_STORE_AUDIT annotation
immediately before (*new_ptr).keys_array = ptr::null_mut() in both affected
allocator branches, including the branch around the corresponding second
initialization. Preserve the full explanatory comment above each assignment and
leave the initialization logic unchanged.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: b4e0ef1c-f0fd-495f-ad2f-2d21d9d8eb53
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (6)
CLAUDE.mdCargo.tomlchangelog.d/7726-clone-keys-array-init.mdcrates/perry-runtime/src/gc/tests/clone_keys_array_init.rscrates/perry-runtime/src/gc/tests/mod.rscrates/perry-runtime/src/object/alloc.rs
Fixed
js_object_clone_with_extrapublished a half-built object whosekeys_arrayslot held recycled heap bytes (gc: perry-runtime lib tests SIGSEGV in typed_feedback::object_shape via js_object_clone_with_extra (1 in ~102 full-suite runs, no test reports FAILED) #7683). Both branches initialiseobject_type,class_id,parent_class_id,field_countandmetaimmediately after allocation, then setkeys_arrayonly at the end viaset_object_keys_array. Between those two points sitscrate::array::js_array_alloc.That call allocates, so it can collect — and the collector reads exactly this slot as a child edge (
object::gc_keys_array_slot, enumerated bygc_child_slots). A collection landing in that window scans a pointer the mutator never wrote.The bytes are not zero.
arena_alloc_gc_old's fast path deliberately reuses a swept, non-zeroed hole (GC: old-gen fragmentation — scattered survivors pin 105 MB of blocks for a ~1 MB live set #7437: "reuse a swept same-size hole … otherwise a block with any live object never yields its dead bytes back"), so the slot holds real leftover heap content from whatever last occupied it. Whether that content happens to look like a plausible-but-unmapped address depends on allocation history — which is the shape of the ~1-in-102typed_feedback::object_shapeSIGSEGV reported in gc: perry-runtime lib tests SIGSEGV in typed_feedback::object_shape via js_object_clone_with_extra (1 in ~102 full-suite runs, no test reports FAILED) #7683.Every sibling allocator in
object/alloc.rsalready nulls the slot at this point. This function was the one that did not.On the test, and why it checks source rather than behaviour. The runtime version was written first: force a collection into the window (
force_next_general_arena_alloc_slow+GC_OLD_RECLAIM_PENDING, the levers GC: the two lazy intrinsic-tower builders need #7217's no-move window, and need a gate that can fail first #7251 established), then assert the published clone'skeys_arrayis sane. It passed with the fix deleted. Two independent reasons: by the time the function returns,set_object_keys_arrayhas written the slot correctly, so nothing observable survives the window; and a fresh arena block is zeroed, so even inside the window the garbage reads as null unless the allocation lands in a recycled old-space hole with the right history.Reproducing it in-suite therefore needs a specific swept-hole layout and a collection landing in a few-instruction window. That is exactly why the fix is a by-construction initialisation rather than a guard, and why the guard asserts the invariant where it is decidable — the source, in the style of
scripts/gc_pin_sites.py's custody check forGC_FLAG_PINNED. Removing either initialisation fails the test.One note for anyone writing a similar source-level check: the first version matched the phrase
arena_alloc_gcinside its own explanatory comment, registering a third allocation site and failing against correct code. It now strips comments before scanning. A source check that reads its own documentation as code is worse than no check.Summary by CodeRabbit
Bug Fixes
Tests
Documentation
Chores