Skip to content

Commit f9169ce

Browse files
takluyverncoghlan
authored andcommitted
bpo-25532: Protect against infinite loops in inspect.unwrap() (#1717)
Some objects (like test mocks) auto-generate new objects on attribute access, which can lead to an infinite loop in inspect.unwrap(). Ensuring references are retained to otherwise temporary objects and capping the size of the memo dict turns this case into a conventional exception instead.
1 parent e377416 commit f9169ce

File tree

3 files changed

+28
-3
lines changed

3 files changed

+28
-3
lines changed

Lib/inspect.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -505,13 +505,16 @@ def _is_wrapper(f):
505505
def _is_wrapper(f):
506506
return hasattr(f, '__wrapped__') and not stop(f)
507507
f = func # remember the original func for error reporting
508-
memo = {id(f)} # Memoise by id to tolerate non-hashable objects
508+
# Memoise by id to tolerate non-hashable objects, but store objects to
509+
# ensure they aren't destroyed, which would allow their IDs to be reused.
510+
memo = {id(f): f}
511+
recursion_limit = sys.getrecursionlimit()
509512
while _is_wrapper(func):
510513
func = func.__wrapped__
511514
id_func = id(func)
512-
if id_func in memo:
515+
if (id_func in memo) or (len(memo) >= recursion_limit):
513516
raise ValueError('wrapper loop when unwrapping {!r}'.format(f))
514-
memo.add(id_func)
517+
memo[id_func] = func
515518
return func
516519

517520
# -------------------------------------------------- source code extraction

Lib/test/test_inspect.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3554,6 +3554,19 @@ def test_builtins_have_signatures(self):
35543554
self.assertIsNone(obj.__text_signature__)
35553555

35563556

3557+
class NTimesUnwrappable:
3558+
def __init__(self, n):
3559+
self.n = n
3560+
self._next = None
3561+
3562+
@property
3563+
def __wrapped__(self):
3564+
if self.n <= 0:
3565+
raise Exception("Unwrapped too many times")
3566+
if self._next is None:
3567+
self._next = NTimesUnwrappable(self.n - 1)
3568+
return self._next
3569+
35573570
class TestUnwrap(unittest.TestCase):
35583571

35593572
def test_unwrap_one(self):
@@ -3609,6 +3622,11 @@ class C:
36093622
__wrapped__ = func
36103623
self.assertIsNone(inspect.unwrap(C()))
36113624

3625+
def test_recursion_limit(self):
3626+
obj = NTimesUnwrappable(sys.getrecursionlimit() + 1)
3627+
with self.assertRaisesRegex(ValueError, 'wrapper loop'):
3628+
inspect.unwrap(obj)
3629+
36123630
class TestMain(unittest.TestCase):
36133631
def test_only_source(self):
36143632
module = importlib.import_module('unittest')

Misc/NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -995,6 +995,10 @@ Library
995995
- Issue #29581: ABCMeta.__new__ now accepts ``**kwargs``, allowing abstract base
996996
classes to use keyword parameters in __init_subclass__. Patch by Nate Soares.
997997

998+
- Issue #25532: inspect.unwrap() will now only try to unwrap an object
999+
sys.getrecursionlimit() times, to protect against objects which create a new
1000+
object on every attribute access.
1001+
9981002
Windows
9991003
-------
10001004

0 commit comments

Comments
 (0)