fix(codegen): #5459 — store array head back to module-global GC root on push from a closure - #5461
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughFixes a GC use-after-free (issue ChangesGC Root Store-Back Fix for Module-Global Array Push
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related issues
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
crates/perry/tests/issue_5459_module_global_array_push_in_closure.rs (1)
24-45: 💤 Low valueConsider capturing stderr for better crash diagnostics.
When the compiled binary crashes (e.g., SIGSEGV), diagnostic information may appear in stderr rather than stdout. Returning stderr alongside stdout would improve debugging when assertions fail.
💡 Optional improvement
-fn compile_and_run(dir: &std::path::Path, entry: &std::path::Path) -> (bool, String) { +fn compile_and_run(dir: &std::path::Path, entry: &std::path::Path) -> (bool, String, String) { let output = dir.join("main_bin"); let compile = Command::new(perry_bin()) .current_dir(dir) .arg("compile") .arg(entry) .arg("-o") .arg(&output) .output() .expect("run perry compile"); assert!( compile.status.success(), "perry compile failed\nstdout:\n{}\nstderr:\n{}", String::from_utf8_lossy(&compile.stdout), String::from_utf8_lossy(&compile.stderr) ); let run = Command::new(&output).output().expect("run compiled binary"); ( run.status.success(), String::from_utf8_lossy(&run.stdout).to_string(), + String::from_utf8_lossy(&run.stderr).to_string(), ) }Then update call sites to include stderr in assertion messages:
let (ok, stdout, stderr) = compile_and_run(dir.path(), &entry); assert!(ok, "binary crashed\nstdout:\n{stdout}\nstderr:\n{stderr}");🤖 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/tests/issue_5459_module_global_array_push_in_closure.rs` around lines 24 - 45, The compile_and_run function currently only captures and returns stdout from the executed binary, but stderr contains critical diagnostic information when the binary crashes. Modify the return type of compile_and_run to return a tuple of three elements (bool, String, String) instead of two, where the third element is stderr. Update the return statement to capture stderr from the run command output using run.stderr and convert it to a string, then include it in the tuple alongside the existing stdout return value. Finally, update all call sites of compile_and_run to unpack the three return values and include the stderr output in assertion error messages for better crash diagnostics.
🤖 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/tests/issue_5459_module_global_array_push_in_closure.rs`:
- Around line 24-45: The compile_and_run function currently only captures and
returns stdout from the executed binary, but stderr contains critical diagnostic
information when the binary crashes. Modify the return type of compile_and_run
to return a tuple of three elements (bool, String, String) instead of two, where
the third element is stderr. Update the return statement to capture stderr from
the run command output using run.stderr and convert it to a string, then include
it in the tuple alongside the existing stdout return value. Finally, update all
call sites of compile_and_run to unpack the three return values and include the
stderr output in assertion error messages for better crash diagnostics.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: a9222876-6818-4d98-981c-b2abed3fe724
📒 Files selected for processing (2)
crates/perry-codegen/src/expr/array_push.rscrates/perry/tests/issue_5459_module_global_array_push_in_closure.rs
…push from a closure `arr.push(...)` whose `arr` is a module-level global accessed from inside a nested function (closure/IIFE) silently skipped the realloc write-back, so a relocated array head was never stored to the registered GC-root global slot. The old head was freed on the next GC and the global dangled — garbage `.length`, freed elements, SIGSEGV under allocation churn. Cause: in `expr/array_push.rs` the `boxed_vars` write-back branch returned early after the captured/local-box cases. A module-global that is in `boxed_vars` (because a nested function references it) but has no box location in the callee context falls through both inner arms; the unconditional early return then skipped the module-global store-back entirely. The load path, meanwhile, read the global directly — so reads saw the global but the grown head was never written there. Fix: only return early when a box location was actually written; otherwise fall through to the existing module-global store-back (`emit_root_nanbox_store_on_block`). Applied to both `ArrayPush` and `ArrayPushSpread`. This is a generational-GC-independent memory-safety bug (reproduces with `PERRY_GEN_GC=0` too). It was surfaced while investigating WeakMap/WeakSet weakness (#2656), but is independent — plain `WeakRef`/`FinalizationRegistry` with live targets held in a module-global array hit the same UAF. Regression test: crates/perry/tests/issue_5459_module_global_array_push_in_closure.rs (direct push + spread push). 30 array/closure/gc gap tests pass; existing weakref/finalization and arraylike-dispatch tests unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
f56c44d to
8c61716
Compare
Fixes #5459.
Problem
arr.push(...)wherearris a module-level global accessed from inside a nested function (closure/IIFE) caused a use-after-free / SIGSEGV under allocation churn:Right after
setup()the array is correct (length 50); the firstgc()corrupts it. The same population done at module scope is fine — the only delta is the IIFE.Root cause
In
crates/perry-codegen/src/expr/array_push.rs, theboxed_varswrite-back branch returned early after handling the captured-box / local-box cases:A module-level global that lands in
boxed_vars(because a nested function references it) but has no box location in the callee context falls through both inner arms — and the unconditionalreturnthen skips the realloc write-back entirely. The load path read the global directly (load @perry_global_…), but the relocated head fromjs_array_push_f64was never stored back. Confirmed in the emitted IR: the closure computesnew_boxand then drops it (nostore, nojs_write_barrier_root_nanbox), whereas the module-scope path emits the store-back.So under GC evacuation the array head moves, the registered GC-root global slot keeps the stale (freed) pointer, and reads dangle.
Fix
Only return early when a box location was actually written; otherwise fall through to the existing module-global store-back (
emit_root_nanbox_store_on_block, which stores to@global+ emits the root write barrier). Applied to bothArrayPushandArrayPushSpread.Notes
PERRY_GEN_GC=0(full mark-sweep) too; it's a codegen root-write bug, not a GC-policy bug.WeakRef/FinalizationRegistrywith live targets held in a module-global array hit the same UAF (FinReg lost 18/50 live registered targets / crashed; now 50/50).Verification
crates/perry/tests/issue_5459_module_global_array_push_in_closure.rs— direct push and spread push into a module-global from an IIFE survive 12× churn+gc (pre-fix: SIGSEGV / garbage length). Both green.test_gap_*files pass; existingissue_2656weakref/finalization,issue_5139arraylike-dispatch, andissue_5195tests unaffected.🤖 Generated with Claude Code
Summary by CodeRabbit
pushand spread-basedpush) could lose or corrupt elements after allocation churn and garbage collection. Arrays now reliably preserve length and contents after these operations.pushand spread-push invoked from an IIFE, validating correct behavior across repeated allocation/gc()cycles.