diff -r cca2ed4e8b41 Doc/c-api/exceptions.rst --- a/Doc/c-api/exceptions.rst Thu Dec 17 10:35:05 2015 +0000 +++ b/Doc/c-api/exceptions.rst Thu Dec 17 17:51:01 2015 -0500 @@ -570,6 +570,10 @@ it. There is no type check to make sure that *ctx* is an exception instance. This steals a reference to *ctx*. + .. versionchanged:: 3.5.2 + If setting *ctx* introduces an object cycle, the context will be + set to *NULL*. + .. c:function:: PyObject* PyException_GetCause(PyObject *ex) diff -r cca2ed4e8b41 Doc/library/exceptions.rst --- a/Doc/library/exceptions.rst Thu Dec 17 10:35:05 2015 +0000 +++ b/Doc/library/exceptions.rst Thu Dec 17 17:51:01 2015 -0500 @@ -66,6 +66,18 @@ exceptions so that the final line of the traceback always shows the last exception that was raised. +.. versionchanged:: 3.5.2 + + An attempt to introduce a object cycle when setting + :attr:`__context__` will trigger a :exc:`RuntimeError` + exception:: + + >>> exc = Exception() + >>> exc.__context__ = exc + Traceback (most recent call last): + File "", line 1, in + RuntimeError: cycle in exception context chain + Base classes ------------ diff -r cca2ed4e8b41 Lib/test/test_exceptions.py --- a/Lib/test/test_exceptions.py Thu Dec 17 10:35:05 2015 +0000 +++ b/Lib/test/test_exceptions.py Thu Dec 17 17:51:01 2015 -0500 @@ -625,6 +625,21 @@ obj = wr() self.assertTrue(obj is None, "%s" % obj) + def testExceptionChainedContext(self): + class Err1(Exception): pass + class Err2(Exception): pass + + err1 = Err1() + with self.assertRaisesRegex(RuntimeError, 'cycle in exception context'): + err1.__context__ = err1 + self.assertIsNone(err1.__context__) + + err2 = Err2() + err2.__context__ = err1 + with self.assertRaisesRegex(RuntimeError, 'cycle in exception context'): + err1.__context__ = err2 + self.assertIsNone(err1.__context__) + def test_exception_target_in_nested_scope(self): # issue 4617: This used to raise a SyntaxError # "can not delete variable 'e' referenced in nested scope" diff -r cca2ed4e8b41 Objects/exceptions.c --- a/Objects/exceptions.c Thu Dec 17 10:35:05 2015 +0000 +++ b/Objects/exceptions.c Thu Dec 17 17:51:01 2015 -0500 @@ -265,6 +265,10 @@ Py_INCREF(arg); } PyException_SetContext(self, arg); + if (((PyBaseExceptionObject *)self)->context != arg) { + PyErr_SetString(PyExc_RuntimeError, "cycle in exception context chain"); + return -1; + } return 0; } @@ -292,6 +296,10 @@ Py_INCREF(arg); } PyException_SetCause(self, arg); + if (arg != NULL && ((PyBaseExceptionObject *)self)->cause == NULL) { + PyErr_SetString(PyExc_RuntimeError, "cycle in exception cause chain"); + return -1; + } return 0; } @@ -331,6 +339,7 @@ /* Steals a reference to cause */ void PyException_SetCause(PyObject *self, PyObject *cause) { + assert(cause == NULL || PyExceptionInstance_Check(cause)); PyObject *old_cause = ((PyBaseExceptionObject *)self)->cause; ((PyBaseExceptionObject *)self)->cause = cause; ((PyBaseExceptionObject *)self)->suppress_context = 1; @@ -346,7 +355,18 @@ /* Steals a reference to context */ void -PyException_SetContext(PyObject *self, PyObject *context) { +PyException_SetContext(PyObject *self, PyObject *context) +{ + PyObject *exc = context; + while (exc != NULL) { + if (exc == self) { + context = NULL; + break; + } + assert(PyExceptionInstance_Check(exc)); + exc = ((PyBaseExceptionObject *)exc)->context; + } + PyObject *old_context = ((PyBaseExceptionObject *)self)->context; ((PyBaseExceptionObject *)self)->context = context; Py_XDECREF(old_context);