Skip to content

Commit 5c2696b

Browse files
[3.13] gh-125859: Fix crash when gc.get_objects is called during GC (GH-125882) (GH-125921)
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. (cherry picked from commit e545ead) Co-authored-by: Sam Gross <[email protected]>
1 parent e334022 commit 5c2696b

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
@@ -73,6 +73,16 @@ _PyObjectStack_Pop(_PyObjectStack *stack)
7373
return obj;
7474
}
7575

76+
static inline Py_ssize_t
77+
_PyObjectStack_Size(_PyObjectStack *stack)
78+
{
79+
Py_ssize_t size = 0;
80+
for (_PyObjectStackChunk *buf = stack->head; buf != NULL; buf = buf->prev) {
81+
size += buf->n;
82+
}
83+
return size;
84+
}
85+
7686
// Merge src into dst, leaving src empty
7787
extern void
7888
_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
@@ -1105,6 +1105,29 @@ def test_get_referents_on_capsule(self):
11051105
self.assertEqual(len(gc.get_referents(untracked_capsule)), 0)
11061106
gc.get_referents(tracked_capsule)
11071107

1108+
@cpython_only
1109+
def test_get_objects_during_gc(self):
1110+
# gh-125859: Calling gc.get_objects() or gc.get_referrers() during a
1111+
# collection should not crash.
1112+
test = self
1113+
collected = False
1114+
1115+
class GetObjectsOnDel:
1116+
def __del__(self):
1117+
nonlocal collected
1118+
collected = True
1119+
objs = gc.get_objects()
1120+
# NB: can't use "in" here because some objects override __eq__
1121+
for obj in objs:
1122+
test.assertTrue(obj is not self)
1123+
test.assertEqual(gc.get_referrers(self), [])
1124+
1125+
obj = GetObjectsOnDel()
1126+
obj.cycle = obj
1127+
del obj
1128+
1129+
gc.collect()
1130+
self.assertTrue(collected)
11081131

11091132
class GCCallbackTests(unittest.TestCase):
11101133
def setUp(self):
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
@@ -1281,10 +1281,32 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason)
12811281
return n + m;
12821282
}
12831283

1284+
static PyObject *
1285+
list_from_object_stack(_PyObjectStack *stack)
1286+
{
1287+
PyObject *list = PyList_New(_PyObjectStack_Size(stack));
1288+
if (list == NULL) {
1289+
PyObject *op;
1290+
while ((op = _PyObjectStack_Pop(stack)) != NULL) {
1291+
Py_DECREF(op);
1292+
}
1293+
return NULL;
1294+
}
1295+
1296+
PyObject *op;
1297+
Py_ssize_t idx = 0;
1298+
while ((op = _PyObjectStack_Pop(stack)) != NULL) {
1299+
assert(idx < PyList_GET_SIZE(list));
1300+
PyList_SET_ITEM(list, idx++, op);
1301+
}
1302+
assert(idx == PyList_GET_SIZE(list));
1303+
return list;
1304+
}
1305+
12841306
struct get_referrers_args {
12851307
struct visitor_args base;
12861308
PyObject *objs;
1287-
struct worklist results;
1309+
_PyObjectStack results;
12881310
};
12891311

12901312
static int
@@ -1308,11 +1330,21 @@ visit_get_referrers(const mi_heap_t *heap, const mi_heap_area_t *area,
13081330
if (op == NULL) {
13091331
return true;
13101332
}
1333+
if (op->ob_gc_bits & (_PyGC_BITS_UNREACHABLE | _PyGC_BITS_FROZEN)) {
1334+
// Exclude unreachable objects (in-progress GC) and frozen
1335+
// objects from gc.get_objects() to match the default build.
1336+
return true;
1337+
}
13111338

13121339
struct get_referrers_args *arg = (struct get_referrers_args *)args;
1340+
if (op == arg->objs) {
1341+
// Don't include the tuple itself in the referrers list.
1342+
return true;
1343+
}
13131344
if (Py_TYPE(op)->tp_traverse(op, referrersvisit, arg->objs)) {
1314-
op->ob_tid = 0; // we will restore the refcount later
1315-
worklist_push(&arg->results, op);
1345+
if (_PyObjectStack_Push(&arg->results, Py_NewRef(op)) < 0) {
1346+
return false;
1347+
}
13161348
}
13171349

13181350
return true;
@@ -1321,48 +1353,25 @@ visit_get_referrers(const mi_heap_t *heap, const mi_heap_area_t *area,
13211353
PyObject *
13221354
_PyGC_GetReferrers(PyInterpreterState *interp, PyObject *objs)
13231355
{
1324-
PyObject *result = PyList_New(0);
1325-
if (!result) {
1326-
return NULL;
1327-
}
1328-
1329-
_PyEval_StopTheWorld(interp);
1330-
1331-
// Append all objects to a worklist. This abuses ob_tid. We will restore
1332-
// it later. NOTE: We can't append to the PyListObject during
1333-
// gc_visit_heaps() because PyList_Append() may reclaim an abandoned
1334-
// mimalloc segments while we are traversing them.
1356+
// NOTE: We can't append to the PyListObject during gc_visit_heaps()
1357+
// because PyList_Append() may reclaim an abandoned mimalloc segments
1358+
// while we are traversing them.
13351359
struct get_referrers_args args = { .objs = objs };
1336-
gc_visit_heaps(interp, &visit_get_referrers, &args.base);
1337-
1338-
bool error = false;
1339-
PyObject *op;
1340-
while ((op = worklist_pop(&args.results)) != NULL) {
1341-
gc_restore_tid(op);
1342-
if (op != objs && PyList_Append(result, op) < 0) {
1343-
error = true;
1344-
break;
1345-
}
1346-
}
1347-
1348-
// In case of error, clear the remaining worklist
1349-
while ((op = worklist_pop(&args.results)) != NULL) {
1350-
gc_restore_tid(op);
1351-
}
1352-
1360+
_PyEval_StopTheWorld(interp);
1361+
int err = gc_visit_heaps(interp, &visit_get_referrers, &args.base);
13531362
_PyEval_StartTheWorld(interp);
13541363

1355-
if (error) {
1356-
Py_DECREF(result);
1357-
return NULL;
1364+
PyObject *list = list_from_object_stack(&args.results);
1365+
if (err < 0) {
1366+
PyErr_NoMemory();
1367+
Py_CLEAR(list);
13581368
}
1359-
1360-
return result;
1369+
return list;
13611370
}
13621371

13631372
struct get_objects_args {
13641373
struct visitor_args base;
1365-
struct worklist objects;
1374+
_PyObjectStack objects;
13661375
};
13671376

13681377
static bool
@@ -1373,54 +1382,36 @@ visit_get_objects(const mi_heap_t *heap, const mi_heap_area_t *area,
13731382
if (op == NULL) {
13741383
return true;
13751384
}
1385+
if (op->ob_gc_bits & (_PyGC_BITS_UNREACHABLE | _PyGC_BITS_FROZEN)) {
1386+
// Exclude unreachable objects (in-progress GC) and frozen
1387+
// objects from gc.get_objects() to match the default build.
1388+
return true;
1389+
}
13761390

13771391
struct get_objects_args *arg = (struct get_objects_args *)args;
1378-
op->ob_tid = 0; // we will restore the refcount later
1379-
worklist_push(&arg->objects, op);
1380-
1392+
if (_PyObjectStack_Push(&arg->objects, Py_NewRef(op)) < 0) {
1393+
return false;
1394+
}
13811395
return true;
13821396
}
13831397

13841398
PyObject *
13851399
_PyGC_GetObjects(PyInterpreterState *interp, int generation)
13861400
{
1387-
PyObject *result = PyList_New(0);
1388-
if (!result) {
1389-
return NULL;
1390-
}
1391-
1392-
_PyEval_StopTheWorld(interp);
1393-
1394-
// Append all objects to a worklist. This abuses ob_tid. We will restore
1395-
// it later. NOTE: We can't append to the list during gc_visit_heaps()
1396-
// because PyList_Append() may reclaim an abandoned mimalloc segment
1397-
// while we are traversing it.
1401+
// NOTE: We can't append to the PyListObject during gc_visit_heaps()
1402+
// because PyList_Append() may reclaim an abandoned mimalloc segments
1403+
// while we are traversing them.
13981404
struct get_objects_args args = { 0 };
1399-
gc_visit_heaps(interp, &visit_get_objects, &args.base);
1400-
1401-
bool error = false;
1402-
PyObject *op;
1403-
while ((op = worklist_pop(&args.objects)) != NULL) {
1404-
gc_restore_tid(op);
1405-
if (op != result && PyList_Append(result, op) < 0) {
1406-
error = true;
1407-
break;
1408-
}
1409-
}
1410-
1411-
// In case of error, clear the remaining worklist
1412-
while ((op = worklist_pop(&args.objects)) != NULL) {
1413-
gc_restore_tid(op);
1414-
}
1415-
1405+
_PyEval_StopTheWorld(interp);
1406+
int err = gc_visit_heaps(interp, &visit_get_objects, &args.base);
14161407
_PyEval_StartTheWorld(interp);
14171408

1418-
if (error) {
1419-
Py_DECREF(result);
1420-
return NULL;
1409+
PyObject *list = list_from_object_stack(&args.objects);
1410+
if (err < 0) {
1411+
PyErr_NoMemory();
1412+
Py_CLEAR(list);
14211413
}
1422-
1423-
return result;
1414+
return list;
14241415
}
14251416

14261417
static bool

0 commit comments

Comments
 (0)