Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions Lib/test/test_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):

Expand Down
Original file line number Diff line number Diff line change
@@ -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.
19 changes: 19 additions & 0 deletions Modules/_testcapi/exceptions.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
21 changes: 12 additions & 9 deletions Python/errors.c
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Loading