Skip to content

Commit aa21b07

Browse files
committed
pythongh-125859: Fix crash when gc.get_objects is called during GC
This fixes a crash when `gc.get_objects()` or `gc.get_referrers()` is called during a GC in the free threading build. Switch to `_PyObjectStack` to avoid corrupting the `struct worklist` linked list maintained by the GC. Also, don't return objects that are frozen (gc.freeze) or in the process of being collected to more closely match the behavior of the default build.
1 parent aaed91c commit aa21b07

File tree

3 files changed

+77
-55
lines changed

3 files changed

+77
-55
lines changed

Lib/test/test_gc.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1065,6 +1065,29 @@ def test_get_referents_on_capsule(self):
10651065
self.assertEqual(len(gc.get_referents(untracked_capsule)), 0)
10661066
gc.get_referents(tracked_capsule)
10671067

1068+
@cpython_only
1069+
def test_get_objects_during_gc(self):
1070+
# gh-125859: Calling gc.get_objects() or gc.get_referrers() during a
1071+
# collection should not crash.
1072+
test = self
1073+
collected = False
1074+
1075+
class GetObjectsOnDel:
1076+
def __del__(self):
1077+
nonlocal collected
1078+
collected = True
1079+
objs = gc.get_objects()
1080+
# NB: can't use "in" here because some objects override __eq__
1081+
for obj in objs:
1082+
test.assertTrue(obj is not self)
1083+
test.assertEqual(gc.get_referrers(self), [])
1084+
1085+
obj = GetObjectsOnDel()
1086+
obj.cycle = obj
1087+
del obj
1088+
1089+
gc.collect()
1090+
self.assertTrue(collected)
10681091

10691092

10701093
class IncrementalGCTests(unittest.TestCase):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix a crash in the free threading build when :func:`gc.get_objects` or
2+
:func:`gc.get_referrers` is called during an in-progress garbage collection.

Python/gc_free_threading.c

Lines changed: 52 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1404,7 +1404,7 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason)
14041404
struct get_referrers_args {
14051405
struct visitor_args base;
14061406
PyObject *objs;
1407-
struct worklist results;
1407+
_PyObjectStack results;
14081408
};
14091409

14101410
static int
@@ -1428,11 +1428,17 @@ visit_get_referrers(const mi_heap_t *heap, const mi_heap_area_t *area,
14281428
if (op == NULL) {
14291429
return true;
14301430
}
1431+
if (op->ob_gc_bits & (_PyGC_BITS_UNREACHABLE | _PyGC_BITS_FROZEN)) {
1432+
// Exclude unreachable objects (in-progress GC) and frozen
1433+
// objects from gc.get_objects() to match the default build.
1434+
return true;
1435+
}
14311436

14321437
struct get_referrers_args *arg = (struct get_referrers_args *)args;
14331438
if (Py_TYPE(op)->tp_traverse(op, referrersvisit, arg->objs)) {
1434-
op->ob_tid = 0; // we will restore the refcount later
1435-
worklist_push(&arg->results, op);
1439+
if (_PyObjectStack_Push(&arg->results, op) < 0) {
1440+
return false;
1441+
}
14361442
}
14371443

14381444
return true;
@@ -1446,43 +1452,36 @@ _PyGC_GetReferrers(PyInterpreterState *interp, PyObject *objs)
14461452
return NULL;
14471453
}
14481454

1455+
// NOTE: We can't append to the PyListObject during gc_visit_heaps()
1456+
// because PyList_Append() may reclaim an abandoned mimalloc segments
1457+
// while we are traversing them.
1458+
struct get_referrers_args args = { .objs = objs };
14491459
_PyEval_StopTheWorld(interp);
1460+
int err = gc_visit_heaps(interp, &visit_get_referrers, &args.base);
1461+
_PyEval_StartTheWorld(interp);
14501462

1451-
// Append all objects to a worklist. This abuses ob_tid. We will restore
1452-
// it later. NOTE: We can't append to the PyListObject during
1453-
// gc_visit_heaps() because PyList_Append() may reclaim an abandoned
1454-
// mimalloc segments while we are traversing them.
1455-
struct get_referrers_args args = { .objs = objs };
1456-
gc_visit_heaps(interp, &visit_get_referrers, &args.base);
1463+
if (err < 0) {
1464+
PyErr_NoMemory();
1465+
goto error;
1466+
}
14571467

1458-
bool error = false;
14591468
PyObject *op;
1460-
while ((op = worklist_pop(&args.results)) != NULL) {
1461-
gc_restore_tid(op);
1469+
while ((op = _PyObjectStack_Pop(&args.results)) != NULL) {
14621470
if (op != objs && PyList_Append(result, op) < 0) {
1463-
error = true;
1464-
break;
1471+
goto error;
14651472
}
14661473
}
1467-
1468-
// In case of error, clear the remaining worklist
1469-
while ((op = worklist_pop(&args.results)) != NULL) {
1470-
gc_restore_tid(op);
1471-
}
1472-
1473-
_PyEval_StartTheWorld(interp);
1474-
1475-
if (error) {
1476-
Py_DECREF(result);
1477-
return NULL;
1478-
}
1479-
14801474
return result;
1475+
1476+
error:
1477+
Py_DECREF(result);
1478+
_PyObjectStack_Clear(&args.results);
1479+
return NULL;
14811480
}
14821481

14831482
struct get_objects_args {
14841483
struct visitor_args base;
1485-
struct worklist objects;
1484+
_PyObjectStack objects;
14861485
};
14871486

14881487
static bool
@@ -1493,11 +1492,16 @@ visit_get_objects(const mi_heap_t *heap, const mi_heap_area_t *area,
14931492
if (op == NULL) {
14941493
return true;
14951494
}
1495+
if (op->ob_gc_bits & (_PyGC_BITS_UNREACHABLE | _PyGC_BITS_FROZEN)) {
1496+
// Exclude unreachable objects (in-progress GC) and frozen
1497+
// objects from gc.get_objects() to match the default build.
1498+
return true;
1499+
}
14961500

14971501
struct get_objects_args *arg = (struct get_objects_args *)args;
1498-
op->ob_tid = 0; // we will restore the refcount later
1499-
worklist_push(&arg->objects, op);
1500-
1502+
if (_PyObjectStack_Push(&arg->objects, op) < 0) {
1503+
return false;
1504+
}
15011505
return true;
15021506
}
15031507

@@ -1509,38 +1513,31 @@ _PyGC_GetObjects(PyInterpreterState *interp, int generation)
15091513
return NULL;
15101514
}
15111515

1516+
// NOTE: We can't append to the PyListObject during gc_visit_heaps()
1517+
// because PyList_Append() may reclaim an abandoned mimalloc segments
1518+
// while we are traversing them.
1519+
struct get_objects_args args = { 0 };
15121520
_PyEval_StopTheWorld(interp);
1521+
int err = gc_visit_heaps(interp, &visit_get_objects, &args.base);
1522+
_PyEval_StartTheWorld(interp);
15131523

1514-
// Append all objects to a worklist. This abuses ob_tid. We will restore
1515-
// it later. NOTE: We can't append to the list during gc_visit_heaps()
1516-
// because PyList_Append() may reclaim an abandoned mimalloc segment
1517-
// while we are traversing it.
1518-
struct get_objects_args args = { 0 };
1519-
gc_visit_heaps(interp, &visit_get_objects, &args.base);
1524+
if (err < 0) {
1525+
PyErr_NoMemory();
1526+
goto error;
1527+
}
15201528

1521-
bool error = false;
15221529
PyObject *op;
1523-
while ((op = worklist_pop(&args.objects)) != NULL) {
1524-
gc_restore_tid(op);
1530+
while ((op = _PyObjectStack_Pop(&args.objects)) != NULL) {
15251531
if (op != result && PyList_Append(result, op) < 0) {
1526-
error = true;
1527-
break;
1532+
goto error;
15281533
}
15291534
}
1530-
1531-
// In case of error, clear the remaining worklist
1532-
while ((op = worklist_pop(&args.objects)) != NULL) {
1533-
gc_restore_tid(op);
1534-
}
1535-
1536-
_PyEval_StartTheWorld(interp);
1537-
1538-
if (error) {
1539-
Py_DECREF(result);
1540-
return NULL;
1541-
}
1542-
15431535
return result;
1536+
1537+
error:
1538+
Py_DECREF(result);
1539+
_PyObjectStack_Clear(&args.objects);
1540+
return NULL;
15441541
}
15451542

15461543
static bool

0 commit comments

Comments
 (0)