Skip to content

Commit c9d7132

Browse files
committed
gh-122697: Fix free-threading memory leaks at shutdown
We were not properly accounting for interpreter memory leaks at shutdown and had two sources of leaks: * Objects that use deferred reference counting and were reachable via static types outlive the final GC. We now disable deferred reference counting on all objects if we are calling the GC due to interpreter shutdown. * `_PyMem_FreeDelayed` did not properly check for interpreter shutdown so we had some memory blocks that were enqueued to be freed, but never actually freed.
1 parent 5bd7291 commit c9d7132

File tree

2 files changed

+17
-3
lines changed

2 files changed

+17
-3
lines changed

Objects/obmalloc.c

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1109,9 +1109,9 @@ free_delayed(uintptr_t ptr)
11091109
#ifndef Py_GIL_DISABLED
11101110
free_work_item(ptr);
11111111
#else
1112-
if (_PyRuntime.stoptheworld.world_stopped) {
1113-
// Free immediately if the world is stopped, including during
1114-
// interpreter shutdown.
1112+
if (_PyRuntime.stoptheworld.world_stopped || Py_IsFinalizing()) {
1113+
// Free immediately if the world is stopped and during interpreter
1114+
// shutdown.
11151115
free_work_item(ptr);
11161116
return;
11171117
}
@@ -1474,6 +1474,8 @@ _PyInterpreterState_FinalizeAllocatedBlocks(PyInterpreterState *interp)
14741474
{
14751475
#ifdef WITH_MIMALLOC
14761476
if (_PyMem_MimallocEnabled()) {
1477+
Py_ssize_t leaked = _PyInterpreterState_GetAllocatedBlocks(interp);
1478+
interp->runtime->obmalloc.interpreter_leaks += leaked;
14771479
return;
14781480
}
14791481
#endif

Python/gc_free_threading.c

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ struct collection_state {
5454
struct visitor_args base;
5555
PyInterpreterState *interp;
5656
GCState *gcstate;
57+
_PyGC_Reason reason;
5758
Py_ssize_t collected;
5859
Py_ssize_t uncollectable;
5960
Py_ssize_t long_lived_total;
@@ -571,6 +572,16 @@ scan_heap_visitor(const mi_heap_t *heap, const mi_heap_area_t *area,
571572
worklist_push(&state->unreachable, op);
572573
}
573574
}
575+
else if (state->reason == _Py_GC_REASON_SHUTDOWN &&
576+
_PyObject_HasDeferredRefcount(op))
577+
{
578+
// Disable deferred refcounting for reachable objects as well during
579+
// interpreter shutdown. This ensures that these objects are collected
580+
// immediately when their last reference is removed.
581+
disable_deferred_refcounting(op);
582+
merge_refcount(op, 0);
583+
state->long_lived_total++;
584+
}
574585
else {
575586
// object is reachable, restore `ob_tid`; we're done with these objects
576587
gc_restore_tid(op);
@@ -1221,6 +1232,7 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason)
12211232
struct collection_state state = {
12221233
.interp = interp,
12231234
.gcstate = gcstate,
1235+
.reason = reason,
12241236
};
12251237

12261238
gc_collect_internal(interp, &state, generation);

0 commit comments

Comments
 (0)