Skip to content

Commit 701a7c5

Browse files
authored
gh-152405: Do not expose the internal mapping when rich-comparing MappingProxyType objects (#152483)
1 parent 1fece44 commit 701a7c5

3 files changed

Lines changed: 138 additions & 11 deletions

File tree

Lib/test/test_types.py

Lines changed: 111 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from test.support.import_helper import import_fresh_module
99

1010
import collections.abc
11-
from collections import namedtuple, UserDict
11+
from collections import namedtuple, UserDict, OrderedDict
1212
import copy
1313
import _datetime
1414
import gc
@@ -1391,26 +1391,127 @@ def __hash__(self):
13911391
view = self.mappingproxy(mapping)
13921392
self.assertEqual(hash(view), hash(mapping))
13931393

1394-
def test_richcompare(self):
1395-
mp1 = self.mappingproxy({'a': 1})
1396-
mp1_2 = self.mappingproxy({'a': 1})
1397-
mp2 = self.mappingproxy({'a': 2})
1394+
def check_richcompare(self, mapping_type):
1395+
data1 = mapping_type({'a': 1})
1396+
data2 = mapping_type({'a': 2})
1397+
mp1 = self.mappingproxy(data1)
1398+
copy1 = self.mappingproxy(data1)
1399+
mp2 = self.mappingproxy(data2)
1400+
1401+
self.assertTrue(mp1 == data1)
1402+
self.assertTrue(data1 == mp1)
1403+
self.assertTrue(copy1 == data1)
1404+
1405+
self.assertTrue(mp1 == copy1)
1406+
self.assertFalse(mp1 != copy1)
13981407

1399-
self.assertTrue(mp1 == mp1_2)
1400-
self.assertFalse(mp1 != mp1_2)
14011408
self.assertFalse(mp1 == mp2)
14021409
self.assertTrue(mp1 != mp2)
1410+
self.assertFalse(mp2 == data1)
1411+
self.assertTrue(mp2 != data1)
1412+
self.assertTrue(data1 != mp2)
14031413

14041414
msg = "not supported between instances of 'mappingproxy' and 'mappingproxy'"
1405-
14061415
with self.assertRaisesRegex(TypeError, msg):
14071416
mp1 > mp2
14081417
with self.assertRaisesRegex(TypeError, msg):
1409-
mp1 < mp1_2
1418+
mp1 < copy1
14101419
with self.assertRaisesRegex(TypeError, msg):
14111420
mp2 >= mp2
14121421
with self.assertRaisesRegex(TypeError, msg):
1413-
mp1_2 <= mp1
1422+
copy1 <= mp1
1423+
1424+
if mapping_type.__module__ == 'collections':
1425+
mapping_name = f'{mapping_type.__module__}.{mapping_type.__name__}'
1426+
else:
1427+
mapping_name = mapping_type.__name__
1428+
with self.assertRaisesRegex(
1429+
TypeError,
1430+
f"not supported between instances of 'mappingproxy' and '{mapping_name}'",
1431+
):
1432+
copy1 <= data1
1433+
1434+
class Evil:
1435+
def __eq__(self, other):
1436+
return other
1437+
1438+
result = (mp1 == Evil())
1439+
self.assertIs(type(result), mapping_type)
1440+
if mapping_type == dict:
1441+
# Evil.__eq__() gets a copy of the mapping
1442+
self.assertEqual(result, data1)
1443+
else:
1444+
self.assertIs(result, data1)
1445+
1446+
def test_richcompare_dict(self):
1447+
self.check_richcompare(dict)
1448+
1449+
def test_richcompare_custom_mapping(self):
1450+
class CustomMapping(collections.abc.Mapping):
1451+
def __init__(self, data):
1452+
self._data = data
1453+
1454+
def __getitem__(self, key):
1455+
return self._data[key]
1456+
1457+
def __iter__(self):
1458+
return iter(self._data)
1459+
1460+
def __len__(self):
1461+
return len(self._data)
1462+
1463+
def __contains__(self, item):
1464+
return item in self._data
1465+
1466+
self.check_richcompare(CustomMapping)
1467+
1468+
class CustomMapping2(CustomMapping):
1469+
def __eq__(self, other):
1470+
return (
1471+
isinstance(other, CustomMapping)
1472+
and self._data == other._data
1473+
)
1474+
1475+
mp1 = self.mappingproxy(CustomMapping({'a': 1}))
1476+
self.assertEqual(mp1, CustomMapping2({'a': 1}))
1477+
self.assertNotEqual(CustomMapping2({'a': 1}), mp1)
1478+
1479+
def test_richcompare_odict(self):
1480+
self.check_richcompare(OrderedDict)
1481+
1482+
def test_richcompare_frozendict(self):
1483+
self.check_richcompare(frozendict)
1484+
1485+
def test_richcompare_evil(self):
1486+
# This test used to mutate the list dictionary,
1487+
# but MappingProxyType now creates a copy to call `__eq__`:
1488+
# https://github.com/python/cpython/issues/152405
1489+
key = "__mappingproxy_crash_key__"
1490+
1491+
class Evil:
1492+
def __eq__(self, other):
1493+
other[key] = 1
1494+
return other
1495+
1496+
# Checks that it does not mutate the internals of `MappingProxyType`:
1497+
dc = {}
1498+
leaked = self.mappingproxy(dc) == Evil()
1499+
self.assertIs(type(leaked), dict)
1500+
self.assertIn(key, leaked)
1501+
self.assertNotIn(key, dc)
1502+
1503+
# Exposes the internals of `MappingProxyType` via richcompare:
1504+
leaked = vars(list) == Evil()
1505+
self.assertIs(type(leaked), dict)
1506+
self.assertIn(key, leaked)
1507+
name = "__mappingproxy_crash_probe__"
1508+
leaked[name] = lambda self: "probe"
1509+
self.assertIn(name, leaked)
1510+
self.assertNotHasAttr(list, key)
1511+
self.assertNotHasAttr(list, name) # it used to return `True`
1512+
del leaked[name]
1513+
self.assertNotIn(name, leaked)
1514+
self.assertNotHasAttr(list, name) # it used to crash
14141515

14151516

14161517
class ClassCreationTests(unittest.TestCase):
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Do not expose the internal mapping of :class:`types.MappingProxyType`
2+
when performing rich-compare operations with non-stdlib types.
3+
Previously, it was possible to mutate the internal mapping of a proxy there.
4+
Now we pass not the original :class:`dict` instance, but its copy,
5+
when dealing with custom types.

Objects/descrobject.c

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1232,8 +1232,29 @@ mappingproxy_traverse(PyObject *self, visitproc visit, void *arg)
12321232
static PyObject *
12331233
mappingproxy_richcompare(PyObject *self, PyObject *w, int op)
12341234
{
1235-
mappingproxyobject *v = (mappingproxyobject *)self;
12361235
if (op == Py_EQ || op == Py_NE) {
1236+
mappingproxyobject *v = (mappingproxyobject *)self;
1237+
// We have to guard the mutable `dict` instances, because it can
1238+
// otherwise mutate the type's `__dict__` entries and cause crashes.
1239+
// But, do not create copies on known types like `OrderedDict`
1240+
// or immutable types like `frozendict`
1241+
// for memory optimization. See gh-152405 for the details.
1242+
if (
1243+
PyDict_CheckExact(v->mapping) &&
1244+
!(PyAnyDict_CheckExact(w) ||
1245+
Py_TYPE(w) == &PyDictProxy_Type ||
1246+
PyODict_CheckExact(w))
1247+
) {
1248+
// So, instead we send a copy:
1249+
PyObject *copy = PyDict_Copy(v->mapping);
1250+
if (copy == NULL) {
1251+
return NULL;
1252+
}
1253+
PyObject *res = PyObject_RichCompare(copy, w, op);
1254+
Py_DECREF(copy);
1255+
return res;
1256+
}
1257+
// Otherwise we are free to share the mapping directly:
12371258
return PyObject_RichCompare(v->mapping, w, op);
12381259
}
12391260
Py_RETURN_NOTIMPLEMENTED;

0 commit comments

Comments
 (0)