]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.13] gh-140607: Validate returned byte count in RawIOBase.read (GH-140611) (#140730)
authorCody Maloney <cmaloney@users.noreply.github.com>
Wed, 29 Oct 2025 12:31:57 +0000 (05:31 -0700)
committerGitHub <noreply@github.com>
Wed, 29 Oct 2025 12:31:57 +0000 (13:31 +0100)
* [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 <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 d65e32252123ca92911e81bc62e1cb7e99014a3a..48c8f770f81f1b9de67d0ad2b96ff0beaccb4d1a 100644 (file)
@@ -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)
 
index 0da611a23cf070d81de39f8c4401307bc95f152e..5d83464a0dd44046cd337168f76cb41ff4092d47 100644 (file)
@@ -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 (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 12f8b430f9ceb1c970078df7bbb58bb9b82104e0..618652b371abe232db57b8f6f12cdcae12aea375 100644 (file)
@@ -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;
 }