Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Doc/library/types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,12 @@ Standard names are defined for the following types:
Updated to support the new union (``|``) operator from :pep:`584`, which
simply delegates to the underlying mapping.

.. versionchanged:: 3.10

To avoid exposing the actual proxied object to arbitrary code, union and
rich comparison operations now delegate to a copy of the underlying
mapping instead.

.. describe:: key in proxy

Return ``True`` if the underlying mapping has a key *key*, else
Expand Down
41 changes: 41 additions & 0 deletions Lib/test/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -1131,6 +1131,47 @@ def test_union(self):
self.assertDictEqual(mapping, {'a': 0, 'b': 1, 'c': 2})
self.assertDictEqual(other, {'c': 3, 'p': 0})

def test_delegated_operators(self):
class MyDict(dict):
def __eq__(self, other):
return super().__eq__(other)
def __or__(self, other):
return super().__or__(other)
def __ror__(self, other):
return super().__ror__(other)
dict_a = {0: "a"}
dict_b = {0: "b"}
mydict_a = MyDict(dict_a)
mydict_b = MyDict(dict_b)
view_dict_a = self.mappingproxy(dict_a)
view_mydict_a = self.mappingproxy(mydict_a)
view_view_dict_a = self.mappingproxy(view_dict_a)
a_mappings = (dict_a, mydict_a)
b_mappings = (dict_b, mydict_b)
a_views = (view_dict_a, view_mydict_a, view_view_dict_a)
for view in a_views:
for mapping in a_mappings + a_views:
self.assertDictEqual(view | mapping, dict_a)
self.assertDictEqual(mapping | view, dict_a)
self.assertEqual(view, mapping)
self.assertEqual(mapping, view)
for mapping in b_mappings:
self.assertDictEqual(view | mapping, dict_b)
self.assertDictEqual(mapping | view, dict_a)
self.assertNotEqual(view, mapping)
self.assertNotEqual(mapping, view)

def test_bpo_43838(self):
mapping = {}
proxy = self.mappingproxy(mapping)
class Sneaky:
def __eq__(self, other):
other['x'] = 42
return None
self.assertIs(proxy == Sneaky(), None)
self.assertDictEqual(mapping, {})
self.assertEqual(proxy, {})


class ClassCreationTests(unittest.TestCase):

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
To avoid exposing the actual proxied object to arbitrary code, union and
rich comparison operations on :class:`types.MappingProxyType` now delegate
to a copy of the underlying mapping instead.
37 changes: 35 additions & 2 deletions Objects/descrobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -1032,13 +1032,36 @@ static PyMappingMethods mappingproxy_as_mapping = {
static PyObject *
mappingproxy_or(PyObject *left, PyObject *right)
{
// bpo-43838: We can't just return PyNumber_Or(left->mapping, right) or
// PyNumber_Or(left, right->mapping) here, because the operator dispatch dance can
// expose our hidden mapping to arbitrary code. Perform the operation with a copy of
// the mapping instead:
_Py_IDENTIFIER(copy);
if (PyObject_TypeCheck(left, &PyDictProxy_Type)) {
left = ((mappingproxyobject*)left)->mapping;
left = _PyObject_CallMethodIdNoArgs(left, &PyId_copy);
if (left == NULL) {
return NULL;
}
}
else {
Py_INCREF(left);
}
if (PyObject_TypeCheck(right, &PyDictProxy_Type)) {
right = ((mappingproxyobject*)right)->mapping;
right = _PyObject_CallMethodIdNoArgs(right, &PyId_copy);
if (right == NULL) {
Py_DECREF(left);
return NULL;
}
}
return PyNumber_Or(left, right);
else {
Py_INCREF(right);
}
PyObject *result = PyNumber_Or(left, right);
Py_DECREF(left);
Py_DECREF(right);
return result;
}

static PyObject *
Expand Down Expand Up @@ -1188,7 +1211,17 @@ mappingproxy_traverse(PyObject *self, visitproc visit, void *arg)
static PyObject *
mappingproxy_richcompare(mappingproxyobject *v, PyObject *w, int op)
{
return PyObject_RichCompare(v->mapping, w, op);
// bpo-43838: We can't just return PyObject_RichCompare(v->mapping, w, op) here,
// because the operator dispatch dance can expose our hidden mapping to arbitrary
// code. Perform the operation with a copy of the mapping instead:
_Py_IDENTIFIER(copy);
PyObject *copy = _PyObject_CallMethodIdNoArgs(v->mapping, &PyId_copy);
if (copy == NULL) {
return NULL;
}
PyObject *result = PyObject_RichCompare(copy, w, op);
Py_DECREF(copy);
return result;
}

static int
Expand Down