From 3cfb631398f10dac2b60ee1c802c21bc062b19fd Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Wed, 24 Dec 2025 13:42:25 -0500 Subject: [PATCH 1/4] Queue frozen objects for decref. --- Python/gc_free_threading.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Python/gc_free_threading.c b/Python/gc_free_threading.c index 04b9b8f3f85603..0449f46054165c 100644 --- a/Python/gc_free_threading.c +++ b/Python/gc_free_threading.c @@ -906,7 +906,11 @@ gc_visit_thread_stacks_mark_alive(PyInterpreterState *interp, gc_mark_args_t *ar static void queue_untracked_obj_decref(PyObject *op, struct collection_state *state) { - if (!_PyObject_GC_IS_TRACKED(op)) { + assert(Py_REFCNT(op) == 0); + // We have to treat frozen objects as untracked in this function or else + // they might be picked up in a future collection, which breaks the assumption + // that all incoming objects have a non-zero reference count. + if (!_PyObject_GC_IS_TRACKED(op) || gc_is_frozen(op)) { // GC objects with zero refcount are handled subsequently by the // GC as if they were cyclic trash, but we have to handle dead // non-GC objects here. Add one to the refcount so that we can From bf52ecffdfbe21716db0762e082e712de89fbece Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Wed, 24 Dec 2025 13:44:26 -0500 Subject: [PATCH 2/4] Add blurb. --- .../2025-12-24-13-44-24.gh-issue-142975.8C4vIP.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-12-24-13-44-24.gh-issue-142975.8C4vIP.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-12-24-13-44-24.gh-issue-142975.8C4vIP.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-12-24-13-44-24.gh-issue-142975.8C4vIP.rst new file mode 100644 index 00000000000000..9d7f57ee60aa47 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-12-24-13-44-24.gh-issue-142975.8C4vIP.rst @@ -0,0 +1,2 @@ +Fix crash after unfreezing all objects tracked by the garbage collector on +the :term:`free threaded ` build. From 498222b410231f72cac13d691585f65006a537be Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Wed, 24 Dec 2025 13:47:54 -0500 Subject: [PATCH 3/4] Add a test. --- Lib/test/test_gc.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Lib/test/test_gc.py b/Lib/test/test_gc.py index 6aa6361d5d0b92..83e2856aae51df 100644 --- a/Lib/test/test_gc.py +++ b/Lib/test/test_gc.py @@ -1249,6 +1249,18 @@ def test_tuple_untrack_counts(self): self.assertTrue(new_count - count > (n // 2)) + @threading_helper.requires_working_threading() + def test_concurrent_freeze_unfreeze(): + # GH-142975: On the free-threaded build, this would cause problems + # with objects that had a per-thread reference count. + def weird(): + gc.freeze() + gc.collect() + gc.unfreeze() + + threading_helper.run_concurrently(weird, 4) + + class IncrementalGCTests(unittest.TestCase): @unittest.skipIf(_testinternalcapi is None, "requires _testinternalcapi") @requires_gil_enabled("Free threading does not support incremental GC") From 514f81d33851f1eb97c404c8d070b1be257e6074 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Wed, 24 Dec 2025 14:22:34 -0500 Subject: [PATCH 4/4] Add a better test case. --- Lib/test/test_free_threading/test_gc.py | 32 +++++++++++++++++++++++++ Lib/test/test_gc.py | 12 ---------- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/Lib/test/test_free_threading/test_gc.py b/Lib/test/test_free_threading/test_gc.py index 3b83e0431efa6b..8b45b6e2150c28 100644 --- a/Lib/test/test_free_threading/test_gc.py +++ b/Lib/test/test_free_threading/test_gc.py @@ -62,6 +62,38 @@ def mutator_thread(): with threading_helper.start_threads(gcs + mutators): pass + def test_freeze_object_in_brc_queue(self): + # GH-142975: Freezing objects in the BRC queue could result in some + # objects having a zero refcount without being deallocated. + + class Weird: + # We need a destructor to trigger the check for object resurrection + def __del__(self): + pass + + # This is owned by the main thread, so the subthread will have to increment + # this object's reference count. + weird = Weird() + + def evil(): + gc.freeze() + + # Decrement the reference count from this thread, which will trigger the + # slow path during resurrection and add our weird object to the BRC queue. + nonlocal weird + del weird + + # Collection will merge the object's reference count and make it zero. + gc.collect() + + # Unfreeze the object, making it visible to the GC. + gc.unfreeze() + gc.collect() + + thread = Thread(target=evil) + thread.start() + thread.join() + if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_gc.py b/Lib/test/test_gc.py index 83e2856aae51df..6aa6361d5d0b92 100644 --- a/Lib/test/test_gc.py +++ b/Lib/test/test_gc.py @@ -1249,18 +1249,6 @@ def test_tuple_untrack_counts(self): self.assertTrue(new_count - count > (n // 2)) - @threading_helper.requires_working_threading() - def test_concurrent_freeze_unfreeze(): - # GH-142975: On the free-threaded build, this would cause problems - # with objects that had a per-thread reference count. - def weird(): - gc.freeze() - gc.collect() - gc.unfreeze() - - threading_helper.run_concurrently(weird, 4) - - class IncrementalGCTests(unittest.TestCase): @unittest.skipIf(_testinternalcapi is None, "requires _testinternalcapi") @requires_gil_enabled("Free threading does not support incremental GC")