https://github.com/python/cpython/commit/e3262d9c3b9492d22b4f7dc4df903299846bf320
commit: e3262d9c3b9492d22b4f7dc4df903299846bf320
branch: main
author: Pablo Galindo Salgado <[email protected]>
committer: pablogsal <[email protected]>
date: 2026-11-09T17:50:17+01:00
summary:

gh-160267: Keep thread and interpreter state memory alive for remote profilers 
(#161674)

files:
A 
Misc/NEWS.d/next/Core_and_Builtins/2026-11-09-14-30-00.gh-issue-160267.sTfrEe.rst
M Python/pystate.c
M Tools/c-analyzer/cpython/ignored.tsv

diff --git 
a/Misc/NEWS.d/next/Core_and_Builtins/2026-11-09-14-30-00.gh-issue-160267.sTfrEe.rst
 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-11-09-14-30-00.gh-issue-160267.sTfrEe.rst
new file mode 100644
index 000000000000000..c4467c7b64cddf1
--- /dev/null
+++ 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-11-09-14-30-00.gh-issue-160267.sTfrEe.rst
@@ -0,0 +1,4 @@
+Fix a crash when :mod:`profiling.sampling` profiles a process that creates and
+destroys threads or subinterpreters quickly. Memory of deleted thread and
+interpreter states is now reused for new states instead of being freed until
+the runtime is finalized.
diff --git a/Python/pystate.c b/Python/pystate.c
index 81360f4999849d7..ce7197c4e054283 100644
--- a/Python/pystate.c
+++ b/Python/pystate.c
@@ -309,9 +309,25 @@ _Py_COMP_DIAG_IGNORE_DEPR_DECLS
 static const _PyRuntimeState initial = _PyRuntimeState_INIT(_PyRuntime, "");
 _Py_COMP_DIAG_POP
 
+/* Remote profilers access interpreter and thread states without synchronizing
+   with their lifetime.  Until the runtime is finalized, keep deleted states in
+   per-type free lists, so that a stale access only reaches a state of the same
+   type. */
+typedef struct state_header {
+    struct state_header *next;  // Next entry in the free list
+    void *mem;                  // Block to free at finalization
+} state_header;
+
+static struct {
+    PyMutex mutex;
+    state_header *tstates;
+    state_header *interpreters;
+} state_freelists;
+
 #define LOCKS_INIT(runtime) \
     { \
         &(runtime)->interpreters.mutex, \
+        &state_freelists.mutex, \
         &(runtime)->xi.data_lookup.registry.mutex, \
         &(runtime)->unicode_state.ids.mutex, \
         &(runtime)->imports.extensions.mutex, \
@@ -390,6 +406,19 @@ _PyRuntimeState_Fini(_PyRuntimeState *runtime)
     assert(runtime->object_state.interpreter_leaks == 0);
 #endif
     gilstate_clear();
+
+    state_header **lists[] = {&state_freelists.tstates, 
&state_freelists.interpreters};
+    for (size_t i = 0; i < Py_ARRAY_LENGTH(lists); i++) {
+        PyMutex_LockFlags(&state_freelists.mutex, _Py_LOCK_DONT_DETACH);
+        state_header *entry = *lists[i];
+        *lists[i] = NULL;
+        PyMutex_Unlock(&state_freelists.mutex);
+        while (entry != NULL) {
+            state_header *next = entry->next;
+            PyMem_RawFree(entry->mem);
+            entry = next;
+        }
+    }
 }
 
 #ifdef HAVE_FORK
@@ -445,24 +474,50 @@ _PyInterpreterState_Enable(_PyRuntimeState *runtime)
     return _PyStatus_OK();
 }
 
-static PyInterpreterState *
-alloc_interpreter(void)
+static void *
+alloc_state(state_header **freelist, size_t size, size_t alignment)
 {
-    // Aligned allocation for PyInterpreterState.
-    // the first word of the memory block is used to store
-    // the original pointer to be used later to free the memory.
-    size_t alignment = _Alignof(PyInterpreterState);
-    size_t allocsize = sizeof(PyInterpreterState) + sizeof(void *) + alignment 
- 1;
+    PyMutex_LockFlags(&state_freelists.mutex, _Py_LOCK_DONT_DETACH);
+    state_header *entry = *freelist;
+    if (entry != NULL) {
+        *freelist = entry->next;
+    }
+    PyMutex_Unlock(&state_freelists.mutex);
+    if (entry != NULL) {
+        return entry + 1;
+    }
+
+    size_t allocsize = size + sizeof(state_header) + alignment - 1;
     void *mem = PyMem_RawCalloc(1, allocsize);
     if (mem == NULL) {
         return NULL;
     }
-    void *ptr = _Py_ALIGN_UP((char *)mem + sizeof(void *), alignment);
-    ((void **)ptr)[-1] = mem;
+    void *ptr = _Py_ALIGN_UP((char *)mem + sizeof(state_header), alignment);
+    ((state_header *)ptr - 1)->mem = mem;
     assert(_Py_IS_ALIGNED(ptr, alignment));
     return ptr;
 }
 
+static void
+recycle_state(state_header **freelist, void *state, size_t size)
+{
+    memset(state, 0, size);
+    state_header *entry = (state_header *)state - 1;
+
+    PyMutex_LockFlags(&state_freelists.mutex, _Py_LOCK_DONT_DETACH);
+    entry->next = *freelist;
+    // Keep the list consistent for a child forked by another thread.
+    _Py_atomic_store_ptr_release(freelist, entry);
+    PyMutex_Unlock(&state_freelists.mutex);
+}
+
+static PyInterpreterState *
+alloc_interpreter(void)
+{
+    return alloc_state(&state_freelists.interpreters, 
sizeof(PyInterpreterState),
+                       _Alignof(PyInterpreterState));
+}
+
 static void
 free_interpreter(PyInterpreterState *interp)
 {
@@ -491,7 +546,13 @@ free_interpreter(PyInterpreterState *interp)
             interp->obmalloc = NULL;
         }
         assert(_Py_IS_ALIGNED(interp, _Alignof(PyInterpreterState)));
-        PyMem_RawFree(((void **)interp)[-1]);
+        // A daemon thread may still own the embedded initial thread state.
+        if (interp->_initialized
+            && _Py_atomic_load_ptr(&interp->threads.preallocated) == NULL)
+        {
+            return;
+        }
+        recycle_state(&state_freelists.interpreters, interp, sizeof(*interp));
     }
 }
 
@@ -1517,7 +1578,8 @@ alloc_threadstate(PyInterpreterState *interp)
 
     // Fall back to the allocator.
     if (tstate == NULL) {
-        tstate = PyMem_RawCalloc(1, sizeof(_PyThreadStateImpl));
+        tstate = alloc_state(&state_freelists.tstates, sizeof(*tstate),
+                             _Alignof(_PyThreadStateImpl));
         if (tstate == NULL) {
             return NULL;
         }
@@ -1544,7 +1606,7 @@ free_threadstate(_PyThreadStateImpl *tstate)
         _Py_atomic_store_ptr(&interp->threads.preallocated, tstate);
     }
     else {
-        PyMem_RawFree(tstate);
+        recycle_state(&state_freelists.tstates, tstate, sizeof(*tstate));
     }
 }
 
diff --git a/Tools/c-analyzer/cpython/ignored.tsv 
b/Tools/c-analyzer/cpython/ignored.tsv
index a51ac686268f411..3863872fd542624 100644
--- a/Tools/c-analyzer/cpython/ignored.tsv
+++ b/Tools/c-analyzer/cpython/ignored.tsv
@@ -56,6 +56,7 @@ Python/pyhash.c       -       _Py_HashSecret  -
 
 ## thread-safe hashtable (internal locks)
 Python/parking_lot.c   -       buckets -
+Python/pystate.c       -       state_freelists -
 
 ## shared empty sentinel for the per-type method cache
 Python/typecache.c     -       empty_cache     -

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to