From bf301eb12d06734f0f9581c5d3f5898c2bbb7bab Mon Sep 17 00:00:00 2001 From: Maik Schulze Date: Thu, 5 Nov 2026 11:54:54 +0100 Subject: [PATCH 1/2] gh-161407: Allocate audit hook entries with malloc() A hook added with PySys_AddAuditHook() before Py_Initialize() was allocated with PyMem_RawMalloc() before the startup configuration was applied, and freed with PyMem_RawFree() at finalization. When PYTHONMALLOC=mimalloc, -X dev or a debug allocator changed the raw allocator in between, the entry was freed with a different allocator and the process crashed at exit. Allocate and free the entries with plain malloc() and free(), which nothing replaces at startup, and test _testembed test_audit under the affected settings. --- Lib/test/test_embed.py | 18 ++++++++++++++++++ ...6-11-05-10-51-17.gh-issue-161407.rVoqaQ.rst | 3 +++ Python/sysmodule.c | 11 ++++++++--- 3 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-11-05-10-51-17.gh-issue-161407.rVoqaQ.rst diff --git a/Lib/test/test_embed.py b/Lib/test/test_embed.py index 773b97f631f2189..caeebecd99f660c 100644 --- a/Lib/test/test_embed.py +++ b/Lib/test/test_embed.py @@ -2062,6 +2062,24 @@ def test_open_code_hook(self): def test_audit(self): self.run_embedded_interpreter("test_audit") + def test_audit_hook_survives_allocator_change(self): + # A hook added before Py_Initialize() is freed at finalization, after + # PYTHONMALLOC changed the raw allocator: the entry must not be + # allocated with one allocator and freed with another. + allocators = ['debug'] + if not support.Py_GIL_DISABLED: + allocators.append('malloc_debug') + if support.with_mimalloc(): + allocators.extend(['mimalloc', 'mimalloc_debug']) + for allocator in allocators: + with self.subTest(allocator=allocator): + self.run_embedded_interpreter( + "test_audit", env={'PYTHONMALLOC': allocator}) + # The development mode installs the debug hooks too + with self.subTest(dev_mode=True): + self.run_embedded_interpreter( + "test_audit", env={'PYTHONDEVMODE': '1'}) + def test_audit_tuple(self): self.run_embedded_interpreter("test_audit_tuple") diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-11-05-10-51-17.gh-issue-161407.rVoqaQ.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-11-05-10-51-17.gh-issue-161407.rVoqaQ.rst new file mode 100644 index 000000000000000..421329930f961a7 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-11-05-10-51-17.gh-issue-161407.rVoqaQ.rst @@ -0,0 +1,3 @@ +Fix a crash at exit when an audit hook was added with :c:func:`PySys_AddAuditHook` +before :c:func:`Py_Initialize` and the raw memory allocator changed during startup, for +example with ``PYTHONMALLOC=mimalloc`` or in the :ref:`Python Development Mode `. diff --git a/Python/sysmodule.c b/Python/sysmodule.c index 6f336dac058379e..036c92ec8f7e41d 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -32,6 +32,7 @@ Data members: #include "pycore_pylifecycle.h" // _PyErr_WriteUnraisableDefaultHook() #include "pycore_pymath.h" // _PY_SHORT_FLOAT_REPR #include "pycore_pymem.h" // _PyMem_DefaultRawFree() +#include "pycore_pymem_init.h" // _PyMem_RawMalloc() #include "pycore_pystate.h" // _PyThreadState_GET() #include "pycore_pystats.h" // _Py_PrintSpecializationStats() #include "pycore_runtime.h" // _PyRuntimeState_Get*() @@ -448,7 +449,7 @@ _PySys_ClearAuditHooks(PyThreadState *ts) runtime->audit_hooks.head = NULL; while (e) { n = e->next; - PyMem_RawFree(e); + _PyMem_RawFree(NULL, e); e = n; } } @@ -496,8 +497,12 @@ PySys_AddAuditHook(Py_AuditHookFunction hook, void *userData) } } - _Py_AuditHookEntry *e = (_Py_AuditHookEntry*)PyMem_RawMalloc( - sizeof(_Py_AuditHookEntry)); + /* Use malloc(), not PyMem_RawMalloc(): a hook can be added before + Py_Initialize(), and the entry is freed during finalization, after + PYTHONMALLOC may have changed the raw allocator (and, in debug builds, + the allocator under the debug hooks). */ + _Py_AuditHookEntry *e = (_Py_AuditHookEntry*)_PyMem_RawMalloc( + NULL, sizeof(_Py_AuditHookEntry)); if (!e) { if (tstate != NULL) { _PyErr_NoMemory(tstate); From e5d45dbfe767961910e131cec1ed90b7dbd6ece1 Mon Sep 17 00:00:00 2001 From: Maik Schulze Date: Sat, 7 Nov 2026 07:20:49 +0100 Subject: [PATCH 2/2] Call malloc() and free() directly, remove the test As suggested in review: the _PyMem_RawMalloc() wrapper adds nothing here, and with malloc() the audit hook entries no longer depend on PYTHONMALLOC. --- Lib/test/test_embed.py | 18 ------------------ Python/sysmodule.c | 7 +++---- 2 files changed, 3 insertions(+), 22 deletions(-) diff --git a/Lib/test/test_embed.py b/Lib/test/test_embed.py index caeebecd99f660c..773b97f631f2189 100644 --- a/Lib/test/test_embed.py +++ b/Lib/test/test_embed.py @@ -2062,24 +2062,6 @@ def test_open_code_hook(self): def test_audit(self): self.run_embedded_interpreter("test_audit") - def test_audit_hook_survives_allocator_change(self): - # A hook added before Py_Initialize() is freed at finalization, after - # PYTHONMALLOC changed the raw allocator: the entry must not be - # allocated with one allocator and freed with another. - allocators = ['debug'] - if not support.Py_GIL_DISABLED: - allocators.append('malloc_debug') - if support.with_mimalloc(): - allocators.extend(['mimalloc', 'mimalloc_debug']) - for allocator in allocators: - with self.subTest(allocator=allocator): - self.run_embedded_interpreter( - "test_audit", env={'PYTHONMALLOC': allocator}) - # The development mode installs the debug hooks too - with self.subTest(dev_mode=True): - self.run_embedded_interpreter( - "test_audit", env={'PYTHONDEVMODE': '1'}) - def test_audit_tuple(self): self.run_embedded_interpreter("test_audit_tuple") diff --git a/Python/sysmodule.c b/Python/sysmodule.c index 036c92ec8f7e41d..e8dd956c3f48828 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -32,7 +32,6 @@ Data members: #include "pycore_pylifecycle.h" // _PyErr_WriteUnraisableDefaultHook() #include "pycore_pymath.h" // _PY_SHORT_FLOAT_REPR #include "pycore_pymem.h" // _PyMem_DefaultRawFree() -#include "pycore_pymem_init.h" // _PyMem_RawMalloc() #include "pycore_pystate.h" // _PyThreadState_GET() #include "pycore_pystats.h" // _Py_PrintSpecializationStats() #include "pycore_runtime.h" // _PyRuntimeState_Get*() @@ -449,7 +448,7 @@ _PySys_ClearAuditHooks(PyThreadState *ts) runtime->audit_hooks.head = NULL; while (e) { n = e->next; - _PyMem_RawFree(NULL, e); + free(e); e = n; } } @@ -501,8 +500,8 @@ PySys_AddAuditHook(Py_AuditHookFunction hook, void *userData) Py_Initialize(), and the entry is freed during finalization, after PYTHONMALLOC may have changed the raw allocator (and, in debug builds, the allocator under the debug hooks). */ - _Py_AuditHookEntry *e = (_Py_AuditHookEntry*)_PyMem_RawMalloc( - NULL, sizeof(_Py_AuditHookEntry)); + _Py_AuditHookEntry *e = (_Py_AuditHookEntry*)malloc( + sizeof(_Py_AuditHookEntry)); if (!e) { if (tstate != NULL) { _PyErr_NoMemory(tstate);