]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.15] gh-153570: Fix use-after-free in bytearray.take_bytes() with a reentrant __ind...
authorMiss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
Fri, 24 Jul 2026 22:06:49 +0000 (00:06 +0200)
committerGitHub <noreply@github.com>
Fri, 24 Jul 2026 22:06:49 +0000 (00:06 +0200)
gh-153570: Fix use-after-free in bytearray.take_bytes() with a reentrant __index__ (GH-153572)

bytearray.take_bytes() cached the size before the argument's __index__ call and
used it for the bounds check and the buffer reads, so an __index__ that resizes
the bytearray left it reading freed memory. Re-read the size after __index__,
matching bytearray.__setitem__ (GH-91153).
(cherry picked from commit ee21d992d6117eb7f0c6087ecb93d90cd0ed610c)

Co-authored-by: tonghuaroot (童话) <tonghuaroot@gmail.com>
Lib/test/test_bytes.py
Misc/NEWS.d/next/Core_and_Builtins/2026-07-11-15-30-00.gh-issue-153570.Kb7Xd2.rst [new file with mode: 0644]
Objects/bytearrayobject.c

index e3e703fef1eb5d607c8c83bd47d8dcb1ec3b226c..720b38cb508cbe673275c59c3e21d1afc17ac6e0 100644 (file)
@@ -1637,6 +1637,35 @@ class ByteArrayTest(BaseBytesTest, unittest.TestCase):
         bytes_header_size = sys.getsizeof(b'')
         self.assertEqual(ba.__alloc__(), 499 + bytes_header_size)
 
+    def test_take_bytes_reentrant_resize(self):
+        # gh-153570: n.__index__() can resize the bytearray, so take_bytes()
+        # must re-read the size afterwards.  It cached the size before the
+        # call and used it for the bounds check and the buffer reads, so a
+        # reentrant clear() returned freed memory (a use-after-free read).
+        def take(target, resize, n):
+            class Evil:
+                def __index__(self):
+                    resize(target)
+                    return n
+            return target.take_bytes(Evil())
+
+        # clear() during __index__: nothing is left to take.
+        ba = bytearray(b'abcdefgh')
+        with self.assertRaises(IndexError):
+            take(ba, lambda b: b.clear(), 8)
+        self.assertEqual(ba, b'')
+
+        # shrink during __index__: n past the new size is out of range.
+        ba = bytearray(b'abcdefgh')
+        with self.assertRaises(IndexError):
+            take(ba, lambda b: b.__delitem__(slice(4, None)), 8)
+        self.assertEqual(ba, b'abcd')
+
+        # grow during __index__: the take runs against the new, larger size.
+        ba = bytearray(b'abcd')
+        self.assertEqual(take(ba, lambda b: b.extend(b'efgh'), 8), b'abcdefgh')
+        self.assertEqual(ba, b'')
+
     def test_setitem(self):
         def setitem_as_mapping(b, i, val):
             b[i] = val
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-07-11-15-30-00.gh-issue-153570.Kb7Xd2.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-11-15-30-00.gh-issue-153570.Kb7Xd2.rst
new file mode 100644 (file)
index 0000000..655fd61
--- /dev/null
@@ -0,0 +1,2 @@
+Fix a use-after-free in :meth:`bytearray.take_bytes` when the argument's
+:meth:`~object.__index__` method resizes the bytearray. Patch by tonghuaroot.
index 70e9e87210b60b87d3194cb2814fd5f3f81fff0c..b16fb8751f5f73f5b56206d87d7efa884187525a 100644 (file)
@@ -1576,6 +1576,8 @@ bytearray_take_bytes_impl(PyByteArrayObject *self, PyObject *n)
         if (to_take == -1 && PyErr_Occurred()) {
             return NULL;
         }
+        // n.__index__() may have resized self; use the current size.
+        size = Py_SIZE(self);
         if (to_take < 0) {
             to_take += size;
         }