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..90dbb1ced3f90e3 100644 --- a/Python/gc.c +++ b/Python/gc.c @@ -996,11 +996,55 @@ clear_weakrefs(PyGC_Head *unreachable) } } -static void +/* "gc: " + " <" + " 0x" + 16 hex digits + ">\n" + NUL, excluding msg and + * tp_name */ +#define DEBUG_LINE_FIXED_LEN 32 + +static char * debug_cycle(const char *msg, PyObject *op) { - PySys_FormatStderr("gc: %s <%s %p>\n", - msg, Py_TYPE(op)->tp_name, op); + const char *name = Py_TYPE(op)->tp_name; + size_t size = strlen(msg) + strlen(name) + DEBUG_LINE_FIXED_LEN; + char *line = PyMem_RawMalloc(size); + if (line == NULL) { + return NULL; + } + /* Not %p: its output is platform-specific (no "0x" prefix on Windows). */ + PyOS_snprintf(line, size, "gc: %s <%s 0x%" PRIxPTR ">\n", + msg, name, (uintptr_t)op); + return line; +} + +/* Format a line for each object in `list`, then print them. + * sys.stderr.write(). Stores into buffers so can iterate + * to avoid calling into Python whilst we iterate the objects + */ +static Py_ssize_t +debug_cycles(const char *msg, PyGC_Head *list) +{ + Py_ssize_t n = gc_list_size(list); + if (n == 0) { + return n; + } + + char **lines = PyMem_RawCalloc(n, sizeof(char *)); + if (lines == NULL) { + return n; + } + + Py_ssize_t i = 0; + for (PyGC_Head *gc = GC_NEXT(list); gc != list; gc = GC_NEXT(gc), i++) { + lines[i] = debug_cycle(msg, FROM_GC(gc)); + } + + for (i = 0; i < n; i++) { + if (lines[i] != NULL) { + PySys_FormatStderr("%s", lines[i]); + } + PyMem_RawFree(lines[i]); + } + PyMem_RawFree(lines); + return n; } /* Handle uncollectable garbage (cycles with tp_del slots, and stuff reachable @@ -1435,7 +1479,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 +1601,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 +1644,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) {