https://github.com/python/cpython/commit/edabbff90bc61029068f671547de23f9fd297627
commit: edabbff90bc61029068f671547de23f9fd297627
branch: 3.13
author: Victor Stinner <[email protected]>
committer: vstinner <[email protected]>
date: 2026-09-23T21:14:26Z
summary:

[3.13] gh-157335: Fix out-of-bounds write in mmap.mmap.__setitem__ (#157438) 
(#158028)

gh-157335: Fix out-of-bounds write in mmap.mmap.__setitem__ (#157438)

Fix out-of-bounds write in mmap.mmap.__setitem__() that could occur
when converting the index or the assigned value (via __index__()
for a single item, or via the buffer protocol for a slice) resized or closed
the mmap object during the assignment.


(cherry picked from commit 09bf4c525871db3ad02c8a1359da5cf0668f27b0)

Co-authored-by: Joseph Kerry <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-09-13-15-58-28.gh-issue-157335.efaMah.rst
M Lib/test/test_mmap.py
M Modules/mmapmodule.c

diff --git a/Lib/test/test_mmap.py b/Lib/test/test_mmap.py
index 1d91cee994ef10..4a1375d957714e 100644
--- a/Lib/test/test_mmap.py
+++ b/Lib/test/test_mmap.py
@@ -71,7 +71,7 @@ def test_basic(self):
 
         # Shouldn't crash on boundary (Issue #5292)
         self.assertRaises(IndexError, m.__getitem__, len(m))
-        self.assertRaises(IndexError, m.__setitem__, len(m), b'\0')
+        self.assertRaises(IndexError, m.__setitem__, len(m), 0)
 
         # Modify the file's content
         m[0] = b'3'[0]
@@ -964,6 +964,55 @@ def test_resize_down_anonymous_mapping(self):
                     with self.assertRaises(ValueError):
                         m.resize(start_size)
 
+    @unittest.skipUnless(hasattr(mmap.mmap, 'resize'), 'requires mmap.resize')
+    def test_setitem_resize_reentrancy(self):
+        """Resizing the mmap from inside __index__ while assigning to a
+        single item must not access memory past the new bounds (gh-157335).
+        """
+        size = 2 * PAGESIZE
+        new_size = PAGESIZE
+
+        class ResizeOnIndex:
+            def __init__(self, m):
+                self.m = m
+            def __index__(self):
+                self.m.resize(new_size)
+                return 0
+
+        with mmap.mmap(-1, size) as m:
+            try:
+                with self.assertRaises(IndexError):
+                    m[size - 1] = ResizeOnIndex(m)
+            except SystemError as exc:
+                self.skipTest(f"resize() is not available: {exc!r}")
+            self.assertEqual(len(m), new_size)
+
+    @unittest.skipUnless(hasattr(mmap.mmap, 'resize'), 'requires mmap.resize')
+    def test_setitem_slice_resize_reentrancy(self):
+        """Resizing the mmap from inside a value's buffer-protocol
+        callback while assigning to a slice must not access memory past
+        the new bounds (gh-157335).
+        """
+        size = 2 * PAGESIZE
+        new_size = PAGESIZE
+
+        class ResizeOnBuffer:
+            def __init__(self, m, data):
+                self.m = m
+                self.data = data
+            def __buffer__(self, flags):
+                self.m.resize(new_size)
+                return memoryview(self.data)
+
+        with mmap.mmap(-1, size) as m:
+            value = ResizeOnBuffer(m, bytes(size))
+            try:
+                with self.assertRaises(IndexError):
+                    m[0:size] = value
+            except SystemError as exc:
+                self.skipTest(f"resize() is not available: {exc!r}")
+            self.assertEqual(len(m), new_size)
+
     @unittest.skipUnless(os.name == 'nt', 'requires Windows')
     def test_resize_fails_if_mapping_held_elsewhere(self):
         """If more than one mapping is held against a named file on Windows, 
neither
diff --git 
a/Misc/NEWS.d/next/Library/2026-09-13-15-58-28.gh-issue-157335.efaMah.rst 
b/Misc/NEWS.d/next/Library/2026-09-13-15-58-28.gh-issue-157335.efaMah.rst
new file mode 100644
index 00000000000000..fb0bcad3060564
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-09-13-15-58-28.gh-issue-157335.efaMah.rst
@@ -0,0 +1,4 @@
+Fix out-of-bounds write in ``mmap.mmap.__setitem__`` that could occur
+when converting the index or the assigned value (via :meth:`~object.__index__`
+for a single item, or via the buffer protocol for a slice) resized or closed 
the mmap
+object during the assignment.
diff --git a/Modules/mmapmodule.c b/Modules/mmapmodule.c
index 060f60c666964b..ff86c44d3aaec3 100644
--- a/Modules/mmapmodule.c
+++ b/Modules/mmapmodule.c
@@ -1347,24 +1347,14 @@ mmap_ass_item(mmap_object *self, Py_ssize_t i, PyObject 
*v)
 static int
 mmap_ass_subscript(mmap_object *self, PyObject *item, PyObject *value)
 {
-    CHECK_VALID(-1);
-
     if (!is_writable(self))
         return -1;
 
     if (PyIndex_Check(item)) {
         Py_ssize_t i = PyNumber_AsSsize_t(item, PyExc_IndexError);
-        Py_ssize_t v;
-
         if (i == -1 && PyErr_Occurred())
             return -1;
-        if (i < 0)
-            i += self->size;
-        if (i < 0 || i >= self->size) {
-            PyErr_SetString(PyExc_IndexError,
-                            "mmap index out of range");
-            return -1;
-        }
+
         if (value == NULL) {
             PyErr_SetString(PyExc_TypeError,
                             "mmap doesn't support item deletion");
@@ -1375,7 +1365,7 @@ mmap_ass_subscript(mmap_object *self, PyObject *item, 
PyObject *value)
                             "mmap item value must be an int");
             return -1;
         }
-        v = PyNumber_AsSsize_t(value, PyExc_TypeError);
+        Py_ssize_t v = PyNumber_AsSsize_t(value, PyExc_TypeError);
         if (v == -1 && PyErr_Occurred())
             return -1;
         if (v < 0 || v > 255) {
@@ -1384,7 +1374,18 @@ mmap_ass_subscript(mmap_object *self, PyObject *item, 
PyObject *value)
                             "in range(0, 256)");
             return -1;
         }
+
+        /* Converting item or value above may have run arbitrary code
+         * (e.g. __index__) that resized or closed the mmap, so bounds
+         * are only checked now, against the current size. */
         CHECK_VALID(-1);
+        if (i < 0)
+            i += self->size;
+        if (i < 0 || i >= self->size) {
+            PyErr_SetString(PyExc_IndexError,
+                            "mmap index out of range");
+            return -1;
+        }
 
         char v_char = (char) v;
         if (safe_byte_copy(self->data + i, &v_char) < 0) {
@@ -1399,7 +1400,6 @@ mmap_ass_subscript(mmap_object *self, PyObject *item, 
PyObject *value)
         if (PySlice_Unpack(item, &start, &stop, &step) < 0) {
             return -1;
         }
-        slicelen = PySlice_AdjustIndices(self->size, &start, &stop, step);
         if (value == NULL) {
             PyErr_SetString(PyExc_TypeError,
                 "mmap object doesn't support slice deletion");
@@ -1407,6 +1407,12 @@ mmap_ass_subscript(mmap_object *self, PyObject *item, 
PyObject *value)
         }
         if (PyObject_GetBuffer(value, &vbuf, PyBUF_SIMPLE) < 0)
             return -1;
+
+        /* Acquiring the buffer above may have run arbitrary code (e.g. a
+         * __buffer__ method) that resized or closed this mmap, so the slice 
bounds
+         * are only computed now, against the current size. */
+        CHECK_VALID_OR_RELEASE(-1, vbuf);
+        slicelen = PySlice_AdjustIndices(self->size, &start, &stop, step);
         if (vbuf.len != slicelen) {
             PyErr_SetString(PyExc_IndexError,
                 "mmap slice assignment is wrong size");
@@ -1414,7 +1420,6 @@ mmap_ass_subscript(mmap_object *self, PyObject *item, 
PyObject *value)
             return -1;
         }
 
-        CHECK_VALID_OR_RELEASE(-1, vbuf);
         int result = 0;
         if (slicelen == 0) {
         }

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to