From ba4142a2113f469f237a6d1d6e463264cc7cb9d0 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Wed, 14 Oct 2026 11:36:17 -0700 Subject: [PATCH] gh-159431: Handle immortal objects in _PyObject_XDecRefDelayed() Skip immortal objects both when queueing and when processing the queue, and remove the now redundant caller-side checks. --- Lib/test/test_capi/test_object.py | 13 ++++++++++++ ...-10-14-18-45-35.gh-issue-159431.mVTMsA.rst | 4 ++++ Objects/obmalloc.c | 21 ++++++++++--------- Objects/typeobject.c | 4 +--- 4 files changed, 29 insertions(+), 13 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-10-14-18-45-35.gh-issue-159431.mVTMsA.rst diff --git a/Lib/test/test_capi/test_object.py b/Lib/test/test_capi/test_object.py index b4585adb07ece4b..338fc61ea1eb5f5 100644 --- a/Lib/test/test_capi/test_object.py +++ b/Lib/test/test_capi/test_object.py @@ -275,6 +275,19 @@ def __del__(self): obj = MyObj() _testinternalcapi.incref_decref_delayed(obj) + def test_decref_delayed_immortal(self): + # _PyObject_XDecRefDelayed() must handle immortal objects, including + # objects that become immortal after they were queued. The GC + # processes the queue with the world stopped. + import gc + _testinternalcapi.incref_decref_delayed(None) + gc.collect() + + s = ''.join(['decref', '_delayed', '_immortal']) + _testinternalcapi.incref_decref_delayed(s) + s = sys.intern(s) + gc.collect() + def test_is_unique_temporary(self): self.assertTrue(_testcapi.pyobject_is_unique_temporary(object())) obj = object() diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-14-18-45-35.gh-issue-159431.mVTMsA.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-14-18-45-35.gh-issue-159431.mVTMsA.rst new file mode 100644 index 000000000000000..7ed642e4c707918 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-14-18-45-35.gh-issue-159431.mVTMsA.rst @@ -0,0 +1,4 @@ +Fix an assertion failure, or an immortal object becoming mortal, on the +:term:`free threaded ` build when an object passed to the +internal delayed decref mechanism is immortal or becomes immortal before the +delayed decref is processed by the garbage collector. diff --git a/Objects/obmalloc.c b/Objects/obmalloc.c index 841fb796e1ced15..096b8b08aa73eba 100644 --- a/Objects/obmalloc.c +++ b/Objects/obmalloc.c @@ -1384,6 +1384,10 @@ free_work_item(uintptr_t ptr, delayed_dealloc_cb cb, void *state) return; } assert(_PyInterpreterState_GET()->stoptheworld.world_stopped); + if (_Py_IsImmortal(obj)) { + // The object may have become immortal after it was queued. + return; + } Py_ssize_t refcount = _Py_ExplicitMergeRefcount(obj, -1); if (refcount == 0) { cb(obj, state); @@ -1476,9 +1480,11 @@ free_delayed(uintptr_t ptr, size_t size) _PyEval_StopTheWorld(tstate->base.interp); if (work_item_should_decref(ptr)) { PyObject *obj = (PyObject *)(ptr - 1); - Py_ssize_t refcount = _Py_ExplicitMergeRefcount(obj, -1); - if (refcount == 0) { - to_dealloc = obj; + if (!_Py_IsImmortal(obj)) { + Py_ssize_t refcount = _Py_ExplicitMergeRefcount(obj, -1); + if (refcount == 0) { + to_dealloc = obj; + } } } else { @@ -1526,7 +1532,7 @@ void _PyObject_XDecRefDelayed(PyObject *ptr) { assert(!((uintptr_t)ptr & 0x01)); - if (ptr != NULL) { + if (ptr != NULL && !_Py_IsImmortal(ptr)) { // We use 0 as the size since we don't have an easy way to know the // actual size. If we are freeing many objects, the write sequence // will be advanced due to QSBR_DEFERRED_LIMIT. @@ -1541,12 +1547,7 @@ _PyObject_XSetRefDelayed(PyObject **ptr, PyObject *value) { PyObject *old = *ptr; FT_ATOMIC_STORE_PTR_RELEASE(*ptr, value); - if (old == NULL) { - return; - } - if (!_Py_IsImmortal(old)) { - _PyObject_XDecRefDelayed(old); - } + _PyObject_XDecRefDelayed(old); } #endif diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 15c3367013e4cac..5e772e5608d4c66 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -6738,9 +6738,7 @@ type_setattro(PyObject *self, PyObject *name, PyObject *value) Py_DECREF(name); Py_XDECREF(descr); // delay decref of the old value as lock-free type cache readers may access it - if (old_value != NULL && !_Py_IsImmortal(old_value)) { - _PyObject_XDecRefDelayed(old_value); - } + _PyObject_XDecRefDelayed(old_value); return res; }