From ff1dca18e8952aab638657f04536a66a625d0991 Mon Sep 17 00:00:00 2001 From: Ralph Date: Wed, 8 Jul 2026 13:26:07 -0700 Subject: [PATCH] fix(gc): release side-table locks before visiting prototype slots (self-deadlock) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every runtime-scoped cargo-test run since this morning's merge train wedged at the 3h job timeout. lldb on the parked test binary shows a single-thread self-deadlock in the copying minor collector: visit_closure_static_prototype_slot_mut (holds closure-prototypes Mutex) -> visit -> move_young (prototype closure is young) -> gc_type_after_payload_move -> closure_dynamic_props_owner_moved (locks the SAME Mutex; parks) std::sync::Mutex is non-reentrant, so the collector parks forever inside gc::tests::runtime_roots::callback_scanners:: test_geisterhand_callback_then_json_reviver_copied_minor_gc, and every other test queues behind the copying-nursery isolation lock — cargo-test then sits silent until the job timeout kills it. visit_object_static_prototype_slot_mut (#2820 Object.setPrototypeOf side table) has the identical shape: an ordinary-object move re-enters object_static_prototype_owner_moved on the same lock. Fix both with the pattern their sibling visitors already use (visit_closure_dynamic_prop_values_mut, scan_closure_dynamic_props_roots_mut): remove the entry, drop the lock, run the visit, re-insert — re-keying to the forwarded owner address in case the visit moved the owner itself. Verified: gc::tests::runtime_roots::callback_scanners 26/26 (was: wedged indefinitely), full perry-runtime lib suite 1162/1162 in 3.6s. Co-Authored-By: Claude Fable 5 --- .../src/closure/dynamic_props.rs | 23 +++++++++++++--- .../src/object/prototype_chain.rs | 26 ++++++++++++++++--- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/crates/perry-runtime/src/closure/dynamic_props.rs b/crates/perry-runtime/src/closure/dynamic_props.rs index c2090c9b84..d50a40a3ec 100644 --- a/crates/perry-runtime/src/closure/dynamic_props.rs +++ b/crates/perry-runtime/src/closure/dynamic_props.rs @@ -205,10 +205,27 @@ pub(crate) fn visit_closure_static_prototype_slot_mut( if owner == 0 { return; } + // Take the entry OUT and run the visit with the lock RELEASED: a + // copying-minor rewrite visitor can move the prototype closure, and + // move fixup re-enters `closure_dynamic_props_owner_moved`, which + // takes this same lock — visiting under it self-deadlocks the + // collector (the geisterhand+reviver GC test wedged CI's cargo-test + // at the 3h job timeout). Same remove → visit → merge-back pattern + // as `visit_closure_dynamic_prop_values_mut` above and the roots + // scanner below. + let Some(mut proto_bits) = get_closure_prototypes() + .lock() + .ok() + .and_then(|mut prototypes| prototypes.remove(&owner)) + else { + return; + }; + visit(&mut proto_bits as *mut u64); + // The visit can forward the owner itself (self-referential + // prototype); re-key like the roots scanner does. + let new_owner = forwarded_heap_owner(owner).unwrap_or(owner); if let Ok(mut prototypes) = get_closure_prototypes().lock() { - if let Some(proto_bits) = prototypes.get_mut(&owner) { - visit(proto_bits as *mut u64); - } + prototypes.insert(new_owner, proto_bits); } } diff --git a/crates/perry-runtime/src/object/prototype_chain.rs b/crates/perry-runtime/src/object/prototype_chain.rs index 1371f1740d..493ab9a701 100644 --- a/crates/perry-runtime/src/object/prototype_chain.rs +++ b/crates/perry-runtime/src/object/prototype_chain.rs @@ -162,10 +162,30 @@ pub(crate) fn visit_object_static_prototype_slot_mut( if owner == 0 { return; } + // Take the entry OUT and run the visit with the lock RELEASED: a + // copying-minor rewrite visitor can move the prototype object, and + // move fixup re-enters `object_static_prototype_owner_moved`, which + // takes this same lock — visiting under it self-deadlocks the + // collector. Same hazard and fix as the closure static-prototype + // visitor in `closure::dynamic_props`. + let Some(mut proto_bits) = get_object_prototypes() + .lock() + .ok() + .and_then(|mut map| map.remove(&owner)) + else { + return; + }; + visit(&mut proto_bits as *mut u64); + // The visit can forward the owner itself (self-referential + // prototype); re-key the entry to the forwarded address. + let new_owner = unsafe { + crate::value::addr_class::try_read_gc_header(owner) + .filter(|h| h.gc_flags & crate::gc::GC_FLAG_FORWARDED != 0) + .map(|h| crate::gc::forwarding_address(h as *const _) as usize) + .unwrap_or(owner) + }; if let Ok(mut map) = get_object_prototypes().lock() { - if let Some(proto_bits) = map.get_mut(&owner) { - visit(proto_bits as *mut u64); - } + map.insert(new_owner, proto_bits); } }