https://github.com/python/cpython/commit/795d5c5b44c5ac4b7d1619800f341ec2d0332430
commit: 795d5c5b44c5ac4b7d1619800f341ec2d0332430
branch: main
author: Neil Schemenauer <[email protected]>
committer: encukou <[email protected]>
date: 2026-01-20T14:45:12+01:00
summary:

gh-144054: shutdown fix for deferred ref counting (GH-144055)

When shutting down, disable deferred refcounting for all GC objects. It
is important to do this also for untracked objects, which before this
change were getting missed.

Small code cleanup:
We can remove the shutdown case disable_deferred_refcounting() call
inside scan_heap_visitor() if we are careful about it.  The key is
that frame_disable_deferred_refcounting() might fail if the object
is untracked.

files:
M Python/gc_free_threading.c

diff --git a/Python/gc_free_threading.c b/Python/gc_free_threading.c
index 51261cea0cfe2c..beb3fa588f40e7 100644
--- a/Python/gc_free_threading.c
+++ b/Python/gc_free_threading.c
@@ -308,17 +308,18 @@ disable_deferred_refcounting(PyObject *op)
         // should also be disabled when we turn off deferred refcounting.
         _PyObject_DisablePerThreadRefcounting(op);
     }
-
-    // Generators and frame objects may contain deferred references to other
-    // objects. If the pointed-to objects are part of cyclic trash, we may
-    // have disabled deferred refcounting on them and need to ensure that we
-    // use strong references, in case the generator or frame object is
-    // resurrected by a finalizer.
-    if (PyGen_CheckExact(op) || PyCoro_CheckExact(op) || 
PyAsyncGen_CheckExact(op)) {
-        frame_disable_deferred_refcounting(&((PyGenObject *)op)->gi_iframe);
-    }
-    else if (PyFrame_Check(op)) {
-        frame_disable_deferred_refcounting(((PyFrameObject *)op)->f_frame);
+    if (_PyObject_GC_IS_TRACKED(op)) {
+        // Generators and frame objects may contain deferred references to 
other
+        // objects. If the pointed-to objects are part of cyclic trash, we may
+        // have disabled deferred refcounting on them and need to ensure that 
we
+        // use strong references, in case the generator or frame object is
+        // resurrected by a finalizer.
+        if (PyGen_CheckExact(op) || PyCoro_CheckExact(op) || 
PyAsyncGen_CheckExact(op)) {
+            frame_disable_deferred_refcounting(&((PyGenObject 
*)op)->gi_iframe);
+        }
+        else if (PyFrame_Check(op)) {
+            frame_disable_deferred_refcounting(((PyFrameObject *)op)->f_frame);
+        }
     }
 }
 
@@ -1240,19 +1241,30 @@ scan_heap_visitor(const mi_heap_t *heap, const 
mi_heap_area_t *area,
         return true;
     }
 
-    if (state->reason == _Py_GC_REASON_SHUTDOWN) {
-        // Disable deferred refcounting for reachable objects as well during
-        // interpreter shutdown. This ensures that these objects are collected
-        // immediately when their last reference is removed.
-        disable_deferred_refcounting(op);
-    }
-
     // object is reachable, restore `ob_tid`; we're done with these objects
     gc_restore_tid(op);
     gc_clear_alive(op);
     return true;
 }
 
+// Disable deferred refcounting for reachable objects during interpreter
+// shutdown. This ensures that these objects are collected immediately when
+// their last reference is removed. This needs to consider both tracked and
+// untracked GC objects, since either might have deferred refcounts enabled.
+static bool
+scan_heap_disable_deferred(const mi_heap_t *heap, const mi_heap_area_t *area,
+        void *block, size_t block_size, void *args)
+{
+    PyObject *op = op_from_block_all_gc(block, args);
+    if (op == NULL) {
+        return true;
+    }
+    if (!_Py_IsImmortal(op)) {
+        disable_deferred_refcounting(op);
+    }
+    return true;
+}
+
 static int
 move_legacy_finalizer_reachable(struct collection_state *state);
 
@@ -1487,6 +1499,10 @@ deduce_unreachable_heap(PyInterpreterState *interp,
     // Restores ob_tid for reachable objects.
     gc_visit_heaps(interp, &scan_heap_visitor, &state->base);
 
+    if (state->reason == _Py_GC_REASON_SHUTDOWN) {
+        gc_visit_heaps(interp, &scan_heap_disable_deferred, &state->base);
+    }
+
     if (state->legacy_finalizers.head) {
         // There may be objects reachable from legacy finalizers that are in
         // the unreachable set. We need to mark them as reachable.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to