Skip to content

Commit edd566c

Browse files
committed
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`.
1 parent ef834de commit edd566c

3 files changed

Lines changed: 76 additions & 0 deletions

File tree

Lib/test/test_memoryview.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,67 @@ def test_compare(self):
228228
self.assertRaises(TypeError, lambda: m >= c)
229229
self.assertRaises(TypeError, lambda: c > m)
230230

231+
def test_compare_use_after_free(self):
232+
# Prevent crash in comparisons of memoryview objs with re-entrant struct.unpack_from.
233+
# Regression test for https://github.com/python/cpython/issues/142663.
234+
235+
class ST(struct.Struct):
236+
# Context set by the subtests
237+
view = None
238+
source = None
239+
240+
def unpack_from(self, buf, /, offset=0):
241+
# Attempt to release the buffer while it's being used in comparison loop.
242+
if self.view is not None:
243+
self.view.release()
244+
245+
# array resize invalidates the buffer pointer used by the comparison loop.
246+
if self.source is not None:
247+
self.source.append(3.14)
248+
249+
return (1,)
250+
251+
with support.swap_attr(struct, "Struct", ST):
252+
# Case 1: 1-D comparison (uses cmp_base optimized loop)
253+
# Use mixed types ('d' vs 'l') to force struct unpacking path.
254+
with self.subTest(ndim=1):
255+
a = array.array("d", [1.0, 2.0])
256+
b = array.array("l", [1, 2])
257+
mv_a = memoryview(a)
258+
mv_b = memoryview(b)
259+
260+
ST.view = mv_a
261+
ST.source = a
262+
try:
263+
with self.assertRaises(BufferError):
264+
# Expect BufferError because the memoryview is locked during comparison
265+
mv_a == mv_b
266+
finally:
267+
ST.view = None
268+
ST.source = None
269+
mv_a.release()
270+
mv_b.release()
271+
272+
# Case 2: N-D comparison (uses cmp_rec recursive function)
273+
# Use mixed types ('d' vs 'l') to force struct unpacking path.
274+
with self.subTest(ndim=2):
275+
a = array.array("d", [1.0, 2.0])
276+
b = array.array("l", [1, 2])
277+
mv_a = memoryview(a).cast("B").cast("d", shape=(1, 2))
278+
mv_b = memoryview(b).cast("B").cast("l", shape=(1, 2))
279+
280+
ST.view = mv_a
281+
ST.source = a
282+
try:
283+
with self.assertRaises(BufferError):
284+
# Expect BufferError because the memoryview is locked during comparison
285+
mv_a == mv_b
286+
finally:
287+
ST.view = None
288+
ST.source = None
289+
mv_a.release()
290+
mv_b.release()
291+
231292
def check_attributes_with_type(self, tp):
232293
m = self._view(tp(self._source))
233294
self.assertEqual(m.format, self.format)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:class:`memoryview`: Fix a use-after-free crash during comparison when an
2+
overridden :meth:`struct.Struct.unpack_from` releases and resizes the
3+
underlying buffer.

Objects/memoryobject.c

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3165,6 +3165,13 @@ memory_richcompare(PyObject *v, PyObject *w, int op)
31653165
goto result;
31663166
}
31673167
}
3168+
/* Prevent memoryview object from being released and its underlying buffer
3169+
reshaped during a mixed format comparison loop. */
3170+
// See https://github.com/python/cpython/issues/142663.
3171+
((PyMemoryViewObject *)v)->exports++;
3172+
if (PyMemoryView_Check(w)) {
3173+
((PyMemoryViewObject *)w)->exports++;
3174+
}
31683175

31693176
if (vv->ndim == 0) {
31703177
equal = unpack_cmp(vv->buf, ww->buf,
@@ -3183,6 +3190,11 @@ memory_richcompare(PyObject *v, PyObject *w, int op)
31833190
vfmt, unpack_v, unpack_w);
31843191
}
31853192

3193+
((PyMemoryViewObject *)v)->exports--;
3194+
if (PyMemoryView_Check(w)) {
3195+
((PyMemoryViewObject *)w)->exports--;
3196+
}
3197+
31863198
result:
31873199
if (equal < 0) {
31883200
if (equal == MV_COMPARE_NOT_IMPL)

0 commit comments

Comments
 (0)