diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index f9a5f2fc53e1e9..5b362cfc7d3e8c 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -632,6 +632,54 @@ def outer_raise(): self.check_zero_div(blocks[0]) self.assertIn('inner_raise() # Marker', blocks[2]) + def test_rho_context(self): + # Make sure that rho-shaped linked lists of __context__s do + # not get stuck in an infinite loop. See bpo-40696. + with self.assertRaises(ValueError) as cm: + # ValueError -> RuntimeError ↺ + try: + raise RuntimeError #rho1 + except Exception as exc: + x = exc + x.__context__ = x + raise ValueError #rho1 + + blocks = boundaries.split(self.get_report(cm.exception)) + self.assertEqual(len(blocks), 3, blocks) + self.assertIn('RuntimeError #rho1', blocks[0]) + self.assertEqual(blocks[1], context_message) + self.assertIn('ValueError', blocks[2]) + + def test_rho_context_2(self): + with self.assertRaises(ValueError) as cm: + # ValueError -> IndexError -> SyntaxError <=> ZeroDivisionError + try: + raise ZeroDivisionError #rho2 + except Exception as exc: + y = exc + + try: + raise SyntaxError #rho2 + except Exception as exc: + z = exc + z.__context__ = y + y.__context__ = z + try: + raise IndexError #rho2 + except IndexError: + raise ValueError #rho2 + + blocks = boundaries.split(self.get_report(cm.exception)) + self.assertEqual(len(blocks), 7) + self.assertIn('ZeroDivisionError #rho2', blocks[0]) + self.assertEqual(blocks[1], context_message) + self.assertIn('SyntaxError #rho2', blocks[2]) + self.assertEqual(blocks[3], context_message) + self.assertIn('IndexError #rho2', blocks[4]) + self.assertEqual(blocks[5], context_message) + self.assertIn('ValueError', blocks[6]) + + def test_cause_recursive(self): def inner_raise(): try: diff --git a/Misc/NEWS.d/next/Core and Builtins/2020-05-30-12-13-35.bpo-40696.NJZ364.rst b/Misc/NEWS.d/next/Core and Builtins/2020-05-30-12-13-35.bpo-40696.NJZ364.rst new file mode 100644 index 00000000000000..cc9af5b45354cb --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2020-05-30-12-13-35.bpo-40696.NJZ364.rst @@ -0,0 +1 @@ +The interpreter no longer hangs when it encounters exceptions whose ``__context__`` attributes formed a cycle. Patch by Dennis Sweeney. diff --git a/Python/errors.c b/Python/errors.c index 70365aaca585bb..c2911832fee103 100644 --- a/Python/errors.c +++ b/Python/errors.c @@ -98,6 +98,16 @@ _PyErr_CreateException(PyObject *exception, PyObject *value) } } +static inline PyObject * +GET_CONTEXT(PyObject *exc) +{ + if (exc == NULL) { + return NULL; + } + assert(PyExceptionInstance_Check(exc)); + return ((PyBaseExceptionObject *)exc)->context; +} + void _PyErr_SetObject(PyThreadState *tstate, PyObject *exception, PyObject *value) { @@ -136,25 +146,49 @@ _PyErr_SetObject(PyThreadState *tstate, PyObject *exception, PyObject *value) value = fixed_value; } - /* Avoid reference cycles through the context chain. - This is O(chain length) but context chains are - usually very short. Sensitive readers may try - to inline the call to PyException_GetContext. */ - if (exc_value != value) { - PyObject *o = exc_value, *context; - while ((context = PyException_GetContext(o))) { - Py_DECREF(context); - if (context == value) { - PyException_SetContext(o, NULL); - break; - } - o = context; - } - PyException_SetContext(value, exc_value); + if (value == exc_value) { + /* This exception is already on top of the stack. */ + Py_DECREF(exc_value); } else { - Py_DECREF(exc_value); + /* Otherwise, push the new value on top. */ + PyException_SetContext(value, exc_value); + + /* Now we need to destroy any reference cycles that might + exist in the chain of contexts. Use Floyd's cycle-finding + algorithm to determine if the chain has a cycle or if it + reaches NULL. */ + PyObject *tortoise = GET_CONTEXT(value); + PyObject *hare = GET_CONTEXT(GET_CONTEXT(value)); + while (hare != NULL && hare != tortoise) { + tortoise = GET_CONTEXT(tortoise); + hare = GET_CONTEXT(GET_CONTEXT(hare)); + } + if (hare != NULL) { + /* Making it here means there is a cycle. + Now find the _first_ self-intersection. */ + tortoise = value; + while (tortoise != hare) { + tortoise = GET_CONTEXT(tortoise); + hare = GET_CONTEXT(hare); + } + /* Now hare is the first intersection. + We want to disconnect hare from its second predecessor. + For example: + A --> B --> C --> D --> E --> C --> ... + becomes + A --> B --> C --> D --> E --> NULL, + since C is the first intersection. + */ + PyObject *prev = hare, *next = GET_CONTEXT(hare); + while (next != hare) { + prev = next; + next = GET_CONTEXT(next); + } + PyException_SetContext(prev, NULL); + } } + } if (value != NULL && PyExceptionInstance_Check(value)) tb = PyException_GetTraceback(value);