Skip to content

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

Closed

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]

Linked PRs

Activity

  1. added a commit that references this issue on Jan 4, 2026
  2. superboy-zjc commented on Jan 4, 2026

    @superboy-zjc
    Contributor

    PyObject_GetBuffer() can execute user code (e.g. via __buffer__), which may close or otherwise mutate a BytesIO object while write() or writelines() is in progress. This could invalidate the internal buffer and lead to a use-after-free.

    I've proposed a PR here: #143408. Temporarily bump the exports counter while acquiring the input buffer to block re-entrant mutation, and add regression tests to ensure such cases raise BufferError instead of crashing.

  3. added 8 commits that reference this issue on Jan 4, 2026
  4. added a commit that references this issue on Jan 9, 2026
  5. added a commit that references this issue on Jan 9, 2026
  6. added a commit that references this issue on Jan 9, 2026
  7. added 2 commits that reference this issue on Jan 9, 2026
  8. added a commit that references this issue on Jan 12, 2026
  9. added a commit that references this issue on Feb 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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