From f9917c23ed0cd0b9a57404abd4d71aa015aa14a3 Mon Sep 17 00:00:00 2001 From: Cody Maloney Date: Sat, 25 Oct 2025 21:09:20 -0700 Subject: [PATCH] gh-104607: Validate returned byte count in RawIOBase.read 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 reasonable. --- Lib/_pyio.py | 2 ++ Lib/test/test_io/test_general.py | 17 +++++++++++++++++ ...25-10-25-21-04-00.gh-issue-104607.oOZGxS.rst | 2 ++ Modules/_io/iobase.c | 13 ++++++++++--- 4 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-10-25-21-04-00.gh-issue-104607.oOZGxS.rst diff --git a/Lib/_pyio.py b/Lib/_pyio.py index 9ae72743919a32..8ae2de51184708 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..6a502f14e536ba 100644 --- a/Lib/test/test_io/test_general.py +++ b/Lib/test/test_io/test_general.py @@ -592,6 +592,23 @@ 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 oustside + # (0, len(buffer)) raises. + class Misbehaved(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' oustside buffer size '1'") + self.assertRaises(ValueError, Misbehaved(2147483647).read) + self.assertRaises(ValueError, Misbehaved(sys.maxsize).read) + self.assertRaises(ValueError, Misbehaved(-1).read) + self.assertRaises(ValueError, Misbehaved(-1000).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-104607.oOZGxS.rst b/Misc/NEWS.d/next/Library/2025-10-25-21-04-00.gh-issue-104607.oOZGxS.rst new file mode 100644 index 00000000000000..eb0e542488f8dd --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-10-25-21-04-00.gh-issue-104607.oOZGxS.rst @@ -0,0 +1,2 @@ +Inside :meth:`io.RawIOBase.read` validate that the count of bytes returned by +:meth:`io.RawIOBase.readinto` is reasonable (inside the provided buffer). diff --git a/Modules/_io/iobase.c b/Modules/_io/iobase.c index acadbcc4d59c38..e62dbeded16950 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 (readinto_len == -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' oustside 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; }