Skip to content

gh-152405: Prevent mappingproxy operator dispatch from exposing underlying mappings#152449

Open
zainnadeem786 wants to merge 1 commit into
python:mainfrom
zainnadeem786:fix/mappingproxy-richcompare-leak
Open

gh-152405: Prevent mappingproxy operator dispatch from exposing underlying mappings#152449
zainnadeem786 wants to merge 1 commit into
python:mainfrom
zainnadeem786:fix/mappingproxy-richcompare-leak

Conversation

@zainnadeem786

Copy link
Copy Markdown
Contributor

Summary

Closes gh-152405.

This PR prevents mappingproxy rich comparison (==, !=) and union (|) operations from exposing the underlying proxied mapping to arbitrary reflected operator methods.

Previously, reflected methods such as __eq__ and __ror__ could receive the original internal mapping object, allowing user code to bypass the read-only guarantees of mappingproxy. In the case of built-in type dictionaries (for example vars(list)), this made it possible to mutate normally immutable type dictionaries and could ultimately lead to interpreter crashes.


Problem

mappingproxy exists to provide a read-only view of another mapping.

However, the current implementation forwards the underlying mapping directly into generic operator dispatch.

For equality:

mappingproxy_richcompare()
    -> PyObject_RichCompare(proxy->mapping, other, op)

For union:

mappingproxy_or()
    -> PyNumber_Or(proxy->mapping, other)

If the right-hand operand implements reflected methods such as:

__eq__()
__ror__()

those methods receive the original proxied mapping object instead of the mappingproxy.

For example:

class Evil:
    def __eq__(self, other):
        return other

raw = (vars(list) == Evil())

returns the internal dictionary backing list.

That dictionary can then be modified directly:

(raw)["prepend"] = ...

bypassing the intended immutability provided by mappingproxy.

During investigation I also confirmed that the same encapsulation issue exists in mappingproxy_or().


Root Cause

The implementation passed the original proxied mapping directly into generic operator dispatch.

This violates the design intent documented in Objects/descrobject.c, where mappingproxy operations are expected not to expose the underlying mapping.

Because reflected operator dispatch executes arbitrary Python code, user-defined methods could obtain references to internal dictionaries.


Fix

This change avoids passing the original proxied mapping into operator dispatch.

Instead, mappingproxy operands are delegated through safe temporary operands before invoking:

  • PyObject_RichCompare()
  • PyNumber_Or()

As a result:

  • reflected operator methods no longer receive the original mapping,
  • internal dictionaries remain encapsulated,
  • existing comparison behaviour is preserved as closely as possible.

Mappings that do not provide an appropriate copy path safely fall back without exposing the underlying mapping.


Compatibility

The implementation is intentionally conservative.

It preserves the existing behaviour for normal operations such as comparisons with:

  • dictionaries
  • dictionary subclasses
  • ChainMap
  • other mappingproxy instances

while preventing arbitrary reflected operator methods from obtaining the original internal mapping.

No public Python API changes were introduced.


Tests

Added focused regression coverage in Lib/test/test_types.py for:

  • preventing rich comparison from exposing the underlying mapping
  • preventing union operations from exposing the underlying mapping
  • safe behaviour when the proxied mapping does not provide a copy implementation
  • preventing exposure of built-in type dictionaries via subprocess tests
  • preserving existing mappingproxy behaviour for normal comparisons

The regression tests intentionally avoid crashing the main test process.


Validation

The following focused test suites were executed successfully:

PCbuild\amd64\python_d.exe -m unittest -v test.test_types.MappingProxyTests
Ran 18 tests

OK
PCbuild\amd64\python_d.exe -m test test_types test_descr test_builtin test_dict
Result: SUCCESS

test_types
test_descr
test_builtin
test_dict

Additionally:

git diff --check

completed successfully with no whitespace errors.


Notes

During investigation I confirmed the issue on a local debug build by reproducing mutation of built-in type dictionaries and observing an interpreter crash after the mutated internal state was exercised.

The implementation intentionally avoids relying on crash-based regression tests and instead validates the encapsulation guarantees through focused behavioural tests.

@da-woods

Copy link
Copy Markdown
Contributor

To me the copy looks expensive and .copy isn't part of the documented Mapping interface (https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping) so this makes it unusable with the general concept of Mapping.

Comment thread Objects/descrobject.c
mappingproxy_copy_mapping(PyObject *mapping)
{
PyObject *copy_method;
int res = PyObject_GetOptionalAttr(mapping, &_Py_ID(copy), &copy_method);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't think that it is correct: see my proposed alternative #152483

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the review @sobolevn and for putting together the alternative approach.

I agree that relying on .copy() for arbitrary mappings is not ideal since it is not part of the Mapping interface. I’ll leave my PR as-is for now and follow the discussion on #152483.

If you would prefer, I can close this PR in favor of #152483, or help with tests/review there.

@bedevere-app

bedevere-app Bot commented Jun 28, 2026

Copy link
Copy Markdown

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 have made the requested changes; please review again. I will then notify any core developers who have left a review that you're ready for them to take another look at this pull request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants