Skip to content

bpo-35780: Fix errors in lru_cache() C code #11623

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
merged 30 commits into from
Jan 26, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
9f073b9
Communicate that the pointer updates doen't change link->prev and lin…
rhettinger Jan 19, 2019
42ab331
Save space for scalar keys
rhettinger Jan 19, 2019
fb148f3
Fix diction
rhettinger Jan 19, 2019
c8558d6
Fix bug arising in recursive user calls
rhettinger Jan 20, 2019
98a638c
Add blurb
rhettinger Jan 20, 2019
1fecffd
Fix bug where in an error case, the old key about to be evicted
rhettinger Jan 20, 2019
79bfd59
Convert an always true test into an assertion.
rhettinger Jan 20, 2019
4d66413
Reset the "full" flag when a link is dropped. Update comments.
rhettinger Jan 20, 2019
8f582dc
Fix missing decref.
rhettinger Jan 20, 2019
2db2de6
Minor code clarity improvements. Group link updates together.
rhettinger Jan 20, 2019
f4f969e
Sync-up C code and pure Python code for scalar argument handling.
rhettinger Jan 20, 2019
0b8bf71
Improve clarity and accuracy of new comments
rhettinger Jan 20, 2019
a64e21b
Cleaner test using a nonlocal variable instead of a global
rhettinger Jan 22, 2019
a300047
Restore cache invariants on a path where an error is raised.
rhettinger Jan 22, 2019
84811b5
Add general notes on possible sources of reentrancy
rhettinger Jan 22, 2019
cc50597
We can restore the old link, so need to make as no longer full.
rhettinger Jan 22, 2019
304c104
Mention the decref policy in the general notes about reentrancy
rhettinger Jan 22, 2019
7ae7208
Fix grammar in comment block
rhettinger Jan 22, 2019
75ba665
Fix one more bug for the full status flag.
rhettinger Jan 23, 2019
2fb27c0
Fix bug in handling of negative maxsize (should be treated as zero).
rhettinger Jan 24, 2019
3a6973c
Remove the "full" variable.
rhettinger Jan 24, 2019
be775c8
Drop unneeded maxsize_O attribute. Prefer plain maxsize as a single …
rhettinger Jan 24, 2019
23114ef
Make control clearer by favoring intermediate return statements inste…
rhettinger Jan 24, 2019
e207d41
Arrange structure members in order of typical access
rhettinger Jan 24, 2019
614bfc8
Order initial assignments and deallocations to match structure order
rhettinger Jan 24, 2019
ed69975
Update NEWS entry
rhettinger Jan 24, 2019
f86e311
Remove one more layer of if-logic to make the code easier to follow.
rhettinger Jan 24, 2019
c6fa049
Fix erroneous count of cache misses
rhettinger Jan 25, 2019
14281bb
Add more comments on reentrancy issues
rhettinger Jan 25, 2019
3b04baa
Guard against exotic cases until they can be proven not to occur
rhettinger Jan 25, 2019
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
11 changes: 7 additions & 4 deletions Lib/functools.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,7 @@ def __hash__(self):

def _make_key(args, kwds, typed,
kwd_mark = (object(),),
fasttypes = {int, str, frozenset, type(None)},
fasttypes = {int, str},
tuple=tuple, type=type, len=len):
"""Make a cache key from optionally typed positional and keyword arguments

Expand Down Expand Up @@ -510,8 +510,11 @@ def lru_cache(maxsize=128, typed=False):

# Early detection of an erroneous call to @lru_cache without any arguments
# resulting in the inner function being passed to maxsize instead of an
# integer or None.
if maxsize is not None and not isinstance(maxsize, int):
# integer or None. Negative maxsize is treated as 0.
if isinstance(maxsize, int):
if maxsize < 0:
maxsize = 0
elif maxsize is not None:
raise TypeError('Expected maxsize to be an integer or None')

def decorating_function(user_function):
Expand Down Expand Up @@ -578,6 +581,7 @@ def wrapper(*args, **kwds):
link[NEXT] = root
hits += 1
return result
misses += 1
result = user_function(*args, **kwds)
with lock:
if key in cache:
Expand Down Expand Up @@ -615,7 +619,6 @@ def wrapper(*args, **kwds):
# Use the cache_len bound method instead of the len() function
# which could potentially be wrapped in an lru_cache itself.
full = (cache_len() >= maxsize)
misses += 1
return result

def cache_info():
Expand Down
29 changes: 28 additions & 1 deletion Lib/test/test_functools.py
Original file line number Diff line number Diff line change
Expand Up @@ -1233,6 +1233,33 @@ def f(x):
self.assertEqual(misses, 4)
self.assertEqual(currsize, 2)

def test_lru_bug_35780(self):
# C version of the lru_cache was not checking to see if
# the user function call has already modified the cache
# (this arises in recursive calls and in multi-threading).
# This cause the cache to have orphan links not referenced
# by the cache dictionary.

once = True # Modified by f(x) below

@self.module.lru_cache(maxsize=10)
def f(x):
nonlocal once
rv = f'.{x}.'
if x == 20 and once:
once = False
rv = f(x)
return rv

# Fill the cache
for x in range(15):
self.assertEqual(f(x), f'.{x}.')
self.assertEqual(f.cache_info().currsize, 10)

# Make a recursive call and make sure the cache remains full
self.assertEqual(f(20), '.20.')
self.assertEqual(f.cache_info().currsize, 10)

def test_lru_hash_only_once(self):
# To protect against weird reentrancy bugs and to improve
# efficiency when faced with slow __hash__ methods, the
Expand Down Expand Up @@ -1329,7 +1356,7 @@ def eq(n):
for i in (0, 1):
self.assertEqual([eq(n) for n in range(150)], list(range(150)))
self.assertEqual(eq.cache_info(),
self.module._CacheInfo(hits=0, misses=300, maxsize=-10, currsize=1))
self.module._CacheInfo(hits=0, misses=300, maxsize=0, currsize=0))

def test_lru_with_exceptions(self):
# Verify that user_function exceptions get passed through without
Expand Down
11 changes: 11 additions & 0 deletions Misc/NEWS.d/next/Library/2019-01-19-17-01-43.bpo-35780.CLf7fT.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Fix lru_cache() errors arising in recursive, reentrant, or
multi-threaded code. These errors could result in orphan links and in
the cache being trapped in a state with fewer than the specified maximum
number of links. Fix handling of negative maxsize which should have
been treated as zero. Fix errors in toggling the "full" status flag.
Fix misordering of links when errors are encountered. Sync-up the C
code and pure Python code for the space saving path in functions with a
single positional argument. In this common case, the space overhead of
an lru cache entry is reduced by almost half. Fix counting of cache
misses. In error cases, the miss count was out of sync with the actual
number of times the underlying user function was called.
Loading