Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
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. Object addresses
in this output now use the same ``0x``-prefixed lowercase format on all
platforms, including Windows.
64 changes: 53 additions & 11 deletions Python/gc.c
Original file line number Diff line number Diff line change
Expand Up @@ -945,11 +945,55 @@ handle_weakrefs(PyGC_Head *unreachable, PyGC_Head *old, bool allow_callbacks)
return num_freed;
}

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` before printing any of them,
* since sys.stderr.write() can run Python code that frees objects in the
* list.
*/
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
Expand Down Expand Up @@ -1319,7 +1363,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;
PyTime_t t1 = 0; /* initialize to prevent a compiler warning */
GCState *gcstate = &tstate->interp->gc;

Expand Down Expand Up @@ -1432,9 +1475,7 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason)

/* Print debugging information. */
if (gcstate->debug & _PyGC_DEBUG_COLLECTABLE) {
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. */
Expand Down Expand Up @@ -1468,10 +1509,11 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason)

/* Collect statistics on uncollectable objects found and print
* debugging information. */
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) {
n = debug_cycles("uncollectable", &finalizers);
}
else {
n = gc_list_size(&finalizers);
}
if (gcstate->debug & _PyGC_DEBUG_STATS) {
PyTime_t t2;
Expand Down
Loading