Skip to content

Commit 6cb791a

Browse files
committed
pythongh-130519: Fix crash in QSBR when destructor reenter QSBR
The `free_work_item()` function in QSBR may call arbitrary code via Python object destructors, which may reenter the QSBR code. Reorder the processing of work items to be robust to reentrancy. Also fix the TODO for the out of memory situation.
1 parent 5c43730 commit 6cb791a

File tree

4 files changed

+53
-10
lines changed

4 files changed

+53
-10
lines changed

Include/internal/pycore_pymem.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ extern void _PyMem_FreeDelayed(void *ptr);
121121

122122
// Enqueue an object to be freed possibly after some delay
123123
#ifdef Py_GIL_DISABLED
124-
extern void _PyObject_XDecRefDelayed(PyObject *obj);
124+
PyAPI_FUNC(void) _PyObject_XDecRefDelayed(PyObject *obj);
125125
#else
126126
static inline void _PyObject_XDecRefDelayed(PyObject *obj)
127127
{

Lib/test/test_capi/test_object.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,5 +210,18 @@ def test_decref_freed_object(self):
210210
"""
211211
self.check_negative_refcount(code)
212212

213+
def test_decref_delayed(self):
214+
# gh-130519: Test that _PyObject_XDecRefDelayed() and QSBR code path
215+
# handles destructors that are possibly re-entrant or trigger a GC.
216+
import gc
217+
218+
class MyObj:
219+
def __del__(self):
220+
gc.collect()
221+
222+
for _ in range(1000):
223+
obj = MyObj()
224+
_testinternalcapi.incref_decref_delayed(obj)
225+
213226
if __name__ == "__main__":
214227
unittest.main()

Modules/_testinternalcapi.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1995,6 +1995,13 @@ is_static_immortal(PyObject *self, PyObject *op)
19951995
Py_RETURN_FALSE;
19961996
}
19971997

1998+
static PyObject *
1999+
incref_decref_delayed(PyObject *self, PyObject *op)
2000+
{
2001+
_PyObject_XDecRefDelayed(Py_NewRef(op));
2002+
Py_RETURN_NONE;
2003+
}
2004+
19982005
static PyMethodDef module_functions[] = {
19992006
{"get_configs", get_configs, METH_NOARGS},
20002007
{"get_recursion_depth", get_recursion_depth, METH_NOARGS},
@@ -2089,6 +2096,7 @@ static PyMethodDef module_functions[] = {
20892096
{"has_deferred_refcount", has_deferred_refcount, METH_O},
20902097
{"get_tracked_heap_size", get_tracked_heap_size, METH_NOARGS},
20912098
{"is_static_immortal", is_static_immortal, METH_O},
2099+
{"incref_decref_delayed", incref_decref_delayed, METH_O},
20922100
{NULL, NULL} /* sentinel */
20932101
};
20942102

Objects/obmalloc.c

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1143,18 +1143,24 @@ struct _mem_work_chunk {
11431143
struct _mem_work_item array[WORK_ITEMS_PER_CHUNK];
11441144
};
11451145

1146+
static int
1147+
work_item_should_decref(uintptr_t ptr)
1148+
{
1149+
return ptr & 0x01;
1150+
}
1151+
11461152
static void
11471153
free_work_item(uintptr_t ptr, delayed_dealloc_cb cb, void *state)
11481154
{
1149-
if (ptr & 0x01) {
1155+
if (work_item_should_decref(ptr)) {
11501156
PyObject *obj = (PyObject *)(ptr - 1);
11511157
#ifdef Py_GIL_DISABLED
11521158
if (cb == NULL) {
11531159
assert(!_PyInterpreterState_GET()->stoptheworld.world_stopped);
11541160
Py_DECREF(obj);
11551161
return;
11561162
}
1157-
1163+
assert(_PyInterpreterState_GET()->stoptheworld.world_stopped);
11581164
Py_ssize_t refcount = _Py_ExplicitMergeRefcount(obj, -1);
11591165
if (refcount == 0) {
11601166
cb(obj, state);
@@ -1180,7 +1186,7 @@ free_delayed(uintptr_t ptr)
11801186
{
11811187
// Free immediately during interpreter shutdown or if the world is
11821188
// stopped.
1183-
assert(!interp->stoptheworld.world_stopped || !(ptr & 0x01));
1189+
assert(!interp->stoptheworld.world_stopped || !work_item_should_decref(ptr));
11841190
free_work_item(ptr, NULL, NULL);
11851191
return;
11861192
}
@@ -1207,10 +1213,22 @@ free_delayed(uintptr_t ptr)
12071213

12081214
if (buf == NULL) {
12091215
// failed to allocate a buffer, free immediately
1216+
PyObject *to_dealloc = NULL;
12101217
_PyEval_StopTheWorld(tstate->base.interp);
1211-
// TODO: Fix me
1212-
free_work_item(ptr, NULL, NULL);
1218+
if (work_item_should_decref(ptr)) {
1219+
PyObject *obj = (PyObject *)(ptr - 1);
1220+
Py_ssize_t refcount = _Py_ExplicitMergeRefcount(obj, -1);
1221+
if (refcount == 0) {
1222+
to_dealloc = obj;
1223+
}
1224+
}
1225+
else {
1226+
PyMem_Free((void *)ptr);
1227+
}
12131228
_PyEval_StartTheWorld(tstate->base.interp);
1229+
if (to_dealloc != NULL) {
1230+
_Py_Dealloc(to_dealloc);
1231+
}
12141232
return;
12151233
}
12161234

@@ -1257,14 +1275,16 @@ process_queue(struct llist_node *head, struct _qsbr_thread_state *qsbr,
12571275
while (!llist_empty(head)) {
12581276
struct _mem_work_chunk *buf = work_queue_first(head);
12591277

1260-
while (buf->rd_idx < buf->wr_idx) {
1278+
if (buf->rd_idx < buf->wr_idx) {
12611279
struct _mem_work_item *item = &buf->array[buf->rd_idx];
12621280
if (!_Py_qsbr_poll(qsbr, item->qsbr_goal)) {
12631281
return;
12641282
}
12651283

1266-
free_work_item(item->ptr, cb, state);
12671284
buf->rd_idx++;
1285+
// NB: free_work_item may re-enter or execute arbitrary code
1286+
free_work_item(item->ptr, cb, state);
1287+
continue;
12681288
}
12691289

12701290
assert(buf->rd_idx == buf->wr_idx);
@@ -1360,12 +1380,14 @@ _PyMem_FiniDelayed(PyInterpreterState *interp)
13601380
while (!llist_empty(head)) {
13611381
struct _mem_work_chunk *buf = work_queue_first(head);
13621382

1363-
while (buf->rd_idx < buf->wr_idx) {
1383+
if (buf->rd_idx < buf->wr_idx) {
13641384
// Free the remaining items immediately. There should be no other
13651385
// threads accessing the memory at this point during shutdown.
13661386
struct _mem_work_item *item = &buf->array[buf->rd_idx];
1367-
free_work_item(item->ptr, NULL, NULL);
13681387
buf->rd_idx++;
1388+
// NB: free_work_item may re-enter or execute arbitrary code
1389+
free_work_item(item->ptr, NULL, NULL);
1390+
continue;
13691391
}
13701392

13711393
llist_remove(&buf->node);

0 commit comments

Comments
 (0)