From a1a71efa6e596363e6c42071f97b73a6b9e847f6 Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Wed, 29 Oct 2025 05:31:57 -0700 Subject: [PATCH] [3.13] gh-140607: Validate returned byte count in RawIOBase.read (GH-140611) (#140730) * [3.13] gh-140607: Validate returned byte count in RawIOBase.read (GH-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. (cherry picked from commit 0f0a362768aecb4c791724cce486d8317533a94d) Co-authored-by: Cody Maloney Co-authored-by: Shamil Co-authored-by: Victor Stinner * fixup: Use older attribute name --------- Co-authored-by: Shamil Co-authored-by: Victor Stinner --- Lib/_pyio.py | 2 ++ Lib/test/test_io.py | 16 ++++++++++++++++ ...025-10-25-21-04-00.gh-issue-140607.oOZGxS.rst | 2 ++ Modules/_io/iobase.c | 13 ++++++++++--- 4 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-10-25-21-04-00.gh-issue-140607.oOZGxS.rst diff --git a/Lib/_pyio.py b/Lib/_pyio.py index d65e32252123..48c8f770f81f 100644 --- a/Lib/_pyio.py +++ b/Lib/_pyio.py @@ -623,6 +623,8 @@ class RawIOBase(IOBase): 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.py b/Lib/test/test_io.py index 0da611a23cf0..5d83464a0dd4 100644 --- a/Lib/test/test_io.py +++ b/Lib/test/test_io.py @@ -869,6 +869,22 @@ class IOTest(unittest.TestCase): 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.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 000000000000..cc33217c9f56 --- /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 12f8b430f9ce..618652b371ab 100644 --- a/Modules/_io/iobase.c +++ b/Modules/_io/iobase.c @@ -931,14 +931,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; } -- 2.47.3