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))`.
--- /dev/null
+Fix an invalid pointer dereference that could occur when calling :c:func:`PyObject_Realloc` with a NULL pointer in :term:`free-threaded builds <free-threaded build>` or with :envvar:`PYTHONMALLOC` set to ``mimalloc``.
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;
_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;