Skip to content

Commit 20d5e67

Browse files
authored
gh-156204: Guard recursion in PyErr_GivenExceptionMatches (GH-156205)
1 parent 00a1c3a commit 20d5e67

5 files changed

Lines changed: 102 additions & 10 deletions

File tree

‎Lib/test/test_exceptions.py‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2806,6 +2806,32 @@ def test_except_star_invalid_exception_type(self):
28062806
except (ValueError, 42):
28072807
pass
28082808

2809+
@cpython_only
2810+
@unittest.skipIf(_testcapi is None, "requires _testcapi")
2811+
def test_given_exception_matches_nested_tuple(self):
2812+
# Nested tuples are searched recursively.
2813+
self.assertTrue(
2814+
_testcapi.err_givenexceptionmatches(ValueError(), ((ValueError,),)))
2815+
self.assertFalse(
2816+
_testcapi.err_givenexceptionmatches(TypeError(), ((ValueError,),)))
2817+
2818+
@cpython_only
2819+
@unittest.skipIf(_testcapi is None, "requires _testcapi")
2820+
@support.skip_emscripten_stack_overflow()
2821+
@support.skip_wasi_stack_overflow()
2822+
@support.run_with_limited_c_stack(depth=500_000)
2823+
def test_given_exception_matches_deeply_nested_tuple(self):
2824+
# gh-156204: PyErr_GivenExceptionMatches() used to exhaust the C stack
2825+
# and crash the interpreter on deeply nested tuples of exception types.
2826+
tup = (ValueError,)
2827+
for _ in range(500_000):
2828+
tup = (tup,)
2829+
2830+
with support.catch_unraisable_exception() as cm:
2831+
self.assertFalse(
2832+
_testcapi.err_givenexceptionmatches(ValueError(), tup))
2833+
self.assertIsInstance(cm.unraisable.exc_value, RecursionError)
2834+
28092835

28102836
class PEP626Tests(unittest.TestCase):
28112837

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix a crash in :c:func:`PyErr_GivenExceptionMatches` when evaluating deeply
2+
nested exception tuples. The recursion is now bounded, and exceeding the
3+
limit is reported as an unraisable exception.

‎Modules/_testcapi/clinic/exceptions.c.h‎

Lines changed: 32 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Modules/_testcapi/exceptions.c‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,26 @@ err_restore(PyObject *self, PyObject *args) {
5454
return NULL;
5555
}
5656

57+
/*[clinic input]
58+
_testcapi.err_givenexceptionmatches
59+
err: object
60+
exc: object
61+
/
62+
63+
Test PyErr_GivenExceptionMatches().
64+
[clinic start generated code]*/
65+
66+
static PyObject *
67+
_testcapi_err_givenexceptionmatches_impl(PyObject *module, PyObject *err,
68+
PyObject *exc)
69+
/*[clinic end generated code: output=e40994ab6dd75001 input=7b8ef542df07575b]*/
70+
{
71+
assert(!PyErr_Occurred());
72+
int res = PyErr_GivenExceptionMatches(err, exc);
73+
assert(!PyErr_Occurred());
74+
return PyBool_FromLong(res);
75+
}
76+
5777
/*[clinic input]
5878
_testcapi.exception_print
5979
exception as exc: object
@@ -552,6 +572,7 @@ static PyMethodDef test_methods[] = {
552572
_TESTCAPI_MAKE_EXCEPTION_WITH_DOC_METHODDEF
553573
_TESTCAPI_EXC_SET_OBJECT_METHODDEF
554574
_TESTCAPI_EXC_SET_OBJECT_FETCH_METHODDEF
575+
_TESTCAPI_ERR_GIVENEXCEPTIONMATCHES_METHODDEF
555576
_TESTCAPI_ERR_SETSTRING_METHODDEF
556577
_TESTCAPI_ERR_SETFROMERRNOWITHFILENAME_METHODDEF
557578
_TESTCAPI_RAISE_EXCEPTION_METHODDEF

‎Python/errors.c‎

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include "Python.h"
55
#include "pycore_audit.h" // _PySys_Audit()
66
#include "pycore_call.h" // _PyObject_CallNoArgs()
7+
#include "pycore_ceval.h" // _Py_ReachedRecursionLimitWithMargin()
78
#include "pycore_fileutils.h" // _PyFile_Flush
89
#include "pycore_initconfig.h" // _PyStatus_ERR()
910
#include "pycore_pyerrors.h" // _PyErr_Format()
@@ -337,17 +338,27 @@ PyErr_GivenExceptionMatches(PyObject *err, PyObject *exc)
337338
return 0;
338339
}
339340
if (PyTuple_Check(exc)) {
340-
Py_ssize_t i, n;
341-
n = PyTuple_Size(exc);
342-
for (i = 0; i < n; i++) {
341+
PyThreadState *tstate = _PyThreadState_GET();
342+
if (_Py_ReachedRecursionLimitWithMargin(tstate, 2)) {
343+
PyObject *exc_value = _PyErr_GetRaisedException(tstate);
344+
_PyErr_SetString(tstate, PyExc_RecursionError,
345+
"maximum recursion depth exceeded while "
346+
"checking exception tuple");
347+
PyErr_FormatUnraisable("Exception ignored while "
348+
"checking exception tuple");
349+
_PyErr_SetRaisedException(tstate, exc_value);
350+
return 0;
351+
}
352+
int res = 0;
353+
Py_ssize_t n = PyTuple_GET_SIZE(exc);
354+
for (Py_ssize_t i = 0; i < n; i++) {
343355
/* Test recursively */
344-
if (PyErr_GivenExceptionMatches(
345-
err, PyTuple_GET_ITEM(exc, i)))
346-
{
347-
return 1;
348-
}
356+
if (PyErr_GivenExceptionMatches(err, PyTuple_GET_ITEM(exc, i))) {
357+
res = 1;
358+
break;
359+
}
349360
}
350-
return 0;
361+
return res;
351362
}
352363
/* err might be an instance, so check its class. */
353364
if (PyExceptionInstance_Check(err))

0 commit comments

Comments
 (0)