Skip to content
Open
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
62 changes: 62 additions & 0 deletions Lib/test/test_gc.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,68 @@ def test_saveall(self):
obj = gc.garbage.pop()
self.assertEqual(id(obj), id_L)

@unittest.skipIf(Py_GIL_DISABLED,
"the free-threaded GC prints while the world is stopped")
def test_debug_collectable_write_frees_object(self):
class Node:
def __init__(self):
self.cycle = [self]

class Stderr:
def write(self, text):
node = wr()
if node is not None:
node.cycle.clear()
return len(text)

gc.collect()
node = Node()
wr = weakref.ref(node)
del node
debug = gc.get_debug()
with support.swap_attr(sys, 'stderr', Stderr()):
gc.set_debug(gc.DEBUG_COLLECTABLE)
try:
gc.collect()
finally:
gc.set_debug(debug)
self.assertIsNone(wr())

def test_debug_uncollectable_write_frees_object(self):
@with_tp_del
class Legacy:
def __tp_del__(self):
pass

class Child:
pass

class Stderr:
def write(self, text):
legacy = wr()
if legacy is not None and '<Child ' in text:
del legacy.child
return len(text)

gc.collect()
legacy = Legacy()
legacy.cycle = legacy
legacy.child = Child()
wr = weakref.ref(legacy)
del legacy
debug = gc.get_debug()
with support.swap_attr(sys, 'stderr', Stderr()):
gc.set_debug(gc.DEBUG_UNCOLLECTABLE)
try:
gc.collect()
finally:
gc.set_debug(debug)
legacy = wr()
self.assertFalse(hasattr(legacy, 'child'))
self.assertIn(legacy, gc.garbage)
del legacy.cycle
gc.garbage.remove(legacy)

def test_del(self):
# __del__ methods can trigger collection, make this to happen
thresholds = gc.get_threshold()
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
35 changes: 25 additions & 10 deletions Python/gc.c
Original file line number Diff line number Diff line change
Expand Up @@ -1003,6 +1003,27 @@ 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.
*/
static void
debug_cycles(const char *msg, PyGC_Head *list)
{
PyGC_Head seen;
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);
Py_INCREF(op);
debug_cycle(msg, op);
Py_DECREF(op);
}
gc_list_merge(&seen, list);
}

/* 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
Expand Down Expand Up @@ -1435,7 +1456,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
Expand Down Expand Up @@ -1558,9 +1578,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. */
Expand Down Expand Up @@ -1603,13 +1621,10 @@ 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) {
debug_cycles("uncollectable", &finalizers);
}
stats.uncollectable = n;
stats.uncollectable = gc_list_size(&finalizers);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This requires an extra pass, which we should avoid.

(void)PyTime_PerfCounterRaw(&stats.ts_stop);
stats.duration = PyTime_AsSecondsDouble(stats.ts_stop - stats.ts_start);
if (gcstate->debug & _PyGC_DEBUG_STATS) {
Expand Down
Loading