From a25bab87b381e95ebf7df68710a10892746f7d11 Mon Sep 17 00:00:00 2001 From: Shamil Date: Tue, 14 Oct 2025 17:42:17 +0300 Subject: [PATCH 1/5] [3.14] gh-140067: Fix memory leak in sub-interpreter creation (GH-140111) Fix memory leak in sub-interpreter creation caused by overwriting of the previously used `_malloced` field. Now the pointer is stored in the first word of the memory block to avoid it being overwritten accidentally. (cherry picked from commit 59547a251f7069dc6e08cb6082dd21872671e381) Co-authored-by: Shamil Co-authored-by: Kumar Aditya --- Include/internal/pycore_interp_structs.h | 6 ------ ...2025-10-14-17-07-37.gh-issue-140067.ID2gOm.rst | 1 + Python/pystate.c | 15 +++++++++------ 3 files changed, 10 insertions(+), 12 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-10-14-17-07-37.gh-issue-140067.ID2gOm.rst diff --git a/Include/internal/pycore_interp_structs.h b/Include/internal/pycore_interp_structs.h index d1f916fa7f727c..3ce5bd398628a1 100644 --- a/Include/internal/pycore_interp_structs.h +++ b/Include/internal/pycore_interp_structs.h @@ -768,12 +768,6 @@ struct _is { * and should be placed at the beginning. */ struct _ceval_state ceval; - /* This structure is carefully allocated so that it's correctly aligned - * to avoid undefined behaviors during LOAD and STORE. The '_malloced' - * field stores the allocated pointer address that will later be freed. - */ - void *_malloced; - PyInterpreterState *next; int64_t id; diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-10-14-17-07-37.gh-issue-140067.ID2gOm.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-14-17-07-37.gh-issue-140067.ID2gOm.rst new file mode 100644 index 00000000000000..3c5a828101d9a8 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-14-17-07-37.gh-issue-140067.ID2gOm.rst @@ -0,0 +1 @@ +Fix memory leak in sub-interpreter creation. diff --git a/Python/pystate.c b/Python/pystate.c index 6859dbc9a7f469..4005a0ef48baf6 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -565,16 +565,19 @@ _PyInterpreterState_Enable(_PyRuntimeState *runtime) static PyInterpreterState * alloc_interpreter(void) { + // 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) + alignment - 1; + size_t allocsize = sizeof(PyInterpreterState) + sizeof(void *) + alignment - 1; void *mem = PyMem_RawCalloc(1, allocsize); if (mem == NULL) { return NULL; } - PyInterpreterState *interp = _Py_ALIGN_UP(mem, alignment); - assert(_Py_IS_ALIGNED(interp, alignment)); - interp->_malloced = mem; - return interp; + void *ptr = _Py_ALIGN_UP((char *)mem + sizeof(void *), alignment); + ((void **)ptr)[-1] = mem; + assert(_Py_IS_ALIGNED(ptr, alignment)); + return ptr; } static void @@ -589,7 +592,7 @@ free_interpreter(PyInterpreterState *interp) interp->obmalloc = NULL; } assert(_Py_IS_ALIGNED(interp, _Alignof(PyInterpreterState))); - PyMem_RawFree(interp->_malloced); + PyMem_RawFree(((void **)interp)[-1]); } } From 344b4913cdbcd4a47ca64e4a848c327549cbb2ee Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Tue, 14 Oct 2025 20:18:31 +0530 Subject: [PATCH 2/5] fix ABI compatibility --- Include/internal/pycore_interp_structs.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Include/internal/pycore_interp_structs.h b/Include/internal/pycore_interp_structs.h index 3ce5bd398628a1..15885b5f228c8b 100644 --- a/Include/internal/pycore_interp_structs.h +++ b/Include/internal/pycore_interp_structs.h @@ -768,6 +768,9 @@ struct _is { * and should be placed at the beginning. */ struct _ceval_state ceval; + // unused, kept for ABI compatibility + void *_malloced; + PyInterpreterState *next; int64_t id; From c0dee2609e483d9cc24a4ac26d7db8f12c59a940 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Fri, 17 Oct 2025 22:05:03 +0530 Subject: [PATCH 3/5] add suppressions --- Tools/tsan/suppressions.txt | 3 +++ Tools/tsan/suppressions_free_threading.txt | 3 +++ 2 files changed, 6 insertions(+) diff --git a/Tools/tsan/suppressions.txt b/Tools/tsan/suppressions.txt index 6bda5ecd570889..8f8910e23f9ca2 100644 --- a/Tools/tsan/suppressions.txt +++ b/Tools/tsan/suppressions.txt @@ -3,3 +3,6 @@ # https://gist.github.com/mpage/daaf32b39180c1989572957b943eb665 thread:pthread_create + +# gh-140138 +race:drop_gil \ No newline at end of file diff --git a/Tools/tsan/suppressions_free_threading.txt b/Tools/tsan/suppressions_free_threading.txt index c10af91faae17c..be9f7940429c6e 100644 --- a/Tools/tsan/suppressions_free_threading.txt +++ b/Tools/tsan/suppressions_free_threading.txt @@ -52,3 +52,6 @@ race:PyObject_Realloc race_top:update_one_slot race_top:set_tp_bases race_top:type_set_bases_unlocked + +# gh-140138 +race:drop_gil \ No newline at end of file From 415e5e2a62a0bbdf3ba6d1887d671709d97908e5 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Fri, 17 Oct 2025 22:18:28 +0530 Subject: [PATCH 4/5] Revert "add suppressions" This reverts commit c0dee2609e483d9cc24a4ac26d7db8f12c59a940. --- Tools/tsan/suppressions.txt | 3 --- Tools/tsan/suppressions_free_threading.txt | 3 --- 2 files changed, 6 deletions(-) diff --git a/Tools/tsan/suppressions.txt b/Tools/tsan/suppressions.txt index 8f8910e23f9ca2..6bda5ecd570889 100644 --- a/Tools/tsan/suppressions.txt +++ b/Tools/tsan/suppressions.txt @@ -3,6 +3,3 @@ # https://gist.github.com/mpage/daaf32b39180c1989572957b943eb665 thread:pthread_create - -# gh-140138 -race:drop_gil \ No newline at end of file diff --git a/Tools/tsan/suppressions_free_threading.txt b/Tools/tsan/suppressions_free_threading.txt index be9f7940429c6e..c10af91faae17c 100644 --- a/Tools/tsan/suppressions_free_threading.txt +++ b/Tools/tsan/suppressions_free_threading.txt @@ -52,6 +52,3 @@ race:PyObject_Realloc race_top:update_one_slot race_top:set_tp_bases race_top:type_set_bases_unlocked - -# gh-140138 -race:drop_gil \ No newline at end of file From d7e44f7d74bdbf76e2c10df290530e4c4bf25a55 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Fri, 17 Oct 2025 22:20:49 +0530 Subject: [PATCH 5/5] add skip marker --- Lib/test/test_threading.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index 59db91b0ffce5e..bb51ddd38e2cad 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -1727,6 +1727,7 @@ def task(): self.assertEqual(os.read(r_interp, 1), DONE) @cpython_only + @support.skip_if_sanitizer(thread=True, memory=True) def test_daemon_threads_fatal_error(self): import_module("_testcapi") subinterp_code = f"""if 1: