diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-04-01-12-52-31.gh-issue-144319.iZk4hs.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-04-01-12-52-31.gh-issue-144319.iZk4hs.rst new file mode 100644 index 00000000000000..f3f07ab35dbb01 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-04-01-12-52-31.gh-issue-144319.iZk4hs.rst @@ -0,0 +1,3 @@ +Fix a bug that could cause applications with specific allocation patterns to +leak memory via Huge Pages if compiled with Huge Page support. Patch by +Pablo Galindo diff --git a/Objects/obmalloc.c b/Objects/obmalloc.c index 983bdddbf026a8..2e0db6976aaa22 100644 --- a/Objects/obmalloc.c +++ b/Objects/obmalloc.c @@ -14,6 +14,7 @@ #include // malloc() #include #include // fopen(), fgets(), sscanf() +#include // errno #ifdef WITH_MIMALLOC // Forward declarations of functions used in our mimalloc modifications static void _PyMem_mi_page_clear_qsbr(mi_page_t *page); @@ -648,7 +649,11 @@ _PyMem_ArenaFree(void *Py_UNUSED(ctx), void *ptr, if (ptr == NULL) { return; } - munmap(ptr, size); + if (munmap(ptr, size) < 0) { + _Py_FatalErrorFormat(__func__, + "munmap(%p, %zu) failed with errno %d", + ptr, size, errno); + } #else free(ptr); #endif diff --git a/Python/pystate.c b/Python/pystate.c index 143175da0f45c7..91d6d57dc776c3 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -1433,16 +1433,30 @@ tstate_is_alive(PyThreadState *tstate) // lifecycle //---------- +/* When huge pages are enabled, _PyObject_VirtualAlloc may back small requests + * with a full 2MB huge page. We must pass the *mapped* size (not the + * requested size) to _PyObject_VirtualFree, otherwise munmap receives a + * sub-hugepage length and fails with EINVAL, leaking the huge page. */ +#if defined(PYMALLOC_USE_HUGEPAGES) && defined(__linux__) +# define HUGEPAGE_SIZE (2U << 20) /* 2 MB */ +# define ROUND_UP_HUGEPAGE(n) \ + (((n) + HUGEPAGE_SIZE - 1) & ~(HUGEPAGE_SIZE - 1)) +#else +# define ROUND_UP_HUGEPAGE(n) (n) +#endif + + static _PyStackChunk* allocate_chunk(int size_in_bytes, _PyStackChunk* previous) { assert(size_in_bytes % sizeof(PyObject **) == 0); - _PyStackChunk *res = _PyObject_VirtualAlloc(size_in_bytes); + size_t mapped_size = ROUND_UP_HUGEPAGE(size_in_bytes); + _PyStackChunk *res = _PyObject_VirtualAlloc(mapped_size); if (res == NULL) { return NULL; } res->previous = previous; - res->size = size_in_bytes; + res->size = mapped_size; res->top = 0; return res; } @@ -3082,7 +3096,7 @@ push_chunk(PyThreadState *tstate, int size) &tstate->datastack_chunk->data[0]; } tstate->datastack_chunk = new; - tstate->datastack_limit = (PyObject **)(((char *)new) + allocate_size); + tstate->datastack_limit = (PyObject **)(((char *)new) + new->size); // When new is the "root" chunk (i.e. new->previous == NULL), we can keep // _PyThreadState_PopFrame from freeing it later by "skipping" over the // first element: