Skip to content

Commit a87b7ed

Browse files
bpo-25782: Prevent cycles in the __context__ chain.
Setting __context__ to self has no any effect. If set __context__ to the chain contained original exception, it is moved to the start of the context chain.
1 parent 7b78e7f commit a87b7ed

4 files changed

Lines changed: 55 additions & 1 deletion

File tree

‎Doc/c-api/exceptions.rst‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,10 @@ Exception Objects
601601
it. There is no type check to make sure that *ctx* is an exception instance.
602602
This steals a reference to *ctx*.
603603
604+
.. versionchanged:: 3.5.2
605+
If *ctx* is *ex*, the function has no any effect. If *ex* is in the
606+
context chain started from *ctx*, it is moved to the start of the chain.
607+
604608
605609
.. c:function:: PyObject* PyException_GetCause(PyObject *ex)
606610

‎Doc/library/exceptions.rst‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@ In either case, the exception itself is always shown after any chained
6666
exceptions so that the final line of the traceback always shows the last
6767
exception that was raised.
6868

69+
.. versionchanged:: 3.5.2
70+
71+
Setting :attr:`__context__` to self has no any effect. If set
72+
``__context__`` to the chain contained original exception, it is moved to
73+
the start of the context chain.
74+
6975

7076
Base classes
7177
------------

‎Lib/test/test_exceptions.py‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,26 @@ def __exit__ (self, exc_type, exc_value, exc_tb):
716716
obj = wr()
717717
self.assertIsNone(obj)
718718

719+
def test_context_loop(self):
720+
exc1 = Exception(1)
721+
exc1.__context__ = exc1
722+
self.assertIsNone(exc1.__context__)
723+
724+
exc2 = Exception(2)
725+
exc2.__context__ = exc1
726+
self.assertIs(exc2.__context__, exc1)
727+
exc1.__context__ = exc2
728+
self.assertIs(exc1.__context__, exc2)
729+
self.assertIsNone(exc2.__context__)
730+
731+
exc3 = Exception(3)
732+
exc3.__context__ = exc1
733+
self.assertIs(exc3.__context__, exc1)
734+
exc1.__context__ = exc3
735+
self.assertIs(exc1.__context__, exc3)
736+
self.assertIs(exc3.__context__, exc2)
737+
self.assertIsNone(exc2.__context__)
738+
719739
def test_exception_target_in_nested_scope(self):
720740
# issue 4617: This used to raise a SyntaxError
721741
# "can not delete variable 'e' referenced in nested scope"

‎Objects/exceptions.c‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,9 +356,33 @@ PyException_GetContext(PyObject *self)
356356
void
357357
PyException_SetContext(PyObject *self, PyObject *context)
358358
{
359-
Py_XSETREF(_PyBaseExceptionObject_cast(self)->context, context);
359+
assert(PyExceptionInstance_Check(self));
360+
assert(context == NULL || PyExceptionInstance_Check(context));
361+
362+
PyObject *exc = context;
363+
PyObject *prev_exc = NULL;
364+
365+
while (exc != NULL) {
366+
if (exc == self) {
367+
if (prev_exc != NULL) {
368+
/* Move self to the start of the chain. */
369+
((PyBaseExceptionObject *)prev_exc)->context =
370+
((PyBaseExceptionObject *)self)->context;
371+
((PyBaseExceptionObject *)self)->context = context;
372+
}
373+
assert(Py_REFCNT(self) > 1);
374+
Py_DECREF(self);
375+
return;
376+
}
377+
assert(PyExceptionInstance_Check(exc));
378+
prev_exc = exc;
379+
exc = ((PyBaseExceptionObject *)exc)->context;
380+
}
381+
382+
Py_XSETREF(((PyBaseExceptionObject *)self)->context, context);
360383
}
361384

385+
362386
#undef PyExceptionClass_Name
363387

364388
const char *

0 commit comments

Comments
 (0)