diff --git a/Lib/_pyio.py b/Lib/_pyio.py index 9ae72743919a32..423178e87a8684 100644 --- a/Lib/_pyio.py +++ b/Lib/_pyio.py @@ -617,6 +617,8 @@ def read(self, size=-1): n = self.readinto(b) if n is None: return None + if n < 0 or n > len(b): + raise ValueError(f"readinto returned {n} outside buffer size {len(b)}") del b[n:] return bytes(b) diff --git a/Lib/test/test_io/test_general.py b/Lib/test/test_io/test_general.py index ac9c5a425d7ea2..a1cdd6876c2892 100644 --- a/Lib/test/test_io/test_general.py +++ b/Lib/test/test_io/test_general.py @@ -592,6 +592,22 @@ def test_RawIOBase_read(self): self.assertEqual(rawio.read(2), None) 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): test = ( self.IOBase(), diff --git a/Misc/NEWS.d/next/Library/2025-10-25-21-04-00.gh-issue-140607.oOZGxS.rst b/Misc/NEWS.d/next/Library/2025-10-25-21-04-00.gh-issue-140607.oOZGxS.rst new file mode 100644 index 00000000000000..cc33217c9f563e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-10-25-21-04-00.gh-issue-140607.oOZGxS.rst @@ -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). diff --git a/Modules/_io/iobase.c b/Modules/_io/iobase.c index acadbcc4d59c38..e304fc8bee2bea 100644 --- a/Modules/_io/iobase.c +++ b/Modules/_io/iobase.c @@ -939,14 +939,21 @@ _io__RawIOBase_read_impl(PyObject *self, Py_ssize_t n) return res; } - n = PyNumber_AsSsize_t(res, PyExc_ValueError); + Py_ssize_t bytes_filled = PyNumber_AsSsize_t(res, PyExc_ValueError); Py_DECREF(res); - if (n == -1 && PyErr_Occurred()) { + if (bytes_filled == -1 && PyErr_Occurred()) { Py_DECREF(b); 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); return res; }