Skip to content

Commit

Permalink
bpo-42183: Fix a stack overflow error for asyncio Task or Future repr…
Browse files Browse the repository at this point in the history
…() (GH-23020)

The overflow occurs under some circumstances when a task or future
recursively returns itself.

Co-authored-by: Kyle Stanley <aeros167@gmail.com>
(cherry picked from commit 42d873c)

Co-authored-by: Andrew Svetlov <andrew.svetlov@gmail.com>
  • Loading branch information
miss-islington and asvetlov committed Nov 10, 2020
1 parent 2a86ade commit 90115a2
Show file tree
Hide file tree
Showing 3 changed files with 44 additions and 3 deletions.
25 changes: 22 additions & 3 deletions Lib/asyncio/base_futures.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
__all__ = ()

import reprlib
from _thread import get_ident

from . import format_helpers

Expand Down Expand Up @@ -41,6 +42,16 @@ def format_cb(callback):
return f'cb=[{cb}]'


# bpo-42183: _repr_running is needed for repr protection
# when a Future or Task result contains itself directly or indirectly.
# The logic is borrowed from @reprlib.recursive_repr decorator.
# Unfortunately, the direct decorator usage is impossible because of
# AttributeError: '_asyncio.Task' object has no attribute '__module__' error.
#
# After fixing this thing we can return to the decorator based approach.
_repr_running = set()


def _future_repr_info(future):
# (Future) -> str
"""helper function for Future.__repr__"""
Expand All @@ -49,9 +60,17 @@ def _future_repr_info(future):
if future._exception is not None:
info.append(f'exception={future._exception!r}')
else:
# use reprlib to limit the length of the output, especially
# for very long strings
result = reprlib.repr(future._result)
key = id(future), get_ident()
if key in _repr_running:
result = '...'
else:
_repr_running.add(key)
try:
# use reprlib to limit the length of the output, especially
# for very long strings
result = reprlib.repr(future._result)
finally:
_repr_running.discard(key)
info.append(f'result={result}')
if future._callbacks:
info.append(_format_callbacks(future._callbacks))
Expand Down
18 changes: 18 additions & 0 deletions Lib/test/test_asyncio/test_futures2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# IsolatedAsyncioTestCase based tests
import asyncio
import unittest


class FutureTests(unittest.IsolatedAsyncioTestCase):
async def test_recursive_repr_for_pending_tasks(self):
# The call crashes if the guard for recursive call
# in base_futures:_future_repr_info is absent
# See Also: https://bugs.python.org/issue42183

async def func():
return asyncio.all_tasks()

# The repr() call should not raise RecursiveError at first.
# The check for returned string is not very reliable but
# exact comparison for the whole string is even weaker.
self.assertIn('...', repr(await asyncio.wait_for(func(), timeout=10)))
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fix a stack overflow error for asyncio Task or Future repr().

The overflow occurs under some circumstances when a Task or Future
recursively returns itself.

0 comments on commit 90115a2

Please sign in to comment.