From 0d266cb2462b029cf92ad5dd0949de84333832d5 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Mon, 19 Jan 2026 15:37:11 -0800 Subject: [PATCH 1/2] gh-144054: shutdown fix for deferred ref counting 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. --- Python/gc_free_threading.c | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Python/gc_free_threading.c b/Python/gc_free_threading.c index 51261cea0cfe2c..9706e15c1d06bd 100644 --- a/Python/gc_free_threading.c +++ b/Python/gc_free_threading.c @@ -1253,6 +1253,26 @@ scan_heap_visitor(const mi_heap_t *heap, const mi_heap_area_t *area, return true; } +// Disables deferred refcounting for all GC objects during interpreter +// shutdown. The scan_heap_visitor() skips untracked objects but those +// could have deferred refcounts enabled as well. This is separated +// into a separate scan pass since we only need to do it at shutdown. +// Note that untracked GC objects are typically exact tuples but could +// also be GC objects that were never tracked or manually untracked. +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) && _PyObject_HasDeferredRefcount(op)) { + disable_deferred_refcounting(op); + } + return true; +} + static int move_legacy_finalizer_reachable(struct collection_state *state); @@ -1487,6 +1507,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. From 296eba9eb52104f9e348c90c641367a6f44188b3 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Mon, 19 Jan 2026 21:22:57 -0800 Subject: [PATCH 2/2] 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. --- Python/gc_free_threading.c | 42 +++++++++++++++----------------------- 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/Python/gc_free_threading.c b/Python/gc_free_threading.c index 9706e15c1d06bd..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,25 +1241,16 @@ 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; } -// Disables deferred refcounting for all GC objects during interpreter -// shutdown. The scan_heap_visitor() skips untracked objects but those -// could have deferred refcounts enabled as well. This is separated -// into a separate scan pass since we only need to do it at shutdown. -// Note that untracked GC objects are typically exact tuples but could -// also be GC objects that were never tracked or manually untracked. +// 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) @@ -1267,7 +1259,7 @@ scan_heap_disable_deferred(const mi_heap_t *heap, const mi_heap_area_t *area, if (op == NULL) { return true; } - if (!_Py_IsImmortal(op) && _PyObject_HasDeferredRefcount(op)) { + if (!_Py_IsImmortal(op)) { disable_deferred_refcounting(op); } return true;