Skip to content

Commit 6f4cded

Browse files
bpo-25782: avoid hang in PyErr_SetObject when current exception has a cycle in its context chain (GH-27626) (GH-27707)
Co-authored-by: Dennis Sweeney [email protected] (cherry picked from commit d5c2174) Co-authored-by: Irit Katriel <[email protected]>
1 parent c7dfbd2 commit 6f4cded

File tree

3 files changed

+158
-1
lines changed

3 files changed

+158
-1
lines changed

Lib/test/test_exceptions.py

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -923,6 +923,148 @@ def __del__(self):
923923
pass
924924
self.assertEqual(e, (None, None, None))
925925

926+
def test_raise_does_not_create_context_chain_cycle(self):
927+
class A(Exception):
928+
pass
929+
class B(Exception):
930+
pass
931+
class C(Exception):
932+
pass
933+
934+
# Create a context chain:
935+
# C -> B -> A
936+
# Then raise A in context of C.
937+
try:
938+
try:
939+
raise A
940+
except A as a_:
941+
a = a_
942+
try:
943+
raise B
944+
except B as b_:
945+
b = b_
946+
try:
947+
raise C
948+
except C as c_:
949+
c = c_
950+
self.assertIsInstance(a, A)
951+
self.assertIsInstance(b, B)
952+
self.assertIsInstance(c, C)
953+
self.assertIsNone(a.__context__)
954+
self.assertIs(b.__context__, a)
955+
self.assertIs(c.__context__, b)
956+
raise a
957+
except A as e:
958+
exc = e
959+
960+
# Expect A -> C -> B, without cycle
961+
self.assertIs(exc, a)
962+
self.assertIs(a.__context__, c)
963+
self.assertIs(c.__context__, b)
964+
self.assertIsNone(b.__context__)
965+
966+
def test_no_hang_on_context_chain_cycle1(self):
967+
# See issue 25782. Cycle in context chain.
968+
969+
def cycle():
970+
try:
971+
raise ValueError(1)
972+
except ValueError as ex:
973+
ex.__context__ = ex
974+
raise TypeError(2)
975+
976+
try:
977+
cycle()
978+
except Exception as e:
979+
exc = e
980+
981+
self.assertIsInstance(exc, TypeError)
982+
self.assertIsInstance(exc.__context__, ValueError)
983+
self.assertIs(exc.__context__.__context__, exc.__context__)
984+
985+
def test_no_hang_on_context_chain_cycle2(self):
986+
# See issue 25782. Cycle at head of context chain.
987+
988+
class A(Exception):
989+
pass
990+
class B(Exception):
991+
pass
992+
class C(Exception):
993+
pass
994+
995+
# Context cycle:
996+
# +-----------+
997+
# V |
998+
# C --> B --> A
999+
with self.assertRaises(C) as cm:
1000+
try:
1001+
raise A()
1002+
except A as _a:
1003+
a = _a
1004+
try:
1005+
raise B()
1006+
except B as _b:
1007+
b = _b
1008+
try:
1009+
raise C()
1010+
except C as _c:
1011+
c = _c
1012+
a.__context__ = c
1013+
raise c
1014+
1015+
self.assertIs(cm.exception, c)
1016+
# Verify the expected context chain cycle
1017+
self.assertIs(c.__context__, b)
1018+
self.assertIs(b.__context__, a)
1019+
self.assertIs(a.__context__, c)
1020+
1021+
def test_no_hang_on_context_chain_cycle3(self):
1022+
# See issue 25782. Longer context chain with cycle.
1023+
1024+
class A(Exception):
1025+
pass
1026+
class B(Exception):
1027+
pass
1028+
class C(Exception):
1029+
pass
1030+
class D(Exception):
1031+
pass
1032+
class E(Exception):
1033+
pass
1034+
1035+
# Context cycle:
1036+
# +-----------+
1037+
# V |
1038+
# E --> D --> C --> B --> A
1039+
with self.assertRaises(E) as cm:
1040+
try:
1041+
raise A()
1042+
except A as _a:
1043+
a = _a
1044+
try:
1045+
raise B()
1046+
except B as _b:
1047+
b = _b
1048+
try:
1049+
raise C()
1050+
except C as _c:
1051+
c = _c
1052+
a.__context__ = c
1053+
try:
1054+
raise D()
1055+
except D as _d:
1056+
d = _d
1057+
e = E()
1058+
raise e
1059+
1060+
self.assertIs(cm.exception, e)
1061+
# Verify the expected context chain cycle
1062+
self.assertIs(e.__context__, d)
1063+
self.assertIs(d.__context__, c)
1064+
self.assertIs(c.__context__, b)
1065+
self.assertIs(b.__context__, a)
1066+
self.assertIs(a.__context__, c)
1067+
9261068
def test_unicode_change_attributes(self):
9271069
# See issue 7309. This was a crasher.
9281070

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix bug where ``PyErr_SetObject`` hangs when the current exception has a cycle in its context chain.

Python/errors.c

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,19 +148,33 @@ _PyErr_SetObject(PyThreadState *tstate, PyObject *exception, PyObject *value)
148148
value = fixed_value;
149149
}
150150

151-
/* Avoid reference cycles through the context chain.
151+
/* Avoid creating new reference cycles through the
152+
context chain, while taking care not to hang on
153+
pre-existing ones.
152154
This is O(chain length) but context chains are
153155
usually very short. Sensitive readers may try
154156
to inline the call to PyException_GetContext. */
155157
if (exc_value != value) {
156158
PyObject *o = exc_value, *context;
159+
PyObject *slow_o = o; /* Floyd's cycle detection algo */
160+
int slow_update_toggle = 0;
157161
while ((context = PyException_GetContext(o))) {
158162
Py_DECREF(context);
159163
if (context == value) {
160164
PyException_SetContext(o, NULL);
161165
break;
162166
}
163167
o = context;
168+
if (o == slow_o) {
169+
/* pre-existing cycle - all exceptions on the
170+
path were visited and checked. */
171+
break;
172+
}
173+
if (slow_update_toggle) {
174+
slow_o = PyException_GetContext(slow_o);
175+
Py_DECREF(slow_o);
176+
}
177+
slow_update_toggle = !slow_update_toggle;
164178
}
165179
PyException_SetContext(value, exc_value);
166180
}

0 commit comments

Comments
 (0)