diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index 4cdd269c7761ff4..963da1b0eae27ce 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -2806,6 +2806,32 @@ 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 support.catch_unraisable_exception() as cm: + self.assertFalse( + _testcapi.err_givenexceptionmatches(ValueError(), tup)) + self.assertIsInstance(cm.unraisable.exc_value, RecursionError) + 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..fade87c2d09ae1b --- /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 a crash in :c:func:`PyErr_GivenExceptionMatches` when evaluating deeply +nested exception tuples. The recursion is now bounded, and exceeding the +limit is reported as an unraisable exception. diff --git a/Modules/_testcapi/clinic/exceptions.c.h b/Modules/_testcapi/clinic/exceptions.c.h index cac5d288f437b72..89b78923bdb2d0c 100644 --- a/Modules/_testcapi/clinic/exceptions.c.h +++ b/Modules/_testcapi/clinic/exceptions.c.h @@ -16,6 +16,37 @@ PyDoc_STRVAR(_testcapi_err_set_raised__doc__, #define _TESTCAPI_ERR_SET_RAISED_METHODDEF \ {"err_set_raised", (PyCFunction)_testcapi_err_set_raised, METH_O, _testcapi_err_set_raised__doc__}, +PyDoc_STRVAR(_testcapi_err_givenexceptionmatches__doc__, +"err_givenexceptionmatches($module, err, exc, /)\n" +"--\n" +"\n" +"Test PyErr_GivenExceptionMatches()."); + +#define _TESTCAPI_ERR_GIVENEXCEPTIONMATCHES_METHODDEF \ + {"err_givenexceptionmatches", _PyCFunction_CAST(_testcapi_err_givenexceptionmatches), METH_FASTCALL, _testcapi_err_givenexceptionmatches__doc__}, + +static PyObject * +_testcapi_err_givenexceptionmatches_impl(PyObject *module, PyObject *err, + PyObject *exc); + +static PyObject * +_testcapi_err_givenexceptionmatches(PyObject *module, PyObject *const *args, Py_ssize_t nargs) +{ + PyObject *return_value = NULL; + PyObject *err; + PyObject *exc; + + if (!_PyArg_CheckPositional("err_givenexceptionmatches", nargs, 2, 2)) { + goto exit; + } + err = args[0]; + exc = args[1]; + return_value = _testcapi_err_givenexceptionmatches_impl(module, err, exc); + +exit: + return return_value; +} + PyDoc_STRVAR(_testcapi_exception_print__doc__, "exception_print($module, exception, legacy=False, /)\n" "--\n" @@ -459,4 +490,4 @@ _testcapi_unstable_exc_prep_reraise_star(PyObject *module, PyObject *const *args exit: return return_value; } -/*[clinic end generated code: output=357caea020348789 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=a21ce5554900dba1 input=a9049054013a1b77]*/ diff --git a/Modules/_testcapi/exceptions.c b/Modules/_testcapi/exceptions.c index c0254e044bc2d5f..cc62b47c99414a0 100644 --- a/Modules/_testcapi/exceptions.c +++ b/Modules/_testcapi/exceptions.c @@ -54,6 +54,26 @@ err_restore(PyObject *self, PyObject *args) { return NULL; } +/*[clinic input] +_testcapi.err_givenexceptionmatches + err: object + exc: object + / + +Test PyErr_GivenExceptionMatches(). +[clinic start generated code]*/ + +static PyObject * +_testcapi_err_givenexceptionmatches_impl(PyObject *module, PyObject *err, + PyObject *exc) +/*[clinic end generated code: output=e40994ab6dd75001 input=7b8ef542df07575b]*/ +{ + assert(!PyErr_Occurred()); + int res = PyErr_GivenExceptionMatches(err, exc); + assert(!PyErr_Occurred()); + return PyBool_FromLong(res); +} + /*[clinic input] _testcapi.exception_print exception as exc: object @@ -552,6 +572,7 @@ static PyMethodDef test_methods[] = { _TESTCAPI_MAKE_EXCEPTION_WITH_DOC_METHODDEF _TESTCAPI_EXC_SET_OBJECT_METHODDEF _TESTCAPI_EXC_SET_OBJECT_FETCH_METHODDEF + _TESTCAPI_ERR_GIVENEXCEPTIONMATCHES_METHODDEF _TESTCAPI_ERR_SETSTRING_METHODDEF _TESTCAPI_ERR_SETFROMERRNOWITHFILENAME_METHODDEF _TESTCAPI_RAISE_EXCEPTION_METHODDEF diff --git a/Python/errors.c b/Python/errors.c index eb148998fc4652d..edb1557e23f63bc 100644 --- a/Python/errors.c +++ b/Python/errors.c @@ -4,6 +4,7 @@ #include "Python.h" #include "pycore_audit.h" // _PySys_Audit() #include "pycore_call.h" // _PyObject_CallNoArgs() +#include "pycore_ceval.h" // _Py_ReachedRecursionLimitWithMargin() #include "pycore_fileutils.h" // _PyFile_Flush #include "pycore_initconfig.h" // _PyStatus_ERR() #include "pycore_pyerrors.h" // _PyErr_Format() @@ -337,17 +338,27 @@ 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++) { + PyThreadState *tstate = _PyThreadState_GET(); + if (_Py_ReachedRecursionLimitWithMargin(tstate, 2)) { + PyObject *exc_value = _PyErr_GetRaisedException(tstate); + _PyErr_SetString(tstate, PyExc_RecursionError, + "maximum recursion depth exceeded while " + "checking exception tuple"); + PyErr_FormatUnraisable("Exception ignored while " + "checking exception tuple"); + _PyErr_SetRaisedException(tstate, exc_value); + 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; + return res; } /* err might be an instance, so check its class. */ if (PyExceptionInstance_Check(err))