Skip to content

Null pointer dereference in BufferedWriter.seek during re-entrant close #143375

@jackfromeast

Description

@jackfromeast

What happened?

BufferedWriter.seek asks the offset object for __index__ before it flushes pending bytes, so a crafted type can close the writer during that conversion, leaving the buffered write state freed while _bufferedwriter_raw_write retries the partial write and dereferences the stale pointer.

Proof of Concept:

import io

class R:
    def __init__(self):
        self.calls = 0
        self.closed = False
    def writable(self): return True
    def seekable(self): return True
    def seek(self, off, whence=0): return 0
    def write(self, mv):
        self.calls += 1
        if self.calls == 1: return len(mv) - 1
        if self.calls == 2: raise BlockingIOError
        mv.tobytes()
        return len(mv)

bw = io.BufferedWriter(R())
bw.write(b"ABCD")

class T:
    def __index__(self):
        try: bw.close()
        except Exception: pass
        return 0

bw.seek(T())

Vulnerable Code Snippet:

Click to expand
/* Buggy Re-entrant Path */
static PyObject *
_io__Buffered_seek_impl(buffered *self, PyObject *targetobj, int whence)
{
    Py_off_t target;
    /* ... */
    target = PyNumber_AsOff_t(targetobj, PyExc_ValueError);  /* Reentrant call site */
    if (target == -1 && PyErr_Occurred())
        return NULL;
    /* ... */
    if (self->writable) {
        res = _bufferedwriter_flush_unlocked(self);
        /* ... */
    }
    /* ... */
}

static PyObject *
_bufferedwriter_flush_unlocked(buffered *self)
{
    /* ... */
    while (self->write_pos < self->write_end) {
        n = _bufferedwriter_raw_write(self,
            self->buffer + self->write_pos,  /* crashing pointer derived */
            Py_SAFE_DOWNCAST(self->write_end - self->write_pos,
                             Py_off_t, Py_ssize_t));
        /* ... */
    }
    /* ... */
}

static Py_ssize_t
_bufferedwriter_raw_write(buffered *self, char *start, Py_ssize_t len)
{
    Py_buffer buf;
    PyObject *memobj, *res;
    /* NOTE: the buffer needn't be released as its object is NULL. */
    if (PyBuffer_FillInfo(&buf, NULL, start, len, 1, PyBUF_CONTIG_RO) == -1)
        return -1;
    memobj = PyMemoryView_FromBuffer(&buf);
    /* ... raw.write(memobj) ... */
    return n;
}

int
PyBuffer_ToContiguous(void *buf, const Py_buffer *src, Py_ssize_t len, char order)
{
    if (PyBuffer_IsContiguous(src, order)) {
        memcpy((char *)buf, src->buf, len);  /* Crash site */
        return 0;
    }
    /* ... */
    return 0;
}

/* Clobbering Path */
static PyObject *
_io__Buffered_close_impl(buffered *self)
{
    /* ... */
    if (self->buffer) {
        PyMem_Free(self->buffer);  /* state mutate site */
        self->buffer = NULL;
    }
    /* ... */
    return res;
}

Sanitizer Output:

Click to expand
AddressSanitizer:DEADLYSIGNAL
=================================================================
==2188767==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000003 (pc 0x79e00cf88a84 bp 0x7ffd2d3db980 sp 0x7ffd2d3db928 T0)
==2188767==The signal is caused by a READ memory access.
==2188767==Hint: address points to the zero page.
    #0 0x79e00cf88a84 in __memmove_avx_unaligned_erms ../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S:318
    #1 0x654cee9496ed in memcpy /usr/include/x86_64-linux-gnu/bits/string_fortified.h:29
    #2 0x654cee9496ed in PyBuffer_ToContiguous Objects/memoryobject.c:1063
    #3 0x654cee949fd9 in memoryview_tobytes_impl Objects/memoryobject.c:2310
    #4 0x654cee949fd9 in memoryview_tobytes Objects/clinic/memoryobject.c.h:335
    #5 0x654cee83b3e7 in _PyObject_VectorcallTstate Include/internal/pycore_call.h:169
    #6 0x654cee83b3e7 in PyObject_Vectorcall Objects/call.c:327
    #7 0x654cee6ef5a2 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:1620
    #8 0x654ceebba2a5 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
    #9 0x654ceebba2a5 in _PyEval_Vector Python/ceval.c:2001
    #10 0x654cee83d863 in _PyObject_VectorcallTstate Include/internal/pycore_call.h:169
    #11 0x654cee83d863 in PyObject_VectorcallMethod Objects/call.c:859
    #12 0x654ceee3bfd5 in PyObject_CallMethodOneArg Include/cpython/abstract.h:74
    #13 0x654ceee3bfd5 in _bufferedwriter_raw_write Modules/_io/bufferedio.c:1985
    #14 0x654ceee3d3b6 in _bufferedwriter_flush_unlocked Modules/_io/bufferedio.c:2029
    #15 0x654ceee4613e in _io__Buffered_seek_impl Modules/_io/bufferedio.c:1425
    #16 0x654ceee4613e in _io__Buffered_seek Modules/_io/clinic/bufferedio.c.h:885
    #17 0x654cee83b3e7 in _PyObject_VectorcallTstate Include/internal/pycore_call.h:169
    #18 0x654cee83b3e7 in PyObject_Vectorcall Objects/call.c:327
    #19 0x654cee6ef5a2 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:1620
    #20 0x654ceebb9ad6 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
    #21 0x654ceebb9ad6 in _PyEval_Vector Python/ceval.c:2001
    #22 0x654ceebb9ad6 in PyEval_EvalCode Python/ceval.c:884
    #23 0x654ceecff16e in run_eval_code_obj Python/pythonrun.c:1365
    #24 0x654ceecff16e in run_mod Python/pythonrun.c:1459
    #25 0x654ceed03e17 in pyrun_file Python/pythonrun.c:1293
    #26 0x654ceed03e17 in _PyRun_SimpleFileObject Python/pythonrun.c:521
    #27 0x654ceed0493c in _PyRun_AnyFileObject Python/pythonrun.c:81
    #28 0x654ceed77e3c in pymain_run_file_obj Modules/main.c:410
    #29 0x654ceed77e3c in pymain_run_file Modules/main.c:429
    #30 0x654ceed77e3c in pymain_run_python Modules/main.c:691
    #31 0x654ceed7971e in Py_RunMain Modules/main.c:772
    #32 0x654ceed7971e in pymain_main Modules/main.c:802
    #33 0x654ceed7971e in Py_BytesMain Modules/main.c:826
    #34 0x79e00ce2a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #35 0x79e00ce2a28a in __libc_start_main_impl ../csu/libc-start.c:360
    #36 0x654cee713634 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 ../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S:318 in __memmove_avx_unaligned_erms
==2188767==ABORTING

CPython versions tested on:

Details
Python Version Status Exit Code
Python 3.9.24+ (heads/3.9:111bbc15b26, Oct 28 2025, 16:51:20) ASAN 1
Python 3.10.19+ (heads/3.10:014261980b1, Oct 28 2025, 16:52:08) [Clang 18.1.3 (1ubuntu1)] ASAN 1
Python 3.11.14+ (heads/3.11:88f3f5b5f11, Oct 28 2025, 16:53:08) [Clang 18.1.3 (1ubuntu1)] ASAN 1
Python 3.12.12+ (heads/3.12:8cb2092bd8c, Oct 28 2025, 16:54:14) [Clang 18.1.3 (1ubuntu1)] ASAN 1
Python 3.13.9+ (heads/3.13:9c8eade20c6, Oct 28 2025, 16:55:18) [Clang 18.1.3 (1ubuntu1)] ASAN 1
Python 3.14.0+ (heads/3.14:2e216728038, Oct 28 2025, 16:56:16) [Clang 18.1.3 (1ubuntu1)] ASAN 1
Python 3.15.0a1+ (heads/main:f5394c257ce, Oct 28 2025, 19:29:54) [GCC 13.3.0] ASAN 1

Operating systems tested on:

Linux

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