From 9f114fa9f9405f5a302d875c28f91b70edacb5ff Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Mon, 27 Oct 2025 11:06:46 -0700 Subject: [PATCH 1/2] [3.14] 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 --- 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 fb2a6d049caab6..612e4a175e5a65 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.py b/Lib/test/test_io.py index 925fe7771611b5..b4d755b35099fb 100644 --- a/Lib/test/test_io.py +++ b/Lib/test/test_io.py @@ -893,6 +893,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 044f6b7803c571..17a9d2800e9d7a 100644 --- a/Modules/_io/iobase.c +++ b/Modules/_io/iobase.c @@ -938,14 +938,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; } From d7ea5eff573540d3aab55888b7f76f2f515bd483 Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Tue, 28 Oct 2025 15:04:27 -0700 Subject: [PATCH 2/2] fixup: Use older attribute name --- Lib/test/test_io.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_io.py b/Lib/test/test_io.py index b4d755b35099fb..2ed95530ba534a 100644 --- a/Lib/test/test_io.py +++ b/Lib/test/test_io.py @@ -896,7 +896,7 @@ def test_RawIOBase_read(self): 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): + class Misbehaved(self.RawIOBase): def __init__(self, readinto_return) -> None: self._readinto_return = readinto_return def readinto(self, b):