diff --git a/Lib/test/test_json/test_dump.py b/Lib/test/test_json/test_dump.py index 9880698455ca5e..bf041e4babfe37 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) + class TestPyDump(TestDump, PyTest): pass 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 cbede8f44dc065..de11b76dc9e8f0 100644 --- a/Modules/_json.c +++ b/Modules/_json.c @@ -1774,24 +1774,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; }