You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
vars's mapping proxy can expose internal dictionary even for built-in types #152405
By creating a type or class with a definition of __eq__ that simply returns the other object, it is possible to access the underlying dictionary behind the mapping proxy returned by vars.
This can be (ab)used to bypass mutability restrictions on built-in types, as the example below shows:
classEvil:
def__eq__(self, other):
returnother# less readable version of Evil:# type("Evil", (), {"__eq__": lambda self, other: other})# this statement adds a method `prepend` to the built-in type `list`# 1. vars(list) == Evil() returns the underlying dictionary of `list`# 2. we can then use the dict[name] = value statement to set the attribute `name`
(vars(list) ==Evil())["prepend"] =lambdaself, value: self.insert(0, value)
x= [3, 5, 2]
x.prepend(7)
print(x) # [7, 3, 5, 2]
This can be used to cause a segmentation fault:
# reusing Evil from earlier
(vars(type) ==Evil())["__instancecheck__"] =lambda*_: False# fish: Job 1, 'python' terminated by signal SIGSEGV
No FFI, not a single line of C code required.
This also works on CPython 2.7 and 3.9. It might even be possible to do on even earlier versions.
Disclaimer
I have independently discovered this by myself (no LLM).
I investigated this on current main and was able to reproduce both the dictionary exposure and built-in type mutation behavior.
The root cause appears to be in mappingproxy_richcompare(). During comparison, it delegates directly with the proxied mapping as the left operand:
PyObject_RichCompare(v->mapping, w, op)
For a non-dict RHS, the dict comparison returns NotImplemented, after which generic rich comparison can call the reflected comparison on the RHS with the raw underlying dict. A user-defined __eq__ can therefore receive and return the internal dictionary.
I also found a sibling issue in the mappingproxy union path. mappingproxy_or() similarly unwraps the proxy and passes the raw mapping into PyNumber_Or(), allowing __ror__ on the RHS to receive the underlying dict.
I confirmed that the leaked dict can mutate a built-in type dictionary, and I was also able to reproduce a crash in a subprocess after mutating and then removing an attribute from list.__dict__.
This seems related to the older MappingProxyType encapsulation discussions such as GH-88004 / bpo-43838 and GH-88762. Based on that prior direction, a copy-backed delegation approach for rich comparison and union seems like the safest option because it avoids passing proxy->mapping to arbitrary user code while preserving existing comparison behavior better than simply returning NotImplemented.
For completeness, I reproduced this on a locally built CPython debug interpreter (python_d.exe) from the latest main. After mutating the leaked built-in type dictionary through the exposed mapping, the subprocess terminated with a Windows access violation. The attached screenshot shows the resulting crash dialog and serves as supporting evidence alongside the textual reproduction.
I’m going to prepare a focused PR with regression tests unless there is a preferred different direction.
The ability to use comparison operators to get at the underlying mapping is already well-know: #88004 (I'm not sure that the ability to cause a crash with it)
Yes, during my investigation I also found the earlier MappingProxyType discussions (#88004 / bpo-43838 and related issues), which is why I tried to follow the previous copy-backed direction in the PR rather than changing comparison semantics.
The additional point I observed was that the exposed mapping can be used to mutate built-in type dictionaries, and in my local debug build this led to a reproducible subprocess crash after the mutation. That crash behavior is what motivated the extra regression coverage in the PR.
If the crash is considered a separate issue from the long-known encapsulation leak, I'm happy to adjust the scope of the PR accordingly.
classEvil:
def__eq__(self, other):
returnother# less readable version of Evil:# type("Evil", (), {"__eq__": lambda self, other: other})
(vars(type) ==Evil())["__instancecheck__"] =lambda*_: False# this statement adds a method `prepend` to the built-in type `list`# 1. vars(list) == Evil() returns the underlying dictionary of `list`# 2. we can then use the dict[name] = value statement to set the attribute `name`
(vars(list) ==Evil())["prepend"] =lambdaself, value: self.insert(0, value)
x= [3, 5, 2]
x.prepend(7)
print(x) # [7, 3, 5, 2]
I agree that the dictionary exposure itself is already known. The additional observation from my investigation was the crash, but I may not have described the exact reproduction sequence clearly enough.
The crash did not occur immediately after adding the attribute. In my local Windows debug build, it occurred after mutating the built-in type dictionary, exercising the added attribute, removing it again, and then performing another attribute lookup. That resulted in an access violation in a subprocess.
I'll reduce this to the smallest reproducible example and verify it again on the current main. If I can't reproduce it reliably, I'm happy to treat the crash separately and keep the PR focused on preventing the mapping exposure through operator dispatch.
I re-ran the investigation from a clean checkout of the latest upstream/main (commit 95bfaffd522675287dc176d75a4d23e708435e6d) to verify the crash independently.
You're right that the simple reproducer from the issue does not crash on current main; it only demonstrates that the underlying mapping can be exposed and mutated.
However, I can still reproduce an access violation in a Windows debug build with a slightly different sequence:
Leak the underlying list.__dict__ via vars(list) == Evil().
Inject a temporary method.
Call the injected method successfully.
Remove the injected entry from the leaked dictionary.
Perform another lookup of the removed attribute.
Using that sequence in a subprocess (with test.support.SuppressCrashReport()), the subprocess exits consistently with 0xC0000005 (Windows fatal exception: access violation).
So I agree that the original reproducer should not be presented as the crash reproducer. The crash appears to require the additional mutation/cleanup sequence rather than simply exposing the mapping.
If it would be helpful, I can reduce this further into a standalone minimal reproducer and share it separately.
The simple “add a list method and call it” example does not crash for me either. The crash requires a successful lookup of the injected name, deletion through the leaked dict, and then another lookup of the same name.
Here is the smallest reliable subprocess reproducer I found:
The ability to use comparison operators to get at the underlying mapping is already well-know: #88004 (I'm not sure that the ability to cause a crash with it)
classEvil:
def__eq__(self, other):
returnother# less readable version of Evil:# type("Evil", (), {"__eq__": lambda self, other: other})
(vars(type) ==Evil())["__instancecheck__"] =lambda*_: False# this statement adds a method `prepend` to the built-in type `list`# 1. vars(list) == Evil() returns the underlying dictionary of `list`# 2. we can then use the dict[name] = value statement to set the attribute `name`
(vars(list) ==Evil())["prepend"] =lambdaself, value: self.insert(0, value)
x= [3, 5, 2]
x.prepend(7)
print(x) # [7, 3, 5, 2]
Without a crash - this is not an actual issue :(
this part doesn't crash, it's only where it modifies type.__instancecheck__ that it does
@zainnadeem786 I'm sure we all appreciate your willingness to help on the issue but that would be great if you (or what seems to be the output of your LLM/agent) were a bit more concise and discussed a little more thoroughly with other CPython's contributors before jumping into making a PR
Reacted by Inada Naoki, Ivy Xu, XEATEK and jawadcode
Thanks for the feedback @qexat , and apologies for the noise.
You’re right I should have kept the investigation comments more concise and waited for more discussion before opening a PR, especially for an interpreter-core issue.
I’ll keep future comments shorter and make sure to discuss the intended direction more clearly before preparing larger changes.
I've put a quick 3rd proposal of how to solve it (copying free...) in #152489. That's mostly for discussion though. It doesn't have any specific tests for example...
After reading #88004 more carefully, I feel like we can consider this issue a duplicate, and close it. I'll leave the decision to a triage team member.
Activity
zainnadeem786 commentedon Jun 27, 2026
Hi @qexat @sobolevn
I investigated this on current
mainand was able to reproduce both the dictionary exposure and built-in type mutation behavior.The root cause appears to be in
mappingproxy_richcompare(). During comparison, it delegates directly with the proxied mapping as the left operand:For a non-dict RHS, the dict comparison returns
NotImplemented, after which generic rich comparison can call the reflected comparison on the RHS with the raw underlying dict. A user-defined__eq__can therefore receive and return the internal dictionary.I also found a sibling issue in the mappingproxy union path.
mappingproxy_or()similarly unwraps the proxy and passes the raw mapping intoPyNumber_Or(), allowing__ror__on the RHS to receive the underlying dict.Minimal repros:
I confirmed that the leaked dict can mutate a built-in type dictionary, and I was also able to reproduce a crash in a subprocess after mutating and then removing an attribute from
list.__dict__.This seems related to the older MappingProxyType encapsulation discussions such as GH-88004 / bpo-43838 and GH-88762. Based on that prior direction, a copy-backed delegation approach for rich comparison and union seems like the safest option because it avoids passing
proxy->mappingto arbitrary user code while preserving existing comparison behavior better than simply returningNotImplemented.For completeness, I reproduced this on a locally built CPython debug interpreter (
python_d.exe) from the latestmain. After mutating the leaked built-in type dictionary through the exposed mapping, the subprocess terminated with a Windows access violation. The attached screenshot shows the resulting crash dialog and serves as supporting evidence alongside the textual reproduction.I’m going to prepare a focused PR with regression tests unless there is a preferred different direction.
da-woods commentedon Jun 28, 2026
The ability to use comparison operators to get at the underlying mapping is already well-know: #88004 (I'm not sure that the ability to cause a crash with it)
zainnadeem786 commentedon Jun 28, 2026
Thanks for the reference @da-woods .
Yes, during my investigation I also found the earlier MappingProxyType discussions (#88004 / bpo-43838 and related issues), which is why I tried to follow the previous copy-backed direction in the PR rather than changing comparison semantics.
The additional point I observed was that the exposed mapping can be used to mutate built-in type dictionaries, and in my local debug build this led to a reproducible subprocess crash after the mutation. That crash behavior is what motivated the extra regression coverage in the PR.
If the crash is considered a separate issue from the long-known encapsulation leak, I'm happy to adjust the scope of the PR accordingly.
sobolevn commentedon Jun 28, 2026
I can't reproduce the crash on
main:Without a crash - this is not an actual issue :(
zainnadeem786 commentedon Jun 28, 2026
Thanks for checking. @sobolevn
I agree that the dictionary exposure itself is already known. The additional observation from my investigation was the crash, but I may not have described the exact reproduction sequence clearly enough.
The crash did not occur immediately after adding the attribute. In my local Windows debug build, it occurred after mutating the built-in type dictionary, exercising the added attribute, removing it again, and then performing another attribute lookup. That resulted in an access violation in a subprocess.
I'll reduce this to the smallest reproducible example and verify it again on the current
main. If I can't reproduce it reliably, I'm happy to treat the crash separately and keep the PR focused on preventing the mapping exposure through operator dispatch.zainnadeem786 commentedon Jun 28, 2026
Thanks for checking. @sobolevn
I re-ran the investigation from a clean checkout of the latest
upstream/main(commit95bfaffd522675287dc176d75a4d23e708435e6d) to verify the crash independently.You're right that the simple reproducer from the issue does not crash on current
main; it only demonstrates that the underlying mapping can be exposed and mutated.However, I can still reproduce an access violation in a Windows debug build with a slightly different sequence:
list.__dict__viavars(list) == Evil().Using that sequence in a subprocess (with
test.support.SuppressCrashReport()), the subprocess exits consistently with0xC0000005(Windows fatal exception: access violation).So I agree that the original reproducer should not be presented as the crash reproducer. The crash appears to require the additional mutation/cleanup sequence rather than simply exposing the mapping.
If it would be helpful, I can reduce this further into a standalone minimal reproducer and share it separately.
sobolevn commentedon Jun 28, 2026
can you pease post the actual reproducer here? :)
zainnadeem786 commentedon Jun 28, 2026
Sure @sobolevn I re-tested this on a clean checkout of current
upstream/main.Commit tested:
95bfaffd522675287dc176d75a4d23e708435e6dThe simple “add a list method and call it” example does not crash for me either. The crash requires a successful lookup of the injected name, deletion through the leaked dict, and then another lookup of the same name.
Here is the smallest reliable subprocess reproducer I found:
I tested this on Windows with both debug and release builds from current
main.Debug build: reproduced
5/5Release build: reproduced
5/5Both exited with:
which is
0xC0000005.stderrcontains:The required sequence appears to be:
list.__dict__throughvars(list) == Evil();The final lookup is where the subprocess crashes for me.
sobolevn commentedon Jun 28, 2026
yes, I can verify that
crashes. Traceback:
qexat commentedon Jun 28, 2026
ah oops, I completely missed that, apologies
qexat commentedon Jun 28, 2026
this part doesn't crash, it's only where it modifies
type.__instancecheck__that it doesqexat commentedon Jun 28, 2026
@zainnadeem786 I'm sure we all appreciate your willingness to help on the issue but that would be great if you (or what seems to be the output of your LLM/agent) were a bit more concise and discussed a little more thoroughly with other CPython's contributors before jumping into making a PR
zainnadeem786 commentedon Jun 28, 2026
Thanks for the feedback @qexat , and apologies for the noise.
You’re right I should have kept the investigation comments more concise and waited for more discussion before opening a PR, especially for an interpreter-core issue.
I’ll keep future comments shorter and make sure to discuss the intended direction more clearly before preparing larger changes.
da-woods commentedon Jun 28, 2026
I've put a quick 3rd proposal of how to solve it (copying free...) in #152489. That's mostly for discussion though. It doesn't have any specific tests for example...
zainnadeem786 commentedon Jun 28, 2026
qexat commentedon Jul 13, 2026
After reading #88004 more carefully, I feel like we can consider this issue a duplicate, and close it. I'll leave the decision to a triage team member.
gh-152405: Do not expose the internal mapping when rich-comparing `Ma…
[3.15] gh-152405: Do not expose the internal mapping when rich-compar…
serhiy-storchaka commentedon Jul 17, 2026
What is the problem? Note that intentionally written malicious code is not the problem which we should fix.
The decision that was made in #88004 was to leave the code as is.
methane commentedon Aug 11, 2026
#88004 (comment)
Let's add a note to MappingProxyType and close this.