Repository navigation
PyObject_Realloc(NULL, size) crashes free-threaded 32-bit Python 3.15.0b2 #151297
Description
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 for python3.15 or python3.14t.
It only crashes for the i686 image, not the x86-64 image.
(gdb) bt
#0 0x5ebd5a4e in ?? ()
#1 0x5ebd7a21 in PyObject_Realloc ()
#2 0xf2cc37dc in ffi_call_i386 () at ../src/x86/sysv.S:121
...
(gdb) x/30i 0x5ebd5a4e - 50
0x5ebd5a1c: nopl 0x0(%eax)
0x5ebd5a20: mov 0xc(%ebp),%edi
0x5ebd5a23: mov (%edi,%edx,1),%edi
0x5ebd5a26: mov %edi,(%esi,%edx,1)
0x5ebd5a29: mov 0xc(%ebp),%edi
0x5ebd5a2c: add $0x4,%edx
0x5ebd5a2f: cmp %edx,%eax
0x5ebd5a31: jne 0x5ebd5a20
0x5ebd5a33: lea (%esi,%eax,1),%edx
0x5ebd5a36: add %edi,%eax
0x5ebd5a38: sub $0x4,%esp
0x5ebd5a3b: push %ecx
0x5ebd5a3c: push %eax
0x5ebd5a3d: push %edx
0x5ebd5a3e: call 0x5edd6220 <memcpy@plt>
0x5ebd5a43: add $0x10,%esp
0x5ebd5a46: lea -0x1(%edi),%eax
0x5ebd5a49: and $0xffc00000,%eax
=> 0x5ebd5a4e: mov 0x6c(%eax),%edx
0x5ebd5a51: sub %eax,%edi
0x5ebd5a53: shr $0x9,%edi
0x5ebd5a56: and $0xffffffc0,%edi
0x5ebd5a59: lea (%eax,%edi,1),%ecx
0x5ebd5a5c: sub 0x74(%eax,%edi,1),%ecx
0x5ebd5a60: mov %gs:0x0,%edi
0x5ebd5a67: add $0x70,%ecx
0x5ebd5a6a: cmp %edx,%edi
0x5ebd5a6c: jne 0x5ebd5acf
0x5ebd5a6e: cmpb $0x0,0xe(%ecx)
0x5ebd5a72: jne 0x5ebd5ad6
It looks like it's crashing after the memcpy call that realloc makes to copy from the old buffer to the new. The only other thing _PyObject_MiRealloc does after that point is call mi_free(ptr) to free the old pointer, but that should be special casing null...
The crash is from trying to access:
(gdb) p $_siginfo._sifields._sigfault.si_addr
$1 = (void *) 0xffc0006c
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 ]
Activity
[-]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[/+]godlygeek commentedon Jun 11, 2026
Ooh, it also seems to be intermittent:
sergey-miryanov commentedon Jun 11, 2026
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.
godlygeek commentedon 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.godlygeek commentedon Jun 11, 2026
I'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...
Somehow, 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...sergey-miryanov commentedon Jun 11, 2026
Thanks for clarification.
ZeroIntensity commentedon 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.Is this still i686? I've been trying to reproduce this on x86-64, but I haven't been able to.
godlygeek commentedon Jun 11, 2026
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.
godlygeek commentedon Jun 11, 2026
Specifically I got it reproducing with:
and then
godlygeek commentedon Jun 11, 2026
Ah, and here's our root cause:
/usr/include/string.hdoes: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.ZeroIntensity commentedon Jun 11, 2026
Makes sense, but I am still very confused as to why this doesn't reproduce on x86-64.
gh-151297: Fix undefined behavior in `_PyObject_MiRealloc` (GH-151358)
[3.15] gh-151297: Fix undefined behavior in `_PyObject_MiRealloc` (GH…
pythongh-151297: Fix undefined behavior in `_PyObject_MiRealloc` (pyt…