diff --git a/Include/internal/pycore_pystate.h b/Include/internal/pycore_pystate.h index 16f876140020bb7..e838effc07c5cd3 100644 --- a/Include/internal/pycore_pystate.h +++ b/Include/internal/pycore_pystate.h @@ -62,6 +62,18 @@ _Py_IsMainInterpreter(PyInterpreterState *interp) return (interp == _PyInterpreterState_Main()); } +/* Return whether tracked objects may belong to another interpreter lifetime. + * + * Result: + * - false before any subinterpreter reservation or runtime reinitialization. + * - true after either event, including across Py_Finalize()/Py_Initialize(). + * + * Extension-owned objects may outlive the interpreter whose GC list held them, + * so callers must not assume every tracked object belongs to the current + * runtime when this returns true. + */ +PyAPI_FUNC(bool) _PyInterpreterState_MayHaveForeignGcObjects(void); + extern int _Py_IsMainInterpreterFinalizing(PyInterpreterState *interp); // Export for _interpreters module. diff --git a/Lib/test/test_gc.py b/Lib/test/test_gc.py index 919100f2d5ce041..8f0932506cae01b 100644 --- a/Lib/test/test_gc.py +++ b/Lib/test/test_gc.py @@ -330,6 +330,7 @@ def f(): self.assertEqual(gc.collect(), 1) def test_saveall(self): + self.addCleanup(gc.collect) # Verify that cyclic garbage like lists show up in gc.garbage if the # SAVEALL option is enabled. @@ -352,6 +353,31 @@ def test_saveall(self): self.assertEqual(len(gc.garbage), 1) obj = gc.garbage.pop() self.assertEqual(id(obj), id_L) + obj.clear() + + # A no-op generator finalizer must still set the finalized marker. + gen = (item for item in ()) + next(gen, None) + self.assertTrue(gc.is_tracked(gen)) + self.assertIsNone(gen.gi_frame) + self.assertFalse(gc.is_finalized(gen)) + L = [gen] + L.append(L) + id_L = id(L) + del gen + + try: + gc.set_debug(gc.DEBUG_SAVEALL) + del L + gc.collect() + saved = next(obj for obj in gc.garbage if id(obj) == id_L) + self.assertTrue(gc.is_finalized(saved[0])) + finally: + gc.set_debug(debug) + for obj in gc.garbage: + if id(obj) == id_L: + obj.clear() + gc.garbage.clear() def test_del(self): # __del__ methods can trigger collection, make this to happen @@ -658,6 +684,123 @@ def __del__(self): lazarus = storage.pop() self.assertTrue(gc.is_finalized(lazarus)) + def test_weakref_callback_installs_finalizer(self): + """A callback can make a finalizer eligible before the object is cleared.""" + self.addCleanup(gc.collect) + events = [] + + class C: + """A self-cycle whose weakref callback installs its finalizer.""" + __slots__ = ("cycle", "__weakref__") + + def finalize(obj): + """Record whether finalization sees the intact cycle.""" + events.append(("finalizer", getattr(obj, "cycle", None) is obj)) + + def callback(reference): + """Install the finalizer after observing the cleared weakref.""" + events.append(("callback", reference() is None)) + C.__del__ = finalize + + obj = C() + obj.cycle = obj + reference = weakref.ref(obj, callback) + gc.collect() + del obj + + self.assertEqual(gc.collect(), 1) + self.assertEqual(events, [("callback", True), ("finalizer", True)]) + self.assertIsNone(reference()) + + @requires_gil_enabled("tests the generation-list collector") + def test_debug_output_resurrects_without_callbacks(self): + """Debug output can resurrect garbage without pending callbacks.""" + was_enabled = gc.isenabled() + gc.disable() + self.addCleanup(gc.enable if was_enabled else gc.disable) + self.addCleanup(gc.collect) + resurrected = [] + + class C: + """An object with a cycle and no finalizer.""" + __slots__ = ("cycle", "__weakref__") + + class Stderr: + """Retain the target when collection reports it as collectable.""" + + def write(self, text): + """Retain the target on collectable debug output. + + text: Debug text written by the collector. + Returns: Number of characters accepted. + """ + if text.startswith("gc: collectable") and not resurrected: + target = reference() + if target is not None: + resurrected.append(target) + return len(text) + + writer = Stderr() + gc.collect() + obj = C() + obj.cycle = obj + reference = weakref.ref(obj) + del obj + + debug = gc.get_debug() + try: + with unittest.mock.patch.object(sys, "stderr", writer): + gc.set_debug(gc.DEBUG_COLLECTABLE) + self.assertEqual(gc.collect(), 0) + self.assertEqual(len(resurrected), 1) + self.assertIs(reference(), resurrected[0]) + self.assertIs(resurrected[0].cycle, resurrected[0]) + finally: + gc.set_debug(debug) + for obj in resurrected: + obj.cycle = None + resurrected.clear() + + @requires_gil_enabled("tests the generation-list collector") + def test_debug_output_precedes_weakref_preparation(self): + """Debug output can observe weakrefs with callbacks before they are cleared.""" + was_enabled = gc.isenabled() + debug = gc.get_debug() + gc.disable() + gc.set_debug(0) + self.addCleanup(gc.enable if was_enabled else gc.disable) + self.addCleanup(gc.collect) + self.addCleanup(gc.set_debug, debug) + observed = [] + callbacks = [] + self.addCleanup(callbacks.clear) + + class Stderr: + """Record whether the target is visible during collectable debug output.""" + + def write(self, text): + """Record target visibility on the first collectable debug message. + + text: Debug text written by the collector. + Returns: Number of characters accepted. + """ + if text.startswith("gc: collectable") and not observed: + observed.append(reference() is not None) + return len(text) + + writer = Stderr() + gc.collect() + obj = C1055820(0) + reference = weakref.ref(obj, callbacks.append) + del obj + + with unittest.mock.patch.object(sys, "stderr", writer): + gc.set_debug(gc.DEBUG_COLLECTABLE) + gc.collect(0) + self.assertEqual(observed, [True]) + self.assertEqual(callbacks, [reference]) + self.assertIsNone(reference()) + def test_bug1055820b(self): # Corresponds to temp2b.py in the bug report. @@ -1093,6 +1236,36 @@ def getstats(): gc.enable() + @requires_gil_enabled("tests the generation-list collector") + def test_callback_free_weakref_clear_without_pending_callbacks(self): + """Clear callback-free weakrefs before tp_clear can expose garbage.""" + was_enabled = gc.isenabled() + gc.disable() + self.addCleanup(gc.enable if was_enabled else gc.disable) + self.addCleanup(gc.collect) + observed = [] + self.addCleanup(observed.clear) + + class Observer: + """Inspect a young target when its clearing releases this object.""" + + def __del__(self): + """Record whether the weakref still exposes the target.""" + observed.append(reference()) + + observer = Observer() + gc.collect() + # The older observer does not open the callback path for generation 0. + # Clearing the target releases it, so the weakref must already be dead. + target = C1055820(0) + target.observer = observer + reference = weakref.ref(target) + del observer, target + + self.assertEqual(gc.collect(0), 1) + self.assertEqual(observed, [None]) + self.assertIsNone(reference()) + @unittest.skipIf(ContainerNoGC is None, 'requires ContainerNoGC extension type') def test_trash_weakref_clear(self): @@ -1289,6 +1462,54 @@ def test_heap_size(self): del l self.assertEqual(count, _testinternalcapi.get_tracked_heap_size()) + @requires_gil_enabled("tests fused full-collection reference accounting") + @unittest.skipIf(_testinternalcapi is None, "requires _testinternalcapi") + def test_full_collection_preserves_refs_while_untracking_immortal(self): + """Untracking an immortal preserves adjacent candidates and heap accounting.""" + import_module("ctypes") + source = textwrap.dedent(""" + import ctypes + import gc + import _testinternalcapi + + set_immortal = ctypes.pythonapi._Py_SetImmortal + set_immortal.argtypes = [ctypes.py_object] + set_immortal.restype = None + track = ctypes.pythonapi.PyObject_GC_Track + track.argtypes = [ctypes.py_object] + track.restype = None + may_have_foreign_gc_objects = ctypes.pythonapi._PyInterpreterState_MayHaveForeignGcObjects + may_have_foreign_gc_objects.argtypes = [] + may_have_foreign_gc_objects.restype = ctypes.c_bool + + gc.disable() + gc.collect(2) + candidates_before = gc.get_stats()[2]["candidates"] + parent = [] + middle = [] + set_immortal(middle) + track(middle) + child = [] + parent.append(child) + child.append(parent) + + # Traversing parent initializes child before the intervening + # immortal is removed from the forward chain. + del parent, child + assert gc.get_freeze_count() == 0 + assert not may_have_foreign_gc_objects() + # Every tracked object except middle is a candidate here. + expected_candidates = _testinternalcapi.get_tracked_heap_size() - 1 + assert gc.collect(2) == 2 + assert not gc.is_tracked(middle) + assert gc.get_stats()[2]["candidates"] - candidates_before == expected_candidates + + # get_objects() omits the result list itself. + objects = gc.get_objects() + assert _testinternalcapi.get_tracked_heap_size() == len(objects) + 1 + """) + assert_python_ok("-c", source) + class GCCallbackTests(unittest.TestCase): def setUp(self): @@ -1384,7 +1605,8 @@ def test_collect_garbage(self): Uncollectable() Uncollectable() C1055820(666) - gc.collect() + # Exercise legacy-finalizer discovery in a partial collection. + gc.collect(0) for v in self.visit: if v[1] != "stop": continue diff --git a/Lib/test/test_gdb/test_backtrace.py b/Lib/test/test_gdb/test_backtrace.py index 714853c7b4732d9..b975ced3574dd56 100644 --- a/Lib/test/test_gdb/test_backtrace.py +++ b/Lib/test/test_gdb/test_backtrace.py @@ -97,15 +97,21 @@ def test_gc(self): 'def bar():\n' ' foo()\n' 'bar()\n') + # Full GIL collections may skip update_refs(). Stop at a visible phase + # after the collector marks collection active. + if support.Py_GIL_DISABLED: + gc_breakpoint = 'update_refs' + else: + gc_breakpoint = 'prepare_callbacks' # Verify with "py-bt": gdb_output = self.get_stack_trace(cmd, - cmds_after_breakpoint=['break update_refs', 'continue', 'py-bt'], + cmds_after_breakpoint=[f'break {gc_breakpoint}', 'continue', 'py-bt'], ) self.assertIn('Garbage-collecting', gdb_output) # Verify with "py-bt-full": gdb_output = self.get_stack_trace(cmd, - cmds_after_breakpoint=['break update_refs', 'continue', 'py-bt-full'], + cmds_after_breakpoint=[f'break {gc_breakpoint}', 'continue', 'py-bt-full'], ) self.assertIn('Garbage-collecting', gdb_output) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-14-21-44-05.gh-issue-159437.count1.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-14-21-44-05.gh-issue-159437.count1.rst new file mode 100644 index 000000000000000..a98b6aee7ef4ff2 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-14-21-44-05.gh-issue-159437.count1.rst @@ -0,0 +1 @@ +Count collectable objects while clearing weak references, avoiding a separate pass during garbage collection. diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-15-23-43-15.gh-issue-159437.LHuMLA.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-15-23-43-15.gh-issue-159437.LHuMLA.rst new file mode 100644 index 000000000000000..0052c33ea970de9 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-15-23-43-15.gh-issue-159437.LHuMLA.rst @@ -0,0 +1,3 @@ +With the GIL enabled, garbage collection skips the finalization scan when no +finalizers or weakref callbacks are pending and debug output for collectable +objects is disabled. diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-16-00-00-00.gh-issue-159437.weakrf.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-16-00-00-00.gh-issue-159437.weakrf.rst new file mode 100644 index 000000000000000..0dbba4740256a4a --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-16-00-00-00.gh-issue-159437.weakrf.rst @@ -0,0 +1 @@ +Garbage collection in builds with the GIL now reuses the initial garbage count and skips late weakref cleanup when no weakrefs need clearing and the unreachable set remains valid. diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-16-01-07-10.gh-issue-159437.reffus.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-16-01-07-10.gh-issue-159437.reffus.rst new file mode 100644 index 000000000000000..ee5b07d1ce3dac1 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-16-01-07-10.gh-issue-159437.reffus.rst @@ -0,0 +1 @@ +With the GIL enabled, combine reference initialization and subtraction in the initial pass of a full collection when the permanent generation is empty and tracked objects cannot belong to another interpreter lifetime. diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-16-01-50-35.gh-issue-159437.resbook.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-16-01-50-35.gh-issue-159437.resbook.rst new file mode 100644 index 000000000000000..a17f00a3a15fe0d --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-16-01-50-35.gh-issue-159437.resbook.rst @@ -0,0 +1 @@ +With the GIL enabled, avoid separate passes that clear collecting bits before resurrection analysis and unreachable tags afterward. The existing reference and late weak-reference passes now perform that bookkeeping. diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-16-05-37-12.gh-issue-159437.legacy.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-16-05-37-12.gh-issue-159437.legacy.rst new file mode 100644 index 000000000000000..3bb142bb378d3ce --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-16-05-37-12.gh-issue-159437.legacy.rst @@ -0,0 +1 @@ +Garbage collection in builds with the GIL now avoids legacy finalizer scans when no candidate defines a legacy finalizer, and combines unreachable list cleanup with weakref preparation when possible. diff --git a/Programs/_testembed.c b/Programs/_testembed.c index 71bc37194e4b41c..c1cf1c1fff0ea61 100644 --- a/Programs/_testembed.c +++ b/Programs/_testembed.c @@ -191,6 +191,10 @@ static int test_repeated_init_and_subinterpreters(void) for (int i=1; i <= INIT_LOOPS; i++) { printf("--- Pass %d ---\n", i); _testembed_initialize(); + if (i > 1 && !_PyInterpreterState_MayHaveForeignGcObjects()) { + error("foreign GC object risk was reset"); + return 1; + } mainstate = PyThreadState_Get(); PyEval_ReleaseThread(mainstate); @@ -204,6 +208,10 @@ static int test_repeated_init_and_subinterpreters(void) print_subinterp(); Py_EndInterpreter(substate); } + if (!_PyInterpreterState_MayHaveForeignGcObjects()) { + error("subinterpreter lifetime did not record foreign GC object risk"); + return 1; + } PyThreadState_Swap(mainstate); print_subinterp(); @@ -279,6 +287,14 @@ static int test_repeated_simple_init(void) fflush(stderr); _testembed_initialize(); + if (i == 1 && _PyInterpreterState_MayHaveForeignGcObjects()) { + error("initial main interpreter may have foreign GC objects"); + return 1; + } + if (i > 1 && !_PyInterpreterState_MayHaveForeignGcObjects()) { + error("runtime reinitialization lost foreign GC object risk"); + return 1; + } Py_Finalize(); printf("Finalized\n"); // Give test_embed some output to check } diff --git a/Python/gc.c b/Python/gc.c index 5dc79e20191ebc7..db8cb0c88161664 100644 --- a/Python/gc.c +++ b/Python/gc.c @@ -29,9 +29,8 @@ typedef struct _gc_runtime_state GCState; #define GC_NEXT _PyGCHead_NEXT #define GC_PREV _PyGCHead_PREV -// update_refs() set this bit for all objects in current generation. -// subtract_refs() and move_unreachable() uses this to distinguish -// visited object is in GCing or not. +// Reference initialization marks objects in the current generation. +// Reference subtraction and reachability use this bit to recognize them. // // move_unreachable() removes this flag from reachable objects. // Only unreachable objects have this flag. @@ -45,7 +44,9 @@ typedef struct _gc_runtime_state GCState; // // Although this flag is used only in move_unreachable(), move_unreachable() // doesn't clear this flag to skip unnecessary iteration. -// move_legacy_finalizers() removes this flag instead. +// move_legacy_finalizers(), clear_unreachable_mask(), or prepare_callbacks() +// remove this flag after initial deduction. clear_weakrefs() removes it after +// a resurrection recheck. // Between them, unreachable list is not normal list and we can not use // most gc_list_* functions for it. #define NEXT_MASK_UNREACHABLE (1) @@ -168,15 +169,14 @@ During a collection, _gc_prev is temporary used for gc_refs, and the gc list is singly linked until _gc_prev is restored. gc_refs - At the start of a collection, update_refs() copies the true refcount - to gc_refs, for each object in the generation being collected. - subtract_refs() then adjusts gc_refs so that it equals the number of - times an object is referenced directly from outside the generation - being collected. + The true refcount is copied to gc_refs and reduced by references from + objects in the generation being collected. The remaining count is the + number of direct references from outside that generation. Full collections + may initialize and subtract these counts in one traversal. PREV_MASK_COLLECTING - Objects in generation being collected are marked PREV_MASK_COLLECTING in - update_refs(). + Reference initialization marks objects in the generation being collected + with PREV_MASK_COLLECTING. _gc_next values @@ -202,7 +202,9 @@ NEXT_MASK_UNREACHABLE "unreachable" set. The flag is unset and the object is moved back to "reachable" set. - move_legacy_finalizers() will remove this flag from "unreachable" set. + move_legacy_finalizers(), clear_unreachable_mask(), or prepare_callbacks() + remove this flag after initial deduction. clear_weakrefs() removes it after + a resurrection recheck. */ /*** list functions ***/ @@ -304,16 +306,6 @@ gc_list_size(PyGC_Head *list) return n; } -/* Walk the list and mark all objects as non-collecting */ -static inline void -gc_list_clear_collecting(PyGC_Head *collectable) -{ - PyGC_Head *gc; - for (gc = GC_NEXT(collectable); gc != collectable; gc = GC_NEXT(gc)) { - gc_clear_collecting(gc); - } -} - /* Append objects in a GC list to a Python list. * Return 0 if all OK, < 0 if error (out of memory for list) */ @@ -456,6 +448,43 @@ visit_decref(PyObject *op, void *parent) return 0; } +/* Subtract a GC reference, initializing and marking the referent on first visit. + * + * This visitor is used only when every tracked mortal object it may reach + * belongs to the collection. Non-GC, untracked, and immortal referents remain + * unchanged. + * + * Arguments: + * - `op`: the object referenced by the traversed parent. + * - `parent`: the traversed owner used by debug reference validation. + * + * Return zero, as required by traversal callbacks. + */ +static int +gc_decref_and_mark_collecting(PyObject *op, void *parent) +{ + OBJECT_STAT_INC(object_visits); + _PyObject_ASSERT(_PyObject_CAST(parent), !_PyObject_IsFreed(op)); + + if (!_PyObject_IS_GC(op)) { + return 0; + } + + PyGC_Head *gc = AS_GC(op); + if (gc_is_collecting(gc)) { + gc_decref(gc); + return 0; + } + + if (!_PyObject_GC_IS_TRACKED(op) || _Py_IsImmortal(op)) { + return 0; + } + Py_ssize_t refs = Py_REFCNT(op); + _PyObject_ASSERT(op, refs != 0); + gc_reset_refs(gc, refs - 1); + return 0; +} + int _PyGC_VisitStackRef(_PyStackRef *ref, visitproc visit, void *arg) { @@ -463,7 +492,8 @@ _PyGC_VisitStackRef(_PyStackRef *ref, visitproc visit, void *arg) // refcounts when computing the incoming references, but otherwise treat // them like normal. assert(!PyStackRef_IsTaggedInt(*ref)); - if (!PyStackRef_RefcountOnObject(*ref) && (visit == visit_decref)) { + if (!PyStackRef_RefcountOnObject(*ref) && + (visit == visit_decref || visit == gc_decref_and_mark_collecting)) { return 0; } Py_VISIT(PyStackRef_AsPyObjectBorrow(*ref)); @@ -486,19 +516,85 @@ _PyGC_VisitFrameStack(_PyInterpreterFrame *frame, visitproc visit, void *arg) /* Subtract internal references from gc_refs. After this, gc_refs is >= 0 * for all objects in containers. The ones with gc_refs > 0 are directly * reachable from outside containers, and so can't be collected. + * + * Return the number of objects whose type defines tp_del. A zero count + * excludes legacy finalizers from the eventual unreachable subset. */ -static void +static Py_ssize_t subtract_refs(PyGC_Head *containers) { + Py_ssize_t legacy_finalizer_count = 0; traverseproc traverse; PyGC_Head *gc = GC_NEXT(containers); for (; gc != containers; gc = GC_NEXT(gc)) { PyObject *op = FROM_GC(gc); - traverse = Py_TYPE(op)->tp_traverse; + PyTypeObject *type = Py_TYPE(op); + legacy_finalizer_count += type->tp_del != NULL; + traverse = type->tp_traverse; (void) traverse(op, visit_decref, op); } + return legacy_finalizer_count; +} + +/* Initialize and subtract references in one traversal of all ordinary generations. + * + * Mortal candidates retain adjusted gc_refs. Dynamic immortal candidates become + * untracked without changing other candidates' temporary counts. + * + * Prerequisites: + * - This is the initial pass of a full collection, not a resurrection recheck. + * - `containers` is a valid list containing all ordinary generations with no masks set. + * - The permanent generation is empty. + * - `_PyInterpreterState_MayHaveForeignGcObjects()` is false. + * + * Arguments: + * - `containers`: tracked objects belonging to the initial main interpreter. + * - `legacy_finalizer_count_out`: receives the number of mortal candidates whose type defines tp_del. + * + * Return the number of mortal collection candidates. + */ +static Py_ssize_t +update_and_subtract_refs(PyGC_Head *containers, Py_ssize_t *legacy_finalizer_count_out) +{ + Py_ssize_t candidates = 0; + Py_ssize_t legacy_finalizer_count = 0; + PyGC_Head *previous = containers; + PyGC_Head *gc = GC_NEXT(containers); + + while (gc != containers) { + PyGC_Head *next = GC_NEXT(gc); + PyObject *op = FROM_GC(gc); + + if (!gc_is_collecting(gc)) { + if (_Py_IsImmortal(op)) { + assert(!_Py_IsStaticImmortal(op)); + // The next node may already hold gc_refs instead of a predecessor, + // so ordinary untracking would corrupt its count. + _PyGCHead_SET_NEXT(previous, next); + gc->_gc_prev &= _PyGC_PREV_MASK_FINALIZED; + gc->_gc_next = 0; + _PyInterpreterState_GET()->gc.heap_size--; + gc = next; + continue; + } + gc_reset_refs(gc, Py_REFCNT(op)); + _PyObject_ASSERT(op, gc_get_refs(gc) != 0); + } + + candidates++; + PyTypeObject *type = Py_TYPE(op); + legacy_finalizer_count += type->tp_del != NULL; + (void)type->tp_traverse(op, gc_decref_and_mark_collecting, op); + previous = gc; + gc = next; + } + + // The tail must remain valid until reachability restores predecessor links. + containers->_gc_prev = (uintptr_t)previous; + *legacy_finalizer_count_out = legacy_finalizer_count; + return candidates; } /* A traversal callback for move_unreachable. */ @@ -756,7 +852,7 @@ move_legacy_finalizer_reachable(PyGC_Head *finalizers) } } -/* Handle weakref callbacks. Note that it's possible for such weakrefs to be +/* Prepare weakref callbacks. Note that it's possible for such weakrefs to be * outside the unreachable set -- indeed, those are precisely the weakrefs * whose callbacks must be invoked. See gc_weakref.txt for overview & some * details. @@ -794,17 +890,29 @@ move_legacy_finalizer_reachable(PyGC_Head *finalizers) * or missing `tp_traverse` methods. When that object goes away, the callback * for weakref can be executed and that could reveal unreachable objects to * Python-level code. See bpo-38006 as an example bug. + * + * Arguments: + * - `unreachable`: collecting list with an untagged head; member _gc_next links must all be normalized or all carry NEXT_MASK_UNREACHABLE. + * - `wrcb_to_call`: initialized list receiving weakrefs whose callbacks are safe to invoke. + * - `collectable_count_out`: receives the number of objects in `unreachable`. + * - `needs_weakref_cleanup_out`: receives whether garbage weakrefs or remaining incoming weakrefs may need cleanup. + * + * Return whether any finalizer is pending or a weakref callback is queued. + * On return, `unreachable` is normalized. This function does not invoke callbacks. + * The count and cleanup decision remain valid only while the unreachable set and its weakrefs cannot change. */ -static int -handle_weakref_callbacks(PyGC_Head *unreachable, PyGC_Head *old, - bool *needs_resurrection_check) +Py_NO_INLINE static bool +prepare_callbacks(PyGC_Head *unreachable, PyGC_Head *wrcb_to_call, + Py_ssize_t *collectable_count_out, bool *needs_weakref_cleanup_out) { + assert((unreachable->_gc_next & NEXT_MASK_UNREACHABLE) == 0); + validate_list(unreachable, (GC_NEXT(unreachable)->_gc_next & NEXT_MASK_UNREACHABLE) ? collecting_set_unreachable_set : collecting_set_unreachable_clear); + PyGC_Head *gc; - PyGC_Head wrcb_to_call; /* weakrefs with callbacks to call */ PyGC_Head *next; - int num_freed = 0; - - gc_list_init(&wrcb_to_call); + Py_ssize_t count = 0; + bool has_pending_finalizer = false; + bool needs_weakref_cleanup = false; /* Find all weakrefs with callbacks and move into `wrcb_to_call` if the * callback needs to be invoked. We make another pass over wrcb_to_call, @@ -814,9 +922,19 @@ handle_weakref_callbacks(PyGC_Head *unreachable, PyGC_Head *old, PyWeakReference **wrlist; PyObject *op = FROM_GC(gc); + PyTypeObject *type = Py_TYPE(op); + count++; + if (type->tp_finalize != NULL && !has_pending_finalizer) { + has_pending_finalizer = !_PyGC_FINALIZED(op); + } + gc->_gc_next &= ~NEXT_MASK_UNREACHABLE; next = GC_NEXT(gc); - if (! _PyType_SUPPORTS_WEAKREFS(Py_TYPE(op))) { + // Garbage weakrefs need clearing even if their referents are outside + // this collection (bpo-38006). + needs_weakref_cleanup |= PyWeakref_Check(op); + + if (! _PyType_SUPPORTS_WEAKREFS(type)) { continue; } @@ -891,21 +1009,40 @@ handle_weakref_callbacks(PyGC_Head *unreachable, PyGC_Head *old, PyGC_Head *wrasgc = AS_GC((PyObject *)wr); // wrasgc is reachable, but next isn't, so they can't be the same _PyObject_ASSERT((PyObject *)wr, wrasgc != next); - gc_list_move(wrasgc, &wrcb_to_call); + gc_list_move(wrasgc, wrcb_to_call); } + // Callback-free weakrefs remain linked until finalization. + needs_weakref_cleanup |= *wrlist != NULL; } + validate_list(unreachable, collecting_set_unreachable_clear); + *collectable_count_out = count; + *needs_weakref_cleanup_out = needs_weakref_cleanup; + return has_pending_finalizer || !gc_list_is_empty(wrcb_to_call); +} + +/* Invoke queued weakref callbacks and move surviving weakrefs to `old`. + * + * Arguments: + * - `wrcb_to_call`: initialized queue to drain, with a temporary reference to each weakref. + * - `old`: generation receiving weakrefs that survive callback invocation. + * + * Return the number of weakrefs reclaimed while callbacks ran. + */ +static int +invoke_weakref_callbacks(PyGC_Head *wrcb_to_call, PyGC_Head *old) +{ + PyGC_Head *gc; + int num_freed = 0; + /* Invoke the callbacks we decided to honor. It's safe to invoke them * because they can't reference unreachable objects. */ - if (!*needs_resurrection_check) { - *needs_resurrection_check = !gc_list_is_empty(&wrcb_to_call); - } - while (! gc_list_is_empty(&wrcb_to_call)) { + while (! gc_list_is_empty(wrcb_to_call)) { PyObject *temp; PyObject *callback; - gc = (PyGC_Head*)wrcb_to_call._gc_next; + gc = (PyGC_Head*)wrcb_to_call->_gc_next; PyObject *op = FROM_GC(gc); _PyObject_ASSERT(op, PyWeakref_Check(op)); PyWeakReference *wr = (PyWeakReference *)op; @@ -934,7 +1071,7 @@ handle_weakref_callbacks(PyGC_Head *unreachable, PyGC_Head *old, * ours). */ Py_DECREF(op); - if (wrcb_to_call._gc_next == (uintptr_t)gc) { + if (wrcb_to_call->_gc_next == (uintptr_t)gc) { /* object is still alive -- move it */ gc_list_move(gc, old); } @@ -946,25 +1083,33 @@ handle_weakref_callbacks(PyGC_Head *unreachable, PyGC_Head *old, return num_freed; } -/* Clear all weakrefs to unreachable objects. When this returns, no object in - * `unreachable` is weakly referenced anymore. See the comments above - * handle_weakref_callbacks() for why these weakrefs need to be cleared. +/* Clear all weakrefs to unreachable objects without invoking callbacks. + * See the comments above prepare_callbacks() for why these weakrefs need to be cleared. + * + * Arguments: + * - `unreachable`: final collectable list with an untagged head; member _gc_next links must all be normalized or all carry NEXT_MASK_UNREACHABLE. + * + * Return the number of objects in `unreachable`. On return, the list is + * normalized, membership is unchanged, and no member is weakly referenced. */ -static void +static Py_ssize_t clear_weakrefs(PyGC_Head *unreachable) { PyGC_Head *gc; PyGC_Head *next; + Py_ssize_t count = 0; for (gc = GC_NEXT(unreachable); gc != unreachable; gc = next) { PyWeakReference **wrlist; PyObject *op = FROM_GC(gc); + gc->_gc_next &= ~NEXT_MASK_UNREACHABLE; next = GC_NEXT(gc); + count++; if (PyWeakref_Check(op)) { /* A weakref inside the unreachable set is always cleared. See - * the comments above handle_weakref_callbacks() for why these + * the comments above prepare_callbacks() for why these * must be cleared. */ _PyWeakref_ClearRef((PyWeakReference *)op); @@ -994,6 +1139,8 @@ clear_weakrefs(PyGC_Head *unreachable) _PyObject_ASSERT((PyObject *)wr, wr->wr_object == Py_None); } } + + return count; } static void @@ -1036,13 +1183,14 @@ handle_legacy_finalizers(PyThreadState *tstate, /* Run first-time finalizers (if any) on all the objects in collectable. * Note that this may remove some (or even all) of the objects from the * list, due to refcounts falling to 0. + * Return whether a finalizer may have run Python. */ -static void -finalize_garbage(PyThreadState *tstate, PyGC_Head *collectable, - bool *needs_resurrection_check) +static bool +finalize_garbage(PyThreadState *tstate, PyGC_Head *collectable) { destructor finalize; PyGC_Head seen; + bool needs_resurrection_check = false; /* While we're going through the loop, `finalize(op)` may cause op, or * other objects, to be reclaimed via refcounts falling to zero. So @@ -1062,7 +1210,7 @@ finalize_garbage(PyThreadState *tstate, PyGC_Head *collectable, (finalize = Py_TYPE(op)->tp_finalize) != NULL) { if (!_PyGen_FinalizerIsNoop(op)) { - *needs_resurrection_check = true; + needs_resurrection_check = true; } _PyGC_SET_FINALIZED(op); Py_INCREF(op); @@ -1072,6 +1220,7 @@ finalize_garbage(PyThreadState *tstate, PyGC_Head *collectable, } } gc_list_merge(&seen, collectable); + return needs_resurrection_check; } /* Break reference cycles by clearing the containers involved. This is @@ -1140,6 +1289,8 @@ show_stats_each_generations(GCState *gcstate) /* Deduce which objects among "base" are unreachable from outside the list and move them to 'unreachable'. The process consist in the following steps: +The first two steps may be combined for the initial pass of a full collection. + 1. Copy all reference counts to a different field (gc_prev is used to hold this copy to save memory). 2. Traverse all objects in "base" and visit all referred objects using @@ -1154,27 +1305,49 @@ show_stats_each_generations(GCState *gcstate) Contracts: - * The "base" has to be a valid list with no mask set. + * The "base" has to be a valid list. Its members may carry + PREV_MASK_COLLECTING during a resurrection recheck. + NEXT_MASK_UNREACHABLE must be clear. * The "unreachable" list must be uninitialized (this function calls gc_list_init over 'unreachable'). + * "full_collection_pass" means this is the initial pass over all ordinary generations. + Resurrection rechecks pass false even during a full collection. + + * "reachable" must point to storage for the number of survivors that remain tracked. + + * "legacy_finalizer_count_out" receives the number of candidate objects + whose type defines tp_del. Zero excludes legacy finalizers from the + unreachable subset. + + * The return value is the number of mortal collection candidates. + IMPORTANT: This function leaves 'unreachable' with the NEXT_MASK_UNREACHABLE flag set but it does not clear it to skip unnecessary iteration. Before the -flag is cleared (for example, by using 'clear_unreachable_mask' function or -by a call to 'move_legacy_finalizers'), the 'unreachable' list is not a normal -list and we can not use most gc_list_* functions for it. */ +flag is cleared (for example, by 'clear_unreachable_mask', +'move_legacy_finalizers', 'prepare_callbacks', or 'clear_weakrefs'), the +'unreachable' list is not a normal list and we can not use most gc_list_* functions for it. */ static inline Py_ssize_t deduce_unreachable(PyGC_Head *base, PyGC_Head *unreachable, - Py_ssize_t *reachable) { - validate_list(base, collecting_clear_unreachable_clear); + bool full_collection_pass, Py_ssize_t *reachable, Py_ssize_t *legacy_finalizer_count_out) { /* Using ob_refcnt and gc_refs, calculate which objects in the * container set are reachable from outside the set (i.e., have a * refcount greater than 0 when all the references within the * set are taken into account). */ - Py_ssize_t candidates = update_refs(base); // gc_prev is used for gc_refs - subtract_refs(base); + // Lazy initialization can touch any tracked mortal referent. Use it only + // when all such objects belong to this initial full collection. + Py_ssize_t candidates; + if (full_collection_pass && + gc_list_is_empty(&get_gc_state()->permanent_generation.head) && + !_PyInterpreterState_MayHaveForeignGcObjects()) { + candidates = update_and_subtract_refs(base, legacy_finalizer_count_out); + } + else { + candidates = update_refs(base); // gc_prev is used for gc_refs + *legacy_finalizer_count_out = subtract_refs(base); + } /* Leave everything reachable from outside base in base, and move * everything else (in base) to unreachable. @@ -1229,30 +1402,63 @@ deduce_unreachable(PyGC_Head *base, PyGC_Head *unreachable, * The "still_unreachable" list must be uninitialized (this function calls gc_list_init over 'still_unreachable'). -IMPORTANT: After a call to this function, the 'still_unreachable' set will have the -PREV_MARK_COLLECTING set, but the objects in this set are going to be removed so -we can skip the expense of clearing the flag to avoid extra iteration. */ +IMPORTANT: The 'still_unreachable' head is untagged. Its members carry +PREV_MASK_COLLECTING and NEXT_MASK_UNREACHABLE. 'clear_weakrefs' must normalize +the list before ordinary list operations or deletion. */ static inline void handle_resurrected_objects(PyGC_Head *unreachable, PyGC_Head* still_unreachable, PyGC_Head *old_generation) { - // Remove the PREV_MASK_COLLECTING from unreachable - // to prepare it for a new call to 'deduce_unreachable' - gc_list_clear_collecting(unreachable); - - // After the call to deduce_unreachable, the 'still_unreachable' set will - // have the PREV_MARK_COLLECTING set, but the objects are going to be - // removed so we can skip the expense of clearing the flag. PyGC_Head* resurrected = unreachable; + validate_list(resurrected, collecting_set_unreachable_clear); Py_ssize_t reachable; - deduce_unreachable(resurrected, still_unreachable, &reachable); - clear_unreachable_mask(still_unreachable); + Py_ssize_t unused_legacy_finalizer_count; + deduce_unreachable(resurrected, still_unreachable, false, &reachable, &unused_legacy_finalizer_count); // Move the resurrected objects to the old generation for future collection. gc_list_merge(resurrected, old_generation); } +/* Invoke weakref callbacks and finalizers, then handle possible resurrection. + * + * Arguments: + * - `tstate`: the thread state used when invoking finalizers. + * - `collectable`: normalized collecting list whose finalizers may run. + * - `still_unreachable`: distinct, uninitialized list head for remaining garbage. + * - `old`: generation receiving surviving weakrefs and resurrected objects. + * - `wrcb_to_call`: initialized, possibly empty list of callback-bearing weakrefs to drain. + * - `collected_count`: count incremented by weakrefs reclaimed while callbacks run. + * - `needs_resurrection_check`: whether earlier work may have run Python. + * + * Return whether resurrection analysis ran. If true, `collectable` must not be + * used again and `still_unreachable` contains the remaining garbage with + * NEXT_MASK_UNREACHABLE set on every member and an untagged head. It must pass + * through `clear_weakrefs()` before deletion. Otherwise, `collectable` remains + * selected and `still_unreachable` is untouched. + */ +Py_NO_INLINE static bool +handle_callbacks(PyThreadState *tstate, PyGC_Head *collectable, + PyGC_Head *still_unreachable, PyGC_Head *old, + PyGC_Head *wrcb_to_call, Py_ssize_t *collected_count, + bool needs_resurrection_check) +{ + bool weakref_callbacks_invoked = !gc_list_is_empty(wrcb_to_call); + *collected_count += invoke_weakref_callbacks(wrcb_to_call, old); + needs_resurrection_check |= weakref_callbacks_invoked; + validate_list(old, collecting_clear_unreachable_clear); + validate_list(collectable, collecting_set_unreachable_clear); + + // Weakref callbacks can make finalizers newly eligible after the initial + // scan, so we must recheck even when we previously found no pending finalizers. + needs_resurrection_check |= finalize_garbage(tstate, collectable); + if (needs_resurrection_check) { + handle_resurrected_objects(collectable, still_unreachable, old); + } + return needs_resurrection_check; +} + + /* Invoke progress callbacks to notify clients that garbage collection * is starting or stopping */ @@ -1513,9 +1719,11 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason) old = young; } validate_list(old, collecting_clear_unreachable_clear); + validate_list(young, collecting_clear_unreachable_clear); Py_ssize_t reachable; - stats.candidates = deduce_unreachable(young, &unreachable, &reachable); + Py_ssize_t legacy_finalizer_count; + stats.candidates = deduce_unreachable(young, &unreachable, generation == NUM_GENERATIONS - 1, &reachable, &legacy_finalizer_count); /* Move reachable objects to next generation. */ if (young != old) { @@ -1540,22 +1748,28 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason) * legacy finalizers (e.g. tp_del) can't safely be deleted. */ gc_list_init(&finalizers); - // NEXT_MASK_UNREACHABLE is cleared here. - // After move_legacy_finalizers(), unreachable is normal list. - move_legacy_finalizers(&unreachable, &finalizers); - /* finalizers contains the unreachable objects with a legacy finalizer; - * unreachable objects reachable *from* those are also uncollectable, - * and we move those into the finalizers list too. - */ - move_legacy_finalizer_reachable(&finalizers); + if (legacy_finalizer_count != 0) { + // NEXT_MASK_UNREACHABLE is cleared here. + // After move_legacy_finalizers(), unreachable is a normal list. + move_legacy_finalizers(&unreachable, &finalizers); + /* finalizers contains the unreachable objects with a legacy finalizer; + * unreachable objects reachable *from* those are also uncollectable, + * and we move those into the finalizers list too. + */ + move_legacy_finalizer_reachable(&finalizers); + validate_list(&unreachable, collecting_set_unreachable_clear); + } validate_list(&finalizers, collecting_clear_unreachable_clear); - validate_list(&unreachable, collecting_set_unreachable_clear); bool needs_resurrection_check = false; /* Print debugging information. */ if (gcstate->debug & _PyGC_DEBUG_COLLECTABLE) { + if (legacy_finalizer_count == 0) { + // Python invoked by debug output requires normal GC list links. + clear_unreachable_mask(&unreachable); + } /* sys.stderr.write() can resurrect objects. */ needs_resurrection_check = true; for (gc = GC_NEXT(&unreachable); gc != &unreachable; gc = GC_NEXT(gc)) { @@ -1563,28 +1777,25 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason) } } - /* Clear weakrefs and invoke callbacks as necessary. */ - stats.collected += handle_weakref_callbacks(&unreachable, old, - &needs_resurrection_check); - validate_list(old, collecting_clear_unreachable_clear); - validate_list(&unreachable, collecting_set_unreachable_clear); - - /* Call tp_finalize on objects which have one. */ - finalize_garbage(tstate, &unreachable, &needs_resurrection_check); + PyGC_Head wrcb_to_call; + gc_list_init(&wrcb_to_call); + Py_ssize_t collectable_count; + bool needs_weakref_cleanup; + bool has_pending_callbacks = prepare_callbacks(&unreachable, &wrcb_to_call, &collectable_count, &needs_weakref_cleanup); + has_pending_callbacks |= needs_resurrection_check; - /* Handle any objects that may have resurrected after the call - * to 'finalize_garbage' and continue the collection with the - * objects that are still unreachable */ PyGC_Head final_unreachable; - if (needs_resurrection_check) { - handle_resurrected_objects(&unreachable, &final_unreachable, old); - } - else { - /* No Python code ran during finalization, so the original - * unreachable set is still valid. */ - gc_list_init(&final_unreachable); - gc_list_merge(&unreachable, &final_unreachable); + PyGC_Head *collectable = &unreachable; + if (has_pending_callbacks) { + bool resurrection_checked = handle_callbacks(tstate, collectable, &final_unreachable, old, &wrcb_to_call, &stats.collected, needs_resurrection_check); + if (resurrection_checked) { + collectable = &final_unreachable; + // Python code may have changed the garbage set or created weakrefs. + // Refresh cleanup and counting, and normalize the recheck's tags. + needs_weakref_cleanup = true; + } } + validate_list(old, collecting_clear_unreachable_clear); /* Clear weakrefs to objects in the unreachable set. No Python-level * code must be allowed to access those unreachable objects. During @@ -1592,14 +1803,17 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason) * and create new weakrefs. If those weakrefs were not cleared, they * could reveal unreachable objects. Callbacks are not executed. */ - clear_weakrefs(&final_unreachable); + if (needs_weakref_cleanup) { + collectable_count = clear_weakrefs(collectable); + } + validate_list(collectable, collecting_set_unreachable_clear); + stats.collected += collectable_count; - /* Call tp_clear on objects in the final_unreachable set. This will cause + /* Call tp_clear on objects in the collectable set. This will cause * the reference cycles to be broken. It may also cause some objects * in finalizers to be freed. */ - stats.collected += gc_list_size(&final_unreachable); - delete_garbage(tstate, gcstate, &final_unreachable, old); + delete_garbage(tstate, gcstate, collectable, old); /* Collect statistics on uncollectable objects found and print * debugging information. */ diff --git a/Python/pystate.c b/Python/pystate.c index 97323ea2ba2bce1..f55521861d894c8 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -309,6 +309,17 @@ _Py_COMP_DIAG_IGNORE_DEPR_DECLS static const _PyRuntimeState initial = _PyRuntimeState_INIT(_PyRuntime, ""); _Py_COMP_DIAG_POP +/* Monotonic guard for tracked objects that may outlive their interpreter. + * Kept outside _PyRuntime so reinitialization cannot clear it. + */ +static int may_have_foreign_gc_objects; + +bool +_PyInterpreterState_MayHaveForeignGcObjects(void) +{ + return _Py_atomic_load_int_relaxed(&may_have_foreign_gc_objects) != 0; +} + #define LOCKS_INIT(runtime) \ { \ &(runtime)->interpreters.mutex, \ @@ -364,6 +375,7 @@ _PyRuntimeState_Init(_PyRuntimeState *runtime) if (runtime->_initialized) { // Py_Initialize() must be running again. + _Py_atomic_store_int_relaxed(&may_have_foreign_gc_objects, 1); // Reset to _PyRuntimeState_INIT. memcpy(runtime, &initial, sizeof(*runtime)); // Preserve the cookie from the original runtime. @@ -723,6 +735,8 @@ _PyInterpreterState_New(PyThreadState *tstate, PyInterpreterState **pinterp) assert(interpreters->main != NULL); assert(id != 0); + _Py_atomic_store_int_relaxed(&may_have_foreign_gc_objects, 1); + interp = alloc_interpreter(); if (interp == NULL) { status = _PyStatus_NO_MEMORY(); diff --git a/Tools/c-analyzer/cpython/ignored.tsv b/Tools/c-analyzer/cpython/ignored.tsv index db9f74fdbc591f0..ba0d4b94286e3f8 100644 --- a/Tools/c-analyzer/cpython/ignored.tsv +++ b/Tools/c-analyzer/cpython/ignored.tsv @@ -50,6 +50,10 @@ Modules/getbuildinfo.c - initialized - Python/getversion.c - initialized - Python/getversion.c - version - +## monotonic safety guards +# Becomes true once tracked objects may outlive their interpreter. +Python/pystate.c - may_have_foreign_gc_objects - + ## public C-API - set during first init Python/bootstrap_hash.c - _Py_HashSecret_Initialized - Python/pyhash.c - _Py_HashSecret -