]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.14] gh-140607: Validate returned byte count in RawIOBase.read (GH-140611) (#140728)
authorCody Maloney <cmaloney@users.noreply.github.com>
Wed, 29 Oct 2025 12:31:48 +0000 (05:31 -0700)
committerGitHub <noreply@github.com>
Wed, 29 Oct 2025 12:31:48 +0000 (13:31 +0100)
* [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 <cmaloney@users.noreply.github.com>
Co-authored-by: Shamil <ashm.tech@proton.me>
Co-authored-by: Victor Stinner <vstinner@python.org>
* fixup: Use older attribute name

---------

Co-authored-by: Shamil <ashm.tech@proton.me>
Co-authored-by: Victor Stinner <vstinner@python.org>
Lib/_pyio.py
Lib/test/test_io.py
Misc/NEWS.d/next/Library/2025-10-25-21-04-00.gh-issue-140607.oOZGxS.rst [new file with mode: 0644]
Modules/_io/iobase.c

index fb2a6d049caab66d0fdd63311a1733582a520e6b..612e4a175e5a658effadd6b80c987da85967b6a4 100644 (file)
@@ -617,6 +617,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)
 
index 925fe7771611b58ae7a84d9559b5072b857b7862..2ed95530ba534a6da7866047c535dd8f32516a52 100644 (file)
@@ -893,6 +893,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 (file)
index 0000000..cc33217
--- /dev/null
@@ -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).
index 044f6b7803c5716dd110bd5ae9a7fa94b534bcad..17a9d2800e9d7a7a9e09641cc5bc5a73f15319f5 100644 (file)
@@ -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;
 }