diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index c34cf44d722456c..923e0ff9ec6dc84 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -2778,6 +2778,30 @@ def test_except_star_invalid_exception_type(self): except (ValueError, 42): 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): + # 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 new file mode 100644 index 000000000000000..da40d41bc4115a6 --- /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 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 48b03e5fd714b18..2455fb07688d79c 100644 --- a/Python/errors.c +++ b/Python/errors.c @@ -335,17 +335,20 @@ PyErr_GivenExceptionMatches(PyObject *err, PyObject *exc) return 0; } if (PyTuple_Check(exc)) { - Py_ssize_t i, n; - n = PyTuple_Size(exc); - for (i = 0; i < n; i++) { + if (Py_EnterRecursiveCall(" in PyErr_GivenExceptionMatches")) { + return 0; + } + 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))) - { - return 1; - } + if (PyErr_GivenExceptionMatches(err, PyTuple_GET_ITEM(exc, i))) { + res = 1; + break; + } } - return 0; + Py_LeaveRecursiveCall(); + return res; } /* err might be an instance, so check its class. */ if (PyExceptionInstance_Check(err))