From af0c2526a7bd148e03662259c112e9976c375e66 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" <69878+youknowone@users.noreply.github.com> Date: Fri, 27 Mar 2026 12:39:45 +0900 Subject: [PATCH] Fix GC TOCTOU race in collect_inner referent traversal (#7511) Pre-compute referent pointers once per object in step 3 and reuse them in step 4 (BFS reachability). Previously, gc_get_referent_ptrs() was called independently in both steps. If a dict's write lock state changed between the two calls (e.g., held by another thread during one traversal but not the other), the two traversals could return different results. This caused live objects to be incorrectly classified as unreachable and cleared by GC. --- crates/vm/src/gc_state.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/crates/vm/src/gc_state.rs b/crates/vm/src/gc_state.rs index d86c3d4d5..4cadd98a9 100644 --- a/crates/vm/src/gc_state.rs +++ b/crates/vm/src/gc_state.rs @@ -457,12 +457,20 @@ impl GcState { } // Step 3: Subtract internal references + // Pre-compute referent pointers once per object so that both step 3 + // (subtract refs) and step 4 (BFS reachability) see the same snapshot + // of each object's children. Without this, a dict whose write lock is + // held during one traversal but not the other can yield inconsistent + // results, causing live objects to be incorrectly collected. + let mut referents_map: std::collections::HashMap>> = + std::collections::HashMap::new(); for &ptr in &collecting { let obj = unsafe { ptr.0.as_ref() }; if obj.strong_count() == 0 { continue; } let referent_ptrs = unsafe { obj.gc_get_referent_ptrs() }; + referents_map.insert(ptr, referent_ptrs.clone()); for child_ptr in referent_ptrs { let gc_ptr = GcPtr(child_ptr); if collecting.contains(&gc_ptr) @@ -487,7 +495,13 @@ impl GcState { while let Some(ptr) = worklist.pop() { let obj = unsafe { ptr.0.as_ref() }; if obj.is_gc_tracked() { - let referent_ptrs = unsafe { obj.gc_get_referent_ptrs() }; + // Reuse the pre-computed referent pointers from step 3. + // For objects that were skipped in step 3 (strong_count was 0), + // compute them now as a fallback. + let referent_ptrs = referents_map + .get(&ptr) + .cloned() + .unwrap_or_else(|| unsafe { obj.gc_get_referent_ptrs() }); for child_ptr in referent_ptrs { let gc_ptr = GcPtr(child_ptr); if collecting.contains(&gc_ptr) && reachable.insert(gc_ptr) {