From: Carl Sverre Date: Thu, 12 Feb 2015 04:00:02 +0000 (-0800) Subject: Coroutine exception __context__ leak in python 3 X-Git-Tag: v4.2.0b1~119^2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=b2091c407a4483da9dfa8852e080668c0440f428;p=thirdparty%2Ftornado.git Coroutine exception __context__ leak in python 3 In Python 3 every exception has an auto-attached __context__ exception if it was raised while an existing exception was being handled. This is often a very useful property and allows users to see the underlying cause of many problems. However, when dealing with async code via coroutines this feature can suddenly become a pretty big issue. Namely, if you generator.throw back into a coroutine to raise an exception in the correct scope, and that coroutine raises an exception at a later time you can have nested exceptions which shouldn't logically be nested. This patch resolves this issue in the coroutine code by moving the gen.throw outside of the exception handler. By doing this the user-code scope is able to later raise exceptions without worrying about inheriting prior exception context. --- diff --git a/tornado/gen.py b/tornado/gen.py index 3d87d7177..b405b2f7c 100644 --- a/tornado/gen.py +++ b/tornado/gen.py @@ -824,13 +824,20 @@ class Runner(object): self.future = None try: orig_stack_contexts = stack_context._state.contexts + exc_info = None + try: value = future.result() except Exception: self.had_exception = True - yielded = self.gen.throw(*sys.exc_info()) + exc_info = sys.exc_info() + + if exc_info is not None: + yielded = self.gen.throw(*exc_info) + exc_info = None else: yielded = self.gen.send(value) + if stack_context._state.contexts is not orig_stack_contexts: self.gen.throw( stack_context.StackContextInconsistentError( diff --git a/tornado/test/gen_test.py b/tornado/test/gen_test.py index 31acadd8b..15876d598 100644 --- a/tornado/test/gen_test.py +++ b/tornado/test/gen_test.py @@ -844,6 +844,29 @@ class GenCoroutineTest(AsyncTestCase): yield gen.sleep(0.01) self.finished = True + @skipBefore33 + @gen_test + def test_py3_leak_exception_context(self): + class LeakedException(Exception): + pass + + @gen.coroutine + def inner(iteration): + raise LeakedException(iteration) + + try: + yield inner(1) + except LeakedException as e: + self.assertEqual(str(e), "1") + self.assertIsNone(e.__context__) + + try: + yield inner(2) + except LeakedException as e: + self.assertEqual(str(e), "2") + self.assertIsNone(e.__context__) + + self.finished = True class GenSequenceHandler(RequestHandler): @asynchronous