What happened?
bytearray.index(x) converts x to a single byte using index before performing the search.
A crafted __index__ can call bytearray.clear(), freeing or resizing the buffer after str have been captured. The subsequent search (stringlib_find_char) then dereferences the stale pointer, resulting in a heap use-after-free.
Note that, this may share the same root cause as #142559 and #142558.
Proof of Concept:
victim = bytearray(b'A' * 0x1) class Evil: def __index__(self): victim.clear() return 65 victim.index(Evil())
Affected Versions:
Details| Python Version | Status | Exit Code |
|---|---|---|
Python 3.9.24+ (heads/3.9:9c4638d, Oct 17 2025, 11:19:30) |
ASAN | 1 |
Python 3.10.19+ (heads/3.10:0142619, Oct 17 2025, 11:20:05) [GCC 13.3.0] |
ASAN | 1 |
Python 3.11.14+ (heads/3.11:88f3f5b, Oct 17 2025, 11:20:44) [GCC 13.3.0] |
ASAN | 1 |
Python 3.12.12+ (heads/3.12:8cb2092, Oct 17 2025, 11:21:35) [GCC 13.3.0] |
ASAN | 1 |
Python 3.13.9+ (heads/3.13:0760a57, Oct 17 2025, 11:22:25) [GCC 13.3.0] |
ASAN | 1 |
Python 3.14.0+ (heads/3.14:889e918, Oct 17 2025, 11:23:02) [GCC 13.3.0] |
ASAN | 1 |
Python 3.15.0a1+ (heads/main:fbf0843, Oct 17 2025, 11:23:37) [GCC 13.3.0] |
ASAN | 1 |
Vulnerable Code Snippet
Detailsstatic PyObject * bytearray_index_impl(PyByteArrayObject *self, PyObject *sub, Py_ssize_t start, Py_ssize_t end) /*[clinic end generated code: output=067a1e78efc672a7 input=c37f177cfee19fe4]*/ { return _Py_bytes_index(PyByteArray_AS_STRING(self), PyByteArray_GET_SIZE(self), sub, start, end); } PyObject * _Py_bytes_index(const char *str, Py_ssize_t len, PyObject *sub, Py_ssize_t start, Py_ssize_t end) { Py_ssize_t result = find_internal(str, len, "index", sub, start, end, +1); if (result == -2) return NULL; if (result == -1) { PyErr_SetString(PyExc_ValueError, "subsection not found"); return NULL; } return PyLong_FromSsize_t(result); } Py_LOCAL_INLINE(Py_ssize_t) find_internal(const char *str, Py_ssize_t len, const char *function_name, PyObject *subobj, Py_ssize_t start, Py_ssize_t end, int dir) { char byte; Py_buffer subbuf; const char *sub; Py_ssize_t sub_len; Py_ssize_t res; if (!parse_args_finds_byte(function_name, &subobj, &byte)) { return -2; } if (subobj) { if (PyObject_GetBuffer(subobj, &subbuf, PyBUF_SIMPLE) != 0) return -2; sub = subbuf.buf; sub_len = subbuf.len; } else { sub = &byte; sub_len = 1; } ADJUST_INDICES(start, end, len); if (end - start < sub_len) res = -1; else if (sub_len == 1) { if (dir > 0) // Bug: *str has been freed already res = stringlib_find_char( str + start, end - start, *sub); else res = stringlib_rfind_char( str + start, end - start, *sub); if (res >= 0) res += start; } ... } Py_LOCAL_INLINE(int) parse_args_finds_byte(const char *function_name, PyObject **subobj, char *byte) { if (PyObject_CheckBuffer(*subobj)) { return 1; } if (!_PyIndex_Check(*subobj)) { PyErr_Format(PyExc_TypeError, "argument should be integer or bytes-like object, " "not '%.200s'", Py_TYPE(*subobj)->tp_name); return 0; } // Implicitly call __index__ method that free the *subobj buffer Py_ssize_t ival = PyNumber_AsSsize_t(*subobj, NULL); if (ival == -1 && PyErr_Occurred()) { return 0; } if (ival < 0 || ival > 255) { PyErr_SetString(PyExc_ValueError, "byte must be in range(0, 256)"); return 0; } *subobj = NULL; *byte = (char)ival; return 1; }
Sanitizer
Details================================================================= ==1510686==ERROR: AddressSanitizer: heap-use-after-free on address 0x503000001b80 at pc 0x5b0ec932269b bp 0x7ffee1128bf0 sp 0x7ffee1128be0 READ of size 1 at 0x503000001b80 thread T0 #0 0x5b0ec932269a in stringlib_find_char Objects/stringlib/fastsearch.h:99 #1 0x5b0ec9323ef4 in find_internal Objects/bytes_methods.c:485 #2 0x5b0ec9325698 in _Py_bytes_index Objects/bytes_methods.c:526 #3 0x5b0ec932aa9d in bytearray_index_impl Objects/bytearrayobject.c:1295 #4 0x5b0ec932ac14 in bytearray_index Objects/clinic/bytearrayobject.c.h:300 #5 0x5b0ec936e6ed in method_vectorcall_FASTCALL Objects/descrobject.c:402 #6 0x5b0ec934ee7f in _PyObject_VectorcallTstate Include/internal/pycore_call.h:169 #7 0x5b0ec934ef72 in PyObject_Vectorcall Objects/call.c:327 #8 0x5b0ec95cd056 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:1620 #9 0x5b0ec9610e54 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121 #10 0x5b0ec9611148 in _PyEval_Vector Python/ceval.c:2001 #11 0x5b0ec96113f8 in PyEval_EvalCode Python/ceval.c:884 #12 0x5b0ec9708507 in run_eval_code_obj Python/pythonrun.c:1365 #13 0x5b0ec9708723 in run_mod Python/pythonrun.c:1459 #14 0x5b0ec970957a in pyrun_file Python/pythonrun.c:1293 #15 0x5b0ec970c220 in _PyRun_SimpleFileObject Python/pythonrun.c:521 #16 0x5b0ec970c4f6 in _PyRun_AnyFileObject Python/pythonrun.c:81 #17 0x5b0ec975d74d in pymain_run_file_obj Modules/main.c:410 #18 0x5b0ec975d9b4 in pymain_run_file Modules/main.c:429 #19 0x5b0ec975f1b2 in pymain_run_python Modules/main.c:691 #20 0x5b0ec975f842 in Py_RunMain Modules/main.c:772 #21 0x5b0ec975fa2e in pymain_main Modules/main.c:802 #22 0x5b0ec975fdb3 in Py_BytesMain Modules/main.c:826 #23 0x5b0ec91e3645 in main Programs/python.c:15 #24 0x77cc63e2a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58 #25 0x77cc63e2a28a in __libc_start_main_impl ../csu/libc-start.c:360 #26 0x5b0ec91e3574 in _start (/home/jackfromeast/Desktop/entropy/tasks/grammar-afl++-latest/targets/cpython/python+0x2dd574) (BuildId: ff3dc40ea460bd4beb2c3a72283cca525b319bf0) 0x503000001b80 is located 16 bytes inside of 26-byte region [0x503000001b70,0x503000001b8a) freed by thread T0 here: #0 0x77cc642fc778 in realloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:85 #1 0x5b0ec94162d2 in _PyMem_RawRealloc Objects/obmalloc.c:85 #2 0x5b0ec94184e4 in _PyMem_DebugRawRealloc Objects/obmalloc.c:3010 #3 0x5b0ec9418823 in _PyMem_DebugRealloc Objects/obmalloc.c:3108 #4 0x5b0ec943f2e7 in PyMem_Realloc Objects/obmalloc.c:1063 #5 0x5b0ec9327c04 in bytearray_resize_lock_held Objects/bytearrayobject.c:258 #6 0x5b0ec933524c in PyByteArray_Resize Objects/bytearrayobject.c:278 #7 0x5b0ec9336f8e in bytearray_clear_impl Objects/bytearrayobject.c:1260 #8 0x5b0ec9336faf in bytearray_clear Objects/clinic/bytearrayobject.c.h:227 #9 0x5b0ec936e571 in method_vectorcall_NOARGS Objects/descrobject.c:448 #10 0x5b0ec934ee7f in _PyObject_VectorcallTstate Include/internal/pycore_call.h:169 #11 0x5b0ec934ef72 in PyObject_Vectorcall Objects/call.c:327 #12 0x5b0ec95cd056 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:1620 #13 0x5b0ec9610e54 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121 #14 0x5b0ec9611148 in _PyEval_Vector Python/ceval.c:2001 #15 0x5b0ec934e9b8 in _PyFunction_Vectorcall Objects/call.c:413 #16 0x5b0ec946156b in _PyObject_VectorcallTstate Include/internal/pycore_call.h:169 #17 0x5b0ec9461680 in vectorcall_unbound Objects/typeobject.c:3033 #18 0x5b0ec9482976 in vectorcall_method Objects/typeobject.c:3104 #19 0x5b0ec9483664 in slot_nb_index Objects/typeobject.c:10507 #20 0x5b0ec931c6be in _PyNumber_Index Objects/abstract.c:1418 #21 0x5b0ec931c940 in PyNumber_AsSsize_t Objects/abstract.c:1464 #22 0x5b0ec93224f2 in parse_args_finds_byte Objects/bytes_methods.c:418 #23 0x5b0ec9323da9 in find_internal Objects/bytes_methods.c:464 #24 0x5b0ec9325698 in _Py_bytes_index Objects/bytes_methods.c:526 #25 0x5b0ec932aa9d in Objects/bytearrayobject.c:1295 #26 0x5b0ec932ac14 in bytearray_index Objects/clinic/bytearrayobject.c.h:300 #27 0x5b0ec936e6ed in method_vectorcall_FASTCALL Objects/descrobject.c:402 #28 0x5b0ec934ee7f in _PyObject_VectorcallTstate Include/internal/pycore_call.h:169 #29 0x5b0ec934ef72 in PyObject_Vectorcall Objects/call.c:327 previously allocated by thread T0 here: #0 0x77cc642fd9c7 in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69 #1 0x5b0ec9416284 in _PyMem_RawMalloc Objects/obmalloc.c:63 #2 0x5b0ec9415655 in _PyMem_DebugRawAlloc Objects/obmalloc.c:2887 #3 0x5b0ec941864f in _PyMem_DebugRawRealloc Objects/obmalloc.c:2963 #4 0x5b0ec9418823 in _PyMem_DebugRealloc Objects/obmalloc.c:3108 #5 0x5b0ec943f2e7 in PyMem_Realloc Objects/obmalloc.c:1063 #6 0x5b0ec9327c04 in bytearray_resize_lock_held Objects/bytearrayobject.c:258 #7 0x5b0ec933524c in PyByteArray_Resize Objects/bytearrayobject.c:278 #8 0x5b0ec9335850 in bytearray___init___impl Objects/bytearrayobject.c:978 #9 0x5b0ec93362c9 in bytearray___init__ Objects/clinic/bytearrayobject.c.h:102 #10 0x5b0ec94753c0 in type_call Objects/typeobject.c:2460 #11 0x5b0ec934ec71 in _PyObject_MakeTpCall Objects/call.c:242 #12 0x5b0ec934ef19 in _PyObject_VectorcallTstate Include/internal/pycore_call.h:167 #13 0x5b0ec934ef72 in PyObject_Vectorcall Objects/call.c:327 #14 0x5b0ec95cd056 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:1620 #15 0x5b0ec9610e54 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121 #16 0x5b0ec9611148 in _PyEval_Vector Python/ceval.c:2001 #17 0x5b0ec96113f8 in PyEval_EvalCode Python/ceval.c:884 #18 0x5b0ec9708507 in run_eval_code_obj Python/pythonrun.c:1365 #19 0x5b0ec9708723 in run_mod Python/pythonrun.c:1459 #20 0x5b0ec970957a in pyrun_file Python/pythonrun.c:1293 #21 0x5b0ec970c220 in _PyRun_SimpleFileObject Python/pythonrun.c:521 #22 0x5b0ec970c4f6 in _PyRun_AnyFileObject Python/pythonrun.c:81 #23 0x5b0ec975d74d in pymain_run_file_obj Modules/main.c:410 #24 0x5b0ec975d9b4 in pymain_run_file Modules/main.c:429 #25 0x5b0ec975f1b2 in pymain_run_python Modules/main.c:691 #26 0x5b0ec975f842 in Py_RunMain Modules/main.c:772 #27 0x5b0ec975fa2e in pymain_main Modules/main.c:802 #28 0x5b0ec975fdb3 in Py_BytesMain Modules/main.c:826 #29 0x5b0ec91e3645 in main Programs/python.c:15 SUMMARY: AddressSanitizer: heap-use-after-free Objects/stringlib/fastsearch.h:99 in stringlib_find_char Shadow bytes around the buggy address: 0x503000001900: fd fd fd fa fa fa fd fd fd fa fa fa fd fd fd fd 0x503000001980: fa fa fd fd fd fa fa fa fd fd fd fd fa fa fd fd 0x503000001a00: fd fd fa fa fd fd fd fa fa fa fd fd fd fd fa fa 0x503000001a80: fd fd fd fd fa fa fd fd fd fd fa fa fd fd fd fd 0x503000001b00: fa fa fd fd fd fa fa fa fd fd fd fd fa fa fd fd =>0x503000001b80:[fd]fd fa fa 00 00 00 01 fa fa fa fa fa fa fa fa 0x503000001c00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x503000001c80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x503000001d00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x503000001d80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x503000001e00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa Shadow byte legend (one shadow byte represents 8 application bytes): Addressable: 00 Partially addressable: 01 02 03 04 05 06 07 Heap left redzone: fa Freed heap region: fd Stack left redzone: f1 Stack mid redzone: f2 Stack right redzone: f3 Stack after return: f5 Stack use after scope: f8 Global redzone: f9 Global init order: f6 Poisoned by user: f7 Container overflow: fc Array cookie: ac Intra object redzone: bb ASan internal: fe Left alloca redzone: ca Right alloca redzone: cb ==1510686==ABORTING
Linked PRs
- gh-142560: bytearray: prevent UAF in search-like methods by exporting self buffer #142564
- gh-142560: bytearray: prevent UAF in search-like methods by exporting self buffer #142937
- gh-142560: bytearray: prevent UAF in search-like methods by exporting self buffer #142938
- [3.14] gh-142560: prevent use-after-free in search-like methods by exporting buffer in bytearray (GH-142938) #142983
- [3.13] gh-142560: prevent use-after-free in search-like methods by exporting buffer in bytearray (GH-142938) #142986
- gh-142560: Move News to Core and Builtins #143154
- [3.14] Move News for gh-142560 to Core and Builtins (GH-143154) #143155