Repository navigation
PyObject_Realloc(NULL, size) crashes free-threaded 32-bit Python 3.15.0b2 #151297
Description
Activity
- addedtype-crashA hard crash of the interpreter, possibly with a core dumpA hard crash of the interpreter, possibly with a core dump
on Jun 11, 2026 - changed the title
[-]Crash in free-threaded 32-bit Python 3.15.0b2 calling PyObject_Realloc(NULL, size)[/-][+]PyObject_Realloc(NULL, size) crashes free-threaded 32-bit Python 3.15.0b2[/+]on Jun 11, 2026 Ooh, it also seems to be intermittent:
[root@bacf47817bc8 /]# /usr/local/bin/python3.15t -c 'import ctypes; PyObject_Realloc = ctypes.pythonapi.PyObject_Realloc; PyObject_Realloc.argtypes = [ctypes.c_void_p, ctypes.c_size_t]; PyObject_Realloc.restype = ctypes.c_void_p; print(PyObject_Realloc(None, 984))' Segmentation fault (core dumped) [root@bacf47817bc8 /]# /usr/local/bin/python3.15t -c 'import ctypes; PyObject_Realloc = ctypes.pythonapi.PyObject_Realloc; PyObject_Realloc.argtypes = [ctypes.c_void_p, ctypes.c_size_t]; PyObject_Realloc.restype = ctypes.c_void_p; print(PyObject_Realloc(None, 984))' 3777305600 [root@bacf47817bc8 /]# /usr/local/bin/python3.15t -c 'import ctypes; PyObject_Realloc = ctypes.pythonapi.PyObject_Realloc; PyObject_Realloc.argtypes = [ctypes.c_void_p, ctypes.c_size_t]; PyObject_Realloc.restype = ctypes.c_void_p; print(PyObject_Realloc(None, 984))' 3911523328 [root@bacf47817bc8 /]# /usr/local/bin/python3.15t -c 'import ctypes; PyObject_Realloc = ctypes.pythonapi.PyObject_Realloc; PyObject_Realloc.argtypes = [ctypes.c_void_p, ctypes.c_size_t]; PyObject_Realloc.restype = ctypes.c_void_p; print(PyObject_Realloc(None, 984))' Segmentation fault (core dumped)
Using
ctypesto achieve non-standard behavior is a bit too hacky and falls outside our project's scope.Furthermore,
Py_Noneis a statically allocated object, so callingPyObject_Reallocon it results in undefined behavior.Given this, I'm not sure we should try to do anything about it here.
- addedpendingThe issue will be closed if no feedback is providedThe issue will be closed if no feedback is provided
on Jun 11, 2026 ctypes isn't relevant here, you can reproduce this with a C extension if you prefer. I just figured a reproducer that doesn't require compiling extra code would be easier to work with.
This does not try to reallocate
Py_None. When a ctypes function that expects a pointer is passedNone, the FFI layer passesNULLdown to the called function.Reacted by Sergey MiryanovI've just figured out how to reproduce the crash with a debug build: it happens with Clang but not with GCC!
It's possible this is a compiler bug rather than a CPython bug...
mi_free (p=0x0) at Objects/mimalloc/alloc.c:576 576 const bool is_local= (_mi_prim_thread_id() == mi_atomic_load_relaxed(&segment->thread_id)); (gdb) bt #0 mi_free (p=0x0) at Objects/mimalloc/alloc.c:576 #1 _PyObject_MiRealloc (ctx=<optimized out>, ptr=0x0, nbytes=984) at Objects/obmalloc.c:368 #2 0x08135e40 in PyObject_Realloc (ptr=0x0, new_size=984) at Objects/obmalloc.c:1728 #3 0xf5c187dc in ffi_call_i386 () at ../src/x86/sysv.S:121mi_free (p=0x0) at Objects/mimalloc/alloc.c:576 576 const bool is_local= (_mi_prim_thread_id() == mi_atomic_load_relaxed(&segment->thread_id)); (gdb) list 571 // fast path written carefully to prevent spilling on the stack 572 void mi_free(void* p) mi_attr_noexcept 573 { 574 if mi_unlikely(p == NULL) return; 575 mi_segment_t* const segment = mi_checked_ptr_segment(p,"mi_free"); 576 const bool is_local= (_mi_prim_thread_id() == mi_atomic_load_relaxed(&segment->thread_id)); 577 mi_page_t* const page = _mi_segment_page_of(segment, p); 578 579 if mi_likely(is_local) { // thread-local free? 580 if mi_likely(page->flags.full_aligned == 0) // and it is not a full page (full pages need to move from the full bin), nor has aligned blocks (aligned blocks need to be unaligned) (gdb) p p $1 = (void *) 0x0Somehow, despite
pbeing NULL, the Clang-compiled binary has wound up on line 576 ofmi_free, despite that function being gated withif mi_unlikely(p == NULL) return;Either this is a compiler bug, or somewhere we performed an operation on
pthat would be UB if performed on a NULL pointer and so the compiler has branched assuming thatpmust not be NULL...Thanks for clarification.
- removedpendingThe issue will be closed if no feedback is providedThe issue will be closed if no feedback is provided
on Jun 11, 2026 Yeah, this feels a lot like a compiler bug. That
mi_unlikelymight be doing something suspicious when aggressively compiled by Clang. I would try removing that and seeing if the crash magically disappears.I've just figured out how to reproduce the crash with a debug build: it happens with Clang but not with GCC!
Is this still i686? I've been trying to reproduce this on x86-64, but I haven't been able to.
Yes, still needs i686. I've been reproducing it in a quay.io/pypa/manylinux_2_34_i686 container, run from a Linux x86-64 machine.
Specifically I got it reproducing with:
docker run -v $PWD:/workspace -it quay.io/pypa/manylinux_2_34_i686and then
yum install clang cd /tmp git clone https://github.com/python/cpython cd cpython CC=clang CFLAGS_NODIST='-g -O2 -D_FORTIFY_SOURCE=2' ./configure --disable-shared --disable-gil make -j gdb --args ./python -c 'import ctypes; PyObject_Realloc = ctypes.pythonapi.PyObject_Realloc; PyObject_Realloc.argtypes = [ctypes.c_void_p, ctypes.c_size_t]; PyObject_Realloc.restype = ctypes.c_void_p; print(PyObject_Realloc(None, 984))'Ah, and here's our root cause:
/usr/include/string.hdoes:/* Copy N bytes of SRC to DEST. */ extern void *memcpy (void *__restrict __dest, const void *__restrict __src, size_t __n) __THROW __nonnull ((1, 2));
That declares that both
__destand__srcmust be non-null.So, when
_PyObject_MiReallocdoes_mi_memcpy(newp, ptr, 0);the compiler decides it's now free to assume thatptris non-null, so when it inlines inmi_free(ptr)it drops theif mi_unlikely(p == NULL) return;check as something that can never be true.Makes sense, but I am still very confused as to why this doesn't reproduce on x86-64.
Crash report
What happened?
I haven't managed to reproduce this with a build of CPython from source, but it reproduces using the manylinux images shipped by the PyPA on an x86-64 machine:
docker run -it quay.io/pypa/manylinux_2_34_i686 /usr/local/bin/python3.15t -c 'import ctypes; PyObject_Realloc = ctypes.pythonapi.PyObject_Realloc; PyObject_Realloc.argtypes = [ctypes.c_void_p, ctypes.c_size_t]; PyObject_Realloc.restype = ctypes.c_void_p; print(PyObject_Realloc(None, 984))'It only crashes for
python3.15t, not forpython3.15orpython3.14t.It only crashes for the i686 image, not the x86-64 image.
It looks like it's crashing after the
memcpycall thatreallocmakes to copy from the old buffer to the new. The only other thing_PyObject_MiReallocdoes after that point is callmi_free(ptr)to free the old pointer, but that should be special casing null...The crash is from trying to access:
The address's high byte being 0xFF on a 32-bit address space makes me think something has wrapped around by subtracting from the null pointer to find the malloc housekeeping structures or something, but I haven't figured out why, since all of the code does seem to be properly guarded against null pointers.
CPython versions tested on:
3.15
Operating systems tested on:
Linux
Output from running 'python -VV' on the command line:
Python 3.15.0b2 free-threading build (main, Jun 6 2026, 08:05:01) [Clang 22.1.7 ]
Linked PRs
_PyObject_MiRealloc#151358_PyObject_MiRealloc(GH-151358) #151388