Skip to content

Use-after-free in _io.BytesIO.writelines via re-entrant __buffer__ close #143378

@jackfromeast

Description

@jackfromeast

What happened?

_io.BytesIO.writelines pulls buffers from each iterable element inside write_bytes_lock_held, but a crafted object can close the target in __buffer__, freeing self->buf while the write path still runs, which makes write_bytes_lock_held dereference freed memory.

Proof of Concept:

from _io import BytesIO


class Evil:
    def __init__(self, target):
        self.target = target

    def __buffer__(self, flags):
        self.target.close()
        return memoryview(b"A")


bio = BytesIO()
bio.writelines([Evil(bio)])

Vulnerable Code Snippet:

Click to expand
static PyObject *
_io_BytesIO_writelines_impl(bytesio *self, PyObject *lines)
/*[clinic end generated code: output=03a43a75773bc397 input=5d6a616ae39dc9ca]*/
{
    PyObject *it, *item;

    CHECK_CLOSED(self);

    it = PyObject_GetIter(lines);
    if (it == NULL)
        return NULL;

    while ((item = PyIter_Next(it)) != NULL) {
        Py_ssize_t ret = write_bytes_lock_held(self, item);
        Py_DECREF(item);
        if (ret < 0) {
            Py_DECREF(it);
            return NULL;
        }
    }
    Py_DECREF(it);

    /* See if PyIter_Next failed */
    if (PyErr_Occurred())
        return NULL;

    Py_RETURN_NONE;
}

/* Buggy Re-entrant Path */
Py_NO_INLINE static Py_ssize_t
write_bytes_lock_held(bytesio *self, PyObject *b)
{
    _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(self);

    Py_buffer buf;
    if (PyObject_GetBuffer(b, &buf, PyBUF_CONTIG_RO) < 0) {  /* Reentrant call site */
        return -1;
    }
    Py_ssize_t len = buf.len;
    /* ... */

    memcpy(PyBytes_AS_STRING(self->buf) + self->pos,  /* crashing pointer derived */ /* Crash site */
           buf.buf, len);
    /* ... */
    return len;
}

/* Clobbering Path */
static PyObject *
_io_BytesIO_close_impl(bytesio *self)
{
    CHECK_EXPORTS(self);
    Py_CLEAR(self->buf);  /* state mutate site */
    Py_RETURN_NONE;
}

Sanitizer Output:

Click to expand
AddressSanitizer:DEADLYSIGNAL
=================================================================
==2192404==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000010 (pc 0x5d4bc5d1b62d bp 0x7fff6c6ec680 sp 0x7fff6c6ec560 T0)
==2192404==The signal is caused by a READ memory access.
==2192404==Hint: address points to the zero page.
    #0 0x5d4bc5d1b62d in write_bytes_lock_held Modules/_io/bytesio.c:215
    #1 0x5d4bc5d1c0cf in _io_BytesIO_writelines_impl Modules/_io/bytesio.c:802
    #2 0x5d4bc5d1c0cf in _io_BytesIO_writelines Modules/_io/clinic/bytesio.c.h:547
    #3 0x5d4bc57213e7 in _PyObject_VectorcallTstate Include/internal/pycore_call.h:169
    #4 0x5d4bc57213e7 in PyObject_Vectorcall Objects/call.c:327
    #5 0x5d4bc55d55a2 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:1620
    #6 0x5d4bc5a9fad6 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
    #7 0x5d4bc5a9fad6 in _PyEval_Vector Python/ceval.c:2001
    #8 0x5d4bc5a9fad6 in PyEval_EvalCode Python/ceval.c:884
    #9 0x5d4bc5be516e in run_eval_code_obj Python/pythonrun.c:1365
    #10 0x5d4bc5be516e in run_mod Python/pythonrun.c:1459
    #11 0x5d4bc5be9e17 in pyrun_file Python/pythonrun.c:1293
    #12 0x5d4bc5be9e17 in _PyRun_SimpleFileObject Python/pythonrun.c:521
    #13 0x5d4bc5bea93c in _PyRun_AnyFileObject Python/pythonrun.c:81
    #14 0x5d4bc5c5de3c in pymain_run_file_obj Modules/main.c:410
    #15 0x5d4bc5c5de3c in pymain_run_file Modules/main.c:429
    #16 0x5d4bc5c5de3c in pymain_run_python Modules/main.c:691
    #17 0x5d4bc5c5f71e in Py_RunMain Modules/main.c:772
    #18 0x5d4bc5c5f71e in pymain_main Modules/main.c:802
    #19 0x5d4bc5c5f71e in Py_BytesMain Modules/main.c:826
    #20 0x7d5d7d22a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #21 0x7d5d7d22a28a in __libc_start_main_impl ../csu/libc-start.c:360
    #22 0x5d4bc55f9634 in _start (/home/jackfromeast/Desktop/entropy/targets/grammar-afl++-latest/targets/cpython/python+0x206634) (BuildId: 4d105290d0ad566a4d6f4f7b2f05fbc9e317b533)

AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV Modules/_io/bytesio.c:215 in write_bytes_lock_held
==2192404==ABORTING

CPython versions tested on:

3.12, 3.13, 3.14, 3.15

Output from running 'python -VV' on the command line:

Python 3.15.0a1+ (heads/main:f5394c257ce, Oct 28 2025, 19:29:54) [GCC 13.3.0]

Metadata

Metadata

Assignees

No one assigned

    Labels

    extension-modulesC modules in the Modules dirtopic-IOtype-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