From d69416dd21cca9aae9b8c221de5584c15b837024 Mon Sep 17 00:00:00 2001 From: Shamil Abdulaev Date: Wed, 17 Dec 2025 04:04:27 +0300 Subject: [PATCH 1/7] gh-142831: Fix UAF in `_json` module --- Modules/_json.c | 36 +++++++++++------------------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/Modules/_json.c b/Modules/_json.c index 14714d4b346546..33b6c99e31b73f 100644 --- a/Modules/_json.c +++ b/Modules/_json.c @@ -1733,15 +1733,14 @@ _encoder_iterate_mapping_lock_held(PyEncoderObject *s, PyUnicodeWriter *writer, PyObject *key, *value; for (Py_ssize_t i = 0; i < PyList_GET_SIZE(items); i++) { PyObject *item = PyList_GET_ITEM(items, i); -#ifdef Py_GIL_DISABLED - // gh-119438: in the free-threading build the critical section on items can get suspended + + // GH-142831: The item must be strong-referenced to avoid UAF + // if the user code modifies the list during iteration. Py_INCREF(item); -#endif + if (!PyTuple_Check(item) || PyTuple_GET_SIZE(item) != 2) { PyErr_SetString(PyExc_ValueError, "items must return 2-tuples"); -#ifdef Py_GIL_DISABLED Py_DECREF(item); -#endif return -1; } @@ -1750,14 +1749,10 @@ _encoder_iterate_mapping_lock_held(PyEncoderObject *s, PyUnicodeWriter *writer, if (encoder_encode_key_value(s, writer, first, dct, key, value, indent_level, indent_cache, separator) < 0) { -#ifdef Py_GIL_DISABLED Py_DECREF(item); -#endif return -1; } -#ifdef Py_GIL_DISABLED Py_DECREF(item); -#endif } return 0; @@ -1772,24 +1767,20 @@ _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-142831: The key and value must be strong-referenced to avoid UAF + // if the user code modifies the dict during iteration. 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; } @@ -1893,28 +1884,23 @@ _encoder_iterate_fast_seq_lock_held(PyEncoderObject *s, PyUnicodeWriter *writer, { for (Py_ssize_t i = 0; i < PySequence_Fast_GET_SIZE(s_fast); i++) { PyObject *obj = PySequence_Fast_GET_ITEM(s_fast, i); -#ifdef Py_GIL_DISABLED - // gh-119438: in the free-threading build the critical section on s_fast can get suspended + + // GH-142831: The object must be strong-referenced to avoid UAF + // if the user code modifies the sequence during iteration. Py_INCREF(obj); -#endif + if (i) { if (PyUnicodeWriter_WriteStr(writer, separator) < 0) { -#ifdef Py_GIL_DISABLED Py_DECREF(obj); -#endif return -1; } } if (encoder_listencode_obj(s, writer, obj, indent_level, indent_cache)) { _PyErr_FormatNote("when serializing %T item %zd", seq, i); -#ifdef Py_GIL_DISABLED Py_DECREF(obj); -#endif return -1; } -#ifdef Py_GIL_DISABLED Py_DECREF(obj); -#endif } return 0; } From b153c2ac4b16b5a2f0d059108c04e56f20a55a8e Mon Sep 17 00:00:00 2001 From: Shamil Abdulaev Date: Wed, 17 Dec 2025 04:11:02 +0300 Subject: [PATCH 2/7] add news --- .../2025-12-17-04-10-35.gh-issue-142831.ee3t4L.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-12-17-04-10-35.gh-issue-142831.ee3t4L.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-12-17-04-10-35.gh-issue-142831.ee3t4L.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-12-17-04-10-35.gh-issue-142831.ee3t4L.rst new file mode 100644 index 00000000000000..5fa3cd2727a9e5 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-12-17-04-10-35.gh-issue-142831.ee3t4L.rst @@ -0,0 +1,2 @@ +Fix a crash in the :mod:`json` module where a use-after-free could occur if +the object being encoded is modified during serialization. From 64fcd7591015f14e967d70d4e71fa9557f6dfc9e Mon Sep 17 00:00:00 2001 From: Shamil Abdulaev Date: Thu, 18 Dec 2025 14:15:09 +0300 Subject: [PATCH 3/7] add test --- Lib/test/test_json/test_dump.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/Lib/test/test_json/test_dump.py b/Lib/test/test_json/test_dump.py index 39470754003bb6..fb8803d38122ba 100644 --- a/Lib/test/test_json/test_dump.py +++ b/Lib/test/test_json/test_dump.py @@ -65,6 +65,39 @@ def __lt__(self, o): d[1337] = "true.dat" self.assertEqual(self.dumps(d, sort_keys=True), '{"1337": "true.dat"}') + def test_mutate_items_during_encode(self): + c_make_encoder = getattr(self.json.encoder, 'c_make_encoder', None) + if c_make_encoder is None: + self.skipTest("c_make_encoder not available") + + cache = [] + + class BadDict(dict): + def __init__(self): + super().__init__(real=1) + + def items(self): + entries = [("boom", object())] + cache.append(entries) + return entries + + def encode_str(obj): + if cache: + cache.pop().clear() + return '"x"' + + encoder = c_make_encoder( + None, lambda o: "null", + encode_str, None, + ": ", ", ", False, + False, True + ) + + try: + encoder(BadDict(), 0) + except (ValueError, RuntimeError, SystemError): + pass + class TestPyDump(TestDump, PyTest): pass From e3859583e577510d12c5c44cd140a35c5e28f02b Mon Sep 17 00:00:00 2001 From: Shamil Abdulaev Date: Thu, 18 Dec 2025 14:15:19 +0300 Subject: [PATCH 4/7] fix comments --- Modules/_json.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Modules/_json.c b/Modules/_json.c index 33b6c99e31b73f..993ee72d8a0e6f 100644 --- a/Modules/_json.c +++ b/Modules/_json.c @@ -1734,8 +1734,8 @@ _encoder_iterate_mapping_lock_held(PyEncoderObject *s, PyUnicodeWriter *writer, for (Py_ssize_t i = 0; i < PyList_GET_SIZE(items); i++) { PyObject *item = PyList_GET_ITEM(items, i); - // GH-142831: The item must be strong-referenced to avoid UAF - // if the user code modifies the list during iteration. + // GH-142831: The item must be strong-referenced to avoid + // use-after-free if the user code modifies the list during iteration. Py_INCREF(item); if (!PyTuple_Check(item) || PyTuple_GET_SIZE(item) != 2) { @@ -1767,8 +1767,8 @@ _encoder_iterate_dict_lock_held(PyEncoderObject *s, PyUnicodeWriter *writer, PyObject *key, *value; Py_ssize_t pos = 0; while (PyDict_Next(dct, &pos, &key, &value)) { - // GH-142831: The key and value must be strong-referenced to avoid UAF - // if the user code modifies the dict during iteration. + // GH-142831: The key and value must be strong-referenced to avoid + // use-after-free if the user code modifies the dict during iteration. Py_INCREF(key); Py_INCREF(value); @@ -1885,7 +1885,7 @@ _encoder_iterate_fast_seq_lock_held(PyEncoderObject *s, PyUnicodeWriter *writer, for (Py_ssize_t i = 0; i < PySequence_Fast_GET_SIZE(s_fast); i++) { PyObject *obj = PySequence_Fast_GET_ITEM(s_fast, i); - // GH-142831: The object must be strong-referenced to avoid UAF + // GH-142831: The object must be strong-referenced to avoid use-after-free // if the user code modifies the sequence during iteration. Py_INCREF(obj); From 5d6b2a8c9d890107b30c20b27d711ea02082c5d3 Mon Sep 17 00:00:00 2001 From: Shamil Abdulaev Date: Thu, 18 Dec 2025 14:25:30 +0300 Subject: [PATCH 5/7] fix test --- Lib/test/test_json/test_dump.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_json/test_dump.py b/Lib/test/test_json/test_dump.py index fb8803d38122ba..64291d3fd66e20 100644 --- a/Lib/test/test_json/test_dump.py +++ b/Lib/test/test_json/test_dump.py @@ -95,7 +95,7 @@ def encode_str(obj): try: encoder(BadDict(), 0) - except (ValueError, RuntimeError, SystemError): + except (ValueError, RuntimeError): pass From 66c3af1c9609078b76608ee41f9eba45f6866c93 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Fri, 19 Dec 2025 13:36:31 +0530 Subject: [PATCH 6/7] clean up test --- Lib/test/test_json/test_dump.py | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/Lib/test/test_json/test_dump.py b/Lib/test/test_json/test_dump.py index 64291d3fd66e20..8c4c9899b1ebf1 100644 --- a/Lib/test/test_json/test_dump.py +++ b/Lib/test/test_json/test_dump.py @@ -66,10 +66,6 @@ def __lt__(self, o): self.assertEqual(self.dumps(d, sort_keys=True), '{"1337": "true.dat"}') def test_mutate_items_during_encode(self): - c_make_encoder = getattr(self.json.encoder, 'c_make_encoder', None) - if c_make_encoder is None: - self.skipTest("c_make_encoder not available") - cache = [] class BadDict(dict): @@ -84,20 +80,9 @@ def items(self): def encode_str(obj): if cache: cache.pop().clear() - return '"x"' - - encoder = c_make_encoder( - None, lambda o: "null", - encode_str, None, - ": ", ", ", False, - False, True - ) - - try: - encoder(BadDict(), 0) - except (ValueError, RuntimeError): - pass + return 'x' + self.assertEqual(self.dumps(BadDict(), default=encode_str), '{"boom": "x"}') class TestPyDump(TestDump, PyTest): pass From adfeee28e5b3fe53fdc776bc9b07c9416e4b1e4d Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Fri, 19 Dec 2025 14:06:50 +0530 Subject: [PATCH 7/7] Revert "clean up test" This reverts commit 66c3af1c9609078b76608ee41f9eba45f6866c93. --- Lib/test/test_json/test_dump.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_json/test_dump.py b/Lib/test/test_json/test_dump.py index 8c4c9899b1ebf1..64291d3fd66e20 100644 --- a/Lib/test/test_json/test_dump.py +++ b/Lib/test/test_json/test_dump.py @@ -66,6 +66,10 @@ def __lt__(self, o): self.assertEqual(self.dumps(d, sort_keys=True), '{"1337": "true.dat"}') def test_mutate_items_during_encode(self): + c_make_encoder = getattr(self.json.encoder, 'c_make_encoder', None) + if c_make_encoder is None: + self.skipTest("c_make_encoder not available") + cache = [] class BadDict(dict): @@ -80,9 +84,20 @@ def items(self): def encode_str(obj): if cache: cache.pop().clear() - return 'x' + return '"x"' + + encoder = c_make_encoder( + None, lambda o: "null", + encode_str, None, + ": ", ", ", False, + False, True + ) + + try: + encoder(BadDict(), 0) + except (ValueError, RuntimeError): + pass - self.assertEqual(self.dumps(BadDict(), default=encode_str), '{"boom": "x"}') class TestPyDump(TestDump, PyTest): pass