From e2a6b49fcf9935e0ad46d1b83e76fb4ac03d8df5 Mon Sep 17 00:00:00 2001 From: Donghee Na Date: Wed, 26 Aug 2026 01:08:47 +0900 Subject: [PATCH 1/7] gh-156371: Fix missing PyRefTracer_DESTROY events for trashcan objects --- .../2026-08-26-01-08-38.gh-issue-156371.USpwKG.rst | 2 ++ Objects/object.c | 4 ++++ 2 files changed, 6 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-08-26-01-08-38.gh-issue-156371.USpwKG.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-08-26-01-08-38.gh-issue-156371.USpwKG.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-26-01-08-38.gh-issue-156371.USpwKG.rst new file mode 100644 index 000000000000000..31ebff6f247f7e3 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-26-01-08-38.gh-issue-156371.USpwKG.rst @@ -0,0 +1,2 @@ +Fix missing ``PyRefTracer_DESTROY`` events for trashcan deferred objects. +Patch by Donghee Na. diff --git a/Objects/object.c b/Objects/object.c index fadd9273a36607c..9ecd5c274e47575 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -3298,6 +3298,10 @@ _Py_Dealloc(PyObject *op) tstate = _PyThreadState_GET(); margin = _Py_RecursionLimit_GetMargin(tstate); if (margin < 2) { +#ifdef Py_TRACE_REFS + _Py_ForgetReference(op); +#endif + _PyReftracerTrack(op, PyRefTracer_DESTROY); _PyTrash_thread_deposit_object(tstate, (PyObject *)op); return; } From 03274d3224cfbe797665a5838585b44b01501a81 Mon Sep 17 00:00:00 2001 From: Donghee Na Date: Wed, 26 Aug 2026 01:19:47 +0900 Subject: [PATCH 2/7] Add test code --- Lib/test/test_capi/test_object.py | 13 +++++++++++++ Modules/_testcapimodule.c | 32 +++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/Lib/test/test_capi/test_object.py b/Lib/test/test_capi/test_object.py index 433afac875aa7bf..e8647aff90c4e13 100644 --- a/Lib/test/test_capi/test_object.py +++ b/Lib/test/test_capi/test_object.py @@ -342,5 +342,18 @@ def test_pyobject_dump(self): self.assertRegex(output, r'') +class RefTracerTest(unittest.TestCase): + def test_destroy_traced_for_trashcan_deferred_objects(self): + depth = 200_000 + chain = None + for _ in range(depth): + chain = [chain] + with support.disable_gc(): + _testcapi.start_counting_list_destroys() + del chain + destroys = _testcapi.stop_counting_list_destroys() + self.assertEqual(destroys, depth) + + if __name__ == "__main__": unittest.main() diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index fb18a866e628128..e00ae9863c2ab60 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -39,6 +39,7 @@ static struct PyModuleDef _testcapimodule; // Module state typedef struct { PyObject *error; // _testcapi.error object + Py_ssize_t list_destroys; } testcapistate_t; static testcapistate_t* @@ -2418,6 +2419,35 @@ test_reftracer(PyObject *ob, PyObject *Py_UNUSED(ignored)) return NULL; } +static int +_listdestroytracer(PyObject *obj, PyRefTracerEvent event, void *data) +{ + if (event == PyRefTracer_DESTROY && PyList_CheckExact(obj)) { + (*(Py_ssize_t *)data)++; + } + return 0; +} + +static PyObject * +start_counting_list_destroys(PyObject *self, PyObject *Py_UNUSED(ignored)) +{ + testcapistate_t *state = get_testcapi_state(self); + state->list_destroys = 0; + if (PyRefTracer_SetTracer(_listdestroytracer, &state->list_destroys) != 0) { + return NULL; + } + Py_RETURN_NONE; +} + +static PyObject * +stop_counting_list_destroys(PyObject *self, PyObject *Py_UNUSED(ignored)) +{ + if (PyRefTracer_SetTracer(NULL, NULL) != 0) { + return NULL; + } + return PyLong_FromSsize_t(get_testcapi_state(self)->list_destroys); +} + static PyObject * function_set_warning(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) { @@ -3022,6 +3052,8 @@ static PyMethodDef TestMethods[] = { {"test_buildvalue_N", test_buildvalue_N, METH_NOARGS}, {"test_buildvalue_p", test_buildvalue_p, METH_NOARGS}, {"test_reftracer", test_reftracer, METH_NOARGS}, + {"start_counting_list_destroys", start_counting_list_destroys, METH_NOARGS}, + {"stop_counting_list_destroys", stop_counting_list_destroys, METH_NOARGS}, {"_test_thread_state", test_thread_state, METH_VARARGS}, {"gilstate_ensure_release", gilstate_ensure_release, METH_NOARGS}, #ifndef MS_WINDOWS From a38788d741e1734d7f2e4a66c6d7acf9cdc46435 Mon Sep 17 00:00:00 2001 From: Donghee Na Date: Wed, 26 Aug 2026 01:50:42 +0900 Subject: [PATCH 3/7] fix --- Lib/test/test_capi/test_object.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_capi/test_object.py b/Lib/test/test_capi/test_object.py index e8647aff90c4e13..b9f2a9367c996e2 100644 --- a/Lib/test/test_capi/test_object.py +++ b/Lib/test/test_capi/test_object.py @@ -343,6 +343,7 @@ def test_pyobject_dump(self): class RefTracerTest(unittest.TestCase): + @support.skip_wasi_stack_overflow() def test_destroy_traced_for_trashcan_deferred_objects(self): depth = 200_000 chain = None From 991795a3a4f958538c5f2028fd592bd5669ba0d1 Mon Sep 17 00:00:00 2001 From: Donghee Na Date: Wed, 26 Aug 2026 02:22:55 +0900 Subject: [PATCH 4/7] fix --- Lib/test/test_capi/test_object.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_capi/test_object.py b/Lib/test/test_capi/test_object.py index b9f2a9367c996e2..04b30d58669abee 100644 --- a/Lib/test/test_capi/test_object.py +++ b/Lib/test/test_capi/test_object.py @@ -343,6 +343,7 @@ def test_pyobject_dump(self): class RefTracerTest(unittest.TestCase): + @support.skip_emscripten_stack_overflow() @support.skip_wasi_stack_overflow() def test_destroy_traced_for_trashcan_deferred_objects(self): depth = 200_000 From e3ed9a21b4fdf262a30ce72c1df2ddee0a4764e6 Mon Sep 17 00:00:00 2001 From: Donghee Na Date: Wed, 26 Aug 2026 02:27:00 +0900 Subject: [PATCH 5/7] tsan fix --- Modules/_testcapimodule.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index e00ae9863c2ab60..16645faa4d2e717 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -2423,7 +2423,7 @@ static int _listdestroytracer(PyObject *obj, PyRefTracerEvent event, void *data) { if (event == PyRefTracer_DESTROY && PyList_CheckExact(obj)) { - (*(Py_ssize_t *)data)++; + _Py_atomic_add_ssize((Py_ssize_t *)data, 1); } return 0; } @@ -2432,7 +2432,7 @@ static PyObject * start_counting_list_destroys(PyObject *self, PyObject *Py_UNUSED(ignored)) { testcapistate_t *state = get_testcapi_state(self); - state->list_destroys = 0; + _Py_atomic_store_ssize(&state->list_destroys, 0); if (PyRefTracer_SetTracer(_listdestroytracer, &state->list_destroys) != 0) { return NULL; } @@ -2445,7 +2445,8 @@ stop_counting_list_destroys(PyObject *self, PyObject *Py_UNUSED(ignored)) if (PyRefTracer_SetTracer(NULL, NULL) != 0) { return NULL; } - return PyLong_FromSsize_t(get_testcapi_state(self)->list_destroys); + return PyLong_FromSsize_t( + _Py_atomic_load_ssize(&get_testcapi_state(self)->list_destroys)); } static PyObject * From c50f26f932f8a323b5152184333276633c5f90fb Mon Sep 17 00:00:00 2001 From: Donghee Na Date: Wed, 26 Aug 2026 21:36:47 +0900 Subject: [PATCH 6/7] Address code review --- Objects/object.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Objects/object.c b/Objects/object.c index 9ecd5c274e47575..29eec7f5a62672c 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -3224,6 +3224,10 @@ _PyTrash_thread_destroy_chain(PyThreadState *tstate) * up distorting allocation statistics. */ _PyObject_ASSERT(op, Py_REFCNT(op) == 0); +#ifdef Py_TRACE_REFS + _Py_ForgetReference(op); +#endif + _PyReftracerTrack(op, PyRefTracer_DESTROY); (*dealloc)(op); } } @@ -3298,10 +3302,6 @@ _Py_Dealloc(PyObject *op) tstate = _PyThreadState_GET(); margin = _Py_RecursionLimit_GetMargin(tstate); if (margin < 2) { -#ifdef Py_TRACE_REFS - _Py_ForgetReference(op); -#endif - _PyReftracerTrack(op, PyRefTracer_DESTROY); _PyTrash_thread_deposit_object(tstate, (PyObject *)op); return; } From 49d1779489465dd86783da28703774596dd65202 Mon Sep 17 00:00:00 2001 From: Donghee Na Date: Thu, 27 Aug 2026 00:03:47 +0900 Subject: [PATCH 7/7] Improve speed at TSAN --- Lib/test/test_capi/test_object.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_capi/test_object.py b/Lib/test/test_capi/test_object.py index 04b30d58669abee..e1cd5026d6edd51 100644 --- a/Lib/test/test_capi/test_object.py +++ b/Lib/test/test_capi/test_object.py @@ -347,10 +347,10 @@ class RefTracerTest(unittest.TestCase): @support.skip_wasi_stack_overflow() def test_destroy_traced_for_trashcan_deferred_objects(self): depth = 200_000 - chain = None - for _ in range(depth): - chain = [chain] with support.disable_gc(): + chain = None + for _ in range(depth): + chain = [chain] _testcapi.start_counting_list_destroys() del chain destroys = _testcapi.stop_counting_list_destroys()