gh-140607: Validate returned byte count in RawIOBase.read (#140611)

While `RawIOBase.readinto` should return a count of bytes between 0 and
the length of the given buffer, it is not required to. Add validation
inside RawIOBase.read() that the returned byte count is valid.

Co-authored-by: Shamil <ashm.tech@proton.me>
Co-authored-by: Victor Stinner <vstinner@python.org>
This commit is contained in:
Cody Maloney
2025-10-27 11:06:46 -07:00
committed by GitHub
parent 313145eab5
commit 0f0a362768
4 changed files with 30 additions and 3 deletions

View File

@@ -617,6 +617,8 @@ class RawIOBase(IOBase):
n = self.readinto(b) n = self.readinto(b)
if n is None: if n is None:
return None return None
if n < 0 or n > len(b):
raise ValueError(f"readinto returned {n} outside buffer size {len(b)}")
del b[n:] del b[n:]
return bytes(b) return bytes(b)

View File

@@ -592,6 +592,22 @@ class IOTest:
self.assertEqual(rawio.read(2), None) self.assertEqual(rawio.read(2), None)
self.assertEqual(rawio.read(2), b"") self.assertEqual(rawio.read(2), b"")
def test_RawIOBase_read_bounds_checking(self):
# Make sure a `.readinto` call which returns a value outside
# (0, len(buffer)) raises.
class Misbehaved(self.io.RawIOBase):
def __init__(self, readinto_return) -> None:
self._readinto_return = readinto_return
def readinto(self, b):
return self._readinto_return
with self.assertRaises(ValueError) as cm:
Misbehaved(2).read(1)
self.assertEqual(str(cm.exception), "readinto returned 2 outside buffer size 1")
for bad_size in (2147483647, sys.maxsize, -1, -1000):
with self.assertRaises(ValueError):
Misbehaved(bad_size).read()
def test_types_have_dict(self): def test_types_have_dict(self):
test = ( test = (
self.IOBase(), self.IOBase(),

View File

@@ -0,0 +1,2 @@
Inside :meth:`io.RawIOBase.read`, validate that the count of bytes returned by
:meth:`io.RawIOBase.readinto` is valid (inside the provided buffer).

View File

@@ -939,14 +939,21 @@ _io__RawIOBase_read_impl(PyObject *self, Py_ssize_t n)
return res; return res;
} }
n = PyNumber_AsSsize_t(res, PyExc_ValueError); Py_ssize_t bytes_filled = PyNumber_AsSsize_t(res, PyExc_ValueError);
Py_DECREF(res); Py_DECREF(res);
if (n == -1 && PyErr_Occurred()) { if (bytes_filled == -1 && PyErr_Occurred()) {
Py_DECREF(b); Py_DECREF(b);
return NULL; return NULL;
} }
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;
}
res = PyBytes_FromStringAndSize(PyByteArray_AsString(b), n); res = PyBytes_FromStringAndSize(PyByteArray_AsString(b), bytes_filled);
Py_DECREF(b); Py_DECREF(b);
return res; return res;
} }