Skip to content
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`.
Comment thread
vstinner marked this conversation as resolved.
Outdated
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
Copy Markdown
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
Copy Markdown
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)

Comment thread
cmaloney marked this conversation as resolved.
Outdated

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