https://github.com/python/cpython/commit/b70a5128c4ef1572e6aa24f9f3739d17eacfafef
commit: b70a5128c4ef1572e6aa24f9f3739d17eacfafef
branch: main
author: Neil Schemenauer <[email protected]>
committer: nascheme <[email protected]>
date: 2026-10-14T14:11:48-07:00
summary:

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.

files:
A 
Misc/NEWS.d/next/Core_and_Builtins/2026-10-14-18-45-35.gh-issue-159431.mVTMsA.rst
M Lib/test/test_capi/test_object.py
M Objects/obmalloc.c
M Objects/typeobject.c

diff --git a/Lib/test/test_capi/test_object.py 
b/Lib/test/test_capi/test_object.py
index b4585adb07ece4..338fc61ea1eb5f 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 00000000000000..7ed642e4c70791
--- /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 <free threading>` 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 841fb796e1ced1..096b8b08aa73eb 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 15c3367013e4ca..5e772e5608d4c6 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;
 }
 

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to