Skip to content

Commit d5c2174

Browse files
authored
bpo-25782: avoid hang in PyErr_SetObject when current exception has a cycle in its context chain (GH-27626)
Co-authored-by: Dennis Sweeney [email protected]
1 parent 6b37d0d commit d5c2174

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
@@ -953,6 +953,148 @@ def __del__(self):
953953
pass
954954
self.assertEqual(e, (None, None, None))
955955

956+
def test_raise_does_not_create_context_chain_cycle(self):
957+
class A(Exception):
958+
pass
959+
class B(Exception):
960+
pass
961+
class C(Exception):
962+
pass
963+
964+
# Create a context chain:
965+
# C -> B -> A
966+
# Then raise A in context of C.
967+
try:
968+
try:
969+
raise A
970+
except A as a_:
971+
a = a_
972+
try:
973+
raise B
974+
except B as b_:
975+
b = b_
976+
try:
977+
raise C
978+
except C as c_:
979+
c = c_
980+
self.assertIsInstance(a, A)
981+
self.assertIsInstance(b, B)
982+
self.assertIsInstance(c, C)
983+
self.assertIsNone(a.__context__)
984+
self.assertIs(b.__context__, a)
985+
self.assertIs(c.__context__, b)
986+
raise a
987+
except A as e:
988+
exc = e
989+
990+
# Expect A -> C -> B, without cycle
991+
self.assertIs(exc, a)
992+
self.assertIs(a.__context__, c)
993+
self.assertIs(c.__context__, b)
994+
self.assertIsNone(b.__context__)
995+
996+
def test_no_hang_on_context_chain_cycle1(self):
997+
# See issue 25782. Cycle in context chain.
998+
999+
def cycle():
1000+
try:
1001+
raise ValueError(1)
1002+
except ValueError as ex:
1003+
ex.__context__ = ex
1004+
raise TypeError(2)
1005+
1006+
try:
1007+
cycle()
1008+
except Exception as e:
1009+
exc = e
1010+
1011+
self.assertIsInstance(exc, TypeError)
1012+
self.assertIsInstance(exc.__context__, ValueError)
1013+
self.assertIs(exc.__context__.__context__, exc.__context__)
1014+
1015+
def test_no_hang_on_context_chain_cycle2(self):
1016+
# See issue 25782. Cycle at head of context chain.
1017+
1018+
class A(Exception):
1019+
pass
1020+
class B(Exception):
1021+
pass
1022+
class C(Exception):
1023+
pass
1024+
1025+
# Context cycle:
1026+
# +-----------+
1027+
# V |
1028+
# C --> B --> A
1029+
with self.assertRaises(C) as cm:
1030+
try:
1031+
raise A()
1032+
except A as _a:
1033+
a = _a
1034+
try:
1035+
raise B()
1036+
except B as _b:
1037+
b = _b
1038+
try:
1039+
raise C()
1040+
except C as _c:
1041+
c = _c
1042+
a.__context__ = c
1043+
raise c
1044+
1045+
self.assertIs(cm.exception, c)
1046+
# Verify the expected context chain cycle
1047+
self.assertIs(c.__context__, b)
1048+
self.assertIs(b.__context__, a)
1049+
self.assertIs(a.__context__, c)
1050+
1051+
def test_no_hang_on_context_chain_cycle3(self):
1052+
# See issue 25782. Longer context chain with cycle.
1053+
1054+
class A(Exception):
1055+
pass
1056+
class B(Exception):
1057+
pass
1058+
class C(Exception):
1059+
pass
1060+
class D(Exception):
1061+
pass
1062+
class E(Exception):
1063+
pass
1064+
1065+
# Context cycle:
1066+
# +-----------+
1067+
# V |
1068+
# E --> D --> C --> B --> A
1069+
with self.assertRaises(E) as cm:
1070+
try:
1071+
raise A()
1072+
except A as _a:
1073+
a = _a
1074+
try:
1075+
raise B()
1076+
except B as _b:
1077+
b = _b
1078+
try:
1079+
raise C()
1080+
except C as _c:
1081+
c = _c
1082+
a.__context__ = c
1083+
try:
1084+
raise D()
1085+
except D as _d:
1086+
d = _d
1087+
e = E()
1088+
raise e
1089+
1090+
self.assertIs(cm.exception, e)
1091+
# Verify the expected context chain cycle
1092+
self.assertIs(e.__context__, d)
1093+
self.assertIs(d.__context__, c)
1094+
self.assertIs(c.__context__, b)
1095+
self.assertIs(b.__context__, a)
1096+
self.assertIs(a.__context__, c)
1097+
9561098
def test_unicode_change_attributes(self):
9571099
# See issue 7309. This was a crasher.
9581100

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)