]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-140607: Validate returned byte count in RawIOBase.read (#140611)
authorCody Maloney <cmaloney@users.noreply.github.com>
Mon, 27 Oct 2025 18:06:46 +0000 (11:06 -0700)
committerGitHub <noreply@github.com>
Mon, 27 Oct 2025 18:06:46 +0000 (18:06 +0000)
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.

Co-authored-by: Shamil <ashm.tech@proton.me>
Co-authored-by: Victor Stinner <vstinner@python.org>
Lib/_pyio.py
Lib/test/test_io/test_general.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 9ae72743919a329dbf02a332d94de032bd069b38..423178e87a868441afc1912aafc414342b3dd227 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 ac9c5a425d7ea2a0b5653b0815394becbe279a2e..a1cdd6876c28924f7b605284e67fcfe8bc021256 100644 (file)
@@ -592,6 +592,22 @@ class IOTest:
         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 (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 acadbcc4d59c3838e1e82804063f3360da26c934..e304fc8bee2beadd6287fd9300741b59159fc9e2 100644 (file)
@@ -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;
 }