-
-
Notifications
You must be signed in to change notification settings - Fork 33.8k
gh-142663: Fix use-after-free in memoryview comparison #143305
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
|
||
| 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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)) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
|
|
@@ -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) | ||
|
|
||
There was a problem hiding this comment.
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:
and use them as:
(put the helper function after the tests that use it)