Skip to content

Commit af0c252

Browse files
authored
Fix GC TOCTOU race in collect_inner referent traversal (RustPython#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.
1 parent f42ffd6 commit af0c252

1 file changed

Lines changed: 15 additions & 1 deletion

File tree

crates/vm/src/gc_state.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -457,12 +457,20 @@ impl GcState {
457457
}
458458

459459
// Step 3: Subtract internal references
460+
// Pre-compute referent pointers once per object so that both step 3
461+
// (subtract refs) and step 4 (BFS reachability) see the same snapshot
462+
// of each object's children. Without this, a dict whose write lock is
463+
// held during one traversal but not the other can yield inconsistent
464+
// results, causing live objects to be incorrectly collected.
465+
let mut referents_map: std::collections::HashMap<GcPtr, Vec<NonNull<PyObject>>> =
466+
std::collections::HashMap::new();
460467
for &ptr in &collecting {
461468
let obj = unsafe { ptr.0.as_ref() };
462469
if obj.strong_count() == 0 {
463470
continue;
464471
}
465472
let referent_ptrs = unsafe { obj.gc_get_referent_ptrs() };
473+
referents_map.insert(ptr, referent_ptrs.clone());
466474
for child_ptr in referent_ptrs {
467475
let gc_ptr = GcPtr(child_ptr);
468476
if collecting.contains(&gc_ptr)
@@ -487,7 +495,13 @@ impl GcState {
487495
while let Some(ptr) = worklist.pop() {
488496
let obj = unsafe { ptr.0.as_ref() };
489497
if obj.is_gc_tracked() {
490-
let referent_ptrs = unsafe { obj.gc_get_referent_ptrs() };
498+
// Reuse the pre-computed referent pointers from step 3.
499+
// For objects that were skipped in step 3 (strong_count was 0),
500+
// compute them now as a fallback.
501+
let referent_ptrs = referents_map
502+
.get(&ptr)
503+
.cloned()
504+
.unwrap_or_else(|| unsafe { obj.gc_get_referent_ptrs() });
491505
for child_ptr in referent_ptrs {
492506
let gc_ptr = GcPtr(child_ptr);
493507
if collecting.contains(&gc_ptr) && reachable.insert(gc_ptr) {

0 commit comments

Comments
 (0)