8000 [3.9] bpo-43048: RecursionError traceback RecursionError bugfix for cpy3.9 (GH-24460) by vlad17 · Pull Request #24460 · python/cpython · GitHub
[go: up one dir, main page]

Skip to content

[3.9] bpo-43048: RecursionError traceback RecursionError bugfix for cpy3.9 (GH-24460) #24460

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions Lib/test/test_traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,43 @@ def test_base_exception(self):
lst = traceback.format_exception_only(e.__class__, e)
self.assertEqual(lst, ['KeyboardInterrupt\n'])

def test_traceback_context_recursionerror(self):
# Test that for long traceback chains traceback does not itself
# raise a recursion error while printing (Issue43048)

# Calling f() creates a stack-overflowing __context__ chain.
def f():
try:
raise ValueError('hello')
except ValueError:
f()

try:
f()
except RecursionError:
exc_info = sys.exc_info()

traceback.format_exception(exc_info[0], exc_info[1], exc_info[2])

def test_traceback_cause_recursionerror(self):
# Same as test_traceback_context_recursionerror, but with
# a __cause__ chain.

def f():
e = None
try:
f()
except Exception as exc:
e = exc
raise Exception from e

try:
f()
except Exception:
exc_info = sys.exc_info()

traceback.format_exception(exc_info[0], exc_info[1], exc_info[2])

def test_format_exception_only_bad__str__(self):
class X(Exception):
def __str__(self):
Expand Down
56 changes: 38 additions & 18 deletions Lib/traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -476,34 +476,50 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
_seen.add(id(exc_value))
# Gracefully handle (the way Python 2.4 and earlier did) the case of
# being called with no type or value (None, None, None).
self.__suppress_context__ = False
self._truncated = False
if (exc_value and exc_value.__cause__ is not None
and id(exc_value.__cause__) not in _seen):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If recursion into constructing cause exceeded the recursion limit then surely the call to construct context will too. Why do we need to try blocks?

cause = TracebackException(
type(exc_value.__cause__),
exc_value.__cause__,
exc_value.__cause__.__traceback__,
limit=limit,
lookup_lines=False,
capture_locals=capture_locals,
_seen=_seen)
try:
cause = TracebackException(
type(exc_value.__cause__),
exc_value.__cause__,
exc_value.__cause__.__traceback__,
limit=limit,
lookup_lines=False,
capture_locals=capture_locals,
_seen=_seen)
except RecursionError:
# The recursive call to the constructor in the try above
# may result in a stack 8000 overflow for long exception chains,
# so we must truncate.
self._truncated = True
cause = None
# Suppress the context during printing, so that even when
# a cause is truncated we don't print the context.
self.__suppress_context__ = True
else:
cause = None
if (exc_value and exc_value.__context__ is not None
and id(exc_value.__context__) not in _seen):
context = TracebackException(
type(exc_value.__context__),
exc_value.__context__,
exc_value.__context__.__traceback__,
limit=limit,
lookup_lines=False,
capture_locals=capture_locals,
_seen=_seen)
try:
context = TracebackException(
type(exc_value.__context__),
exc_value.__context__,
exc_value.__context__.__traceback__,
limit=limit,
lookup_lines=False,
capture_locals=capture_locals,
_seen=_seen)
except RecursionError:
self._truncated = True
context = None
else:
context = None
self.__cause__ = cause
self.__context__ = context
self.__suppress_context__ = \
exc_value.__suppress_context__ if exc_value else False
self.__suppress_context__ = self.__suppress_context__ or (
exc_value.__suppress_context__ if exc_value else False)
# TODO: locals.
self.stack = StackSummary.extract(
walk_tb(exc_traceback), limit=limit, lookup_lines=lookup_lines,
Expand Down Expand Up @@ -620,6 +636,10 @@ def format(self, *, chain=True):
not self.__suppress_context__):
yield from self.__context__.format(chain=chain)
yield _context_message
if self._truncated:
yield (
'Chained exceptions have been truncated to avoid '
'stack overflow in traceback formatting:\n')
if self.stack:
yield 'Traceback (most recent call last):\n'
yield from self.stack.format()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
When traceback is formatting a RecursionError with a long __context__ chain it runs the risk of itself raising a RecursionError. This patch catches the corresponding recursive call's RecursionError, truncates the traceback and emits a warning instead.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is ambiguous because there is also traceback formatting code in the interpreter. Also probably too long, and mentions context but not cause. This is probably enough to indicate the impact (and there are always more details in the bpo):

Handle RecursionErrors in :class:~traceback.TracebackException's constructor, so that long exceptions chains are truncated instead of causing traceback formatting to fail.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done. thanks for the patience!

0