diff --git a/Lib/test/test_types.py b/Lib/test/test_types.py index 2084b30d71ff6ca..0019a25a639a641 100644 --- a/Lib/test/test_types.py +++ b/Lib/test/test_types.py @@ -1180,6 +1180,28 @@ def test_instantiation(self): class MappingProxyTests(unittest.TestCase): mappingproxy = types.MappingProxyType + class MappingWithoutCopy(collections.abc.Mapping): + def __init__(self, mapping): + self.mapping = mapping + + def __getitem__(self, key): + return self.mapping[key] + + def __iter__(self): + return iter(self.mapping) + + def __len__(self): + return len(self.mapping) + + class RecordingOperand: + def __eq__(self, other): + self.other = other + return None + + def __ror__(self, other): + self.other = other + return None + def test_constructor(self): class userdict(dict): pass @@ -1381,6 +1403,70 @@ def test_union(self): self.assertDictEqual(mapping, {'a': 0, 'b': 1, 'c': 2}) self.assertDictEqual(other, {'c': 3, 'p': 0}) + def test_richcompare_does_not_expose_mapping(self): + mapping = {} + view = self.mappingproxy(mapping) + + class Sneaky: + def __eq__(self, other): + other['x'] = 42 + return None + + self.assertIsNone(view == Sneaky()) + self.assertEqual(mapping, {}) + + other = self.RecordingOperand() + view = self.mappingproxy(self.MappingWithoutCopy(mapping)) + self.assertIsNone(view == other) + self.assertIs(type(other.other), self.mappingproxy) + + def test_union_does_not_expose_mapping(self): + mapping = {} + view = self.mappingproxy(mapping) + mappingproxy = self.mappingproxy + + class SneakyRor: + def __ror__(self, other): + other['x'] = 42 + return None + + class SneakyOr: + def __or__(self, other): + if type(other) is mappingproxy: + return NotImplemented + other['y'] = 42 + return None + + self.assertIsNone(view | SneakyRor()) + self.assertEqual(mapping, {}) + self.assertIsNone(SneakyOr() | view) + self.assertEqual(mapping, {}) + + other = self.RecordingOperand() + view = self.mappingproxy(self.MappingWithoutCopy(mapping)) + self.assertIsNone(view | other) + self.assertIs(type(other.other), self.mappingproxy) + + def test_operator_dispatch_does_not_expose_type_dict(self): + code = textwrap.dedent(""" + class SneakyEq: + def __eq__(self, other): + other['__mappingproxy_test_eq__'] = lambda self: None + return None + + class SneakyRor: + def __ror__(self, other): + other['__mappingproxy_test_or__'] = lambda self: None + return None + + vars(list) == SneakyEq() + vars(dict) | SneakyRor() + + assert not hasattr(list, '__mappingproxy_test_eq__') + assert not hasattr(dict, '__mappingproxy_test_or__') + """) + assert_python_ok("-c", code) + def test_hash(self): class HashableDict(dict): def __hash__(self): diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-06-27-21-30-00.gh-issue-152405.XYp8rQ.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-27-21-30-00.gh-issue-152405.XYp8rQ.rst new file mode 100644 index 000000000000000..83d31717d8ac0df --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-27-21-30-00.gh-issue-152405.XYp8rQ.rst @@ -0,0 +1,4 @@ +Prevent ``mappingproxy`` operator dispatch from exposing the underlying mapping +during rich comparison and union operations. This fixes a case where reflected +operator methods could access and mutate internal dictionaries, including those +of built-in types. diff --git a/Objects/descrobject.c b/Objects/descrobject.c index 30444b7d6774247..2aa59b2e72ded8b 100644 --- a/Objects/descrobject.c +++ b/Objects/descrobject.c @@ -1061,16 +1061,50 @@ static PyMappingMethods mappingproxy_as_mapping = { 0, /* mp_ass_subscript */ }; +/* Use a copy of proxied mappings for operator dispatch, so reflected + methods cannot access the underlying mapping. */ +static PyObject * +mappingproxy_copy_mapping(PyObject *mapping) +{ + PyObject *copy_method; + int res = PyObject_GetOptionalAttr(mapping, &_Py_ID(copy), ©_method); + if (res < 0) { + return NULL; + } + if (res == 0) { + Py_RETURN_NOTIMPLEMENTED; + } + + PyObject *copy = PyObject_CallNoArgs(copy_method); + Py_DECREF(copy_method); + return copy; +} + +static PyObject * +mappingproxy_as_operand(PyObject *operand) +{ + if (PyObject_TypeCheck(operand, &PyDictProxy_Type)) { + return mappingproxy_copy_mapping(((mappingproxyobject *)operand)->mapping); + } + return Py_NewRef(operand); +} + static PyObject * mappingproxy_or(PyObject *left, PyObject *right) { - if (PyObject_TypeCheck(left, &PyDictProxy_Type)) { - left = ((mappingproxyobject*)left)->mapping; + left = mappingproxy_as_operand(left); + if (left == NULL || left == Py_NotImplemented) { + return left; } - if (PyObject_TypeCheck(right, &PyDictProxy_Type)) { - right = ((mappingproxyobject*)right)->mapping; + right = mappingproxy_as_operand(right); + if (right == NULL || right == Py_NotImplemented) { + Py_DECREF(left); + return right; } - return PyNumber_Or(left, right); + PyObject *result = PyNumber_Or(left, right); + Py_DECREF(left); + Py_DECREF(right); + return result; } static PyObject * @@ -1234,7 +1268,13 @@ mappingproxy_richcompare(PyObject *self, PyObject *w, int op) { mappingproxyobject *v = (mappingproxyobject *)self; if (op == Py_EQ || op == Py_NE) { - return PyObject_RichCompare(v->mapping, w, op); + PyObject *mapping = mappingproxy_copy_mapping(v->mapping); + if (mapping == NULL || mapping == Py_NotImplemented) { + return mapping; + } + PyObject *result = PyObject_RichCompare(mapping, w, op); + Py_DECREF(mapping); + return result; } Py_RETURN_NOTIMPLEMENTED; }