From ea3aa00df1e46ea889e1f63beef6db65eac80ed8 Mon Sep 17 00:00:00 2001 From: sweeneyde Date: Sat, 30 May 2020 08:08:09 -0400 Subject: [PATCH 1/5] Do not hang when exception contexts form a cycle. --- Python/errors.c | 66 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 50 insertions(+), 16 deletions(-) diff --git a/Python/errors.c b/Python/errors.c index 70365aaca585bb..6568af969b0660 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); From b942c1cee5fa3478d8e309939ad8cba9edce837c Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Sat, 30 May 2020 12:13:37 +0000 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Core and Builtins/2020-05-30-12-13-35.bpo-40696.NJZ364.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2020-05-30-12-13-35.bpo-40696.NJZ364.rst 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..77229027ef6272 --- /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 encountered exceptions whose ``__context__``s formed a cycle. \ No newline at end of file From 02f33306bbe4cc94a706f36290760e3c088e6e73 Mon Sep 17 00:00:00 2001 From: sweeneyde Date: Sat, 30 May 2020 08:15:18 -0400 Subject: [PATCH 3/5] add tests --- Lib/test/test_traceback.py | 48 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index f9a5f2fc53e1e9..aa5f67673d5790 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -631,6 +631,54 @@ def outer_raise(): self.assertEqual(blocks[1], cause_message) 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(): From 465e2f7e99c4413b3651693fa7d456041cf47a28 Mon Sep 17 00:00:00 2001 From: Dennis Sweeney <36520290+sweeneyde@users.noreply.github.com> Date: Sat, 30 May 2020 08:39:02 -0400 Subject: [PATCH 4/5] Fix ReStructuredText in News entry --- .../Core and Builtins/2020-05-30-12-13-35.bpo-40696.NJZ364.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 77229027ef6272..cc9af5b45354cb 100644 --- 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 @@ -1 +1 @@ -The interpreter no longer hangs when it encountered exceptions whose ``__context__``s formed a cycle. \ No newline at end of file +The interpreter no longer hangs when it encounters exceptions whose ``__context__`` attributes formed a cycle. Patch by Dennis Sweeney. From b0745097e1b61c996220b585289fd00b52fee964 Mon Sep 17 00:00:00 2001 From: sweeneyde Date: Sat, 30 May 2020 15:16:52 -0400 Subject: [PATCH 5/5] remove extra whitespace --- Lib/test/test_traceback.py | 6 +++--- Python/errors.c | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index aa5f67673d5790..5b362cfc7d3e8c 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -631,7 +631,7 @@ def outer_raise(): self.assertEqual(blocks[1], cause_message) 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. @@ -643,13 +643,13 @@ def test_rho_context(self): 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 diff --git a/Python/errors.c b/Python/errors.c index 6568af969b0660..c2911832fee103 100644 --- a/Python/errors.c +++ b/Python/errors.c @@ -173,7 +173,7 @@ _PyErr_SetObject(PyThreadState *tstate, PyObject *exception, PyObject *value) hare = GET_CONTEXT(hare); } /* Now hare is the first intersection. - We want to disconnect hare from its second predecessor. + We want to disconnect hare from its second predecessor. For example: A --> B --> C --> D --> E --> C --> ... becomes