From 94778241a2ea670de57796c64bf6200c27ffb190 Mon Sep 17 00:00:00 2001 From: VanshAgarwal24036 Date: Thu, 8 Jan 2026 18:36:25 +0530 Subject: [PATCH 01/25] gh-143544: Fix use-after-free in _json.raise_errmsg --- Modules/_json.c | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/Modules/_json.c b/Modules/_json.c index 14714d4b346546..fca2390e7b292b 100644 --- a/Modules/_json.c +++ b/Modules/_json.c @@ -413,21 +413,33 @@ write_escaped_unicode(PyUnicodeWriter *writer, PyObject *pystr) static void raise_errmsg(const char *msg, PyObject *s, Py_ssize_t end) { - /* Use JSONDecodeError exception to raise a nice looking ValueError subclass */ _Py_DECLARE_STR(json_decoder, "json.decoder"); - PyObject *JSONDecodeError = - PyImport_ImportModuleAttr(&_Py_STR(json_decoder), &_Py_ID(JSONDecodeError)); - if (JSONDecodeError == NULL) { + + PyObject *json_error = + PyImport_ImportModuleAttr(&_Py_STR(json_decoder), &_Py_ID(JSONDecodeError)); + if (json_error == NULL) { return; } - PyObject *exc; - exc = PyObject_CallFunction(JSONDecodeError, "zOn", msg, s, end); - Py_DECREF(JSONDecodeError); - if (exc) { - PyErr_SetObject(JSONDecodeError, exc); + /* Hold a strong reference across user code execution */ + PyObject *error_type = Py_NewRef(json_error); + + PyObject *exc = PyObject_CallFunction(error_type, "zOn", msg, s, end); + + if (exc != NULL) { + /* Only use it if it's a valid exception type */ + if (PyExceptionClass_Check(error_type)) { + PyErr_SetObject(error_type, exc); + } + else { + /* Fallback: always safe */ + PyErr_SetString(PyExc_ValueError, msg); + } Py_DECREF(exc); } + + Py_DECREF(error_type); + Py_DECREF(json_error); } static void From 946aee311d598933cd13689854015485fb0d6132 Mon Sep 17 00:00:00 2001 From: VanshAgarwal24036 Date: Thu, 8 Jan 2026 19:31:44 +0530 Subject: [PATCH 02/25] gh-143544: Fix use-after-free in _json.raise_errmsg --- Modules/_json.c | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/Modules/_json.c b/Modules/_json.c index fca2390e7b292b..5f104d7bbbb48a 100644 --- a/Modules/_json.c +++ b/Modules/_json.c @@ -413,33 +413,22 @@ write_escaped_unicode(PyUnicodeWriter *writer, PyObject *pystr) static void raise_errmsg(const char *msg, PyObject *s, Py_ssize_t end) { + /* Use JSONDecodeError exception to raise a nice looking ValueError subclass */ _Py_DECLARE_STR(json_decoder, "json.decoder"); - PyObject *json_error = + PyObject *JSONDecodeError = PyImport_ImportModuleAttr(&_Py_STR(json_decoder), &_Py_ID(JSONDecodeError)); - if (json_error == NULL) { + if (JSONDecodeError == NULL) { return; } - /* Hold a strong reference across user code execution */ - PyObject *error_type = Py_NewRef(json_error); - - PyObject *exc = PyObject_CallFunction(error_type, "zOn", msg, s, end); - + PyObject *exc = PyObject_CallFunction(JSONDecodeError, "zOn", msg, s, end); if (exc != NULL) { - /* Only use it if it's a valid exception type */ - if (PyExceptionClass_Check(error_type)) { - PyErr_SetObject(error_type, exc); - } - else { - /* Fallback: always safe */ - PyErr_SetString(PyExc_ValueError, msg); - } + PyErr_SetObject(JSONDecodeError, exc); Py_DECREF(exc); } - Py_DECREF(error_type); - Py_DECREF(json_error); + Py_DECREF(JSONDecodeError); } static void From 55b48505baafad8b524c4ccd214e9080e6930144 Mon Sep 17 00:00:00 2001 From: VanshAgarwal24036 Date: Thu, 8 Jan 2026 19:32:06 +0530 Subject: [PATCH 03/25] gh-143544: Add regression test for re-entrant JSONDecodeError --- Lib/test/test_json/test_fail.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/Lib/test/test_json/test_fail.py b/Lib/test/test_json/test_fail.py index 79c44af2fbf0e1..5eae35c9fe03ec 100644 --- a/Lib/test/test_json/test_fail.py +++ b/Lib/test/test_json/test_fail.py @@ -235,6 +235,36 @@ def test_linecol(self): self.assertEqual(str(err), 'Expecting value: line %s column %d (char %d)' % (line, col, idx)) + + def test_reentrant_jsondecodeerror_does_not_crash(self): + import json + + orig_json_error = json.JSONDecodeError + orig_decoder_error = json.decoder.JSONDecodeError + + class Trigger: + def __call__(self, *args): + import json as mod + # Remove JSONDecodeError during construction to trigger re-entrancy + if hasattr(mod, "JSONDecodeError"): + del mod.JSONDecodeError + if hasattr(mod.decoder, "JSONDecodeError"): + del mod.decoder.JSONDecodeError + return ValueError("boom") + + hook = Trigger() + try: + json.JSONDecodeError = hook + json.decoder.JSONDecodeError = hook + + # The exact exception type is not important here; the test + # only verifies that we do not crash or trigger a SystemError. + with self.assertRaises(Exception): + self.loads('"\\uZZZZ"') + + finally: + json.JSONDecodeError = orig_json_error + json.decoder.JSONDecodeError = orig_decoder_error class TestPyFail(TestFail, PyTest): pass class TestCFail(TestFail, CTest): pass From 1940b1e1777af727a04da5421655ecf4f9434b4a Mon Sep 17 00:00:00 2001 From: VanshAgarwal24036 Date: Thu, 8 Jan 2026 19:40:01 +0530 Subject: [PATCH 04/25] gh-143544: Add NEWS entry --- .../next/Library/2026-01-08-19-38-02.gh-issue-143544.4jRCBJ.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-01-08-19-38-02.gh-issue-143544.4jRCBJ.rst diff --git a/Misc/NEWS.d/next/Library/2026-01-08-19-38-02.gh-issue-143544.4jRCBJ.rst b/Misc/NEWS.d/next/Library/2026-01-08-19-38-02.gh-issue-143544.4jRCBJ.rst new file mode 100644 index 00000000000000..b2f959d43a7efc --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-01-08-19-38-02.gh-issue-143544.4jRCBJ.rst @@ -0,0 +1,2 @@ +Fixed a crash in json decoding when JSONDecodeError is replaced with a +custom callable. From ed2c55c9b7a721c44d308cff18a7796131187796 Mon Sep 17 00:00:00 2001 From: VanshAgarwal24036 Date: Thu, 8 Jan 2026 19:49:21 +0530 Subject: [PATCH 05/25] gh-143544: Fix trailing whitespace in test --- Lib/test/test_json/test_fail.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_json/test_fail.py b/Lib/test/test_json/test_fail.py index 5eae35c9fe03ec..10a10a3fd3821c 100644 --- a/Lib/test/test_json/test_fail.py +++ b/Lib/test/test_json/test_fail.py @@ -235,7 +235,7 @@ def test_linecol(self): self.assertEqual(str(err), 'Expecting value: line %s column %d (char %d)' % (line, col, idx)) - + def test_reentrant_jsondecodeerror_does_not_crash(self): import json @@ -261,7 +261,7 @@ def __call__(self, *args): # only verifies that we do not crash or trigger a SystemError. with self.assertRaises(Exception): self.loads('"\\uZZZZ"') - + finally: json.JSONDecodeError = orig_json_error json.decoder.JSONDecodeError = orig_decoder_error From 099fc4fe623b4f0beeb1c1cc4df228f99f0a8763 Mon Sep 17 00:00:00 2001 From: VanshAgarwal24036 Date: Thu, 8 Jan 2026 21:10:15 +0530 Subject: [PATCH 06/25] gh-143544: Adjust regression test to allow SystemError --- Lib/test/test_json/test_fail.py | 14 ++++++-------- .../2026-01-08-19-38-02.gh-issue-143544.4jRCBJ.rst | 2 -- 2 files changed, 6 insertions(+), 10 deletions(-) delete mode 100644 Misc/NEWS.d/next/Library/2026-01-08-19-38-02.gh-issue-143544.4jRCBJ.rst diff --git a/Lib/test/test_json/test_fail.py b/Lib/test/test_json/test_fail.py index 10a10a3fd3821c..1b3e73bcef9d13 100644 --- a/Lib/test/test_json/test_fail.py +++ b/Lib/test/test_json/test_fail.py @@ -237,6 +237,7 @@ def test_linecol(self): (line, col, idx)) def test_reentrant_jsondecodeerror_does_not_crash(self): + # gh-143544 import json orig_json_error = json.JSONDecodeError @@ -244,12 +245,9 @@ def test_reentrant_jsondecodeerror_does_not_crash(self): class Trigger: def __call__(self, *args): - import json as mod # Remove JSONDecodeError during construction to trigger re-entrancy - if hasattr(mod, "JSONDecodeError"): - del mod.JSONDecodeError - if hasattr(mod.decoder, "JSONDecodeError"): - del mod.decoder.JSONDecodeError + del json.JSONDecodeError + del json.decoder.JSONDecodeError return ValueError("boom") hook = Trigger() @@ -257,10 +255,10 @@ def __call__(self, *args): json.JSONDecodeError = hook json.decoder.JSONDecodeError = hook - # The exact exception type is not important here; the test - # only verifies that we do not crash or trigger a SystemError. + # The exact exception type is not important here; + # this test only ensures we don't crash. with self.assertRaises(Exception): - self.loads('"\\uZZZZ"') + json.loads('"\\uZZZZ"') finally: json.JSONDecodeError = orig_json_error diff --git a/Misc/NEWS.d/next/Library/2026-01-08-19-38-02.gh-issue-143544.4jRCBJ.rst b/Misc/NEWS.d/next/Library/2026-01-08-19-38-02.gh-issue-143544.4jRCBJ.rst deleted file mode 100644 index b2f959d43a7efc..00000000000000 --- a/Misc/NEWS.d/next/Library/2026-01-08-19-38-02.gh-issue-143544.4jRCBJ.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fixed a crash in json decoding when JSONDecodeError is replaced with a -custom callable. From b2383451c5e3200cc293f1eaa9a1b4d70121ea1d Mon Sep 17 00:00:00 2001 From: VanshAgarwal24036 Date: Fri, 9 Jan 2026 17:30:27 +0530 Subject: [PATCH 07/25] gh-143544: Fix use-after-free in _json.raise_errmsg --- Modules/_json.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Modules/_json.c b/Modules/_json.c index 5f104d7bbbb48a..8c1498dee5ade8 100644 --- a/Modules/_json.c +++ b/Modules/_json.c @@ -417,20 +417,23 @@ raise_errmsg(const char *msg, PyObject *s, Py_ssize_t end) _Py_DECLARE_STR(json_decoder, "json.decoder"); PyObject *JSONDecodeError = - PyImport_ImportModuleAttr(&_Py_STR(json_decoder), &_Py_ID(JSONDecodeError)); + PyImport_ImportModuleAttr(&_Py_STR(json_decoder), + &_Py_ID(JSONDecodeError)); if (JSONDecodeError == NULL) { return; } PyObject *exc = PyObject_CallFunction(JSONDecodeError, "zOn", msg, s, end); - if (exc != NULL) { + if (exc) { PyErr_SetObject(JSONDecodeError, exc); Py_DECREF(exc); } + /* Move DECREF after PyErr_SetObject */ Py_DECREF(JSONDecodeError); } + static void raise_stop_iteration(Py_ssize_t idx) { From fa69d1e9dd8aaf5a017fe3dd74e3bc29f848266e Mon Sep 17 00:00:00 2001 From: VanshAgarwal24036 Date: Fri, 9 Jan 2026 17:30:52 +0530 Subject: [PATCH 08/25] gh-143544: Move json import to module level in regression test --- Lib/test/test_json/test_fail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_json/test_fail.py b/Lib/test/test_json/test_fail.py index 1b3e73bcef9d13..9e555a338ab6c0 100644 --- a/Lib/test/test_json/test_fail.py +++ b/Lib/test/test_json/test_fail.py @@ -1,4 +1,5 @@ from test.test_json import PyTest, CTest +import json # 2007-10-05 JSONDOCS = [ @@ -238,7 +239,6 @@ def test_linecol(self): def test_reentrant_jsondecodeerror_does_not_crash(self): # gh-143544 - import json orig_json_error = json.JSONDecodeError orig_decoder_error = json.decoder.JSONDecodeError From 39767c61a237294cf87531ffb69f7dc115363c95 Mon Sep 17 00:00:00 2001 From: VanshAgarwal24036 Date: Fri, 9 Jan 2026 17:47:26 +0530 Subject: [PATCH 09/25] gh-143544: Remove unnecessary comment --- Modules/_json.c | 1 - 1 file changed, 1 deletion(-) diff --git a/Modules/_json.c b/Modules/_json.c index 8c1498dee5ade8..a496ee6402cc81 100644 --- a/Modules/_json.c +++ b/Modules/_json.c @@ -429,7 +429,6 @@ raise_errmsg(const char *msg, PyObject *s, Py_ssize_t end) Py_DECREF(exc); } - /* Move DECREF after PyErr_SetObject */ Py_DECREF(JSONDecodeError); } From b500563b21df982a9d1d7cdc500643b5251d2ab5 Mon Sep 17 00:00:00 2001 From: VanshAgarwal24036 Date: Fri, 9 Jan 2026 19:21:56 +0530 Subject: [PATCH 10/25] gh-143544: Fix reference lifetime in raise_errmsg --- Modules/_json.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Modules/_json.c b/Modules/_json.c index a496ee6402cc81..b786120afc4b86 100644 --- a/Modules/_json.c +++ b/Modules/_json.c @@ -417,8 +417,7 @@ raise_errmsg(const char *msg, PyObject *s, Py_ssize_t end) _Py_DECLARE_STR(json_decoder, "json.decoder"); PyObject *JSONDecodeError = - PyImport_ImportModuleAttr(&_Py_STR(json_decoder), - &_Py_ID(JSONDecodeError)); + PyImport_ImportModuleAttr(&_Py_STR(json_decoder), &_Py_ID(JSONDecodeError)); if (JSONDecodeError == NULL) { return; } From 5841c2c15059604e32f0fcadc00ea7a51a30c450 Mon Sep 17 00:00:00 2001 From: VanshAgarwal24036 <148854295+VanshAgarwal24036@users.noreply.github.com> Date: Fri, 9 Jan 2026 20:11:52 +0530 Subject: [PATCH 11/25] Update Lib/test/test_json/test_fail.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Lib/test/test_json/test_fail.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/Lib/test/test_json/test_fail.py b/Lib/test/test_json/test_fail.py index 9e555a338ab6c0..d9c81069601f62 100644 --- a/Lib/test/test_json/test_fail.py +++ b/Lib/test/test_json/test_fail.py @@ -251,18 +251,14 @@ def __call__(self, *args): return ValueError("boom") hook = Trigger() - try: - json.JSONDecodeError = hook - json.decoder.JSONDecodeError = hook - + with ( + support.swap_attr(json, "JSONDecodeError", hook), + support.swap_attr(json.decoder, "JSONDecodeError", hook) + ): # The exact exception type is not important here; # this test only ensures we don't crash. with self.assertRaises(Exception): json.loads('"\\uZZZZ"') - finally: - json.JSONDecodeError = orig_json_error - json.decoder.JSONDecodeError = orig_decoder_error - class TestPyFail(TestFail, PyTest): pass class TestCFail(TestFail, CTest): pass From c397e8e385107f4e782922a0ebc3caf129365c76 Mon Sep 17 00:00:00 2001 From: VanshAgarwal24036 Date: Fri, 9 Jan 2026 20:10:08 +0530 Subject: [PATCH 12/25] gh-143544: Remove unnecessary json import from regression test --- Lib/test/test_json/test_fail.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/test_json/test_fail.py b/Lib/test/test_json/test_fail.py index d9c81069601f62..1643f3c231fd91 100644 --- a/Lib/test/test_json/test_fail.py +++ b/Lib/test/test_json/test_fail.py @@ -1,5 +1,4 @@ from test.test_json import PyTest, CTest -import json # 2007-10-05 JSONDOCS = [ From 10c61c43e988a10bebb22ed7b82b37c550f360c5 Mon Sep 17 00:00:00 2001 From: VanshAgarwal24036 Date: Fri, 9 Jan 2026 20:28:05 +0530 Subject: [PATCH 13/25] gh-143544: Use support.swap_attr in re-entrant JSONDecodeError test --- Lib/test/test_json/test_fail.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Lib/test/test_json/test_fail.py b/Lib/test/test_json/test_fail.py index 1643f3c231fd91..78738ce5252e5e 100644 --- a/Lib/test/test_json/test_fail.py +++ b/Lib/test/test_json/test_fail.py @@ -1,4 +1,6 @@ from test.test_json import PyTest, CTest +from test import support +import json # 2007-10-05 JSONDOCS = [ @@ -236,6 +238,7 @@ def test_linecol(self): 'Expecting value: line %s column %d (char %d)' % (line, col, idx)) + def test_reentrant_jsondecodeerror_does_not_crash(self): # gh-143544 From 4a7f323df577b6092f19f6f0be94670fac29e984 Mon Sep 17 00:00:00 2001 From: VanshAgarwal24036 Date: Fri, 9 Jan 2026 20:33:18 +0530 Subject: [PATCH 14/25] gh-143544: Fix use-after-free in _json.raise_errmsg --- Modules/_json.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Modules/_json.c b/Modules/_json.c index b786120afc4b86..78a85496575a2c 100644 --- a/Modules/_json.c +++ b/Modules/_json.c @@ -415,14 +415,14 @@ raise_errmsg(const char *msg, PyObject *s, Py_ssize_t end) { /* Use JSONDecodeError exception to raise a nice looking ValueError subclass */ _Py_DECLARE_STR(json_decoder, "json.decoder"); - PyObject *JSONDecodeError = PyImport_ImportModuleAttr(&_Py_STR(json_decoder), &_Py_ID(JSONDecodeError)); if (JSONDecodeError == NULL) { return; } - PyObject *exc = PyObject_CallFunction(JSONDecodeError, "zOn", msg, s, end); + PyObject *exc; + exc = PyObject_CallFunction(JSONDecodeError, "zOn", msg, s, end); if (exc) { PyErr_SetObject(JSONDecodeError, exc); Py_DECREF(exc); @@ -431,7 +431,6 @@ raise_errmsg(const char *msg, PyObject *s, Py_ssize_t end) Py_DECREF(JSONDecodeError); } - static void raise_stop_iteration(Py_ssize_t idx) { From 0b38b8e8b4f217c0ade3e1710e598fab8d37d377 Mon Sep 17 00:00:00 2001 From: VanshAgarwal24036 <148854295+VanshAgarwal24036@users.noreply.github.com> Date: Fri, 9 Jan 2026 21:22:26 +0530 Subject: [PATCH 15/25] Update Lib/test/test_json/test_fail.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Lib/test/test_json/test_fail.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_json/test_fail.py b/Lib/test/test_json/test_fail.py index 78738ce5252e5e..948701645deb7d 100644 --- a/Lib/test/test_json/test_fail.py +++ b/Lib/test/test_json/test_fail.py @@ -262,5 +262,6 @@ def __call__(self, *args): with self.assertRaises(Exception): json.loads('"\\uZZZZ"') + class TestPyFail(TestFail, PyTest): pass class TestCFail(TestFail, CTest): pass From 0593e2f1cba80bca35f28294ba847baf5129eb2e Mon Sep 17 00:00:00 2001 From: VanshAgarwal24036 <148854295+VanshAgarwal24036@users.noreply.github.com> Date: Fri, 9 Jan 2026 21:23:53 +0530 Subject: [PATCH 16/25] Update Lib/test/test_json/test_fail.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Lib/test/test_json/test_fail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_json/test_fail.py b/Lib/test/test_json/test_fail.py index 948701645deb7d..155d073b154ab8 100644 --- a/Lib/test/test_json/test_fail.py +++ b/Lib/test/test_json/test_fail.py @@ -246,7 +246,7 @@ def test_reentrant_jsondecodeerror_does_not_crash(self): orig_decoder_error = json.decoder.JSONDecodeError class Trigger: - def __call__(self, *args): + def __call__(self, *args, **kwargs): # Remove JSONDecodeError during construction to trigger re-entrancy del json.JSONDecodeError del json.decoder.JSONDecodeError From b4856778c00370397b607128722c11bfc9c309d2 Mon Sep 17 00:00:00 2001 From: VanshAgarwal24036 Date: Fri, 9 Jan 2026 21:32:00 +0530 Subject: [PATCH 17/25] gh-143544: Tighten regression test exception assertion --- Lib/test/test_json/test_fail.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Lib/test/test_json/test_fail.py b/Lib/test/test_json/test_fail.py index 155d073b154ab8..d0558dbece57e5 100644 --- a/Lib/test/test_json/test_fail.py +++ b/Lib/test/test_json/test_fail.py @@ -242,9 +242,6 @@ def test_linecol(self): def test_reentrant_jsondecodeerror_does_not_crash(self): # gh-143544 - orig_json_error = json.JSONDecodeError - orig_decoder_error = json.decoder.JSONDecodeError - class Trigger: def __call__(self, *args, **kwargs): # Remove JSONDecodeError during construction to trigger re-entrancy @@ -259,7 +256,7 @@ def __call__(self, *args, **kwargs): ): # The exact exception type is not important here; # this test only ensures we don't crash. - with self.assertRaises(Exception): + with self.assertRaises(self.JSONDecodeError): json.loads('"\\uZZZZ"') From bbc357d6048161242ab276154da293033dbc848c Mon Sep 17 00:00:00 2001 From: VanshAgarwal24036 Date: Fri, 9 Jan 2026 22:01:34 +0530 Subject: [PATCH 18/25] gh-143544: Fix re-entrant JSONDecodeError regression test --- Lib/test/test_json/test_fail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_json/test_fail.py b/Lib/test/test_json/test_fail.py index d0558dbece57e5..7b24db4d70fc79 100644 --- a/Lib/test/test_json/test_fail.py +++ b/Lib/test/test_json/test_fail.py @@ -256,7 +256,7 @@ def __call__(self, *args, **kwargs): ): # The exact exception type is not important here; # this test only ensures we don't crash. - with self.assertRaises(self.JSONDecodeError): + with self.assertRaises(json.JSONDecodeError): json.loads('"\\uZZZZ"') From 765ffb21a4ae55ea6deb599446b50f6e87618bdb Mon Sep 17 00:00:00 2001 From: VanshAgarwal24036 Date: Fri, 9 Jan 2026 22:16:26 +0530 Subject: [PATCH 19/25] test_json: add regression test for re-entrant JSONDecodeError crash --- Lib/test/test_json/test_fail.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/Lib/test/test_json/test_fail.py b/Lib/test/test_json/test_fail.py index 7b24db4d70fc79..76de3070e8198f 100644 --- a/Lib/test/test_json/test_fail.py +++ b/Lib/test/test_json/test_fail.py @@ -1,5 +1,4 @@ from test.test_json import PyTest, CTest -from test import support import json # 2007-10-05 @@ -250,15 +249,20 @@ def __call__(self, *args, **kwargs): return ValueError("boom") hook = Trigger() - with ( - support.swap_attr(json, "JSONDecodeError", hook), - support.swap_attr(json.decoder, "JSONDecodeError", hook) - ): - # The exact exception type is not important here; - # this test only ensures we don't crash. - with self.assertRaises(json.JSONDecodeError): - json.loads('"\\uZZZZ"') + orig_json_error = json.JSONDecodeError + orig_decoder_error = json.decoder.JSONDecodeError + + try: + json.JSONDecodeError = hook + json.decoder.JSONDecodeError = hook + del hook + + with self.assertRaises(ValueError): + json.loads('"\\uZZZZ"') + finally: + json.JSONDecodeError = orig_json_error + json.decoder.JSONDecodeError = orig_decoder_error class TestPyFail(TestFail, PyTest): pass class TestCFail(TestFail, CTest): pass From b745fed8692f47287382e222e77569993c37497b Mon Sep 17 00:00:00 2001 From: VanshAgarwal24036 Date: Fri, 9 Jan 2026 22:28:55 +0530 Subject: [PATCH 20/25] test_json: added Comment --- Lib/test/test_json/test_fail.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Lib/test/test_json/test_fail.py b/Lib/test/test_json/test_fail.py index 76de3070e8198f..0f7a6b1a8bca57 100644 --- a/Lib/test/test_json/test_fail.py +++ b/Lib/test/test_json/test_fail.py @@ -254,6 +254,10 @@ def __call__(self, *args, **kwargs): orig_decoder_error = json.decoder.JSONDecodeError try: + # NOTE: Do not use swap_attr() here. + # swap_attr() keeps the replacement object alive for the duration of + # the context manager, which prevents the crash this test is meant + # to reproduce json.JSONDecodeError = hook json.decoder.JSONDecodeError = hook del hook From 0a034420fd697adb9cb2c8d605e884a1e503f0fa Mon Sep 17 00:00:00 2001 From: VanshAgarwal24036 Date: Sat, 10 Jan 2026 14:28:51 +0530 Subject: [PATCH 21/25] test_json: assert TypeError for invalid JSONDecodeError replacement --- Lib/test/test_json/test_fail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_json/test_fail.py b/Lib/test/test_json/test_fail.py index 0f7a6b1a8bca57..2848c4ab97b4d3 100644 --- a/Lib/test/test_json/test_fail.py +++ b/Lib/test/test_json/test_fail.py @@ -262,7 +262,7 @@ def __call__(self, *args, **kwargs): json.decoder.JSONDecodeError = hook del hook - with self.assertRaises(ValueError): + with self.assertRaises(TypeError): json.loads('"\\uZZZZ"') finally: json.JSONDecodeError = orig_json_error From 4257cdcd0e442d5df7559e3b06d1a10a09493f69 Mon Sep 17 00:00:00 2001 From: VanshAgarwal24036 Date: Sat, 10 Jan 2026 14:48:19 +0530 Subject: [PATCH 22/25] test_json: document and assert refcount for reentrant JSONDecodeError test --- Lib/test/test_json/test_fail.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Lib/test/test_json/test_fail.py b/Lib/test/test_json/test_fail.py index 2848c4ab97b4d3..70bef1d5d84f2f 100644 --- a/Lib/test/test_json/test_fail.py +++ b/Lib/test/test_json/test_fail.py @@ -1,5 +1,6 @@ from test.test_json import PyTest, CTest import json +import sys # 2007-10-05 JSONDOCS = [ @@ -260,6 +261,10 @@ def __call__(self, *args, **kwargs): # to reproduce json.JSONDecodeError = hook json.decoder.JSONDecodeError = hook + + # The hook must be kept alive by these references. + # Deleting it triggers the re-entrant path this test is exercising. + self.assertEqual(sys.getrefcount(hook), 3) del hook with self.assertRaises(TypeError): From 74cfaedca9086fd313c4ee0450300cb4d8d3780b Mon Sep 17 00:00:00 2001 From: VanshAgarwal24036 Date: Sat, 10 Jan 2026 14:51:54 +0530 Subject: [PATCH 23/25] test_json: fix reentrant JSONDecodeError test --- Lib/test/test_json/test_fail.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Lib/test/test_json/test_fail.py b/Lib/test/test_json/test_fail.py index 70bef1d5d84f2f..b3257041bd7c63 100644 --- a/Lib/test/test_json/test_fail.py +++ b/Lib/test/test_json/test_fail.py @@ -1,4 +1,5 @@ from test.test_json import PyTest, CTest +from test import support import json import sys @@ -267,6 +268,8 @@ def __call__(self, *args, **kwargs): self.assertEqual(sys.getrefcount(hook), 3) del hook + support.gc_collect() + with self.assertRaises(TypeError): json.loads('"\\uZZZZ"') finally: From 211cb30256ee83e952e20d290a1e92c72571b513 Mon Sep 17 00:00:00 2001 From: VanshAgarwal24036 Date: Sat, 10 Jan 2026 14:57:32 +0530 Subject: [PATCH 24/25] test_json: fix re-entrant JSONDecodeError test --- Lib/test/test_json/test_fail.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_json/test_fail.py b/Lib/test/test_json/test_fail.py index b3257041bd7c63..535a9c34801025 100644 --- a/Lib/test/test_json/test_fail.py +++ b/Lib/test/test_json/test_fail.py @@ -243,14 +243,14 @@ def test_linecol(self): def test_reentrant_jsondecodeerror_does_not_crash(self): # gh-143544 - class Trigger: + class Trigger(ValueError): def __call__(self, *args, **kwargs): # Remove JSONDecodeError during construction to trigger re-entrancy del json.JSONDecodeError del json.decoder.JSONDecodeError - return ValueError("boom") + raise self - hook = Trigger() + hook = Trigger("boom") orig_json_error = json.JSONDecodeError orig_decoder_error = json.decoder.JSONDecodeError @@ -269,8 +269,8 @@ def __call__(self, *args, **kwargs): del hook support.gc_collect() - - with self.assertRaises(TypeError): + + with self.assertRaises(ValueError): json.loads('"\\uZZZZ"') finally: json.JSONDecodeError = orig_json_error From a2e23bc62a432f7e3c543c4e4205a196a2a25be6 Mon Sep 17 00:00:00 2001 From: VanshAgarwal24036 Date: Sat, 10 Jan 2026 22:28:49 +0530 Subject: [PATCH 25/25] gh-143544: Revert test_json regression test changes --- Lib/test/test_json/test_fail.py | 40 --------------------------------- 1 file changed, 40 deletions(-) diff --git a/Lib/test/test_json/test_fail.py b/Lib/test/test_json/test_fail.py index 535a9c34801025..79c44af2fbf0e1 100644 --- a/Lib/test/test_json/test_fail.py +++ b/Lib/test/test_json/test_fail.py @@ -1,7 +1,4 @@ from test.test_json import PyTest, CTest -from test import support -import json -import sys # 2007-10-05 JSONDOCS = [ @@ -239,42 +236,5 @@ def test_linecol(self): 'Expecting value: line %s column %d (char %d)' % (line, col, idx)) - - def test_reentrant_jsondecodeerror_does_not_crash(self): - # gh-143544 - - class Trigger(ValueError): - def __call__(self, *args, **kwargs): - # Remove JSONDecodeError during construction to trigger re-entrancy - del json.JSONDecodeError - del json.decoder.JSONDecodeError - raise self - - hook = Trigger("boom") - - orig_json_error = json.JSONDecodeError - orig_decoder_error = json.decoder.JSONDecodeError - - try: - # NOTE: Do not use swap_attr() here. - # swap_attr() keeps the replacement object alive for the duration of - # the context manager, which prevents the crash this test is meant - # to reproduce - json.JSONDecodeError = hook - json.decoder.JSONDecodeError = hook - - # The hook must be kept alive by these references. - # Deleting it triggers the re-entrant path this test is exercising. - self.assertEqual(sys.getrefcount(hook), 3) - del hook - - support.gc_collect() - - with self.assertRaises(ValueError): - json.loads('"\\uZZZZ"') - finally: - json.JSONDecodeError = orig_json_error - json.decoder.JSONDecodeError = orig_decoder_error - class TestPyFail(TestFail, PyTest): pass class TestCFail(TestFail, CTest): pass