]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-145244: Fix use-after-free on borrowed dict key in json encoder (GH-145245)
authorRamin Farajpour Cami <ramin.blackhat@gmail.com>
Sat, 11 Apr 2026 22:26:36 +0000 (01:56 +0330)
committerGitHub <noreply@github.com>
Sat, 11 Apr 2026 22:26:36 +0000 (22:26 +0000)
In encoder_encode_key_value(), key is a borrowed reference from
PyDict_Next(). If the default callback mutates or clears the dict,
key becomes a dangling pointer. The error path then calls
_PyErr_FormatNote("%R", key) on freed memory.

Fix by holding strong references to key and value unconditionally
during encoding, not just in the free-threading build.

Co-authored-by: Peter Bierma <zintensitydev@gmail.com>
Lib/test/test_json/test_dump.py
Misc/NEWS.d/next/Library/2026-02-26-12-00-00.gh-issue-145244.Kj31cp.rst [new file with mode: 0644]
Modules/_json.c

index 850e5ceeba0c899b31c78f05a0e0cf54e90a7585..5bc03085e60a3d3f8bd2452abd7f0de8512fb0ac 100644 (file)
@@ -77,6 +77,29 @@ class TestDump:
         d[1337] = "true.dat"
         self.assertEqual(self.dumps(d, sort_keys=True), '{"1337": "true.dat"}')
 
+    # gh-145244: UAF on borrowed key when default callback mutates dict
+    def test_default_clears_dict_key_uaf(self):
+        class Evil:
+            pass
+
+        class AlsoEvil:
+            pass
+
+        # Use a non-interned string key so it can actually be freed
+        key = "A" * 100
+        target = {key: Evil()}
+        del key
+
+        def evil_default(obj):
+            if isinstance(obj, Evil):
+                target.clear()
+                return AlsoEvil()
+            raise TypeError("not serializable")
+
+        with self.assertRaises(TypeError):
+            self.json.dumps(target, default=evil_default,
+                            check_circular=False)
+
     def test_dumps_str_subclass(self):
         # Don't call obj.__str__() on str subclasses
 
diff --git a/Misc/NEWS.d/next/Library/2026-02-26-12-00-00.gh-issue-145244.Kj31cp.rst b/Misc/NEWS.d/next/Library/2026-02-26-12-00-00.gh-issue-145244.Kj31cp.rst
new file mode 100644 (file)
index 0000000..07d7c1f
--- /dev/null
@@ -0,0 +1,2 @@
+Fixed a use-after-free in :mod:`json` encoder when a ``default`` callback
+mutates the dictionary being serialized.
index a20466de8c50e4624a5ebdab52c91bd76efb6863..e36e69b09b2030dff3e588f1a68707270709638f 100644 (file)
@@ -1784,24 +1784,21 @@ _encoder_iterate_dict_lock_held(PyEncoderObject *s, PyUnicodeWriter *writer,
     PyObject *key, *value;
     Py_ssize_t pos = 0;
     while (PyDict_Next(dct, &pos, &key, &value)) {
-#ifdef Py_GIL_DISABLED
-        // gh-119438: in the free-threading build the critical section on dct can get suspended
+        // gh-119438, gh-145244: key and value are borrowed refs from
+        // PyDict_Next(). encoder_encode_key_value() may invoke user
+        // Python code (the 'default' callback) that can mutate or
+        // clear the dict, so we must hold strong references.
         Py_INCREF(key);
         Py_INCREF(value);
-#endif
         if (encoder_encode_key_value(s, writer, first, dct, key, value,
                                     indent_level, indent_cache,
                                     separator) < 0) {
-#ifdef Py_GIL_DISABLED
             Py_DECREF(key);
             Py_DECREF(value);
-#endif
             return -1;
         }
-#ifdef Py_GIL_DISABLED
         Py_DECREF(key);
         Py_DECREF(value);
-#endif
     }
     return 0;
 }