From 3634560fa9ddb342c3a823b78cc63e7000ccabd8 Mon Sep 17 00:00:00 2001 From: Vinay Karanam Date: Thu, 6 Dec 2018 18:28:29 +0530 Subject: [PATCH] Fixed #29393 -- Prevented infinite loop in ExceptionReporter.get_traceback_frames(). --- django/views/debug.py | 3 +++ tests/view_tests/tests/test_debug.py | 39 ++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/django/views/debug.py b/django/views/debug.py index 1128301c57..0efaf0c6af 100644 --- a/django/views/debug.py +++ b/django/views/debug.py @@ -397,6 +397,9 @@ class ExceptionReporter: while exc_value: exceptions.append(exc_value) exc_value = explicit_or_implicit_cause(exc_value) + if exc_value in exceptions: + # Avoid infinite loop if there's a cyclic reference (#29393). + break frames = [] # No exceptions were supplied to ExceptionReporter diff --git a/tests/view_tests/tests/test_debug.py b/tests/view_tests/tests/test_debug.py index 1ff1220803..a6e5275c9f 100644 --- a/tests/view_tests/tests/test_debug.py +++ b/tests/view_tests/tests/test_debug.py @@ -4,6 +4,7 @@ import os import re import sys import tempfile +import threading from io import StringIO from pathlib import Path @@ -403,6 +404,44 @@ class ExceptionReporterTests(SimpleTestCase): text = reporter.get_traceback_text() self.assertIn('"generated" in funcName', text) + def test_reporting_frames_for_cyclic_reference(self): + try: + def test_func(): + try: + raise RuntimeError('outer') from RuntimeError('inner') + except RuntimeError as exc: + raise exc.__cause__ + test_func() + except Exception: + exc_type, exc_value, tb = sys.exc_info() + request = self.rf.get('/test_view/') + reporter = ExceptionReporter(request, exc_type, exc_value, tb) + + def generate_traceback_frames(*args, **kwargs): + nonlocal tb_frames + tb_frames = reporter.get_traceback_frames() + + tb_frames = None + tb_generator = threading.Thread(target=generate_traceback_frames, daemon=True) + tb_generator.start() + tb_generator.join(timeout=5) + if tb_generator.is_alive(): + # tb_generator is a daemon that runs until the main thread/process + # exits. This is resource heavy when running the full test suite. + # Setting the following values to None makes + # reporter.get_traceback_frames() exit early. + exc_value.__traceback__ = exc_value.__context__ = exc_value.__cause__ = None + tb_generator.join() + self.fail('Cyclic reference in Exception Reporter.get_traceback_frames()') + if tb_frames is None: + # can happen if the thread generating traceback got killed + # or exception while generating the traceback + self.fail('Traceback generation failed') + last_frame = tb_frames[-1] + self.assertIn('raise exc.__cause__', last_frame['context_line']) + self.assertEqual(last_frame['filename'], __file__) + self.assertEqual(last_frame['function'], 'test_func') + def test_request_and_message(self): "A message can be provided in addition to a request" request = self.rf.get('/test_view/')