Skip to content

Commit 42e1ea9

Browse files
Issue #28969: Fixed race condition in C implementation of functools.lru_cache.
KeyError could be raised when cached function with full cache was simultaneously called from differen threads with the same uncached arguments.
2 parents 12c4aba + 6779652 commit 42e1ea9

5 files changed

Lines changed: 79 additions & 30 deletions

File tree

Include/dictobject.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ PyAPI_FUNC(int) _PyDict_HasOnlyStringKeys(PyObject *mp);
115115
Py_ssize_t _PyDict_KeysSize(PyDictKeysObject *keys);
116116
Py_ssize_t _PyDict_SizeOf(PyDictObject *);
117117
PyAPI_FUNC(PyObject *) _PyDict_Pop(PyObject *, PyObject *, PyObject *);
118+
PyObject *_PyDict_Pop_KnownHash(PyObject *, PyObject *, Py_hash_t, PyObject *);
118119
PyObject *_PyDict_FromKeys(PyObject *, PyObject *, PyObject *);
119120
#define _PyDict_HasSplitTable(d) ((d)->ma_values != NULL)
120121

Lib/test/test_functools.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from random import choice
88
import sys
99
from test import support
10+
import time
1011
import unittest
1112
from weakref import proxy
1213
import contextlib
@@ -1401,6 +1402,20 @@ def test():
14011402
pause.reset()
14021403
self.assertEqual(f.cache_info(), (0, (i+1)*n, m*n, i+1))
14031404

1405+
@unittest.skipUnless(threading, 'This test requires threading.')
1406+
def test_lru_cache_threaded3(self):
1407+
@self.module.lru_cache(maxsize=2)
1408+
def f(x):
1409+
time.sleep(.01)
1410+
return 3 * x
1411+
def test(i, x):
1412+
with self.subTest(thread=i):
1413+
self.assertEqual(f(x), 3 * x, i)
1414+
threads = [threading.Thread(target=test, args=(i, v))
1415+
for i, v in enumerate([1, 2, 2, 3, 2])]
1416+
with support.start_threads(threads):
1417+
pass
1418+
14041419
def test_need_for_rlock(self):
14051420
# This will deadlock on an LRU cache that uses a regular lock
14061421

Misc/NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ Core and Builtins
4444
Library
4545
-------
4646

47+
- Issue #28969: Fixed race condition in C implementation of functools.lru_cache.
48+
KeyError could be raised when cached function with full cache was
49+
simultaneously called from differen threads with the same uncached arguments.
50+
4751
- Issue #29142: In urllib.request, suffixes in no_proxy environment variable with
4852
leading dots could match related hostnames again (e.g. .b.c matches a.b.c).
4953
Patch by Milan Oberkirch.

Modules/_functoolsmodule.c

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -863,42 +863,56 @@ bounded_lru_cache_wrapper(lru_cache_object *self, PyObject *args, PyObject *kwds
863863
}
864864
if (self->full && self->root.next != &self->root) {
865865
/* Use the oldest item to store the new key and result. */
866-
PyObject *oldkey, *oldresult;
866+
PyObject *oldkey, *oldresult, *popresult;
867867
/* Extricate the oldest item. */
868868
link = self->root.next;
869869
lru_cache_extricate_link(link);
870870
/* Remove it from the cache.
871871
The cache dict holds one reference to the link,
872872
and the linked list holds yet one reference to it. */
873-
if (_PyDict_DelItem_KnownHash(self->cache, link->key,
874-
link->hash) < 0) {
873+
popresult = _PyDict_Pop_KnownHash(self->cache,
874+
link->key, link->hash,
875+
Py_None);
876+
if (popresult == Py_None) {
877+
/* Getting here means that this same key was added to the
878+
cache while the lock was released. Since the link
879+
update is already done, we need only return the
880+
computed result and update the count of misses. */
881+
Py_DECREF(popresult);
882+
Py_DECREF(link);
883+
Py_DECREF(key);
884+
}
885+
else if (popresult == NULL) {
875886
lru_cache_append_link(self, link);
876887
Py_DECREF(key);
877888
Py_DECREF(result);
878889
return NULL;
879890
}
880-
/* Keep a reference to the old key and old result to
881-
prevent their ref counts from going to zero during the
882-
update. That will prevent potentially arbitrary object
883-
clean-up code (i.e. __del__) from running while we're
884-
still adjusting the links. */
885-
oldkey = link->key;
886-
oldresult = link->result;
887-
888-
link->hash = hash;
889-
link->key = key;
890-
link->result = result;
891-
if (_PyDict_SetItem_KnownHash(self->cache, key, (PyObject *)link,
892-
hash) < 0) {
893-
Py_DECREF(link);
891+
else {
892+
Py_DECREF(popresult);
893+
/* Keep a reference to the old key and old result to
894+
prevent their ref counts from going to zero during the
895+
update. That will prevent potentially arbitrary object
896+
clean-up code (i.e. __del__) from running while we're
897+
still adjusting the links. */
898+
oldkey = link->key;
899+
oldresult = link->result;
900+
901+
link->hash = hash;
902+
link->key = key;
903+
link->result = result;
904+
if (_PyDict_SetItem_KnownHash(self->cache, key, (PyObject *)link,
905+
hash) < 0) {
906+
Py_DECREF(link);
907+
Py_DECREF(oldkey);
908+
Py_DECREF(oldresult);
909+
return NULL;
910+
}
911+
lru_cache_append_link(self, link);
912+
Py_INCREF(result); /* for return */
894913
Py_DECREF(oldkey);
895914
Py_DECREF(oldresult);
896-
return NULL;
897915
}
898-
lru_cache_append_link(self, link);
899-
Py_INCREF(result); /* for return */
900-
Py_DECREF(oldkey);
901-
Py_DECREF(oldresult);
902916
} else {
903917
/* Put result in a new link at the front of the queue. */
904918
link = (lru_list_elem *)PyObject_GC_New(lru_list_elem,

Objects/dictobject.c

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1829,9 +1829,8 @@ PyDict_Next(PyObject *op, Py_ssize_t *ppos, PyObject **pkey, PyObject **pvalue)
18291829

18301830
/* Internal version of dict.pop(). */
18311831
PyObject *
1832-
_PyDict_Pop(PyObject *dict, PyObject *key, PyObject *deflt)
1832+
_PyDict_Pop_KnownHash(PyObject *dict, PyObject *key, Py_hash_t hash, PyObject *deflt)
18331833
{
1834-
Py_hash_t hash;
18351834
Py_ssize_t ix, hashpos;
18361835
PyObject *old_value, *old_key;
18371836
PyDictKeyEntry *ep;
@@ -1849,12 +1848,6 @@ _PyDict_Pop(PyObject *dict, PyObject *key, PyObject *deflt)
18491848
_PyErr_SetKeyError(key);
18501849
return NULL;
18511850
}
1852-
if (!PyUnicode_CheckExact(key) ||
1853-
(hash = ((PyASCIIObject *) key)->hash) == -1) {
1854-
hash = PyObject_Hash(key);
1855-
if (hash == -1)
1856-
return NULL;
1857-
}
18581851
ix = (mp->ma_keys->dk_lookup)(mp, key, hash, &value_addr, &hashpos);
18591852
if (ix == DKIX_ERROR)
18601853
return NULL;
@@ -1892,6 +1885,28 @@ _PyDict_Pop(PyObject *dict, PyObject *key, PyObject *deflt)
18921885
return old_value;
18931886
}
18941887

1888+
PyObject *
1889+
_PyDict_Pop(PyObject *dict, PyObject *key, PyObject *deflt)
1890+
{
1891+
Py_hash_t hash;
1892+
1893+
if (((PyDictObject *)dict)->ma_used == 0) {
1894+
if (deflt) {
1895+
Py_INCREF(deflt);
1896+
return deflt;
1897+
}
1898+
_PyErr_SetKeyError(key);
1899+
return NULL;
1900+
}
1901+
if (!PyUnicode_CheckExact(key) ||
1902+
(hash = ((PyASCIIObject *) key)->hash) == -1) {
1903+
hash = PyObject_Hash(key);
1904+
if (hash == -1)
1905+
return NULL;
1906+
}
1907+
return _PyDict_Pop_KnownHash(dict, key, hash, deflt);
1908+
}
1909+
18951910
/* Internal version of dict.from_keys(). It is subclass-friendly. */
18961911
PyObject *
18971912
_PyDict_FromKeys(PyObject *cls, PyObject *iterable, PyObject *value)

0 commit comments

Comments
 (0)