From edd566cbd6b2ce10d397051a0fc6e021eb10d0f8 Mon Sep 17 00:00:00 2001 From: superboy-zjc <1826599908@qq.com> Date: Tue, 30 Dec 2025 22:43:26 -0800 Subject: [PATCH 1/5] gh-142663: Fix use-after-free in memoryview comparison When comparing two memoryview objects with different formats, `memory_richcompare` uses the `struct` module to unpack elements. A custom `struct.Struct.unpack_from` implementation could releases and resizes underlying buffer, which invalidates the buffer pointer, during iteration. This leads to a use-after-free when the comparison loop continued accessing the freed memory. The fix increments the `exports` count of the memoryview objects before performing the comparison, effectively locking the buffers. This mirrors the protection already provided for non-memoryview objects via `PyObject_GetBuffer`. --- Lib/test/test_memoryview.py | 61 +++++++++++++++++++ ...-12-30-22-12-27.gh-issue-142663.gq7iIf.rst | 3 + Objects/memoryobject.c | 12 ++++ 3 files changed, 76 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2025-12-30-22-12-27.gh-issue-142663.gq7iIf.rst diff --git a/Lib/test/test_memoryview.py b/Lib/test/test_memoryview.py index 656318668e6d6e7..6b5f12554ce1817 100644 --- a/Lib/test/test_memoryview.py +++ b/Lib/test/test_memoryview.py @@ -228,6 +228,67 @@ def test_compare(self): self.assertRaises(TypeError, lambda: m >= c) self.assertRaises(TypeError, lambda: c > m) + def test_compare_use_after_free(self): + # Prevent crash in comparisons of memoryview objs with re-entrant struct.unpack_from. + # Regression test for https://github.com/python/cpython/issues/142663. + + class ST(struct.Struct): + # Context set by the subtests + view = None + source = None + + def unpack_from(self, buf, /, offset=0): + # Attempt to release the buffer while it's being used in comparison loop. + if self.view is not None: + self.view.release() + + # array resize invalidates the buffer pointer used by the comparison loop. + if self.source is not None: + self.source.append(3.14) + + return (1,) + + with support.swap_attr(struct, "Struct", ST): + # Case 1: 1-D comparison (uses cmp_base optimized loop) + # Use mixed types ('d' vs 'l') to force struct unpacking path. + with self.subTest(ndim=1): + a = array.array("d", [1.0, 2.0]) + b = array.array("l", [1, 2]) + mv_a = memoryview(a) + mv_b = memoryview(b) + + ST.view = mv_a + ST.source = a + try: + with self.assertRaises(BufferError): + # Expect BufferError because the memoryview is locked during comparison + mv_a == mv_b + finally: + ST.view = None + ST.source = None + mv_a.release() + mv_b.release() + + # Case 2: N-D comparison (uses cmp_rec recursive function) + # Use mixed types ('d' vs 'l') to force struct unpacking path. + with self.subTest(ndim=2): + a = array.array("d", [1.0, 2.0]) + b = array.array("l", [1, 2]) + mv_a = memoryview(a).cast("B").cast("d", shape=(1, 2)) + mv_b = memoryview(b).cast("B").cast("l", shape=(1, 2)) + + ST.view = mv_a + ST.source = a + try: + with self.assertRaises(BufferError): + # Expect BufferError because the memoryview is locked during comparison + mv_a == mv_b + finally: + ST.view = None + ST.source = None + mv_a.release() + mv_b.release() + def check_attributes_with_type(self, tp): m = self._view(tp(self._source)) self.assertEqual(m.format, self.format) diff --git a/Misc/NEWS.d/next/Library/2025-12-30-22-12-27.gh-issue-142663.gq7iIf.rst b/Misc/NEWS.d/next/Library/2025-12-30-22-12-27.gh-issue-142663.gq7iIf.rst new file mode 100644 index 000000000000000..8bb651c8bf615eb --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-12-30-22-12-27.gh-issue-142663.gq7iIf.rst @@ -0,0 +1,3 @@ +:class:`memoryview`: Fix a use-after-free crash during comparison when an +overridden :meth:`struct.Struct.unpack_from` releases and resizes the +underlying buffer. diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index f3b7e4a396b4a10..69a93708633fb07 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -3165,6 +3165,13 @@ memory_richcompare(PyObject *v, PyObject *w, int op) goto result; } } + /* Prevent memoryview object from being released and its underlying buffer + reshaped during a mixed format comparison loop. */ + // See https://github.com/python/cpython/issues/142663. + ((PyMemoryViewObject *)v)->exports++; + if (PyMemoryView_Check(w)) { + ((PyMemoryViewObject *)w)->exports++; + } if (vv->ndim == 0) { equal = unpack_cmp(vv->buf, ww->buf, @@ -3183,6 +3190,11 @@ memory_richcompare(PyObject *v, PyObject *w, int op) vfmt, unpack_v, unpack_w); } + ((PyMemoryViewObject *)v)->exports--; + if (PyMemoryView_Check(w)) { + ((PyMemoryViewObject *)w)->exports--; + } + result: if (equal < 0) { if (equal == MV_COMPARE_NOT_IMPL) From 06846a967c36f14e31482f2cd474cb30878fec95 Mon Sep 17 00:00:00 2001 From: superboy-zjc <1826599908@qq.com> Date: Sat, 3 Jan 2026 12:01:11 -0800 Subject: [PATCH 2/5] gh-142663: resolving comments by refactoring unit tests and cache type check result --- Lib/test/test_memoryview.py | 77 +++++-------------- ...-12-30-22-12-27.gh-issue-142663.gq7iIf.rst | 5 +- Objects/memoryobject.c | 7 +- 3 files changed, 27 insertions(+), 62 deletions(-) diff --git a/Lib/test/test_memoryview.py b/Lib/test/test_memoryview.py index 6b5f12554ce1817..0203779d994a3d2 100644 --- a/Lib/test/test_memoryview.py +++ b/Lib/test/test_memoryview.py @@ -228,66 +228,31 @@ def test_compare(self): self.assertRaises(TypeError, lambda: m >= c) self.assertRaises(TypeError, lambda: c > m) - def test_compare_use_after_free(self): - # Prevent crash in comparisons of memoryview objs with re-entrant struct.unpack_from. + def test_compare_1d_concurrent_mutation(self): + # Prevent crashes during a mixed format 1-D comparison loop. # Regression test for https://github.com/python/cpython/issues/142663. + src1 = array.array("d", [1.0, 2.0]) + src2 = array.array("l", [1, 2]) + mv1, mv2 = memoryview(src1), memoryview(src2) + self.do_test_compare_concurrent_mutation(src1, mv1, mv2) - class ST(struct.Struct): - # Context set by the subtests - view = None - source = None - + def test_compare_2d_concurrent_mutation(self): + # Prevent crashes during a mixed format 2-D comparison loop. + # Regression test for https://github.com/python/cpython/issues/142663. + src1 = array.array("d", [1.0, 2.0]) + src2 = array.array("l", [1, 2]) + mv1 = memoryview(src1).cast("B").cast("d", shape=(1, 2)) + mv2 = memoryview(src2).cast("B").cast("l", shape=(1, 2)) + self.do_test_compare_concurrent_mutation(src1, mv1, mv2) + + def do_test_compare_concurrent_mutation(self, src1, mv1, mv2): + class S(struct.Struct): def unpack_from(self, buf, /, offset=0): - # Attempt to release the buffer while it's being used in comparison loop. - if self.view is not None: - self.view.release() - - # array resize invalidates the buffer pointer used by the comparison loop. - if self.source is not None: - self.source.append(3.14) - + mv1.release() + src1.append(3.14) return (1,) - - with support.swap_attr(struct, "Struct", ST): - # Case 1: 1-D comparison (uses cmp_base optimized loop) - # Use mixed types ('d' vs 'l') to force struct unpacking path. - with self.subTest(ndim=1): - a = array.array("d", [1.0, 2.0]) - b = array.array("l", [1, 2]) - mv_a = memoryview(a) - mv_b = memoryview(b) - - ST.view = mv_a - ST.source = a - try: - with self.assertRaises(BufferError): - # Expect BufferError because the memoryview is locked during comparison - mv_a == mv_b - finally: - ST.view = None - ST.source = None - mv_a.release() - mv_b.release() - - # Case 2: N-D comparison (uses cmp_rec recursive function) - # Use mixed types ('d' vs 'l') to force struct unpacking path. - with self.subTest(ndim=2): - a = array.array("d", [1.0, 2.0]) - b = array.array("l", [1, 2]) - mv_a = memoryview(a).cast("B").cast("d", shape=(1, 2)) - mv_b = memoryview(b).cast("B").cast("l", shape=(1, 2)) - - ST.view = mv_a - ST.source = a - try: - with self.assertRaises(BufferError): - # Expect BufferError because the memoryview is locked during comparison - mv_a == mv_b - finally: - ST.view = None - ST.source = None - mv_a.release() - mv_b.release() + with support.swap_attr(struct, "Struct", S): + self.assertRaises(BufferError, mv1.__eq__, mv2) def check_attributes_with_type(self, tp): m = self._view(tp(self._source)) diff --git a/Misc/NEWS.d/next/Library/2025-12-30-22-12-27.gh-issue-142663.gq7iIf.rst b/Misc/NEWS.d/next/Library/2025-12-30-22-12-27.gh-issue-142663.gq7iIf.rst index 8bb651c8bf615eb..39ee1ef29267a7d 100644 --- a/Misc/NEWS.d/next/Library/2025-12-30-22-12-27.gh-issue-142663.gq7iIf.rst +++ b/Misc/NEWS.d/next/Library/2025-12-30-22-12-27.gh-issue-142663.gq7iIf.rst @@ -1,3 +1,2 @@ -:class:`memoryview`: Fix a use-after-free crash during comparison when an -overridden :meth:`struct.Struct.unpack_from` releases and resizes the -underlying buffer. +Fix use-after-free crashes when a :class:`memoryview` is mutated +during a comparison with another object. diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index 69a93708633fb07..16b8495659c0d85 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -3122,7 +3122,8 @@ memory_richcompare(PyObject *v, PyObject *w, int op) } vv = VIEW_ADDR(v); - if (PyMemoryView_Check(w)) { + int w_is_mv = PyMemoryView_Check(w); + if (w_is_mv) { if (BASE_INACCESSIBLE(w)) { equal = (v == w); goto result; @@ -3169,7 +3170,7 @@ memory_richcompare(PyObject *v, PyObject *w, int op) reshaped during a mixed format comparison loop. */ // See https://github.com/python/cpython/issues/142663. ((PyMemoryViewObject *)v)->exports++; - if (PyMemoryView_Check(w)) { + if (w_is_mv) { ((PyMemoryViewObject *)w)->exports++; } @@ -3191,7 +3192,7 @@ memory_richcompare(PyObject *v, PyObject *w, int op) } ((PyMemoryViewObject *)v)->exports--; - if (PyMemoryView_Check(w)) { + if (w_is_mv) { ((PyMemoryViewObject *)w)->exports--; } From 397c38afa3b1f5035f61b4d2d75b4a5ed9f50451 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 6 Aug 2026 17:49:10 +0200 Subject: [PATCH 3/5] Apply suggestions from code review Co-authored-by: Victor Stinner --- Objects/memoryobject.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index 8d6c5bb2f9012ef..568c5a11c9a06fd 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -3290,9 +3290,9 @@ memory_richcompare(PyObject *v, PyObject *w, int op) /* Prevent memoryview object from being released and its underlying buffer reshaped during a mixed format comparison loop. */ // See https://github.com/python/cpython/issues/142663. - ((PyMemoryViewObject *)v)->exports++; + _PyMemoryView_CAST(v)->exports++; if (w_is_mv) { - ((PyMemoryViewObject *)w)->exports++; + _PyMemoryView_CAST(w)->exports++; } if (vv->ndim == 0) { @@ -3312,9 +3312,9 @@ memory_richcompare(PyObject *v, PyObject *w, int op) vfmt, unpack_v, unpack_w); } - ((PyMemoryViewObject *)v)->exports--; + _PyMemoryView_CAST(v)->exports--; if (w_is_mv) { - ((PyMemoryViewObject *)w)->exports--; + _PyMemoryView_CAST(w)->exports--; } result: From 81acd9bb6267b8371d0a7fd34f4e2a8e0bd45256 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 6 Aug 2026 17:57:16 +0200 Subject: [PATCH 4/5] Apply suggestions from code review Co-authored-by: Victor Stinner --- Objects/memoryobject.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index 6809f49a74c2cdc..eb3c5079e02a22f 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -3307,9 +3307,9 @@ memory_richcompare(PyObject *v, PyObject *w, int op) /* Prevent memoryview object from being released and its underlying buffer reshaped during a mixed format comparison loop. */ // See https://github.com/python/cpython/issues/142663. - _PyMemoryView_CAST(v)->exports++; + FT_ATOMIC_ADD_SSIZE(_PyMemoryView_CAST(v)->exports, 1); if (w_is_mv) { - _PyMemoryView_CAST(w)->exports++; + FT_ATOMIC_ADD_SSIZE(_PyMemoryView_CAST(w)->exports, 1); } if (vv->ndim == 0) { @@ -3329,9 +3329,9 @@ memory_richcompare(PyObject *v, PyObject *w, int op) vfmt, unpack_v, unpack_w); } - _PyMemoryView_CAST(v)->exports--; + FT_ATOMIC_ADD_SSIZE(_PyMemoryView_CAST(v)->exports, -1); if (w_is_mv) { - _PyMemoryView_CAST(w)->exports--; + FT_ATOMIC_ADD_SSIZE(_PyMemoryView_CAST(w)->exports, -1); } result: From 9d6c7d3b382a1cd2f8cdd8f1080886e69e3da445 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 6 Aug 2026 17:58:32 +0200 Subject: [PATCH 5/5] Apply suggestions from code review Co-authored-by: Victor Stinner --- Objects/memoryobject.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index eb3c5079e02a22f..9f4627cc81bd98d 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -3305,8 +3305,8 @@ memory_richcompare(PyObject *v, PyObject *w, int op) } } /* Prevent memoryview object from being released and its underlying buffer - reshaped during a mixed format comparison loop. */ - // See https://github.com/python/cpython/issues/142663. + reshaped during a mixed format comparison loop. + See https://github.com/python/cpython/issues/142663. */ FT_ATOMIC_ADD_SSIZE(_PyMemoryView_CAST(v)->exports, 1); if (w_is_mv) { FT_ATOMIC_ADD_SSIZE(_PyMemoryView_CAST(w)->exports, 1);