From c9b2513b57c996aa1d54659ef80cf504619db8ba Mon Sep 17 00:00:00 2001 From: Joekrry Date: Sun, 13 Sep 2026 16:11:38 +0100 Subject: [PATCH 1/9] gh-157335: Fixed out of bounds write in mmap.mmap.__setitem__. Assigning to a single index causes mmap.mmap.__setitem__ to validate the index against the object's size, which then converts the assigned value via PyNumber_AsSsize_t(). This invokes arbitrary python code via __index__(). mmap.resize(), which shrinks mapping could point past the end fo the new buffer causing an out of bounds error write. --- Lib/test/test_mmap.py | 18 ++++++++++++++++++ ...6-09-13-15-58-28.gh-issue-157335.efaMah.rst | 3 +++ Modules/mmapmodule.c | 7 +++++++ 3 files changed, 28 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-09-13-15-58-28.gh-issue-157335.efaMah.rst diff --git a/Lib/test/test_mmap.py b/Lib/test/test_mmap.py index 053a4ca4db53a57..5726ab84ee53c7f 100644 --- a/Lib/test/test_mmap.py +++ b/Lib/test/test_mmap.py @@ -953,6 +953,24 @@ def test_resize_down_anonymous_mapping(self): with self.assertRaises(ValueError): m.resize(start_size) + 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: + with self.assertRaises(IndexError): + m[size - 1] = ResizeOnIndex(m) + 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 000000000000000..090c6a9f91be934 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-13-15-58-28.gh-issue-157335.efaMah.rst @@ -0,0 +1,3 @@ +Fix out-of-bounds write in :meth:`mmap.mmap.__setitem__` that could occur +when the assigned value's :meth:`~object.__index__` method resized the +mmap object during the assignment. diff --git a/Modules/mmapmodule.c b/Modules/mmapmodule.c index 58f1e3b2ddcca70..4dc10b4aca65c76 100644 --- a/Modules/mmapmodule.c +++ b/Modules/mmapmodule.c @@ -1687,6 +1687,13 @@ mmap_ass_subscript_lock_held(PyObject *op, PyObject *item, PyObject *value) return -1; } CHECK_VALID(-1); + /* value's __index__ may have resized the mmap, invalidating + * the earlier bounds check on i. */ + if (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) { From 40952262e48dd7687948b69d48807a67d8401e13 Mon Sep 17 00:00:00 2001 From: N1077433 Date: Tue, 15 Sep 2026 13:06:42 +0100 Subject: [PATCH 2/9] gh-157335: Address review feedback on mmap setitem reentrancy fix Fix the same reentrancy issue in the slice-assignment path: acquiring the value's buffer (e.g. via a __buffer__ method) can also run arbitrary code that resizes the mmap, invalidating the previously computed slice bounds. Re-validate the bounds after acquiring the buffer, before copying into the mapping. Clarify why the single-item path still needs a bounds check both before and after converting the value: the earlier check preserves existing error precedence (IndexError before TypeError, per test_basic), while the later one is a narrow revalidation of just the upper bound. --- Lib/test/test_mmap.py | 21 +++++++++++++++++ ...-09-13-15-58-28.gh-issue-157335.efaMah.rst | 5 ++-- Modules/mmapmodule.c | 23 +++++++++++++++++-- 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_mmap.py b/Lib/test/test_mmap.py index 5726ab84ee53c7f..3412f1952823d27 100644 --- a/Lib/test/test_mmap.py +++ b/Lib/test/test_mmap.py @@ -971,6 +971,27 @@ def __index__(self): m[size - 1] = ResizeOnIndex(m) self.assertEqual(len(m), new_size) + 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)) + with self.assertRaises(IndexError): + m[0:size] = value + 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 index 090c6a9f91be934..dfcff60cb327bc2 100644 --- 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 @@ -1,3 +1,4 @@ Fix out-of-bounds write in :meth:`mmap.mmap.__setitem__` that could occur -when the assigned value's :meth:`~object.__index__` method resized the -mmap object during the assignment. +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 the mmap +object during the assignment. diff --git a/Modules/mmapmodule.c b/Modules/mmapmodule.c index 4dc10b4aca65c76..ba7a7e4c1ab24fd 100644 --- a/Modules/mmapmodule.c +++ b/Modules/mmapmodule.c @@ -1687,8 +1687,9 @@ mmap_ass_subscript_lock_held(PyObject *op, PyObject *item, PyObject *value) return -1; } CHECK_VALID(-1); - /* value's __index__ may have resized the mmap, invalidating - * the earlier bounds check on i. */ + /* value's __index__ may have resized the mmap, invalidating the + * bounds check on i above (i is already non-negative here, so + * only the upper bound can have changed). */ if (i >= self->size) { PyErr_SetString(PyExc_IndexError, "mmap index out of range"); @@ -1724,6 +1725,24 @@ mmap_ass_subscript_lock_held(PyObject *op, PyObject *item, PyObject *value) } CHECK_VALID_OR_RELEASE(-1, vbuf); + /* Acquiring the buffer above may have run arbitrary code (e.g. a + * __buffer__ method) that resized this mmap, invalidating the + * start/stop/slicelen computed earlier against the old size. */ + if (slicelen > 0) { + Py_ssize_t lo = start; + Py_ssize_t hi = start + (slicelen - 1) * step; + if (lo > hi) { + Py_ssize_t tmp = lo; + lo = hi; + hi = tmp; + } + if (lo < 0 || hi >= self->size) { + PyErr_SetString(PyExc_IndexError, + "mmap slice assignment is out of range"); + PyBuffer_Release(&vbuf); + return -1; + } + } int result = 0; if (slicelen == 0) { } From c9e4016bf35b54c83c1a0f51ac1800bbd79f4521 Mon Sep 17 00:00:00 2001 From: N1077433 Date: Tue, 15 Sep 2026 14:02:20 +0100 Subject: [PATCH 3/9] gh:157335: fixed CI failure from mmap setitem reentrancy --- Lib/test/test_mmap.py | 2 ++ .../next/Library/2026-09-13-15-58-28.gh-issue-157335.efaMah.rst | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_mmap.py b/Lib/test/test_mmap.py index 3412f1952823d27..d9f23e94a486fd0 100644 --- a/Lib/test/test_mmap.py +++ b/Lib/test/test_mmap.py @@ -953,6 +953,7 @@ 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).""" @@ -971,6 +972,7 @@ def __index__(self): m[size - 1] = ResizeOnIndex(m) 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 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 index dfcff60cb327bc2..1e8b3cae98020fb 100644 --- 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 @@ -1,4 +1,4 @@ -Fix out-of-bounds write in :meth:`mmap.mmap.__setitem__` that could occur +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 the mmap object during the assignment. From c9f67ac3d256ecead02cbd47d65fd48bc0f9e9b9 Mon Sep 17 00:00:00 2001 From: N1077433 Date: Tue, 15 Sep 2026 14:57:47 +0100 Subject: [PATCH 4/9] gh:157335: mmap setitem bound checks altered, as per reviews --- Lib/test/test_mmap.py | 2 +- Modules/mmapmodule.c | 49 +++++++++++++------------------------------ 2 files changed, 16 insertions(+), 35 deletions(-) diff --git a/Lib/test/test_mmap.py b/Lib/test/test_mmap.py index d9f23e94a486fd0..f0f20bd0882c544 100644 --- a/Lib/test/test_mmap.py +++ b/Lib/test/test_mmap.py @@ -75,7 +75,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] diff --git a/Modules/mmapmodule.c b/Modules/mmapmodule.c index ba7a7e4c1ab24fd..a4ea76a9c3dfb94 100644 --- a/Modules/mmapmodule.c +++ b/Modules/mmapmodule.c @@ -1656,17 +1656,9 @@ mmap_ass_subscript_lock_held(PyObject *op, PyObject *item, PyObject *value) 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"); @@ -1677,7 +1669,7 @@ mmap_ass_subscript_lock_held(PyObject *op, 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) { @@ -1686,11 +1678,14 @@ mmap_ass_subscript_lock_held(PyObject *op, 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); - /* value's __index__ may have resized the mmap, invalidating the - * bounds check on i above (i is already non-negative here, so - * only the upper bound can have changed). */ - if (i >= self->size) { + if (i < 0) + i += self->size; + if (i < 0 || i >= self->size) { PyErr_SetString(PyExc_IndexError, "mmap index out of range"); return -1; @@ -1709,7 +1704,6 @@ mmap_ass_subscript_lock_held(PyObject *op, 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"); @@ -1717,6 +1711,12 @@ mmap_ass_subscript_lock_held(PyObject *op, 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 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"); @@ -1724,25 +1724,6 @@ mmap_ass_subscript_lock_held(PyObject *op, PyObject *item, PyObject *value) return -1; } - CHECK_VALID_OR_RELEASE(-1, vbuf); - /* Acquiring the buffer above may have run arbitrary code (e.g. a - * __buffer__ method) that resized this mmap, invalidating the - * start/stop/slicelen computed earlier against the old size. */ - if (slicelen > 0) { - Py_ssize_t lo = start; - Py_ssize_t hi = start + (slicelen - 1) * step; - if (lo > hi) { - Py_ssize_t tmp = lo; - lo = hi; - hi = tmp; - } - if (lo < 0 || hi >= self->size) { - PyErr_SetString(PyExc_IndexError, - "mmap slice assignment is out of range"); - PyBuffer_Release(&vbuf); - return -1; - } - } int result = 0; if (slicelen == 0) { } From 0bc662a540197f51f5a4fd72bf16a6d746ecaee8 Mon Sep 17 00:00:00 2001 From: Joseph Kerry Date: Tue, 15 Sep 2026 15:40:48 +0100 Subject: [PATCH 5/9] Update Modules/mmapmodule.c Co-authored-by: Victor Stinner --- Modules/mmapmodule.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/mmapmodule.c b/Modules/mmapmodule.c index a4ea76a9c3dfb94..e8aeaf0e7f8832a 100644 --- a/Modules/mmapmodule.c +++ b/Modules/mmapmodule.c @@ -1713,7 +1713,7 @@ mmap_ass_subscript_lock_held(PyObject *op, PyObject *item, PyObject *value) return -1; /* Acquiring the buffer above may have run arbitrary code (e.g. a - * __buffer__ method) that resized this mmap, so the slice bounds + * __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); From 06d8548dfe6dd7b2764d53a147b82beca34567a9 Mon Sep 17 00:00:00 2001 From: Joseph Kerry Date: Tue, 15 Sep 2026 15:41:00 +0100 Subject: [PATCH 6/9] Update Misc/NEWS.d/next/Library/2026-09-13-15-58-28.gh-issue-157335.efaMah.rst Co-authored-by: Victor Stinner --- .../next/Library/2026-09-13-15-58-28.gh-issue-157335.efaMah.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 1e8b3cae98020fb..fb0bcad30605640 100644 --- 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 @@ -1,4 +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 the mmap +for a single item, or via the buffer protocol for a slice) resized or closed the mmap object during the assignment. From 7de2dd3bc6553dca676aa09bc517092bf8d62b8d Mon Sep 17 00:00:00 2001 From: Joseph Kerry Date: Tue, 15 Sep 2026 15:41:18 +0100 Subject: [PATCH 7/9] Update Lib/test/test_mmap.py Co-authored-by: Victor Stinner --- Lib/test/test_mmap.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_mmap.py b/Lib/test/test_mmap.py index f0f20bd0882c544..a9dc9db094784a0 100644 --- a/Lib/test/test_mmap.py +++ b/Lib/test/test_mmap.py @@ -976,7 +976,8 @@ def __index__(self): 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).""" + the new bounds (gh-157335). + """ size = 2 * PAGESIZE new_size = PAGESIZE From 4c7991bcf58e61b83c4cc135825fc1d8f4d6615e Mon Sep 17 00:00:00 2001 From: Joseph Kerry Date: Tue, 15 Sep 2026 15:41:27 +0100 Subject: [PATCH 8/9] Update Lib/test/test_mmap.py Co-authored-by: Victor Stinner --- Lib/test/test_mmap.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_mmap.py b/Lib/test/test_mmap.py index a9dc9db094784a0..59be17455a4e4e9 100644 --- a/Lib/test/test_mmap.py +++ b/Lib/test/test_mmap.py @@ -956,7 +956,8 @@ def test_resize_down_anonymous_mapping(self): @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).""" + single item must not access memory past the new bounds (gh-157335). + """ size = 2 * PAGESIZE new_size = PAGESIZE From 11cbbe9f3f8d2ba82bf6aa4f46986819f4ca184d Mon Sep 17 00:00:00 2001 From: N1077433 Date: Tue, 15 Sep 2026 15:49:00 +0100 Subject: [PATCH 9/9] gh-157335 Removed redundant CHECK_VALID check. --- Modules/mmapmodule.c | 1 - 1 file changed, 1 deletion(-) diff --git a/Modules/mmapmodule.c b/Modules/mmapmodule.c index e8aeaf0e7f8832a..766e85a1bdba984 100644 --- a/Modules/mmapmodule.c +++ b/Modules/mmapmodule.c @@ -1649,7 +1649,6 @@ static int mmap_ass_subscript_lock_held(PyObject *op, PyObject *item, PyObject *value) { mmap_object *self = mmap_object_CAST(op); - CHECK_VALID(-1); if (!is_writable(self)) return -1;