gh-142663: Fix use-after-free in memoryview comparison#143305
gh-142663: Fix use-after-free in memoryview comparison#143305superboy-zjc wants to merge 4 commits into
Conversation
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`.
|
All resolved. Thanks for the review! |
| mv1.release() | ||
| src1.append(3.14) | ||
| return (1,) | ||
| with support.swap_attr(struct, "Struct", S): |
There was a problem hiding this comment.
This line puzzles me a little bit. When you swap classes in the stdlib, you can get unexpected results. Can we replicate this without monkeypatching?
There was a problem hiding this comment.
Hi @sobolevn I doubt the possibility of replicating this without monkeypatching. The issue stems from, during a mixed format buffer comparison, memory_richcompare function calls a re-entrant struct.Struct.unpack_from that is loaded by a PyImport_ImportModuleAttrString("struct", "Struct") call in the struct_get_unpacker. Is there anyway you would recommend to work around it ?
/* Return a new unpacker for the given format. */
static struct unpacker *
struct_get_unpacker(const char *fmt, Py_ssize_t itemsize)
{
...
// struct.Struct Importation
Struct = PyImport_ImportModuleAttrString("struct", "Struct");
if (Struct == NULL)
return NULL;
x = unpacker_new();
...
x->unpack_from = PyObject_GetAttrString(structobj, "unpack_from");
if (x->unpack_from == NULL)
goto error;
...
}There was a problem hiding this comment.
There are countless potential issues if we replace stdlib types with something else. Let's please first discuss in the issue whether this is even a valid bug report.
There was a problem hiding this comment.
I did consider it as a crash because we also fixed similar occurrences elsewhere. If we just had an exception it would have been ok, but here we have a crash. We do not want this.
There was a problem hiding this comment.
I see there are many tests having monkey patch. Why does this case make difference? Is there any update regarding this pr or should we @ more core dev to take a look?
There was a problem hiding this comment.
struct_get_unpacker looks inefficient. We may add a cache for struct.Struct in future. Or we can cache the bound unpack_from() methods. Anyway, monkey-patching struct.Struct will either not have any effect or will affect all following code. I suspect that after I wrote this, someone might take it as a challenge.
I do not see a good solution for this. We might run the tests in a subprocess to reduce affect on other code, but this is too much hassle (and this is not 100% future proof). Perhaps it would be safer to not add a test.
There was a problem hiding this comment.
Drop the tests, future changes can make it unsafe. Add a simple reproducer script in the PR or in the issue, so we can test the fix manually.
|
This PR is stale because it has been open for 30 days with no activity. |
serhiy-storchaka
left a comment
There was a problem hiding this comment.
I updated the branch and resolved conflict with main. I left few review comments. After addressing them, I will approve and merge this PR. Thank you for your contribution.
| /* 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++; |
There was a problem hiding this comment.
All other modifications of exports use FT_ATOMIC_ADD_SSIZE. Please use it here and in other 3 sites.
| ((PyMemoryViewObject *)v)->exports++; | |
| FT_ATOMIC_ADD_SSIZE(((PyMemoryViewObject *)v)->exports, 1); |
| /* 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. |
There was a problem hiding this comment.
Do not mix different types of comments, especially in such proximity. Drop the URL, the NEWS/PR already carry the issue link.
| mv1.release() | ||
| src1.append(3.14) | ||
| return (1,) | ||
| with support.swap_attr(struct, "Struct", S): |
There was a problem hiding this comment.
Drop the tests, future changes can make it unsafe. Add a simple reproducer script in the PR or in the issue, so we can test the fix manually.
|
A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated. Once you have made the requested changes, please leave a comment on this pull request containing the phrase |
|
I'm not convinced that we have to fix this issue. Sure, if you do silly things like replacing a stdlib method which do weird things, the stdlib can misbehave including crashing. If I understood, the reproducer comes from fuzzing Python. I don't think that a "regular Python code" using struct which doesn't monkey-patch the struct module can crash Python. If someone can show me a code to trigger the bug without replacing struct methods, I would be interested. Otherwise, I suggest closing the issue as "wont't fix". |
@vstinner, regular Python code surely normally doesn’t look like this. But there are usage scenarios where untrusted users are allowed to run arbitrary Python code, such as Python-based sandboxes like RestrictedPython. In these environments, a memory corruption bug can potentially be exploited to achieve remote code execution, which is exactly what we don’t want and why I think it should be fixed. |
|
Replacing a stdlib method which do weird things is just a way to get a stable reproducer for rare race condition. The bug exists, even if it is difficult to reproduce it other way. The fix is simple, we should not hold it. The tests were nice, but they have potential to poison interpreter after future changes in |
|
There are plently of ways of crashing Python with "pure Python code". Just an example: It doesn't mean that we have every ways to crash Python. You're not suppose to override struct method to release and modify a memoryview. |
|
I do not even consider sandbox as a use case. But we have potential race condition. Other code in this file is already guarder, as well as similar code in other file. We should do this just for consistency. |
When comparing two memoryview objects with different formats,
memory_richcompareuses thestructmodule to unpack elements. A customstruct.Struct.unpack_fromimplementation 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
exportscount of the memoryview objects before performing the comparison, effectively locking the buffers. This mirrors the protection already provided for non-memoryview objects viaPyObject_GetBuffer.memoryviewcomparison via re-entrantstruct.Struct.unpack_from#142663