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]