diff --git a/Lib/test/test_capi/test_object.py b/Lib/test/test_capi/test_object.py index 433afac875aa7bf..e1cd5026d6edd51 100644 --- a/Lib/test/test_capi/test_object.py +++ b/Lib/test/test_capi/test_object.py @@ -342,5 +342,20 @@ def test_pyobject_dump(self): self.assertRegex(output, r'') +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 + 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() + self.assertEqual(destroys, depth) + + if __name__ == "__main__": unittest.main() 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/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index fb18a866e628128..16645faa4d2e717 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,36 @@ 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_atomic_add_ssize((Py_ssize_t *)data, 1); + } + return 0; +} + +static PyObject * +start_counting_list_destroys(PyObject *self, PyObject *Py_UNUSED(ignored)) +{ + testcapistate_t *state = get_testcapi_state(self); + _Py_atomic_store_ssize(&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( + _Py_atomic_load_ssize(&get_testcapi_state(self)->list_destroys)); +} + static PyObject * function_set_warning(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) { @@ -3022,6 +3053,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 diff --git a/Objects/object.c b/Objects/object.c index fadd9273a36607c..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); } }