Skip to content

Commit c375992

Browse files
authored
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))`.
1 parent 80f9467 commit c375992

3 files changed

Lines changed: 52 additions & 1 deletion

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
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``.

Modules/_testcapi/mem.c

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,53 @@ test_setallocators(PyMemAllocatorDomain domain)
345345
goto fail;
346346
}
347347

348+
/* realloc(NULL, size) should behave like malloc(size) */
349+
size_t size3 = 100;
350+
void *ptr3;
351+
switch(domain) {
352+
case PYMEM_DOMAIN_RAW:
353+
ptr3 = PyMem_RawRealloc(NULL, size3);
354+
break;
355+
case PYMEM_DOMAIN_MEM:
356+
ptr3 = PyMem_Realloc(NULL, size3);
357+
break;
358+
case PYMEM_DOMAIN_OBJ:
359+
ptr3 = PyObject_Realloc(NULL, size3);
360+
break;
361+
default:
362+
ptr3 = NULL;
363+
break;
364+
}
365+
366+
CHECK_CTX("realloc(NULL, size)");
367+
if (ptr3 == NULL) {
368+
error_msg = "realloc(NULL, size) failed";
369+
goto fail;
370+
}
371+
if (hook.realloc_ptr != NULL || hook.realloc_new_size != size3) {
372+
error_msg = "realloc(NULL, size) invalid parameters";
373+
goto fail;
374+
}
375+
376+
hook.free_ptr = NULL;
377+
switch(domain) {
378+
case PYMEM_DOMAIN_RAW:
379+
PyMem_RawFree(ptr3);
380+
break;
381+
case PYMEM_DOMAIN_MEM:
382+
PyMem_Free(ptr3);
383+
break;
384+
case PYMEM_DOMAIN_OBJ:
385+
PyObject_Free(ptr3);
386+
break;
387+
}
388+
389+
CHECK_CTX("realloc(NULL, size) free");
390+
if (hook.free_ptr != ptr3) {
391+
error_msg = "unexpected pointer passed to free";
392+
goto fail;
393+
}
394+
348395
res = Py_NewRef(Py_None);
349396
goto finally;
350397

Objects/obmalloc.c

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,10 @@ _PyObject_MiRealloc(void *ctx, void *ptr, size_t nbytes)
363363
_mi_memcpy((char*)newp + offset, (char*)ptr + offset, copy_size - offset);
364364
}
365365
else {
366-
_mi_memcpy(newp, ptr, copy_size);
366+
// memcpy(dst, NULL, 0) is undefined behavior. See gh-151297.
367+
if mi_likely(ptr) {
368+
_mi_memcpy(newp, ptr, copy_size);
369+
}
367370
}
368371
mi_free(ptr);
369372
return newp;

0 commit comments

Comments
 (0)