https://github.com/python/cpython/commit/8a466fa3d90a9e1f04d23c05ee2cf3f3c406ba30
commit: 8a466fa3d90a9e1f04d23c05ee2cf3f3c406ba30
branch: main
author: Ramin Farajpour Cami <[email protected]>
committer: gpshead <[email protected]>
date: 2026-04-11T22:26:36Z
summary:

gh-145244: Fix use-after-free on borrowed dict key in json encoder (GH-145245)

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 <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-02-26-12-00-00.gh-issue-145244.Kj31cp.rst
M Lib/test/test_json/test_dump.py
M Modules/_json.c

diff --git a/Lib/test/test_json/test_dump.py b/Lib/test/test_json/test_dump.py
index 850e5ceeba0c89..5bc03085e60a3d 100644
--- a/Lib/test/test_json/test_dump.py
+++ b/Lib/test/test_json/test_dump.py
@@ -77,6 +77,29 @@ def __lt__(self, o):
         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
index 00000000000000..07d7c1fe85e292
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-02-26-12-00-00.gh-issue-145244.Kj31cp.rst
@@ -0,0 +1,2 @@
+Fixed a use-after-free in :mod:`json` encoder when a ``default`` callback
+mutates the dictionary being serialized.
diff --git a/Modules/_json.c b/Modules/_json.c
index a20466de8c50e4..e36e69b09b2030 100644
--- a/Modules/_json.c
+++ b/Modules/_json.c
@@ -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;
 }

_______________________________________________
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