Skip to content

Commit e545ead

Browse files
authored
gh-125859: Fix crash when gc.get_objects is called during GC (#125882)
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 b61fece commit e545ead

File tree

5 files changed

+160
-73
lines changed

5 files changed

+160
-73
lines changed

Include/internal/pycore_object_stack.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,16 @@ _PyObjectStack_Pop(_PyObjectStack *stack)
7171
return obj;
7272
}
7373

74+
static inline Py_ssize_t
75+
_PyObjectStack_Size(_PyObjectStack *stack)
76+
{
77+
Py_ssize_t size = 0;
78+
for (_PyObjectStackChunk *buf = stack->head; buf != NULL; buf = buf->prev) {
79+
size += buf->n;
80+
}
81+
return size;
82+
}
83+
7484
// Merge src into dst, leaving src empty
7585
extern void
7686
_PyObjectStack_Merge(_PyObjectStack *dst, _PyObjectStack *src);
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import unittest
2+
3+
import threading
4+
from threading import Thread
5+
from unittest import TestCase
6+
import gc
7+
8+
from test.support import threading_helper
9+
10+
11+
class MyObj:
12+
pass
13+
14+
15+
@threading_helper.requires_working_threading()
16+
class TestGC(TestCase):
17+
def test_get_objects(self):
18+
event = threading.Event()
19+
20+
def gc_thread():
21+
for i in range(100):
22+
o = gc.get_objects()
23+
event.set()
24+
25+
def mutator_thread():
26+
while not event.is_set():
27+
o1 = MyObj()
28+
o2 = MyObj()
29+
o3 = MyObj()
30+
o4 = MyObj()
31+
32+
gcs = [Thread(target=gc_thread)]
33+
mutators = [Thread(target=mutator_thread) for _ in range(4)]
34+
with threading_helper.start_threads(gcs + mutators):
35+
pass
36+
37+
def test_get_referrers(self):
38+
event = threading.Event()
39+
40+
obj = MyObj()
41+
42+
def gc_thread():
43+
for i in range(100):
44+
o = gc.get_referrers(obj)
45+
event.set()
46+
47+
def mutator_thread():
48+
while not event.is_set():
49+
d1 = { "key": obj }
50+
d2 = { "key": obj }
51+
d3 = { "key": obj }
52+
d4 = { "key": obj }
53+
54+
gcs = [Thread(target=gc_thread) for _ in range(2)]
55+
mutators = [Thread(target=mutator_thread) for _ in range(4)]
56+
with threading_helper.start_threads(gcs + mutators):
57+
pass
58+
59+
60+
if __name__ == "__main__":
61+
unittest.main()

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: 64 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1401,10 +1401,32 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason)
14011401
return n + m;
14021402
}
14031403

1404+
static PyObject *
1405+
list_from_object_stack(_PyObjectStack *stack)
1406+
{
1407+
PyObject *list = PyList_New(_PyObjectStack_Size(stack));
1408+
if (list == NULL) {
1409+
PyObject *op;
1410+
while ((op = _PyObjectStack_Pop(stack)) != NULL) {
1411+
Py_DECREF(op);
1412+
}
1413+
return NULL;
1414+
}
1415+
1416+
PyObject *op;
1417+
Py_ssize_t idx = 0;
1418+
while ((op = _PyObjectStack_Pop(stack)) != NULL) {
1419+
assert(idx < PyList_GET_SIZE(list));
1420+
PyList_SET_ITEM(list, idx++, op);
1421+
}
1422+
assert(idx == PyList_GET_SIZE(list));
1423+
return list;
1424+
}
1425+
14041426
struct get_referrers_args {
14051427
struct visitor_args base;
14061428
PyObject *objs;
1407-
struct worklist results;
1429+
_PyObjectStack results;
14081430
};
14091431

14101432
static int
@@ -1428,11 +1450,21 @@ visit_get_referrers(const mi_heap_t *heap, const mi_heap_area_t *area,
14281450
if (op == NULL) {
14291451
return true;
14301452
}
1453+
if (op->ob_gc_bits & (_PyGC_BITS_UNREACHABLE | _PyGC_BITS_FROZEN)) {
1454+
// Exclude unreachable objects (in-progress GC) and frozen
1455+
// objects from gc.get_objects() to match the default build.
1456+
return true;
1457+
}
14311458

14321459
struct get_referrers_args *arg = (struct get_referrers_args *)args;
1460+
if (op == arg->objs) {
1461+
// Don't include the tuple itself in the referrers list.
1462+
return true;
1463+
}
14331464
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);
1465+
if (_PyObjectStack_Push(&arg->results, Py_NewRef(op)) < 0) {
1466+
return false;
1467+
}
14361468
}
14371469

14381470
return true;
@@ -1441,48 +1473,25 @@ visit_get_referrers(const mi_heap_t *heap, const mi_heap_area_t *area,
14411473
PyObject *
14421474
_PyGC_GetReferrers(PyInterpreterState *interp, PyObject *objs)
14431475
{
1444-
PyObject *result = PyList_New(0);
1445-
if (!result) {
1446-
return NULL;
1447-
}
1448-
1449-
_PyEval_StopTheWorld(interp);
1450-
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.
1476+
// NOTE: We can't append to the PyListObject during gc_visit_heaps()
1477+
// because PyList_Append() may reclaim an abandoned mimalloc segments
1478+
// while we are traversing them.
14551479
struct get_referrers_args args = { .objs = objs };
1456-
gc_visit_heaps(interp, &visit_get_referrers, &args.base);
1457-
1458-
bool error = false;
1459-
PyObject *op;
1460-
while ((op = worklist_pop(&args.results)) != NULL) {
1461-
gc_restore_tid(op);
1462-
if (op != objs && PyList_Append(result, op) < 0) {
1463-
error = true;
1464-
break;
1465-
}
1466-
}
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-
1480+
_PyEval_StopTheWorld(interp);
1481+
int err = gc_visit_heaps(interp, &visit_get_referrers, &args.base);
14731482
_PyEval_StartTheWorld(interp);
14741483

1475-
if (error) {
1476-
Py_DECREF(result);
1477-
return NULL;
1484+
PyObject *list = list_from_object_stack(&args.results);
1485+
if (err < 0) {
1486+
PyErr_NoMemory();
1487+
Py_CLEAR(list);
14781488
}
1479-
1480-
return result;
1489+
return list;
14811490
}
14821491

14831492
struct get_objects_args {
14841493
struct visitor_args base;
1485-
struct worklist objects;
1494+
_PyObjectStack objects;
14861495
};
14871496

14881497
static bool
@@ -1493,54 +1502,36 @@ visit_get_objects(const mi_heap_t *heap, const mi_heap_area_t *area,
14931502
if (op == NULL) {
14941503
return true;
14951504
}
1505+
if (op->ob_gc_bits & (_PyGC_BITS_UNREACHABLE | _PyGC_BITS_FROZEN)) {
1506+
// Exclude unreachable objects (in-progress GC) and frozen
1507+
// objects from gc.get_objects() to match the default build.
1508+
return true;
1509+
}
14961510

14971511
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-
1512+
if (_PyObjectStack_Push(&arg->objects, Py_NewRef(op)) < 0) {
1513+
return false;
1514+
}
15011515
return true;
15021516
}
15031517

15041518
PyObject *
15051519
_PyGC_GetObjects(PyInterpreterState *interp, int generation)
15061520
{
1507-
PyObject *result = PyList_New(0);
1508-
if (!result) {
1509-
return NULL;
1510-
}
1511-
1512-
_PyEval_StopTheWorld(interp);
1513-
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.
1521+
// NOTE: We can't append to the PyListObject during gc_visit_heaps()
1522+
// because PyList_Append() may reclaim an abandoned mimalloc segments
1523+
// while we are traversing them.
15181524
struct get_objects_args args = { 0 };
1519-
gc_visit_heaps(interp, &visit_get_objects, &args.base);
1520-
1521-
bool error = false;
1522-
PyObject *op;
1523-
while ((op = worklist_pop(&args.objects)) != NULL) {
1524-
gc_restore_tid(op);
1525-
if (op != result && PyList_Append(result, op) < 0) {
1526-
error = true;
1527-
break;
1528-
}
1529-
}
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-
1525+
_PyEval_StopTheWorld(interp);
1526+
int err = gc_visit_heaps(interp, &visit_get_objects, &args.base);
15361527
_PyEval_StartTheWorld(interp);
15371528

1538-
if (error) {
1539-
Py_DECREF(result);
1540-
return NULL;
1529+
PyObject *list = list_from_object_stack(&args.objects);
1530+
if (err < 0) {
1531+
PyErr_NoMemory();
1532+
Py_CLEAR(list);
15411533
}
1542-
1543-
return result;
1534+
return list;
15441535
}
15451536

15461537
static bool

0 commit comments

Comments
 (0)