Skip to content

Commit 0d7fb6b

Browse files
godlygeekmiss-islington
authored andcommitted
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))`. (cherry picked from commit c375992) Co-authored-by: Matt Wozniski <mwozniski@bloomberg.net>
1 parent a12a0e9 commit 0d7fb6b

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
@@ -323,6 +323,53 @@ test_setallocators(PyMemAllocatorDomain domain)
323323
goto fail;
324324
}
325325

326+
/* realloc(NULL, size) should behave like malloc(size) */
327+
size_t size3 = 100;
328+
void *ptr3;
329+
switch(domain) {
330+
case PYMEM_DOMAIN_RAW:
331+
ptr3 = PyMem_RawRealloc(NULL, size3);
332+
break;
333+
case PYMEM_DOMAIN_MEM:
334+
ptr3 = PyMem_Realloc(NULL, size3);
335+
break;
336+
case PYMEM_DOMAIN_OBJ:
337+
ptr3 = PyObject_Realloc(NULL, size3);
338+
break;
339+
default:
340+
ptr3 = NULL;
341+
break;
342+
}
343+
344+
CHECK_CTX("realloc(NULL, size)");
345+
if (ptr3 == NULL) {
346+
error_msg = "realloc(NULL, size) failed";
347+
goto fail;
348+
}
349+
if (hook.realloc_ptr != NULL || hook.realloc_new_size != size3) {
350+
error_msg = "realloc(NULL, size) invalid parameters";
351+
goto fail;
352+
}
353+
354+
hook.free_ptr = NULL;
355+
switch(domain) {
356+
case PYMEM_DOMAIN_RAW:
357+
PyMem_RawFree(ptr3);
358+
break;
359+
case PYMEM_DOMAIN_MEM:
360+
PyMem_Free(ptr3);
361+
break;
362+
case PYMEM_DOMAIN_OBJ:
363+
PyObject_Free(ptr3);
364+
break;
365+
}
366+
367+
CHECK_CTX("realloc(NULL, size) free");
368+
if (hook.free_ptr != ptr3) {
369+
error_msg = "unexpected pointer passed to free";
370+
goto fail;
371+
}
372+
326373
res = Py_NewRef(Py_None);
327374
goto finally;
328375

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)