Skip to content

Use-after-free in bytearray_mod (bytes formatting) via re-entrant __repr__ method #142557

@jackfromeast

Description

@jackfromeast

What happened?

When formatting a bytearray (the % operator) the C implementation walks the original format buffer while calling PyObject_Repr/PyObject_ASCII for %a-style conversions. A crafted object's __repr__ can clear/resize the bytearray, freeing its buffer while the C code still holds a pointer into it. Subsequent reads use a stale pointer and crash with a heap use-after-free.

Proof of Concept:

fmt = bytearray(b"%a %a")

class Evil:
    def __repr__(self):
        # Re-entrant mutation: shrink the format buffer while C code
        # still iterates over the original pointer.
        fmt.clear()
        return "E"

fmt % (Evil(), 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

Details
static PyObject *
bytearray_mod(PyObject *v, PyObject *w)
{
    PyObject *ret;
    if (PyByteArray_Check(w)) {
        Py_BEGIN_CRITICAL_SECTION2(v, w);
        ret = bytearray_mod_lock_held(v, w);
        Py_END_CRITICAL_SECTION2();
    }
    
    else {
        Py_BEGIN_CRITICAL_SECTION(v);
        ret = bytearray_mod_lock_held(v, w);
        Py_END_CRITICAL_SECTION();
    }
    return ret;
}

PyObject *
_PyBytes_FormatEx(const char *format, Py_ssize_t format_len,
                  PyObject *args, int use_bytearray)
{
		...
    sign = 0;
    fill = ' ';
    switch (c) {
    case 'r':
        // %r is only for 2/3 code; 3 only code should use %a
    case 'a':
		// Trigger the __repr__ method to free the bytearray while other bytes is still being processed in the loop 
        temp = PyObject_ASCII(v);
        if (temp == NULL)
            goto error;
        assert(PyUnicode_IS_ASCII(temp));
        pbuf = (const char *)PyUnicode_1BYTE_DATA(temp);
        len = PyUnicode_GET_LENGTH(temp);
        if (prec >= 0 && len > prec)
            len = prec;
        break;
    ....
}

Sanitizer

Details
=================================================================
==1533621==ERROR: AddressSanitizer: heap-use-after-free on address 0x503000001c12 at pc 0x64106e00f963 bp 0x7ffee0be1b40 sp 0x7ffee0be1b30
READ of size 1 at 0x503000001c12 thread T0
    #0 0x64106e00f962 in _PyBytes_FormatEx Objects/bytesobject.c:651
    #1 0x64106dff0cf2 in bytearray_mod_lock_held Objects/bytearrayobject.c:2702
    #2 0x64106dff0d33 in bytearray_mod Objects/bytearrayobject.c:2716
    #3 0x64106dfdc613 in binary_op1 Objects/abstract.c:966
    #4 0x64106dfdc777 in binary_op Objects/abstract.c:1005
    #5 0x64106dfdfeea in PyNumber_Remainder Objects/abstract.c:1189
    #6 0x64106e2878f8 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:62
    #7 0x64106e2d4e54 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
    #8 0x64106e2d5148 in _PyEval_Vector Python/ceval.c:2001
    #9 0x64106e2d53f8 in PyEval_EvalCode Python/ceval.c:884
    #10 0x64106e3cc507 in run_eval_code_obj Python/pythonrun.c:1365
    #11 0x64106e3cc723 in run_mod Python/pythonrun.c:1459
    #12 0x64106e3cd57a in pyrun_file Python/pythonrun.c:1293
    #13 0x64106e3d0220 in _PyRun_SimpleFileObject Python/pythonrun.c:521
    #14 0x64106e3d04f6 in _PyRun_AnyFileObject Python/pythonrun.c:81
    #15 0x64106e42174d in pymain_run_file_obj Modules/main.c:410
    #16 0x64106e4219b4 in pymain_run_file Modules/main.c:429
    #17 0x64106e4231b2 in pymain_run_python Modules/main.c:691
    #18 0x64106e423842 in Py_RunMain Modules/main.c:772
    #19 0x64106e423a2e in pymain_main Modules/main.c:802
    #20 0x64106e423db3 in Py_BytesMain Modules/main.c:826
    #21 0x64106dea7645 in main Programs/python.c:15
    #22 0x79ed96e2a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #23 0x79ed96e2a28a in __libc_start_main_impl ../csu/libc-start.c:360
    #24 0x64106dea7574 in _start (/home/jackfromeast/Desktop/entropy/tasks/grammar-afl++-latest/targets/cpython/python+0x2dd574) (BuildId: ff3dc40ea460bd4beb2c3a72283cca525b319bf0)

0x503000001c12 is located 18 bytes inside of 30-byte region [0x503000001c00,0x503000001c1e)
freed by thread T0 here:
    #0 0x79ed972fc778 in realloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:85
    #1 0x64106e0da2d2 in _PyMem_RawRealloc Objects/obmalloc.c:85
    #2 0x64106e0dc4e4 in _PyMem_DebugRawRealloc Objects/obmalloc.c:3010
    #3 0x64106e0dc823 in _PyMem_DebugRealloc Objects/obmalloc.c:3108
    #4 0x64106e1032e7 in PyMem_Realloc Objects/obmalloc.c:1063
    #5 0x64106dfebc04 in bytearray_resize_lock_held Objects/bytearrayobject.c:258
    #6 0x64106dff924c in PyByteArray_Resize Objects/bytearrayobject.c:278
    #7 0x64106dffaf8e in bytearray_clear_impl Objects/bytearrayobject.c:1260
    #8 0x64106dffafaf in bytearray_clear Objects/clinic/bytearrayobject.c.h:227
    #9 0x64106e032571 in method_vectorcall_NOARGS Objects/descrobject.c:448
    #10 0x64106e012e7f in _PyObject_VectorcallTstate Include/internal/pycore_call.h:169
    #11 0x64106e012f72 in PyObject_Vectorcall Objects/call.c:327
    #12 0x64106e291056 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:1620
    #13 0x64106e2d4e54 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
    #14 0x64106e2d5148 in _PyEval_Vector Python/ceval.c:2001
    #15 0x64106e0129b8 in _PyFunction_Vectorcall Objects/call.c:413
    #16 0x64106e012e7f in _PyObject_VectorcallTstate Include/internal/pycore_call.h:169
    #17 0x64106e01303f in PyObject_CallOneArg Objects/call.c:395
    #18 0x64106e125648 in call_unbound_noarg Objects/typeobject.c:3040
    #19 0x64106e140fa0 in maybe_call_special_no_args Objects/typeobject.c:3153
    #20 0x64106e141084 in _PyObject_MaybeCallSpecialNoArgs Objects/typeobject.c:3184
    #21 0x64106e14118f in slot_tp_repr Objects/typeobject.c:10546
    #22 0x64106e0d0792 in PyObject_Repr Objects/object.c:779
    #23 0x64106e0d228d in PyObject_ASCII Objects/object.c:842
    #24 0x64106e010471 in _PyBytes_FormatEx Objects/bytesobject.c:848
    #25 0x64106dff0cf2 in bytearray_mod_lock_held Objects/bytearrayobject.c:2702
    #26 0x64106dff0d33 in bytearray_mod Objects/bytearrayobject.c:2716
    #27 0x64106dfdc613 in binary_op1 Objects/abstract.c:966
    #28 0x64106dfdc777 in binary_op Objects/abstract.c:1005
    #29 0x64106dfdfeea in PyNumber_Remainder Objects/abstract.c:1189

previously allocated by thread T0 here:
    #0 0x79ed972fd9c7 in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
    #1 0x64106e0da284 in _PyMem_RawMalloc Objects/obmalloc.c:63
    #2 0x64106e0d9655 in _PyMem_DebugRawAlloc Objects/obmalloc.c:2887
    #3 0x64106e0dc64f in _PyMem_DebugRawRealloc Objects/obmalloc.c:2963
    #4 0x64106e0dc823 in _PyMem_DebugRealloc Objects/obmalloc.c:3108
    #5 0x64106e1032e7 in PyMem_Realloc Objects/obmalloc.c:1063
    #6 0x64106dfebc04 in bytearray_resize_lock_held Objects/bytearrayobject.c:258
    #7 0x64106dff924c in PyByteArray_Resize Objects/bytearrayobject.c:278
    #8 0x64106dff9850 in bytearray___init___impl Objects/bytearrayobject.c:978
    #9 0x64106dffa2c9 in bytearray___init__ Objects/clinic/bytearrayobject.c.h:102
    #10 0x64106e1393c0 in type_call Objects/typeobject.c:2460
    #11 0x64106e012c71 in _PyObject_MakeTpCall Objects/call.c:242
    #12 0x64106e012f19 in _PyObject_VectorcallTstate Include/internal/pycore_call.h:167
    #13 0x64106e012f72 in PyObject_Vectorcall Objects/call.c:327
    #14 0x64106e291056 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:1620
    #15 0x64106e2d4e54 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
    #16 0x64106e2d5148 in _PyEval_Vector Python/ceval.c:2001
    #17 0x64106e2d53f8 in PyEval_EvalCode Python/ceval.c:884
    #18 0x64106e3cc507 in run_eval_code_obj Python/pythonrun.c:1365
    #19 0x64106e3cc723 in run_mod Python/pythonrun.c:1459
    #20 0x64106e3cd57a in pyrun_file Python/pythonrun.c:1293
    #21 0x64106e3d0220 in _PyRun_SimpleFileObject Python/pythonrun.c:521
    #22 0x64106e3d04f6 in _PyRun_AnyFileObject Python/pythonrun.c:81
    #23 0x64106e42174d in pymain_run_file_obj Modules/main.c:410
    #24 0x64106e4219b4 in pymain_run_file Modules/main.c:429
    #25 0x64106e4231b2 in pymain_run_python Modules/main.c:691
    #26 0x64106e423842 in Py_RunMain Modules/main.c:772
    #27 0x64106e423a2e in pymain_main Modules/main.c:802
    #28 0x64106e423db3 in Py_BytesMain Modules/main.c:826
    #29 0x64106dea7645 in main Programs/python.c:15

SUMMARY: AddressSanitizer: heap-use-after-free Objects/bytesobject.c:651 in _PyBytes_FormatEx
Shadow bytes around the buggy address:
  0x503000001980: fa fa fd fd fd fa fa fa fd fd fd fa 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 fa fa fa fd fd fd fd
  0x503000001b00: fa fa fd fd fd fd fa fa fd fd fd fd fa fa fd fd
  0x503000001b80: fd fd fa fa fd fd fd fa fa fa fd fd fd fd fa fa
=>0x503000001c00: fd fd[fd]fd fa fa 00 00 00 01 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
  0x503000001e80: 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
==1533621==ABORTING

Metadata

Metadata

Assignees

No one assigned

    Labels

    interpreter-core(Objects, Python, Grammar, and Parser dirs)type-crashA hard crash of the interpreter, possibly with a core dump

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions