From a1e9adfa3f1fef3ebef63c116c2a4a4509aaa00a Mon Sep 17 00:00:00 2001 From: Matt Wozniski Date: Thu, 11 Jun 2026 11:41:56 -0400 Subject: [PATCH 1/8] Fix undefined behavior in `_PyObject_MiRealloc` The standard says that a call to `memcpy` must pass a valid source and destination pointer even if the size is 0, so we must avoid calling `memcpy` when our source pointer is NULL. If we don't, an optimizing compiler can decide that the pointer must be non-NULL based on the presence of UB, and optimize out checks for null pointers. Specifically, note that the standard says: Where an argument declared as size_t n specifies the length of the array for a function, n can have the value zero on a call to that function. Unless explicitly stated otherwise in the description of a particular function in this subclause, pointer arguments on such a call shall still have valid values, as described in 7.1.4. And section 7.1.4 says: If an argument to a function has an invalid value (such as a value outside the domain of the function, or a pointer outside the address space of the program, or a null pointer, or a pointer to non-modifiable storage when the corresponding parameter is not const-qualified) or a type (after default argument promotion) not expected by a function with a variable number of arguments, the behavior is undefined. The specification for `memcpy` doesn't state that it's allowed to be called with null pointers, and Linux's `/usr/include/string.h` declares `memcpy` as `__nonnull ((1, 2))`. Signed-off-by: Matt Wozniski --- Objects/obmalloc.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Objects/obmalloc.c b/Objects/obmalloc.c index 1809bd30451327b..4a485c483720217 100644 --- a/Objects/obmalloc.c +++ b/Objects/obmalloc.c @@ -363,7 +363,9 @@ _PyObject_MiRealloc(void *ctx, void *ptr, size_t nbytes) _mi_memcpy((char*)newp + offset, (char*)ptr + offset, copy_size - offset); } else { - _mi_memcpy(newp, ptr, copy_size); + if mi_likely(ptr) { + _mi_memcpy(newp, ptr, copy_size); + } } mi_free(ptr); return newp; From e575e5a67eb19af23b75b5a412175eabc9891139 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 16:03:25 +0000 Subject: [PATCH 2/8] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Security/2026-06-11-16-03-23.gh-issue-151297.NGPkUM.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Security/2026-06-11-16-03-23.gh-issue-151297.NGPkUM.rst diff --git a/Misc/NEWS.d/next/Security/2026-06-11-16-03-23.gh-issue-151297.NGPkUM.rst b/Misc/NEWS.d/next/Security/2026-06-11-16-03-23.gh-issue-151297.NGPkUM.rst new file mode 100644 index 000000000000000..935057ee34971f8 --- /dev/null +++ b/Misc/NEWS.d/next/Security/2026-06-11-16-03-23.gh-issue-151297.NGPkUM.rst @@ -0,0 +1 @@ +Fix an invalid pointer dereference that could occur when calling `PyObject_Realloc` with a NULL pointer in free-threaded builds or with `PYTHONMALLOC=mimalloc`. From a4b2b85c70243c76a775036172a0f5b540eba17b Mon Sep 17 00:00:00 2001 From: Matt Wozniski Date: Thu, 11 Jun 2026 12:05:38 -0400 Subject: [PATCH 3/8] Fix reST syntax --- .../Security/2026-06-11-16-03-23.gh-issue-151297.NGPkUM.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Security/2026-06-11-16-03-23.gh-issue-151297.NGPkUM.rst b/Misc/NEWS.d/next/Security/2026-06-11-16-03-23.gh-issue-151297.NGPkUM.rst index 935057ee34971f8..ff3b9d1f58554fb 100644 --- a/Misc/NEWS.d/next/Security/2026-06-11-16-03-23.gh-issue-151297.NGPkUM.rst +++ b/Misc/NEWS.d/next/Security/2026-06-11-16-03-23.gh-issue-151297.NGPkUM.rst @@ -1 +1 @@ -Fix an invalid pointer dereference that could occur when calling `PyObject_Realloc` with a NULL pointer in free-threaded builds or with `PYTHONMALLOC=mimalloc`. +Fix an invalid pointer dereference that could occur when calling ``PyObject_Realloc`` with a NULL pointer in free-threaded builds or with ``PYTHONMALLOC=mimalloc``. From ef4712bcf9a4ddc1f0f20765e60a0d6f27793597 Mon Sep 17 00:00:00 2001 From: Matt Wozniski Date: Thu, 11 Jun 2026 15:36:06 -0400 Subject: [PATCH 4/8] Update Misc/NEWS.d/next/Security/2026-06-11-16-03-23.gh-issue-151297.NGPkUM.rst Co-authored-by: Peter Bierma --- .../Security/2026-06-11-16-03-23.gh-issue-151297.NGPkUM.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Security/2026-06-11-16-03-23.gh-issue-151297.NGPkUM.rst b/Misc/NEWS.d/next/Security/2026-06-11-16-03-23.gh-issue-151297.NGPkUM.rst index ff3b9d1f58554fb..288d726e0f1004d 100644 --- a/Misc/NEWS.d/next/Security/2026-06-11-16-03-23.gh-issue-151297.NGPkUM.rst +++ b/Misc/NEWS.d/next/Security/2026-06-11-16-03-23.gh-issue-151297.NGPkUM.rst @@ -1 +1 @@ -Fix an invalid pointer dereference that could occur when calling ``PyObject_Realloc`` with a NULL pointer in free-threaded builds or with ``PYTHONMALLOC=mimalloc``. +Fix an invalid pointer dereference that could occur when calling :c:func:`PyObject_Realloc` with a NULL pointer in :term:`free-threaded builds ` or with :envvar:`PYTHONMALLOC` set to ``mimalloc``. From b0fed70d738047582e59bbc708d6c37eb7479f55 Mon Sep 17 00:00:00 2001 From: Matt Wozniski Date: Thu, 11 Jun 2026 15:50:11 -0400 Subject: [PATCH 5/8] Recategorize as Core rather than Security --- .../2026-06-11-16-03-23.gh-issue-151297.NGPkUM.rst | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Misc/NEWS.d/next/{Security => Core_and_Builtins}/2026-06-11-16-03-23.gh-issue-151297.NGPkUM.rst (100%) diff --git a/Misc/NEWS.d/next/Security/2026-06-11-16-03-23.gh-issue-151297.NGPkUM.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-11-16-03-23.gh-issue-151297.NGPkUM.rst similarity index 100% rename from Misc/NEWS.d/next/Security/2026-06-11-16-03-23.gh-issue-151297.NGPkUM.rst rename to Misc/NEWS.d/next/Core_and_Builtins/2026-06-11-16-03-23.gh-issue-151297.NGPkUM.rst From 2082edb57065eb802ee9e1623c8d8258fb45703e Mon Sep 17 00:00:00 2001 From: Matt Wozniski Date: Thu, 11 Jun 2026 15:51:20 -0400 Subject: [PATCH 6/8] Add a comment --- Objects/obmalloc.c | 1 + 1 file changed, 1 insertion(+) diff --git a/Objects/obmalloc.c b/Objects/obmalloc.c index 4a485c483720217..c490e1a63175d9c 100644 --- a/Objects/obmalloc.c +++ b/Objects/obmalloc.c @@ -363,6 +363,7 @@ _PyObject_MiRealloc(void *ctx, void *ptr, size_t nbytes) _mi_memcpy((char*)newp + offset, (char*)ptr + offset, copy_size - offset); } else { + // memcpy(dst, NULL, 0) is undefined behavior. Guard against it. if mi_likely(ptr) { _mi_memcpy(newp, ptr, copy_size); } From 3a60ca340a001bc95bc59e6424603395a55f2e75 Mon Sep 17 00:00:00 2001 From: Matt Wozniski Date: Thu, 11 Jun 2026 16:34:11 -0400 Subject: [PATCH 7/8] Add issue to the comment --- Objects/obmalloc.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Objects/obmalloc.c b/Objects/obmalloc.c index c490e1a63175d9c..0947d47c8a55582 100644 --- a/Objects/obmalloc.c +++ b/Objects/obmalloc.c @@ -363,7 +363,7 @@ _PyObject_MiRealloc(void *ctx, void *ptr, size_t nbytes) _mi_memcpy((char*)newp + offset, (char*)ptr + offset, copy_size - offset); } else { - // memcpy(dst, NULL, 0) is undefined behavior. Guard against it. + // memcpy(dst, NULL, 0) is undefined behavior. See gh-151297. if mi_likely(ptr) { _mi_memcpy(newp, ptr, copy_size); } From 07ba59d7bd373748d1cc3866d0a5788c9d625563 Mon Sep 17 00:00:00 2001 From: Matt Wozniski Date: Thu, 11 Jun 2026 19:51:35 -0400 Subject: [PATCH 8/8] Test realloc(NULL, size) for all domains --- Modules/_testcapi/mem.c | 47 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/Modules/_testcapi/mem.c b/Modules/_testcapi/mem.c index 7909476ac11aa6b..28e89275d61705e 100644 --- a/Modules/_testcapi/mem.c +++ b/Modules/_testcapi/mem.c @@ -345,6 +345,53 @@ test_setallocators(PyMemAllocatorDomain domain) goto fail; } + /* realloc(NULL, size) should behave like malloc(size) */ + size_t size3 = 100; + void *ptr3; + switch(domain) { + case PYMEM_DOMAIN_RAW: + ptr3 = PyMem_RawRealloc(NULL, size3); + break; + case PYMEM_DOMAIN_MEM: + ptr3 = PyMem_Realloc(NULL, size3); + break; + case PYMEM_DOMAIN_OBJ: + ptr3 = PyObject_Realloc(NULL, size3); + break; + default: + ptr3 = NULL; + break; + } + + CHECK_CTX("realloc(NULL, size)"); + if (ptr3 == NULL) { + error_msg = "realloc(NULL, size) failed"; + goto fail; + } + if (hook.realloc_ptr != NULL || hook.realloc_new_size != size3) { + error_msg = "realloc(NULL, size) invalid parameters"; + goto fail; + } + + hook.free_ptr = NULL; + switch(domain) { + case PYMEM_DOMAIN_RAW: + PyMem_RawFree(ptr3); + break; + case PYMEM_DOMAIN_MEM: + PyMem_Free(ptr3); + break; + case PYMEM_DOMAIN_OBJ: + PyObject_Free(ptr3); + break; + } + + CHECK_CTX("realloc(NULL, size) free"); + if (hook.free_ptr != ptr3) { + error_msg = "unexpected pointer passed to free"; + goto fail; + } + res = Py_NewRef(Py_None); goto finally;