From f33574a3ca5ea92b4d7164b2e4159008213b4139 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Fri, 6 Aug 2021 13:21:50 +0100 Subject: [PATCH 1/7] bpo-25782: avoid hang in PyErr_SetObject when current exception has a cycle in its __context__ chain --- Lib/test/test_exceptions.py | 18 ++++++++++++++++++ Python/errors.c | 15 ++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index b280cfea435fd4..2b8e58a0e93631 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -953,6 +953,24 @@ def __del__(self): pass self.assertEqual(e, (None, None, None)) + def test_no_hang_on_context_chain_cycle(self): + # See issue 25782. + + def cycle(): + try: + raise ValueError(1) + except ValueError as ex: + ex.__context__ = ex + raise TypeError(2) + + try: + cycle() + except Exception as e: + exc = e + + self.assertIsInstance(exc, TypeError) + self.assertIsInstance(exc.__context__, ValueError) + def test_unicode_change_attributes(self): # See issue 7309. This was a crasher. diff --git a/Python/errors.c b/Python/errors.c index eeb84e83835e9f..4f48f77d568b6a 100644 --- a/Python/errors.c +++ b/Python/errors.c @@ -148,12 +148,16 @@ _PyErr_SetObject(PyThreadState *tstate, PyObject *exception, PyObject *value) value = fixed_value; } - /* Avoid reference cycles through the context chain. + /* Avoid creating new reference cycles through the + context chain, while taking care not to hang on + pre-existing ones. 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; + PyObject *slow_o = o; /* Floyd's cycle detection algo */ + int slow_update_toggle = 0; while ((context = PyException_GetContext(o))) { Py_DECREF(context); if (context == value) { @@ -161,6 +165,15 @@ _PyErr_SetObject(PyThreadState *tstate, PyObject *exception, PyObject *value) break; } o = context; + if (o == slow_o) { + /* pre-existing cycle - leave it */ + break; + } + if (slow_update_toggle) { + slow_o = PyException_GetContext(slow_o); + Py_DECREF(slow_o); + } + slow_update_toggle = !slow_update_toggle; } PyException_SetContext(value, exc_value); } From 70ebdd4b2519ffdf89ea25442743d658ae5acf39 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Sat, 7 Aug 2021 21:39:20 +0000 Subject: [PATCH 2/7] =?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/2021-08-07-21-39-19.bpo-25782.B22lMx.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2021-08-07-21-39-19.bpo-25782.B22lMx.rst diff --git a/Misc/NEWS.d/next/Core and Builtins/2021-08-07-21-39-19.bpo-25782.B22lMx.rst b/Misc/NEWS.d/next/Core and Builtins/2021-08-07-21-39-19.bpo-25782.B22lMx.rst new file mode 100644 index 00000000000000..1c52059f76c8f3 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2021-08-07-21-39-19.bpo-25782.B22lMx.rst @@ -0,0 +1 @@ +Fix bug where ``PyErr_SetObject`` hangs when the current exception has a cycle in its context chain. \ No newline at end of file From dd4f8ec5e7dc6ab1fdf52081228264eff0e92360 Mon Sep 17 00:00:00 2001 From: iritkatriel Date: Sat, 7 Aug 2021 22:13:06 +0100 Subject: [PATCH 3/7] add test --- Lib/test/test_exceptions.py | 49 ++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index 2b8e58a0e93631..fa4eee29c69f8a 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -953,7 +953,7 @@ def __del__(self): pass self.assertEqual(e, (None, None, None)) - def test_no_hang_on_context_chain_cycle(self): + def test_no_hang_on_context_chain_cycle1(self): # See issue 25782. def cycle(): @@ -971,6 +971,53 @@ def cycle(): self.assertIsInstance(exc, TypeError) self.assertIsInstance(exc.__context__, ValueError) + def test_no_hang_on_context_chain_cycle2(self): + # See issue 25782. + + class A(Exception): + pass + class B(Exception): + pass + class C(Exception): + pass + class D(Exception): + pass + class E(Exception): + pass + + # Context cycle: + # +-----------+ + # V | + # E --> D --> C --> B --> A + with self.assertRaises(E) as cm: + try: + raise A() + except A as _a: + a = _a + try: + raise B() + except B as _b: + b = _b + try: + raise C() + except C as _c: + c = _c + a.__context__ = c + try: + raise D() + except D as _d: + d = _d + e = E() + raise e + + self.assertIs(cm.exception, e) + # Verify the expected context chain cycle + self.assertIs(e.__context__, d) + self.assertIs(d.__context__, c) + self.assertIs(c.__context__, b) + self.assertIs(b.__context__, a) + self.assertIs(a.__context__, c) + def test_unicode_change_attributes(self): # See issue 7309. This was a crasher. From a97fe666901702a23902e5bc870c99f70e339f47 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Sun, 8 Aug 2021 18:43:13 +0100 Subject: [PATCH 4/7] add test with cycle at head --- Lib/test/test_exceptions.py | 47 ++++++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index fa4eee29c69f8a..bd5f83c32c6877 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -954,7 +954,7 @@ def __del__(self): self.assertEqual(e, (None, None, None)) def test_no_hang_on_context_chain_cycle1(self): - # See issue 25782. + # See issue 25782. Cycle in context chain. def cycle(): try: @@ -970,9 +970,50 @@ def cycle(): self.assertIsInstance(exc, TypeError) self.assertIsInstance(exc.__context__, ValueError) + self.assertIs(exc.__context__.__context__, exc.__context__) - def test_no_hang_on_context_chain_cycle2(self): - # See issue 25782. + def test_no_hang_on_context_chain_cycle1(self): + # See issue 25782. Cycle at head of context chain. + + class A(Exception): + pass + class B(Exception): + pass + class C(Exception): + pass + class D(Exception): + pass + class E(Exception): + pass + + # Context cycle: + # +-----------+ + # V | + # C --> B --> A + with self.assertRaises(C) as cm: + try: + raise A() + except A as _a: + a = _a + try: + raise B() + except B as _b: + b = _b + try: + raise C() + except C as _c: + c = _c + a.__context__ = c + raise c + + self.assertIs(cm.exception, c) + # Verify the expected context chain cycle + self.assertIs(c.__context__, b) + self.assertIs(b.__context__, a) + self.assertIs(a.__context__, c) + + def test_no_hang_on_context_chain_cycle3(self): + # See issue 25782. Longer context chain with cycle. class A(Exception): pass From 7b2074931dd024ef3469a74d59e6cf5f41f82fb2 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Sun, 8 Aug 2021 18:49:23 +0100 Subject: [PATCH 5/7] edit comment --- Python/errors.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Python/errors.c b/Python/errors.c index 4f48f77d568b6a..ae1cde690eafa5 100644 --- a/Python/errors.c +++ b/Python/errors.c @@ -166,7 +166,8 @@ _PyErr_SetObject(PyThreadState *tstate, PyObject *exception, PyObject *value) } o = context; if (o == slow_o) { - /* pre-existing cycle - leave it */ + /* pre-existing cycle - all exceptions on the + path were visited and checked. */ break; } if (slow_update_toggle) { From e45bdf2fba0f270410783abc325a265ce28da332 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Sun, 8 Aug 2021 18:52:25 +0100 Subject: [PATCH 6/7] fix test name --- Lib/test/test_exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index bd5f83c32c6877..6b0334ba5f42a3 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -972,7 +972,7 @@ def cycle(): self.assertIsInstance(exc.__context__, ValueError) self.assertIs(exc.__context__.__context__, exc.__context__) - def test_no_hang_on_context_chain_cycle1(self): + def test_no_hang_on_context_chain_cycle2(self): # See issue 25782. Cycle at head of context chain. class A(Exception): From bfc5f6b353f932236ae88fa7277478955a848b7f Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Mon, 9 Aug 2021 14:13:15 +0100 Subject: [PATCH 7/7] add test for not-creating a cycle in raise --- Lib/test/test_exceptions.py | 44 +++++++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index 6b0334ba5f42a3..593107f290dc59 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -953,6 +953,46 @@ def __del__(self): pass self.assertEqual(e, (None, None, None)) + def test_raise_does_not_create_context_chain_cycle(self): + class A(Exception): + pass + class B(Exception): + pass + class C(Exception): + pass + + # Create a context chain: + # C -> B -> A + # Then raise A in context of C. + try: + try: + raise A + except A as a_: + a = a_ + try: + raise B + except B as b_: + b = b_ + try: + raise C + except C as c_: + c = c_ + self.assertIsInstance(a, A) + self.assertIsInstance(b, B) + self.assertIsInstance(c, C) + self.assertIsNone(a.__context__) + self.assertIs(b.__context__, a) + self.assertIs(c.__context__, b) + raise a + except A as e: + exc = e + + # Expect A -> C -> B, without cycle + self.assertIs(exc, a) + self.assertIs(a.__context__, c) + self.assertIs(c.__context__, b) + self.assertIsNone(b.__context__) + def test_no_hang_on_context_chain_cycle1(self): # See issue 25782. Cycle in context chain. @@ -981,10 +1021,6 @@ class B(Exception): pass class C(Exception): pass - class D(Exception): - pass - class E(Exception): - pass # Context cycle: # +-----------+