https://github.com/python/cpython/commit/20d5e67fc51eee8cbf7e38d75830e5edd140d37e
commit: 20d5e67fc51eee8cbf7e38d75830e5edd140d37e
branch: main
author: Bhuvansh <[email protected]>
committer: ZeroIntensity <[email protected]>
date: 2026-10-02T16:22:05-04:00
summary:

gh-156204: Guard recursion in `PyErr_GivenExceptionMatches` (GH-156205)

files:
A 
Misc/NEWS.d/next/Core_and_Builtins/2026-08-21-18-49-12.gh-issue-156204.ZusA7e.rst
M Lib/test/test_exceptions.py
M Modules/_testcapi/clinic/exceptions.c.h
M Modules/_testcapi/exceptions.c
M Python/errors.c

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))

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to