https://github.com/python/cpython/commit/da0ec8c712f68db10a41bb571d752eff60687b3d commit: da0ec8c712f68db10a41bb571d752eff60687b3d branch: 3.14 author: Kumar Aditya <[email protected]> committer: kumaraditya303 <[email protected]> date: 2026-10-22T09:31:58Z summary:
[3.14] gh-159431: Handle immortal objects in _PyObject_XDecRefDelayed() (GH-159433) (#160228) 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 <[email protected]> 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 diff --git a/Lib/test/test_capi/test_object.py b/Lib/test/test_capi/test_object.py index e2345635c47325..fa2bf4fcb521e0 100644 --- a/Lib/test/test_capi/test_object.py +++ b/Lib/test/test_capi/test_object.py @@ -241,6 +241,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 9af59f13dc82d0..9e7506c415c466 100644 --- a/Objects/obmalloc.c +++ b/Objects/obmalloc.c @@ -1181,6 +1181,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); @@ -1273,9 +1277,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 { @@ -1323,7 +1329,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. _______________________________________________ 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]
