From e1de3e01994771812ba318b209753e6decd44b45 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Wed, 21 Oct 2026 16:20:00 +0530 Subject: [PATCH] [3.15] gh-159431: Handle immortal objects in _PyObject_XDecRefDelayed() (GH-159433) Skip immortal objects both when queueing and when processing the queue, and remove the now redundant caller-side checks. (cherry picked from commit b70a5128c4ef1572e6aa24f9f3739d17eacfafef) Co-authored-by: Neil Schemenauer --- Lib/test/test_capi/test_object.py | 13 ++++++++++++ ...-10-14-18-45-35.gh-issue-159431.mVTMsA.rst | 4 ++++ Objects/obmalloc.c | 21 ++++++++++--------- 3 files changed, 28 insertions(+), 10 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 0947d47c8a55582..4fd39309d3da990 100644 --- a/Objects/obmalloc.c +++ b/Objects/obmalloc.c @@ -1378,6 +1378,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); @@ -1470,9 +1474,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 { @@ -1520,7 +1526,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. @@ -1535,12 +1541,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