Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
19 changes: 19 additions & 0 deletions Lib/test/test_io/test_general.py
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,25 @@ def readinto(self, b):
with self.assertRaises(ValueError):
Misbehaved(bad_size).read()

def test_RawIOBase_read_gh60107(self):
# gh-60107: Ensure a "Raw I/O" which keeps a reference to the
# mutable memory doesn't allow making a mutable bytes.
class RawIOKeepsReference(self.MockRawIOWithoutRead):
def __init__(self, *args, **kwargs):
self.buf = None
super().__init__(*args, **kwargs)

def readinto(self, buf):
# buf is the bytearray so keeping a reference to it doesn't keep
# the memory alive; a memoryview does.
self.buf = memoryview(buf)
buf[0:4] = self._read_stack.pop()
return 3

with self.assertRaises(BufferError):
rawio = RawIOKeepsReference([b"1234"])
rawio.read(4)

def test_types_have_dict(self):
test = (
self.IOBase(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Remove a copy from :meth:`io.RawIOBase.read`. If the underlying I/O class
keeps a reference to the mutable memory raise a :exc:`BufferError`.
22 changes: 11 additions & 11 deletions Modules/_io/iobase.c
Original file line number Diff line number Diff line change
Expand Up @@ -927,33 +927,33 @@ _io__RawIOBase_read_impl(PyObject *self, Py_ssize_t n)
return PyObject_CallMethodNoArgs(self, &_Py_ID(readall));
}

/* TODO: allocate a bytes object directly instead and manually construct
a writable memoryview pointing to it. */
b = PyByteArray_FromStringAndSize(NULL, n);
if (b == NULL)
if (b == NULL) {
return NULL;
}

res = PyObject_CallMethodObjArgs(self, &_Py_ID(readinto), b, NULL);
if (res == NULL || res == Py_None) {
Py_DECREF(b);
return res;
goto cleanup;
}

Py_ssize_t bytes_filled = PyNumber_AsSsize_t(res, PyExc_ValueError);
Py_DECREF(res);
Py_CLEAR(res);
if (bytes_filled == -1 && PyErr_Occurred()) {
Py_DECREF(b);
return NULL;
goto cleanup;
}
if (bytes_filled < 0 || bytes_filled > n) {
Py_DECREF(b);
PyErr_Format(PyExc_ValueError,
"readinto returned %zd outside buffer size %zd",
bytes_filled, n);
return NULL;
goto cleanup;
}
if (PyByteArray_Resize(b, bytes_filled) < 0) {
goto cleanup;
}
res = PyObject_CallMethod(b, "take_bytes", NULL);
Copy link
Contributor Author

@cmaloney cmaloney Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vstinner : Not sure how common this "resize/discard then take_bytes" is going to be; might make sense to change to take_bytes(n=None, /, *, discard=False)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for now planning to keep that in back pocket until need many ways (ba.resize(n) or del ba[:n] gives the same capability)


res = PyBytes_FromStringAndSize(PyByteArray_AsString(b), bytes_filled);
cleanup:
Py_DECREF(b);
return res;
}
Expand Down
Loading