python / python/cpython

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

Open
#143,375 0 comments 0 reactions 1 assignee View on GitHub

@tomasr8 is already working on this.

Since Jan 3, 2026.

3.13 3.14 3.15 extension-modules topic-IO type-crash
Dominant language
Python
Stars
77.2k
Forks
35.9k
PR merge metrics
PR metrics pending

Description

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:
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]

Linked PRs
  • gh-143577

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.