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 <zintensitydev@gmail.com>
This commit is contained in:
Ramin Farajpour Cami 2026-04-12 01:56:36 +03:30 committed by GitHub
parent daa2578dc0
commit 8a466fa3d9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 29 additions and 7 deletions

View file

@ -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

View file

@ -0,0 +1,2 @@
Fixed a use-after-free in :mod:`json` encoder when a ``default`` callback
mutates the dictionary being serialized.

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