fix(gc): avoid closure side-table GC deadlock#4956
Merged
proggeramlug merged 3 commits intoJun 11, 2026
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes the
gc_write_barrier_stress::tenured_mutation_stresshang that blocked PR #4928's GitHub Actionscargo-testrun after cancellation, separately from the optimized-lib freshness change.Root cause: during minor copying GC,
scan_closure_dynamic_props_roots_mutheld the closure dynamic-property/prototype side-table mutexes while visiting JS value slots. Visiting those slots can evacuate a closure, and the move hookclosure_dynamic_props_owner_movedre-enters the same side-table lock, deadlocking the runtime inside the first explicitgc()after tenured objects point at nursery values.The fix detaches each closure side-table entry before invoking GC visitor callbacks, then merges it back under the forwarded owner after callbacks complete. It also prevents non-closure receivers from entering closure-only Function.prototype fallback, which the stress exposed after the deadlock was removed.
Verification
cargo test -p perry --test gc_write_barrier_stress tenured_mutation_stress -- --nocapturetimed out after 120s and left nomain_bin/cargoorphan after wrapper cleanup.gc()stall. Stack showedscan_closure_dynamic_props_roots_mut -> visit_nanbox_f64_slot -> move_young -> closure_dynamic_props_owner_moved -> CLOSURE_PROPS.lock.cargo test -p perry-runtime closure::dynamic_props::tests_1802 -- --nocapture=> 4 passed.cargo build -p perry-runtime=> passed, existing warnings only.RUN_STATUS 0 timed_out False elapsed 26.0, stdoutBARRIER_STRESS_OK.cargo test -p perry --test gc_write_barrier_stress tenured_mutation_stress -- --nocapture=> 1 passed, finished in 150.24s.cargo test -p perry --test gc_write_barrier_stress -- --nocapture=> 2 passed, finished in 10.77s.cargo fmt --check --package perry-runtime --package perry=> passed.git diff --check=> passed.gc_write_barrier_stress,main_bin,perry compile, orperry-autoprocess from this worktree.CI note
PR #4928's cancelled
cargo-testappears blocked by this separate GC/runtime stress issue, not by the optimized-lib freshness assertions. After this lands, rerun PR #4928's cancelled check.