Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions Lib/test/test_memoryview.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please rewrite the test as follows:

def do_test_compare_concurrent_mutation(self, src1, mv1, mv2):
    class S(struct.Struct):
        def unpack_from(self, buf, /, offset=0):
            mv1.release()
            src1.append(3.14)
            return (1,)
    with support.swap_attr(struct, "Struct", S):
        self.assertRaises(BufferError, mv1.__eq__, mv2)

and use them as:

def test_compare_1d_concurrent_mutation(self):
    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)

def test_compare_2d_concurrent_mutation(self):
    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)

(put the helper function after the tests that use it)

# 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please avoid useless or trivial comments. You can just use variables in the function directly instead of attributes.

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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Comment on lines +1 to +3
Copy link
Member

@picnixz picnixz Dec 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
: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.

12 changes: 12 additions & 0 deletions Objects/memoryobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can cache this check as we're already using it above.

((PyMemoryViewObject *)w)->exports++;
}

if (vv->ndim == 0) {
equal = unpack_cmp(vv->buf, ww->buf,
Expand All @@ -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)
Expand Down
Loading