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]