From df43d6aba34c955a72613b8ca7fd9afa5ab0314d Mon Sep 17 00:00:00 2001 From: Bhuvansh Kataria Date: Fri, 21 Aug 2026 18:57:27 +0000 Subject: [PATCH 1/4] gh-156204: Guard recursion in PyErr_GivenExceptionMatches --- Lib/test/test_exceptions.py | 16 ++++++++++++++++ ...26-08-21-18-49-12.gh-issue-156204.ZusA7e.rst | 3 +++ Python/errors.c | 17 +++++++++++------ 3 files changed, 30 insertions(+), 6 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-08-21-18-49-12.gh-issue-156204.ZusA7e.rst diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index c34cf44d722456c..1aebdd5ba38fd5e 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -2778,6 +2778,22 @@ def test_except_star_invalid_exception_type(self): except (ValueError, 42): pass + def test_given_exception_matches_deeply_nested_tuple(self): + import ctypes + lib = ctypes.pythonapi + lib.PyErr_GivenExceptionMatches.argtypes = [ctypes.py_object, ctypes.py_object] + lib.PyErr_GivenExceptionMatches.restype = ctypes.c_int + + tup = (1, ValueError) + for _ in range(50_000): + tup = (1, tup) + + # PyErr_GivenExceptionMatches should handle deep recursion safely without SIGSEGV + res = lib.PyErr_GivenExceptionMatches(TypeError(), tup) + self.assertEqual(res, 0) + if lib.PyErr_Occurred(): + lib.PyErr_Clear() + class PEP626Tests(unittest.TestCase): diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-08-21-18-49-12.gh-issue-156204.ZusA7e.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-21-18-49-12.gh-issue-156204.ZusA7e.rst new file mode 100644 index 000000000000000..9c8d1efbd761a00 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-21-18-49-12.gh-issue-156204.ZusA7e.rst @@ -0,0 +1,3 @@ +Fix unhandled recursion in :c:func:`PyErr_GivenExceptionMatches` when +evaluating deeply nested exception tuples, preventing crashes caused by +stack exhaustion. diff --git a/Python/errors.c b/Python/errors.c index 48b03e5fd714b18..6f16107c2385f84 100644 --- a/Python/errors.c +++ b/Python/errors.c @@ -335,16 +335,21 @@ PyErr_GivenExceptionMatches(PyObject *err, PyObject *exc) return 0; } if (PyTuple_Check(exc)) { + if (Py_EnterRecursiveCall(" in PyErr_GivenExceptionMatches")) { + return 0; + } Py_ssize_t i, n; - n = PyTuple_Size(exc); + n = PyTuple_GET_SIZE(exc); for (i = 0; i < n; i++) { /* Test recursively */ - if (PyErr_GivenExceptionMatches( - err, PyTuple_GET_ITEM(exc, i))) - { - return 1; - } + if (PyErr_GivenExceptionMatches( + err, PyTuple_GET_ITEM(exc, i))) + { + Py_LeaveRecursiveCall(); + return 1; + } } + Py_LeaveRecursiveCall(); return 0; } /* err might be an instance, so check its class. */ From d0d85956659696e101e3c5d0645395ab197faee9 Mon Sep 17 00:00:00 2001 From: Bhuvansh Kataria Date: Fri, 21 Aug 2026 19:27:01 +0000 Subject: [PATCH 2/4] gh-156204: Safely skip ctypes test on mobile/wasm and reduce tuple depth --- Lib/test/test_exceptions.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index 1aebdd5ba38fd5e..f77c02c442bc2c7 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -2778,8 +2778,11 @@ def test_except_star_invalid_exception_type(self): except (ValueError, 42): pass + @cpython_only + @support.skip_emscripten_stack_overflow() + @support.skip_wasi_stack_overflow() def test_given_exception_matches_deeply_nested_tuple(self): - import ctypes + ctypes = import_module('ctypes') lib = ctypes.pythonapi lib.PyErr_GivenExceptionMatches.argtypes = [ctypes.py_object, ctypes.py_object] lib.PyErr_GivenExceptionMatches.restype = ctypes.c_int From b6be6c5d9015c774bc82cadf63e2901894f3d54e Mon Sep 17 00:00:00 2001 From: Bhuvansh Kataria Date: Fri, 21 Aug 2026 20:16:21 +0000 Subject: [PATCH 3/4] gh-156204: address review feedback --- Doc/c-api/exceptions.rst | 7 ++++ Lib/test/test_exceptions.py | 33 +++++++++++-------- ...-08-21-18-49-12.gh-issue-156204.ZusA7e.rst | 2 +- Modules/_testcapi/exceptions.c | 19 +++++++++++ Python/errors.c | 16 ++++----- 5 files changed, 53 insertions(+), 24 deletions(-) diff --git a/Doc/c-api/exceptions.rst b/Doc/c-api/exceptions.rst index 40522f8c7b13756..a4d450424594ba0 100644 --- a/Doc/c-api/exceptions.rst +++ b/Doc/c-api/exceptions.rst @@ -486,6 +486,13 @@ Querying the error indicator of a subclass. If *exc* is a tuple, all exception types in the tuple (and recursively in subtuples) are searched for a match. + If the subtuples are nested deeply enough to risk exhausting the C stack, + the search is abandoned, a :exc:`RecursionError` is set, and the function + returns false. + + .. versionchanged:: next + Deeply nested subtuples previously crashed the interpreter. + .. c:function:: PyObject *PyErr_GetRaisedException(void) diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index f77c02c442bc2c7..923e0ff9ec6dc84 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -2779,23 +2779,28 @@ def test_except_star_invalid_exception_type(self): pass @cpython_only + @unittest.skipIf(_testcapi is None, "requires _testcapi") + def test_given_exception_matches_nested_tuple(self): + # Nested tuples are searched recursively. + self.assertTrue( + _testcapi.err_givenexceptionmatches(ValueError(), ((ValueError,),))) + self.assertFalse( + _testcapi.err_givenexceptionmatches(TypeError(), ((ValueError,),))) + + @cpython_only + @unittest.skipIf(_testcapi is None, "requires _testcapi") @support.skip_emscripten_stack_overflow() @support.skip_wasi_stack_overflow() + @support.run_with_limited_c_stack(depth=500_000) def test_given_exception_matches_deeply_nested_tuple(self): - ctypes = import_module('ctypes') - lib = ctypes.pythonapi - lib.PyErr_GivenExceptionMatches.argtypes = [ctypes.py_object, ctypes.py_object] - lib.PyErr_GivenExceptionMatches.restype = ctypes.c_int - - tup = (1, ValueError) - for _ in range(50_000): - tup = (1, tup) - - # PyErr_GivenExceptionMatches should handle deep recursion safely without SIGSEGV - res = lib.PyErr_GivenExceptionMatches(TypeError(), tup) - self.assertEqual(res, 0) - if lib.PyErr_Occurred(): - lib.PyErr_Clear() + # gh-156204: PyErr_GivenExceptionMatches() used to exhaust the C stack + # and crash the interpreter on deeply nested tuples of exception types. + tup = (ValueError,) + for _ in range(500_000): + tup = (tup,) + + with self.assertRaises(RecursionError): + _testcapi.err_givenexceptionmatches(TypeError(), tup) class PEP626Tests(unittest.TestCase): diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-08-21-18-49-12.gh-issue-156204.ZusA7e.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-21-18-49-12.gh-issue-156204.ZusA7e.rst index 9c8d1efbd761a00..da40d41bc4115a6 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2026-08-21-18-49-12.gh-issue-156204.ZusA7e.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-21-18-49-12.gh-issue-156204.ZusA7e.rst @@ -1,3 +1,3 @@ -Fix unhandled recursion in :c:func:`PyErr_GivenExceptionMatches` when +Fix unhandled recursion error in :c:func:`PyErr_GivenExceptionMatches` when evaluating deeply nested exception tuples, preventing crashes caused by stack exhaustion. diff --git a/Modules/_testcapi/exceptions.c b/Modules/_testcapi/exceptions.c index c0254e044bc2d5f..29a2dcf37fdffd7 100644 --- a/Modules/_testcapi/exceptions.c +++ b/Modules/_testcapi/exceptions.c @@ -54,6 +54,24 @@ err_restore(PyObject *self, PyObject *args) { return NULL; } +static PyObject * +err_givenexceptionmatches(PyObject *Py_UNUSED(module), PyObject *args) +{ + PyObject *err, *exc; + if (!PyArg_ParseTuple(args, "OO", &err, &exc)) { + return NULL; + } + assert(!PyErr_Occurred()); + int res = PyErr_GivenExceptionMatches(err, exc); + /* PyErr_GivenExceptionMatches() has no failure return value, but it can + * set RecursionError on a deeply nested tuple; report that to the caller. + */ + if (res == 0 && PyErr_Occurred()) { + return NULL; + } + return PyBool_FromLong(res); +} + /*[clinic input] _testcapi.exception_print exception as exc: object @@ -544,6 +562,7 @@ static PyTypeObject PyRecursingInfinitelyError_Type = { static PyMethodDef test_methods[] = { {"err_restore", err_restore, METH_VARARGS}, + {"err_givenexceptionmatches", err_givenexceptionmatches, METH_VARARGS}, {"err_writeunraisable", err_writeunraisable, METH_VARARGS}, {"err_formatunraisable", err_formatunraisable, METH_VARARGS}, _TESTCAPI_ERR_SET_RAISED_METHODDEF diff --git a/Python/errors.c b/Python/errors.c index 6f16107c2385f84..2455fb07688d79c 100644 --- a/Python/errors.c +++ b/Python/errors.c @@ -338,19 +338,17 @@ PyErr_GivenExceptionMatches(PyObject *err, PyObject *exc) if (Py_EnterRecursiveCall(" in PyErr_GivenExceptionMatches")) { return 0; } - Py_ssize_t i, n; - n = PyTuple_GET_SIZE(exc); - for (i = 0; i < n; i++) { + int res = 0; + Py_ssize_t n = PyTuple_GET_SIZE(exc); + for (Py_ssize_t i = 0; i < n; i++) { /* Test recursively */ - if (PyErr_GivenExceptionMatches( - err, PyTuple_GET_ITEM(exc, i))) - { - Py_LeaveRecursiveCall(); - return 1; + if (PyErr_GivenExceptionMatches(err, PyTuple_GET_ITEM(exc, i))) { + res = 1; + break; } } Py_LeaveRecursiveCall(); - return 0; + return res; } /* err might be an instance, so check its class. */ if (PyExceptionInstance_Check(err)) From 170d3c961eaf6fa7193ef697b78e2a0479871982 Mon Sep 17 00:00:00 2001 From: Bhuvansh Kataria Date: Fri, 21 Aug 2026 20:28:44 +0000 Subject: [PATCH 4/4] gh-156204: drop docs change --- Doc/c-api/exceptions.rst | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Doc/c-api/exceptions.rst b/Doc/c-api/exceptions.rst index a4d450424594ba0..40522f8c7b13756 100644 --- a/Doc/c-api/exceptions.rst +++ b/Doc/c-api/exceptions.rst @@ -486,13 +486,6 @@ Querying the error indicator of a subclass. If *exc* is a tuple, all exception types in the tuple (and recursively in subtuples) are searched for a match. - If the subtuples are nested deeply enough to risk exhausting the C stack, - the search is abandoned, a :exc:`RecursionError` is set, and the function - returns false. - - .. versionchanged:: next - Deeply nested subtuples previously crashed the interpreter. - .. c:function:: PyObject *PyErr_GetRaisedException(void)