Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fix(io): scope UAF patch to TextIOWrapper
  • Loading branch information
ashm-dev committed Oct 6, 2026
commit 7e1eedd3b62ca90cf93d4d8003f81f03437e25dc
21 changes: 0 additions & 21 deletions Lib/test/test_io/test_bufferedio.py
Original file line number Diff line number Diff line change
Expand Up @@ -607,27 +607,6 @@ def test_args_error(self):
with self.assertRaisesRegex(TypeError, "BufferedReader"):
self.tp(self.BytesIO(), 1024, 1024, 1024)

def test_readinto_buffer_keeps_reader_alive(self):
# gh-157364: raw.readinto() may store the memoryview it is given, so
# the memoryview must keep the BufferedReader owning its memory alive.
stored = []

class StoringRawIO(self.MockRawIO):
def readinto(self, b):
stored.append(b)
return super().readinto(b)

bufio = self.tp(StoringRawIO([b"abc"]))
self.assertEqual(bufio.read(3), b"abc")
self.assertIs(stored[0].obj, bufio)
wr = weakref.ref(bufio)
del bufio
support.gc_collect()
self.assertIsNotNone(wr())
stored.clear()
support.gc_collect()
self.assertIsNone(wr())

def test_bad_readinto_value(self):
rawio = self.tp(self.BytesIO(b"12"))
rawio.readinto = lambda buf: -1
Expand Down
3 changes: 3 additions & 0 deletions Lib/test/test_io/test_textio.py
Original file line number Diff line number Diff line change
Expand Up @@ -1659,6 +1659,9 @@ def readinto(self, b):
self.BufferedReader(raw), encoding="utf-8")
method = getattr(wrapper, method_name)
self.assertEqual(method(), "ab\n")
Comment thread
ashm-dev marked this conversation as resolved.
with self.assertRaisesRegex(ValueError,
"underlying buffer has been detached"):
wrapper.buffer
Comment thread
vstinner marked this conversation as resolved.
Outdated

def test_reentrant_seek_during_tell(self):
# gh-153539: reading short of _CHUNK_SIZE leaves residual bytes in the
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
Fix a use-after-free in :class:`io.TextIOWrapper` when a call to the
underlying buffer reentrantly detaches it. The :class:`memoryview` that
:class:`io.BufferedReader` passes to the raw stream's ``readinto()`` now keeps
the reader alive. Patched by Shamil Abdulaev.
underlying buffer reentrantly detaches it. Patched by Shamil Abdulaev.
24 changes: 5 additions & 19 deletions Modules/_io/bufferedio.c
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
#include "Python.h"
#include "pycore_call.h" // _PyObject_CallNoArgs()
#include "pycore_fileutils.h" // _PyFile_Flush
#include "pycore_memoryobject.h" // _PyMemoryView_FromBufferProc()
#include "pycore_object.h" // _PyObject_GC_UNTRACK()
#include "pycore_pyerrors.h" // _Py_FatalErrorFormat()
#include "pycore_pylifecycle.h" // _Py_IsInterpreterFinalizing()
Expand Down Expand Up @@ -255,10 +254,6 @@ typedef struct {
isn't ready for writing. */
Py_off_t write_end;

/* Region handed to the raw stream by _bufferedreader_raw_read(). */
char *raw_read_start;
Py_ssize_t raw_read_len;

PyThread_type_lock lock;
volatile unsigned long owner;

Expand Down Expand Up @@ -1624,25 +1619,16 @@ _io_BufferedReader___init___impl(buffered *self, PyObject *raw,
return 0;
}

/* The memoryview passed to raw.readinto() owns a reference to the buffered
object so that storing it in Python code cannot outlive its memory. */
static int
_bufferedreader_raw_read_getbuffer(PyObject *op, Py_buffer *view, int flags)
{
buffered *self = buffered_CAST(op);
return PyBuffer_FillInfo(view, op, self->raw_read_start,
self->raw_read_len, 0, flags);
}

static Py_ssize_t
_bufferedreader_raw_read(buffered *self, char *start, Py_ssize_t len)
{
Py_buffer buf;
PyObject *memobj, *res;
Py_ssize_t n;
self->raw_read_start = start;
self->raw_read_len = len;
memobj = _PyMemoryView_FromBufferProc((PyObject *)self, PyBUF_CONTIG,
_bufferedreader_raw_read_getbuffer);
/* NOTE: the buffer needn't be released as its object is NULL. */
if (PyBuffer_FillInfo(&buf, NULL, start, len, 0, PyBUF_CONTIG) == -1)
return -1;
memobj = PyMemoryView_FromBuffer(&buf);
if (memobj == NULL)
return -1;
/* NOTE: PyErr_SetFromErrno() calls PyErr_CheckSignals() when EINTR
Expand Down
Loading