Skip to content

Commit 9a7c697

Browse files
committed
Use a different approach, only copy dicts themself
1 parent 936ccf4 commit 9a7c697

3 files changed

Lines changed: 25 additions & 23 deletions

File tree

Lib/test/test_types.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1463,9 +1463,20 @@ def __eq__(self, other):
14631463
return other
14641464

14651465
leaked = mp1 == Evil()
1466-
self.assertIs(type(leaked), dict)
1466+
self.assertIs(type(leaked), CustomMapping)
1467+
self.assertEqual(leaked, mp1)
14671468
self.assertEqual(leaked, dt1)
14681469

1470+
class CustomMapping2(CustomMapping):
1471+
def __eq__(self, other):
1472+
return (
1473+
isinstance(other, CustomMapping)
1474+
and self._data == other._data
1475+
)
1476+
1477+
self.assertEqual(mp1, CustomMapping2({'a': 1}))
1478+
self.assertNotEqual(CustomMapping2({'a': 1}), mp1)
1479+
14691480
def test_richcompare_odict(self):
14701481
od1 = OrderedDict(x=1, y=2)
14711482
od1_2 = OrderedDict(x=1, y=2)

Misc/NEWS.d/next/Core_and_Builtins/2026-06-28-14-39-05.gh-issue-152405.-_3YRo.rst

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,4 @@ Fixes a crash, when modifing :class:`types.MappingProxyType` exposed via a
22
rich-compare operation with :func:`vars` function. It was possible to mutate
33
the internals of builtins types via this technique.
44

5-
It changes the object that is passed to custom rich-compare methods
6-
from the original internal mapping type to be always a new :class:`dict` copy
7-
with all the keys and values from the original mapping.
5+
Now we pass not the original :class:`dict` instance, but its copy.

Objects/descrobject.c

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1234,28 +1234,21 @@ mappingproxy_richcompare(PyObject *self, PyObject *w, int op)
12341234
{
12351235
if (op == Py_EQ || op == Py_NE) {
12361236
mappingproxyobject *v = (mappingproxyobject *)self;
1237-
// Common path optimizations, where we can expose the real mapping:
1238-
if (
1239-
PyAnyDict_CheckExact(w)
1240-
|| PyODict_CheckExact(w)
1241-
|| Py_TYPE(w) == &PyDictProxy_Type
1242-
) {
1243-
return PyObject_RichCompare(v->mapping, w, op);
1244-
}
1245-
// We can't expose the `v->mapping` itself, so we create a dict copy:
1246-
// it was possible to mutate `v->mapping` in rich-compare methods.
1237+
// We have to guard the mutable `dict` instances, because it can
1238+
// otherwise mutate the types `__dict__` keys and cause crashes:
12471239
// See gh-152405 on the details.
1248-
PyObject *copy = PyDict_New();
1249-
if (copy == NULL) {
1250-
return NULL;
1251-
}
1252-
if (PyDict_Update(copy, v->mapping)) {
1240+
if (PyDict_CheckExact(v->mapping)) {
1241+
// So, instead we send a copy:
1242+
PyObject *copy = PyDict_Copy(v->mapping);
1243+
if (copy == NULL) {
1244+
return NULL;
1245+
}
1246+
PyObject *res = PyObject_RichCompare(copy, w, op);
12531247
Py_DECREF(copy);
1254-
return NULL;
1248+
return res;
12551249
}
1256-
PyObject *res = PyObject_RichCompare(copy, w, op);
1257-
Py_DECREF(copy);
1258-
return res;
1250+
// Otherwise we are free to share the mapping directly:
1251+
return PyObject_RichCompare(v->mapping, w, op);
12591252
}
12601253
Py_RETURN_NOTIMPLEMENTED;
12611254
}

0 commit comments

Comments
 (0)