Skip to content

Commit 8a466fa

Browse files
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>
1 parent daa2578 commit 8a466fa

File tree

3 files changed

+29
-7
lines changed

3 files changed

+29
-7
lines changed

Lib/test/test_json/test_dump.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,29 @@ def __lt__(self, o):
7777
d[1337] = "true.dat"
7878
self.assertEqual(self.dumps(d, sort_keys=True), '{"1337": "true.dat"}')
7979

80+
# gh-145244: UAF on borrowed key when default callback mutates dict
81+
def test_default_clears_dict_key_uaf(self):
82+
class Evil:
83+
pass
84+
85+
class AlsoEvil:
86+
pass
87+
88+
# Use a non-interned string key so it can actually be freed
89+
key = "A" * 100
90+
target = {key: Evil()}
91+
del key
92+
93+
def evil_default(obj):
94+
if isinstance(obj, Evil):
95+
target.clear()
96+
return AlsoEvil()
97+
raise TypeError("not serializable")
98+
99+
with self.assertRaises(TypeError):
100+
self.json.dumps(target, default=evil_default,
101+
check_circular=False)
102+
80103
def test_dumps_str_subclass(self):
81104
# Don't call obj.__str__() on str subclasses
82105

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fixed a use-after-free in :mod:`json` encoder when a ``default`` callback
2+
mutates the dictionary being serialized.

Modules/_json.c

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1784,24 +1784,21 @@ _encoder_iterate_dict_lock_held(PyEncoderObject *s, PyUnicodeWriter *writer,
17841784
PyObject *key, *value;
17851785
Py_ssize_t pos = 0;
17861786
while (PyDict_Next(dct, &pos, &key, &value)) {
1787-
#ifdef Py_GIL_DISABLED
1788-
// gh-119438: in the free-threading build the critical section on dct can get suspended
1787+
// gh-119438, gh-145244: key and value are borrowed refs from
1788+
// PyDict_Next(). encoder_encode_key_value() may invoke user
1789+
// Python code (the 'default' callback) that can mutate or
1790+
// clear the dict, so we must hold strong references.
17891791
Py_INCREF(key);
17901792
Py_INCREF(value);
1791-
#endif
17921793
if (encoder_encode_key_value(s, writer, first, dct, key, value,
17931794
indent_level, indent_cache,
17941795
separator) < 0) {
1795-
#ifdef Py_GIL_DISABLED
17961796
Py_DECREF(key);
17971797
Py_DECREF(value);
1798-
#endif
17991798
return -1;
18001799
}
1801-
#ifdef Py_GIL_DISABLED
18021800
Py_DECREF(key);
18031801
Py_DECREF(value);
1804-
#endif
18051802
}
18061803
return 0;
18071804
}

0 commit comments

Comments
 (0)