]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-122697: Fix free-threading memory leaks at shutdown (#122703)
authorSam Gross <colesbury@gmail.com>
Thu, 8 Aug 2024 16:48:17 +0000 (12:48 -0400)
committerGitHub <noreply@github.com>
Thu, 8 Aug 2024 16:48:17 +0000 (12:48 -0400)
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.

 * `_PyType_FinalizeIdPool` wasn't called at interpreter shutdown.

Misc/NEWS.d/next/Core_and_Builtins/2024-08-05-19-28-12.gh-issue-122697.17MvYl.rst [new file with mode: 0644]
Objects/obmalloc.c
Python/gc_free_threading.c
Python/pylifecycle.c
Python/pystate.c

diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2024-08-05-19-28-12.gh-issue-122697.17MvYl.rst b/Misc/NEWS.d/next/Core_and_Builtins/2024-08-05-19-28-12.gh-issue-122697.17MvYl.rst
new file mode 100644 (file)
index 0000000..34ee6a9
--- /dev/null
@@ -0,0 +1,2 @@
+Fixed memory leaks at interpreter shutdown in the free-threaded build, and
+also reporting of leaked memory blocks via :option:`-X showrefcount <-X>`.
index a6a71802ef8e0193505ba807d4986600fffe7f7a..dfeccfa4dd76586a0be4f14fd1d25695580dc515 100644 (file)
@@ -1109,9 +1109,12 @@ free_delayed(uintptr_t ptr)
 #ifndef Py_GIL_DISABLED
     free_work_item(ptr);
 #else
-    if (_PyRuntime.stoptheworld.world_stopped) {
-        // Free immediately if the world is stopped, including during
-        // interpreter shutdown.
+    PyInterpreterState *interp = _PyInterpreterState_GET();
+    if (_PyInterpreterState_GetFinalizing(interp) != NULL ||
+        interp->stoptheworld.world_stopped)
+    {
+        // Free immediately during interpreter shutdown or if the world is
+        // stopped.
         free_work_item(ptr);
         return;
     }
@@ -1474,6 +1477,8 @@ _PyInterpreterState_FinalizeAllocatedBlocks(PyInterpreterState *interp)
 {
 #ifdef WITH_MIMALLOC
     if (_PyMem_MimallocEnabled()) {
+        Py_ssize_t leaked = _PyInterpreterState_GetAllocatedBlocks(interp);
+        interp->runtime->obmalloc.interpreter_leaks += leaked;
         return;
     }
 #endif
index 1e02db00649c750c1a7f9f00c4c70d611f81a50a..543bee24652dc99533df5c7700d4e16fd60005e8 100644 (file)
@@ -55,6 +55,7 @@ struct collection_state {
     struct visitor_args base;
     PyInterpreterState *interp;
     GCState *gcstate;
+    _PyGC_Reason reason;
     Py_ssize_t collected;
     Py_ssize_t uncollectable;
     Py_ssize_t long_lived_total;
@@ -572,6 +573,16 @@ scan_heap_visitor(const mi_heap_t *heap, const mi_heap_area_t *area,
             worklist_push(&state->unreachable, op);
         }
     }
+    else if (state->reason == _Py_GC_REASON_SHUTDOWN &&
+             _PyObject_HasDeferredRefcount(op))
+    {
+        // Disable deferred refcounting for reachable objects as well during
+        // interpreter shutdown. This ensures that these objects are collected
+        // immediately when their last reference is removed.
+        disable_deferred_refcounting(op);
+        merge_refcount(op, 0);
+        state->long_lived_total++;
+    }
     else {
         // object is reachable, restore `ob_tid`; we're done with these objects
         gc_restore_tid(op);
@@ -1228,6 +1239,7 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason)
     struct collection_state state = {
         .interp = interp,
         .gcstate = gcstate,
+        .reason = reason,
     };
 
     gc_collect_internal(interp, &state, generation);
index 6b641c0775f53318886aced9a222b1833acd08d2..3a41c640fd95991404a47b4736772ca5c8205932 100644 (file)
@@ -28,6 +28,7 @@
 #include "pycore_sliceobject.h"   // _PySlice_Fini()
 #include "pycore_sysmodule.h"     // _PySys_ClearAuditHooks()
 #include "pycore_traceback.h"     // _Py_DumpTracebackThreads()
+#include "pycore_typeid.h"        // _PyType_FinalizeIdPool()
 #include "pycore_typeobject.h"    // _PyTypes_InitTypes()
 #include "pycore_typevarobject.h" // _Py_clear_generic_types()
 #include "pycore_unicodeobject.h" // _PyUnicode_InitTypes()
@@ -1832,6 +1833,9 @@ finalize_interp_types(PyInterpreterState *interp)
     _PyTypes_FiniTypes(interp);
 
     _PyTypes_Fini(interp);
+#ifdef Py_GIL_DISABLED
+    _PyType_FinalizeIdPool(interp);
+#endif
 
     _PyCode_Fini(interp);
 
index 8f4818cee00d9dd97a571f701eaddde6d9b74478..bba88b76088e71f72c6e7d046aa034016143736e 100644 (file)
@@ -20,7 +20,7 @@
 #include "pycore_runtime_init.h"  // _PyRuntimeState_INIT
 #include "pycore_sysmodule.h"     // _PySys_Audit()
 #include "pycore_obmalloc.h"      // _PyMem_obmalloc_state_on_heap()
-#include "pycore_typeid.h"        // _PyType_FinalizeIdPool
+#include "pycore_typeid.h"        // _PyType_FinalizeThreadLocalRefcounts()
 
 /* --------------------------------------------------------------------------
 CAUTION