From 8b7b3199ada59639a08df4bf45405b101c7044f1 Mon Sep 17 00:00:00 2001 From: Neko Asakura Date: Thu, 5 Nov 2026 10:22:08 -0500 Subject: [PATCH 1/2] gh-161419: fix weakref reviving a dying object in free-threading --- Include/internal/pycore_object.h | 2 ++ Lib/test/test_weakref.py | 24 +++++++++++++++++++ ...-11-05-10-19-48.gh-issue-161419.wc4DV0.rst | 3 +++ 3 files changed, 29 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-11-05-10-19-48.gh-issue-161419.wc4DV0.rst diff --git a/Include/internal/pycore_object.h b/Include/internal/pycore_object.h index f6a71fe0417601..a27e13f9c460bf 100644 --- a/Include/internal/pycore_object.h +++ b/Include/internal/pycore_object.h @@ -712,6 +712,8 @@ _PyObject_ResurrectEnd(PyObject *op) if (_Py_IsOwnedByCurrentThread(op) && local == 1 && shared == 0) { // Fast-path: object has a single refcount and is owned by this thread _Py_atomic_store_uint32_relaxed(&op->ob_ref_local, 0); + _Py_atomic_store_uintptr_relaxed(&op->ob_tid, _Py_UNOWNED_TID); + _Py_atomic_store_ssize_relaxed(&op->ob_ref_shared, _Py_REF_MERGED); # ifdef Py_TRACE_REFS _Py_ForgetReference(op); # endif diff --git a/Lib/test/test_weakref.py b/Lib/test/test_weakref.py index da4155d09f7328..713ccad11219c8 100644 --- a/Lib/test/test_weakref.py +++ b/Lib/test/test_weakref.py @@ -1,4 +1,5 @@ import gc +import io import sys import doctest import unittest @@ -1056,6 +1057,29 @@ def __del__(self): del head self.assertEqual(found, []) + def test_ref_to_object_dying_after_finalizer(self): + found = [] + class Attr: + def __del__(self): + found.append(self.wr() is None) + + class SetWithDel(set): + __slots__ = ('a',) + def __del__(self): + pass + + class Raw(io.RawIOBase): + __slots__ = ('a',) + + for cls in (SetWithDel, Raw): + with self.subTest(cls=cls.__name__): + found.clear() + o = cls() + o.a = Attr() + o.a.wr = weakref.ref(o) + del o + self.assertEqual(found, [True]) + def test_callback_attribute(self): x = Object(1) callback = lambda ref: None diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-11-05-10-19-48.gh-issue-161419.wc4DV0.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-11-05-10-19-48.gh-issue-161419.wc4DV0.rst new file mode 100644 index 00000000000000..0d6605a9eeb550 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-11-05-10-19-48.gh-issue-161419.wc4DV0.rst @@ -0,0 +1,3 @@ +Fix a crash in the :term:`free-threaded build` where a weak reference could +revive an object whose finalizer had already run during deallocation, causing +it to be deallocated twice. From c753b7a3e268394b9df72562f7347df1459b1ccd Mon Sep 17 00:00:00 2001 From: Neko Asakura Date: Thu, 5 Nov 2026 17:49:41 -0500 Subject: [PATCH 2/2] gh-161419: rename test variable to clarify weakref expectation --- Lib/test/test_weakref.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_weakref.py b/Lib/test/test_weakref.py index 713ccad11219c8..5b4ace36c46143 100644 --- a/Lib/test/test_weakref.py +++ b/Lib/test/test_weakref.py @@ -1058,10 +1058,10 @@ def __del__(self): self.assertEqual(found, []) def test_ref_to_object_dying_after_finalizer(self): - found = [] + cleared = [] class Attr: def __del__(self): - found.append(self.wr() is None) + cleared.append(self.wr() is None) class SetWithDel(set): __slots__ = ('a',) @@ -1073,12 +1073,12 @@ class Raw(io.RawIOBase): for cls in (SetWithDel, Raw): with self.subTest(cls=cls.__name__): - found.clear() + cleared.clear() o = cls() o.a = Attr() o.a.wr = weakref.ref(o) del o - self.assertEqual(found, [True]) + self.assertEqual(cleared, [True]) def test_callback_attribute(self): x = Object(1)