Skip to content

Commit a9eeb9f

Browse files
[3.14] gh-156204: Guard recursion in PyErr_GivenExceptionMatches (GH-156205) (GH-158623)
(cherry picked from commit 20d5e67) Co-authored-by: Bhuvansh <bhuvanshkataria@gmail.com>
1 parent 65ec43d commit a9eeb9f

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
@@ -2622,6 +2622,32 @@ def test_except_star_invalid_exception_type(self):
26222622
except (ValueError, 42):
26232623
pass
26242624

2625+
@cpython_only
2626+
@unittest.skipIf(_testcapi is None, "requires _testcapi")
2627+
def test_given_exception_matches_nested_tuple(self):
2628+
# Nested tuples are searched recursively.
2629+
self.assertTrue(
2630+
_testcapi.err_givenexceptionmatches(ValueError(), ((ValueError,),)))
2631+
self.assertFalse(
2632+
_testcapi.err_givenexceptionmatches(TypeError(), ((ValueError,),)))
2633+
2634+
@cpython_only
2635+
@unittest.skipIf(_testcapi is None, "requires _testcapi")
2636+
@support.skip_emscripten_stack_overflow()
2637+
@support.skip_wasi_stack_overflow()
2638+
@support.run_with_limited_c_stack(depth=500_000)
2639+
def test_given_exception_matches_deeply_nested_tuple(self):
2640+
# gh-156204: PyErr_GivenExceptionMatches() used to exhaust the C stack
2641+
# and crash the interpreter on deeply nested tuples of exception types.
2642+
tup = (ValueError,)
2643+
for _ in range(500_000):
2644+
tup = (tup,)
2645+
2646+
with support.catch_unraisable_exception() as cm:
2647+
self.assertFalse(
2648+
_testcapi.err_givenexceptionmatches(ValueError(), tup))
2649+
self.assertIsInstance(cm.unraisable.exc_value, RecursionError)
2650+
26252651

26262652
class PEP626Tests(unittest.TestCase):
26272653

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
@@ -551,6 +571,7 @@ static PyMethodDef test_methods[] = {
551571
_TESTCAPI_MAKE_EXCEPTION_WITH_DOC_METHODDEF
552572
_TESTCAPI_EXC_SET_OBJECT_METHODDEF
553573
_TESTCAPI_EXC_SET_OBJECT_FETCH_METHODDEF
574+
_TESTCAPI_ERR_GIVENEXCEPTIONMATCHES_METHODDEF
554575
_TESTCAPI_ERR_SETSTRING_METHODDEF
555576
_TESTCAPI_ERR_SETFROMERRNOWITHFILENAME_METHODDEF
556577
_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()
@@ -336,17 +337,27 @@ PyErr_GivenExceptionMatches(PyObject *err, PyObject *exc)
336337
return 0;
337338
}
338339
if (PyTuple_Check(exc)) {
339-
Py_ssize_t i, n;
340-
n = PyTuple_Size(exc);
341-
for (i = 0; i < n; i++) {
340+
PyThreadState *tstate = _PyThreadState_GET();
341+
if (_Py_ReachedRecursionLimitWithMargin(tstate, 2)) {
342+
PyObject *exc_value = _PyErr_GetRaisedException(tstate);
343+
_PyErr_SetString(tstate, PyExc_RecursionError,
344+
"maximum recursion depth exceeded while "
345+
"checking exception tuple");
346+
PyErr_FormatUnraisable("Exception ignored while "
347+
"checking exception tuple");
348+
_PyErr_SetRaisedException(tstate, exc_value);
349+
return 0;
350+
}
351+
int res = 0;
352+
Py_ssize_t n = PyTuple_GET_SIZE(exc);
353+
for (Py_ssize_t i = 0; i < n; i++) {
342354
/* Test recursively */
343-
if (PyErr_GivenExceptionMatches(
344-
err, PyTuple_GET_ITEM(exc, i)))
345-
{
346-
return 1;
347-
}
355+
if (PyErr_GivenExceptionMatches(err, PyTuple_GET_ITEM(exc, i))) {
356+
res = 1;
357+
break;
358+
}
348359
}
349-
return 0;
360+
return res;
350361
}
351362
/* err might be an instance, so check its class. */
352363
if (PyExceptionInstance_Check(err))

0 commit comments

Comments
 (0)