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]