https://github.com/python/cpython/commit/6940c1dc0c211670a4531bc13c10233e2fcf1335
commit: 6940c1dc0c211670a4531bc13c10233e2fcf1335
branch: main
author: Victor Stinner <[email protected]>
committer: vstinner <[email protected]>
date: 2026-02-21T17:21:43+01:00
summary:

gh-141510: Check argument in PyDict_MergeFromSeq2() (#145082)

PyDict_MergeFromSeq2() now fails with SystemError if the first
argument is not a dict or a dict subclass.

PyDict_Update(), PyDict_Merge() and _PyDict_MergeEx() no longer
accept frozendict.

files:
M Lib/test/test_capi/test_dict.py
M Objects/dictobject.c

diff --git a/Lib/test/test_capi/test_dict.py b/Lib/test/test_capi/test_dict.py
index bdd7aa9819fc48..d3cc279cd3f955 100644
--- a/Lib/test/test_capi/test_dict.py
+++ b/Lib/test/test_capi/test_dict.py
@@ -419,6 +419,7 @@ def test_dict_next(self):
         # CRASHES dict_next(NULL, 0)
 
     def test_dict_update(self):
+        # Test PyDict_Update()
         update = _testlimitedcapi.dict_update
         for cls1 in dict, DictSubclass:
             for cls2 in dict, DictSubclass, UserDict:
@@ -429,11 +430,13 @@ def test_dict_update(self):
         self.assertRaises(AttributeError, update, {}, [])
         self.assertRaises(AttributeError, update, {}, 42)
         self.assertRaises(SystemError, update, UserDict(), {})
+        self.assertRaises(SystemError, update, frozendict(), {})
         self.assertRaises(SystemError, update, 42, {})
         self.assertRaises(SystemError, update, {}, NULL)
         self.assertRaises(SystemError, update, NULL, {})
 
     def test_dict_merge(self):
+        # Test PyDict_Merge()
         merge = _testlimitedcapi.dict_merge
         for cls1 in dict, DictSubclass:
             for cls2 in dict, DictSubclass, UserDict:
@@ -447,11 +450,13 @@ def test_dict_merge(self):
         self.assertRaises(AttributeError, merge, {}, [], 0)
         self.assertRaises(AttributeError, merge, {}, 42, 0)
         self.assertRaises(SystemError, merge, UserDict(), {}, 0)
+        self.assertRaises(SystemError, merge, frozendict(), {}, 0)
         self.assertRaises(SystemError, merge, 42, {}, 0)
         self.assertRaises(SystemError, merge, {}, NULL, 0)
         self.assertRaises(SystemError, merge, NULL, {}, 0)
 
     def test_dict_mergefromseq2(self):
+        # Test PyDict_MergeFromSeq2()
         mergefromseq2 = _testlimitedcapi.dict_mergefromseq2
         for cls1 in dict, DictSubclass:
             for cls2 in list, iter:
@@ -466,8 +471,8 @@ def test_dict_mergefromseq2(self):
         self.assertRaises(ValueError, mergefromseq2, {}, [(1, 2, 3)], 0)
         self.assertRaises(TypeError, mergefromseq2, {}, [1], 0)
         self.assertRaises(TypeError, mergefromseq2, {}, 42, 0)
-        # CRASHES mergefromseq2(UserDict(), [], 0)
-        # CRASHES mergefromseq2(42, [], 0)
+        self.assertRaises(SystemError, mergefromseq2, UserDict(), [], 0)
+        self.assertRaises(SystemError, mergefromseq2, 42, [], 0)
         # CRASHES mergefromseq2({}, NULL, 0)
         # CRASHES mergefromseq2(NULL, {}, 0)
 
diff --git a/Objects/dictobject.c b/Objects/dictobject.c
index 8f960352fa4824..276e1df21a80d8 100644
--- a/Objects/dictobject.c
+++ b/Objects/dictobject.c
@@ -140,6 +140,7 @@ static PyObject* frozendict_new(PyTypeObject *type, 
PyObject *args,
                                 PyObject *kwds);
 static PyObject* dict_new(PyTypeObject *type, PyObject *args, PyObject *kwds);
 static int dict_merge(PyObject *a, PyObject *b, int override);
+static int dict_merge_from_seq2(PyObject *d, PyObject *seq2, int override);
 
 
 /*[clinic input]
@@ -3818,16 +3819,16 @@ static int
 dict_update_arg(PyObject *self, PyObject *arg)
 {
     if (PyAnyDict_CheckExact(arg)) {
-        return PyDict_Merge(self, arg, 1);
+        return dict_merge(self, arg, 1);
     }
     int has_keys = PyObject_HasAttrWithError(arg, &_Py_ID(keys));
     if (has_keys < 0) {
         return -1;
     }
     if (has_keys) {
-        return PyDict_Merge(self, arg, 1);
+        return dict_merge(self, arg, 1);
     }
-    return PyDict_MergeFromSeq2(self, arg, 1);
+    return dict_merge_from_seq2(self, arg, 1);
 }
 
 static int
@@ -3846,7 +3847,7 @@ dict_update_common(PyObject *self, PyObject *args, 
PyObject *kwds,
 
     if (result == 0 && kwds != NULL) {
         if (PyArg_ValidateKeywordArguments(kwds))
-            result = PyDict_Merge(self, kwds, 1);
+            result = dict_merge(self, kwds, 1);
         else
             result = -1;
     }
@@ -3960,8 +3961,8 @@ merge_from_seq2_lock_held(PyObject *d, PyObject *seq2, 
int override)
     return Py_SAFE_DOWNCAST(i, Py_ssize_t, int);
 }
 
-int
-PyDict_MergeFromSeq2(PyObject *d, PyObject *seq2, int override)
+static int
+dict_merge_from_seq2(PyObject *d, PyObject *seq2, int override)
 {
     int res;
     Py_BEGIN_CRITICAL_SECTION(d);
@@ -3971,6 +3972,19 @@ PyDict_MergeFromSeq2(PyObject *d, PyObject *seq2, int 
override)
     return res;
 }
 
+int
+PyDict_MergeFromSeq2(PyObject *d, PyObject *seq2, int override)
+{
+    assert(d != NULL);
+    assert(seq2 != NULL);
+    if (!PyDict_Check(d)) {
+        PyErr_BadInternalCall();
+        return -1;
+    }
+
+    return dict_merge_from_seq2(d, seq2, override);
+}
+
 static int
 dict_dict_merge(PyDictObject *mp, PyDictObject *other, int override)
 {
@@ -4070,23 +4084,14 @@ dict_dict_merge(PyDictObject *mp, PyDictObject *other, 
int override)
 static int
 dict_merge(PyObject *a, PyObject *b, int override)
 {
-    PyDictObject *mp, *other;
-
+    assert(a != NULL);
+    assert(b != NULL);
     assert(0 <= override && override <= 2);
 
-    /* We accept for the argument either a concrete dictionary object,
-     * or an abstract "mapping" object.  For the former, we can do
-     * things quite efficiently.  For the latter, we only require that
-     * PyMapping_Keys() and PyObject_GetItem() be supported.
-     */
-    if (a == NULL || !PyAnyDict_Check(a) || b == NULL) {
-        PyErr_BadInternalCall();
-        return -1;
-    }
-    mp = (PyDictObject*)a;
+    PyDictObject *mp = _PyAnyDict_CAST(a);
     int res = 0;
     if (PyAnyDict_Check(b) && (Py_TYPE(b)->tp_iter == dict_iter)) {
-        other = (PyDictObject*)b;
+        PyDictObject *other = (PyDictObject*)b;
         int res;
         Py_BEGIN_CRITICAL_SECTION2(a, b);
         res = dict_dict_merge((PyDictObject *)a, other, override);
@@ -4167,23 +4172,38 @@ dict_merge(PyObject *a, PyObject *b, int override)
     }
 }
 
+static int
+dict_merge_api(PyObject *a, PyObject *b, int override)
+{
+    /* We accept for the argument either a concrete dictionary object,
+     * or an abstract "mapping" object.  For the former, we can do
+     * things quite efficiently.  For the latter, we only require that
+     * PyMapping_Keys() and PyObject_GetItem() be supported.
+     */
+    if (a == NULL || !PyDict_Check(a) || b == NULL) {
+        PyErr_BadInternalCall();
+        return -1;
+    }
+    return dict_merge(a, b, override);
+}
+
 int
 PyDict_Update(PyObject *a, PyObject *b)
 {
-    return dict_merge(a, b, 1);
+    return dict_merge_api(a, b, 1);
 }
 
 int
 PyDict_Merge(PyObject *a, PyObject *b, int override)
 {
     /* XXX Deprecate override not in (0, 1). */
-    return dict_merge(a, b, override != 0);
+    return dict_merge_api(a, b, override != 0);
 }
 
 int
 _PyDict_MergeEx(PyObject *a, PyObject *b, int override)
 {
-    return dict_merge(a, b, override);
+    return dict_merge_api(a, b, override);
 }
 
 /*[clinic input]

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to