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; }