diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-25-12-30-00.gh-issue-112564.gcDbgW.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-25-12-30-00.gh-issue-112564.gcDbgW.rst new file mode 100644 index 000000000000000..84f3d75d9d2d3ed --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-25-12-30-00.gh-issue-112564.gcDbgW.rst @@ -0,0 +1,3 @@ +Fix a crash in the garbage collector when :data:`gc.DEBUG_COLLECTABLE` or +:data:`gc.DEBUG_UNCOLLECTABLE` is set and writing the debug output to +:data:`sys.stderr` frees one of the objects being reported. diff --git a/Python/gc.c b/Python/gc.c index 5dc79e20191ebc7..3a556856a53231f 100644 --- a/Python/gc.c +++ b/Python/gc.c @@ -1003,6 +1003,34 @@ debug_cycle(const char *msg, PyObject *op) msg, Py_TYPE(op)->tp_name, op); } +/* Call debug_cycle() on each object in `list`. sys.stderr.write() can run + * Python code that frees any object in the list, including the one just + * printed, so always take the first object and move it to a temporary + * `seen` list, as finalize_garbage() does. Return the number of seen + * items. + */ +static Py_ssize_t +debug_cycles(const char *msg, PyGC_Head *list) +{ + PyGC_Head seen; + Py_ssize_t n = 0; + /* Always take the next object from the head of `list` rather than from + * the current one, which may be freed (along with others) by debug_cycle(). + */ + gc_list_init(&seen); + while (!gc_list_is_empty(list)) { + PyGC_Head *gc = GC_NEXT(list); + PyObject *op = FROM_GC(gc); + gc_list_move(gc, &seen); + n++; + Py_INCREF(op); + debug_cycle(msg, op); + Py_DECREF(op); + } + gc_list_merge(&seen, list); + return n; +} + /* Handle uncollectable garbage (cycles with tp_del slots, and stuff reachable * only from such cycles). * If _PyGC_DEBUG_SAVEALL, all objects in finalizers are appended to the module @@ -1435,7 +1463,6 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason) PyGC_Head *old; /* next older generation */ PyGC_Head unreachable; /* non-problematic unreachable trash */ PyGC_Head finalizers; /* objects with, & reachable from, __del__ */ - PyGC_Head *gc; GCState *gcstate = &tstate->interp->gc; // gc_collect_main() must not be called before _PyGC_Init @@ -1558,9 +1585,7 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason) if (gcstate->debug & _PyGC_DEBUG_COLLECTABLE) { /* sys.stderr.write() can resurrect objects. */ needs_resurrection_check = true; - for (gc = GC_NEXT(&unreachable); gc != &unreachable; gc = GC_NEXT(gc)) { - debug_cycle("collectable", FROM_GC(gc)); - } + debug_cycles("collectable", &unreachable); } /* Clear weakrefs and invoke callbacks as necessary. */ @@ -1603,13 +1628,12 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason) /* Collect statistics on uncollectable objects found and print * debugging information. */ - Py_ssize_t n = 0; - for (gc = GC_NEXT(&finalizers); gc != &finalizers; gc = GC_NEXT(gc)) { - n++; - if (gcstate->debug & _PyGC_DEBUG_UNCOLLECTABLE) - debug_cycle("uncollectable", FROM_GC(gc)); + if (gcstate->debug & _PyGC_DEBUG_UNCOLLECTABLE) { + stats.uncollectable = debug_cycles("uncollectable", &finalizers); + } + else { + stats.uncollectable = gc_list_size(&finalizers); } - stats.uncollectable = n; (void)PyTime_PerfCounterRaw(&stats.ts_stop); stats.duration = PyTime_AsSecondsDouble(stats.ts_stop - stats.ts_start); if (gcstate->debug & _PyGC_DEBUG_STATS) {