From 2d81b6bea4fcf05837e687d5660c695e7e717453 Mon Sep 17 00:00:00 2001 From: Mark Shannon Date: Wed, 18 Jun 2025 16:49:12 +0100 Subject: [PATCH 1/5] Restrict trashcan to GC'ed objects and thread the trashcan list through the GC header --- ...-06-18-16-45-36.gh-issue-135106.cpl6Aq.rst | 2 + Objects/object.c | 88 ++++++------------- 2 files changed, 31 insertions(+), 59 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-06-18-16-45-36.gh-issue-135106.cpl6Aq.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-06-18-16-45-36.gh-issue-135106.cpl6Aq.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-06-18-16-45-36.gh-issue-135106.cpl6Aq.rst new file mode 100644 index 00000000000000..b6e953a7719be1 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-06-18-16-45-36.gh-issue-135106.cpl6Aq.rst @@ -0,0 +1,2 @@ +Restrict the trashcan mechanism to GC'ed objects and untrack them while in +the trashcan to prevent the GC and trashcan mechanisms conflicting. diff --git a/Objects/object.c b/Objects/object.c index 6a581a19604f9d..ec14328ec12a2d 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -3034,57 +3034,24 @@ Py_ReprLeave(PyObject *obj) /* Trashcan support. */ -#ifndef Py_GIL_DISABLED -/* We need to store a pointer in the refcount field of - * an object. It is important that we never store 0 (NULL). -* It is also important to not make the object appear immortal, -* or it might be untracked by the cycle GC. */ -static uintptr_t -pointer_to_safe_refcount(void *ptr) -{ - uintptr_t full = (uintptr_t)ptr; - assert((full & 3) == 0); -#if SIZEOF_VOID_P > 4 - uint32_t refcnt = (uint32_t)full; - if (refcnt >= (uint32_t)_Py_IMMORTAL_MINIMUM_REFCNT) { - full = full - ((uintptr_t)_Py_IMMORTAL_MINIMUM_REFCNT) + 1; - } - return full + 2; -#else - // Make the top two bits 0, so it appears mortal. - return (full >> 2) + 1; -#endif -} - -static void * -safe_refcount_to_pointer(uintptr_t refcnt) -{ -#if SIZEOF_VOID_P > 4 - if (refcnt & 1) { - refcnt += _Py_IMMORTAL_MINIMUM_REFCNT - 1; - } - return (void *)(refcnt - 2); -#else - return (void *)((refcnt -1) << 2); -#endif -} -#endif - /* Add op to the gcstate->trash_delete_later list. Called when the current - * call-stack depth gets large. op must be a currently untracked gc'ed - * object, with refcount 0. Py_DECREF must already have been called on it. + * call-stack depth gets large. op must be a gc'ed object, with refcount 0. + * Py_DECREF must already have been called on it. */ void _PyTrash_thread_deposit_object(PyThreadState *tstate, PyObject *op) { _PyObject_ASSERT(op, Py_REFCNT(op) == 0); + assert(PyObject_IS_GC(op)); + int tracked = _PyObject_GC_IS_TRACKED(op); + if (tracked) { + _PyObject_GC_UNTRACK(op); + } + uintptr_t tagged_ptr = ((uintptr_t)tstate->delete_later) | tracked; #ifdef Py_GIL_DISABLED - op->ob_tid = (uintptr_t)tstate->delete_later; + op->ob_tid = tagged_ptr; #else - /* Store the delete_later pointer in the refcnt field. */ - uintptr_t refcnt = pointer_to_safe_refcount(tstate->delete_later); - *((uintptr_t*)op) = refcnt; - assert(!_Py_IsImmortal(op)); + _Py_AS_GC(op)->_gc_next = tagged_ptr; #endif tstate->delete_later = op; } @@ -3099,25 +3066,28 @@ _PyTrash_thread_destroy_chain(PyThreadState *tstate) destructor dealloc = Py_TYPE(op)->tp_dealloc; #ifdef Py_GIL_DISABLED - tstate->delete_later = (PyObject*) op->ob_tid; + uintptr_t tagged_ptr = op->ob_tid; op->ob_tid = 0; _Py_atomic_store_ssize_relaxed(&op->ob_ref_shared, _Py_REF_MERGED); #else - /* Get the delete_later pointer from the refcnt field. - * See _PyTrash_thread_deposit_object(). */ - uintptr_t refcnt = *((uintptr_t*)op); - tstate->delete_later = safe_refcount_to_pointer(refcnt); - op->ob_refcnt = 0; + uintptr_t tagged_ptr = _Py_AS_GC(op)->_gc_next; + _Py_AS_GC(op)->_gc_next = 0; #endif - - /* Call the deallocator directly. This used to try to - * fool Py_DECREF into calling it indirectly, but - * Py_DECREF was already called on this object, and in - * assorted non-release builds calling Py_DECREF again ends - * up distorting allocation statistics. - */ - _PyObject_ASSERT(op, Py_REFCNT(op) == 0); - (*dealloc)(op); + tstate->delete_later = (PyObject *)(tagged_ptr & ~1); + if (tagged_ptr & 1) { + _PyObject_GC_TRACK(op); + } + /* It is possible that the object has been accessed through + * a weak ref, so only free if refcount == 0) */ + if (Py_REFCNT(op) == 0) { + /* Call the deallocator directly. This used to try to + * fool Py_DECREF into calling it indirectly, but + * Py_DECREF was already called on this object, and in + * assorted non-release builds calling Py_DECREF again ends + * up distorting allocation statistics. + */ + (*dealloc)(op); + } } } @@ -3186,7 +3156,7 @@ _Py_Dealloc(PyObject *op) destructor dealloc = type->tp_dealloc; PyThreadState *tstate = _PyThreadState_GET(); intptr_t margin = _Py_RecursionLimit_GetMargin(tstate); - if (margin < 2) { + if (margin < 2 && PyObject_IS_GC(op)) { _PyTrash_thread_deposit_object(tstate, (PyObject *)op); return; } From f89ddfd880be425f08b71f696564300b8e3e6f76 Mon Sep 17 00:00:00 2001 From: Mark Shannon Date: Wed, 18 Jun 2025 18:49:01 +0100 Subject: [PATCH 2/5] Go back to asserting refcnt == 0 --- Objects/object.c | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/Objects/object.c b/Objects/object.c index ec14328ec12a2d..29d6e9526f848b 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -3077,17 +3077,14 @@ _PyTrash_thread_destroy_chain(PyThreadState *tstate) if (tagged_ptr & 1) { _PyObject_GC_TRACK(op); } - /* It is possible that the object has been accessed through - * a weak ref, so only free if refcount == 0) */ - if (Py_REFCNT(op) == 0) { - /* Call the deallocator directly. This used to try to - * fool Py_DECREF into calling it indirectly, but - * Py_DECREF was already called on this object, and in - * assorted non-release builds calling Py_DECREF again ends - * up distorting allocation statistics. - */ - (*dealloc)(op); - } + /* Call the deallocator directly. This used to try to + * fool Py_DECREF into calling it indirectly, but + * Py_DECREF was already called on this object, and in + * assorted non-release builds calling Py_DECREF again ends + * up distorting allocation statistics. + */ + _PyObject_ASSERT(op, Py_REFCNT(op) == 0); + (*dealloc)(op); } } From 7101e8f293f8641f3570867a0de92efc8bc5aa05 Mon Sep 17 00:00:00 2001 From: Mark Shannon Date: Thu, 19 Jun 2025 10:30:49 +0100 Subject: [PATCH 3/5] Don't empty trashcan unless object is GC'd --- Objects/object.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Objects/object.c b/Objects/object.c index 29d6e9526f848b..c79b61cf3c8c59 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -3150,10 +3150,11 @@ void _Py_Dealloc(PyObject *op) { PyTypeObject *type = Py_TYPE(op); + unsigned long gc_flags = type->tp_flags & Py_TPFLAGS_HAVE_GC; destructor dealloc = type->tp_dealloc; PyThreadState *tstate = _PyThreadState_GET(); intptr_t margin = _Py_RecursionLimit_GetMargin(tstate); - if (margin < 2 && PyObject_IS_GC(op)) { + if (margin < 2 && gc_flags) { _PyTrash_thread_deposit_object(tstate, (PyObject *)op); return; } @@ -3199,7 +3200,7 @@ _Py_Dealloc(PyObject *op) Py_XDECREF(old_exc); Py_DECREF(type); #endif - if (tstate->delete_later && margin >= 4) { + if (tstate->delete_later && margin >= 4 && gc_flags) { _PyTrash_thread_destroy_chain(tstate); } } From a81bb72c2782152bec8b64303a0d141838e31200 Mon Sep 17 00:00:00 2001 From: Mark Shannon Date: Thu, 19 Jun 2025 10:53:17 +0100 Subject: [PATCH 4/5] Handle tp_is_gc in _PyTrash_thread_deposit_object --- Objects/object.c | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/Objects/object.c b/Objects/object.c index c79b61cf3c8c59..04a31280db9e0f 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -3042,12 +3042,19 @@ void _PyTrash_thread_deposit_object(PyThreadState *tstate, PyObject *op) { _PyObject_ASSERT(op, Py_REFCNT(op) == 0); - assert(PyObject_IS_GC(op)); - int tracked = _PyObject_GC_IS_TRACKED(op); - if (tracked) { - _PyObject_GC_UNTRACK(op); + PyTypeObject *tp = Py_TYPE(op); + assert(tp->tp_flags & Py_TPFLAGS_HAVE_GC); + uintptr_t tagged_ptr; + if (tp->tp_is_gc == NULL || tp->tp_is_gc(op)) { + int tracked = _PyObject_GC_IS_TRACKED(op); + if (tracked) { + _PyObject_GC_UNTRACK(op); + } + tagged_ptr = ((uintptr_t)tstate->delete_later) | tracked; + } + else { + tagged_ptr = ((uintptr_t)tstate->delete_later); } - uintptr_t tagged_ptr = ((uintptr_t)tstate->delete_later) | tracked; #ifdef Py_GIL_DISABLED op->ob_tid = tagged_ptr; #else @@ -3150,11 +3157,11 @@ void _Py_Dealloc(PyObject *op) { PyTypeObject *type = Py_TYPE(op); - unsigned long gc_flags = type->tp_flags & Py_TPFLAGS_HAVE_GC; + unsigned long gc_flag = type->tp_flags & Py_TPFLAGS_HAVE_GC; destructor dealloc = type->tp_dealloc; PyThreadState *tstate = _PyThreadState_GET(); intptr_t margin = _Py_RecursionLimit_GetMargin(tstate); - if (margin < 2 && gc_flags) { + if (margin < 2 && gc_flag) { _PyTrash_thread_deposit_object(tstate, (PyObject *)op); return; } @@ -3200,7 +3207,7 @@ _Py_Dealloc(PyObject *op) Py_XDECREF(old_exc); Py_DECREF(type); #endif - if (tstate->delete_later && margin >= 4 && gc_flags) { + if (tstate->delete_later && margin >= 4 && gc_flag) { _PyTrash_thread_destroy_chain(tstate); } } From 98c9efdd81c28a2c402f89d76ce1717eb2fe0f71 Mon Sep 17 00:00:00 2001 From: Mark Shannon Date: Thu, 19 Jun 2025 10:58:04 +0100 Subject: [PATCH 5/5] Tidy a bit --- Objects/object.c | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Objects/object.c b/Objects/object.c index 04a31280db9e0f..4d60128b092c22 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -3044,17 +3044,14 @@ _PyTrash_thread_deposit_object(PyThreadState *tstate, PyObject *op) _PyObject_ASSERT(op, Py_REFCNT(op) == 0); PyTypeObject *tp = Py_TYPE(op); assert(tp->tp_flags & Py_TPFLAGS_HAVE_GC); - uintptr_t tagged_ptr; + int tracked = 0; if (tp->tp_is_gc == NULL || tp->tp_is_gc(op)) { - int tracked = _PyObject_GC_IS_TRACKED(op); + tracked = _PyObject_GC_IS_TRACKED(op); if (tracked) { _PyObject_GC_UNTRACK(op); } - tagged_ptr = ((uintptr_t)tstate->delete_later) | tracked; - } - else { - tagged_ptr = ((uintptr_t)tstate->delete_later); } + uintptr_t tagged_ptr = ((uintptr_t)tstate->delete_later) | tracked; #ifdef Py_GIL_DISABLED op->ob_tid = tagged_ptr; #else