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..5b4ace36c46143 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): + cleared = [] + class Attr: + def __del__(self): + cleared.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__): + cleared.clear() + o = cls() + o.a = Attr() + o.a.wr = weakref.ref(o) + del o + self.assertEqual(cleared, [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.