From: Matt Wozniski Date: Fri, 12 Jun 2026 01:21:04 +0000 (-0400) Subject: gh-151297: Fix undefined behavior in `_PyObject_MiRealloc` (GH-151358) X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=c37599200f688538efa34a49f262a9a4a899a953;p=thirdparty%2FPython%2Fcpython.git gh-151297: Fix undefined behavior in `_PyObject_MiRealloc` (GH-151358) 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))`. --- diff --git a/Misc/NEWS.d/next/Core_and_Builtins/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 new file mode 100644 index 000000000000..288d726e0f10 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-11-16-03-23.gh-issue-151297.NGPkUM.rst @@ -0,0 +1 @@ +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``. diff --git a/Modules/_testcapi/mem.c b/Modules/_testcapi/mem.c index 7909476ac11a..28e89275d617 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; diff --git a/Objects/obmalloc.c b/Objects/obmalloc.c index 1809bd304513..0947d47c8a55 100644 --- a/Objects/obmalloc.c +++ b/Objects/obmalloc.c @@ -363,7 +363,10 @@ _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); + // memcpy(dst, NULL, 0) is undefined behavior. See gh-151297. + if mi_likely(ptr) { + _mi_memcpy(newp, ptr, copy_size); + } } mi_free(ptr); return newp;