Skip to content

Commit 082aa06

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 7b049a3 + a02df97 commit 082aa06

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
@@ -117,6 +117,7 @@ PyAPI_FUNC(int) _PyDict_HasOnlyStringKeys(PyObject *mp);
117117
Py_ssize_t _PyDict_KeysSize(PyDictKeysObject *keys);
118118
Py_ssize_t _PyDict_SizeOf(PyDictObject *);
119119
PyAPI_FUNC(PyObject *) _PyDict_Pop(PyObject *, PyObject *, PyObject *);
120+
PyObject *_PyDict_Pop_KnownHash(PyObject *, PyObject *, Py_hash_t, PyObject *);
120121
PyObject *_PyDict_FromKeys(PyObject *, PyObject *, PyObject *);
121122
#define _PyDict_HasSplitTable(d) ((d)->ma_values != NULL)
122123

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
import unittest.mock
1213
from weakref import proxy
@@ -1446,6 +1447,20 @@ def test():
14461447
pause.reset()
14471448
self.assertEqual(f.cache_info(), (0, (i+1)*n, m*n, i+1))
14481449

1450+
@unittest.skipUnless(threading, 'This test requires threading.')
1451+
def test_lru_cache_threaded3(self):
1452+
@self.module.lru_cache(maxsize=2)
1453+
def f(x):
1454+
time.sleep(.01)
1455+
return 3 * x
1456+
def test(i, x):
1457+
with self.subTest(thread=i):
1458+
self.assertEqual(f(x), 3 * x, i)
1459+
threads = [threading.Thread(target=test, args=(i, v))
1460+
for i, v in enumerate([1, 2, 2, 3, 2])]
1461+
with support.start_threads(threads):
1462+
pass
1463+
14491464
def test_need_for_rlock(self):
14501465
# This will deadlock on an LRU cache that uses a regular lock
14511466

Misc/NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,10 @@ Core and Builtins
212212
Library
213213
-------
214214

215+
- Issue #28969: Fixed race condition in C implementation of functools.lru_cache.
216+
KeyError could be raised when cached function with full cache was
217+
simultaneously called from differen threads with the same uncached arguments.
218+
215219
- Issue #20804: The unittest.mock.sentinel attributes now preserve their
216220
identity when they are copied or pickled.
217221

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
@@ -1817,9 +1817,8 @@ PyDict_Next(PyObject *op, Py_ssize_t *ppos, PyObject **pkey, PyObject **pvalue)
18171817

18181818
/* Internal version of dict.pop(). */
18191819
PyObject *
1820-
_PyDict_Pop(PyObject *dict, PyObject *key, PyObject *deflt)
1820+
_PyDict_Pop_KnownHash(PyObject *dict, PyObject *key, Py_hash_t hash, PyObject *deflt)
18211821
{
1822-
Py_hash_t hash;
18231822
Py_ssize_t ix, hashpos;
18241823
PyObject *old_value, *old_key;
18251824
PyDictKeyEntry *ep;
@@ -1836,12 +1835,6 @@ _PyDict_Pop(PyObject *dict, PyObject *key, PyObject *deflt)
18361835
_PyErr_SetKeyError(key);
18371836
return NULL;
18381837
}
1839-
if (!PyUnicode_CheckExact(key) ||
1840-
(hash = ((PyASCIIObject *) key)->hash) == -1) {
1841-
hash = PyObject_Hash(key);
1842-
if (hash == -1)
1843-
return NULL;
1844-
}
18451838
ix = (mp->ma_keys->dk_lookup)(mp, key, hash, &old_value, &hashpos);
18461839
if (ix == DKIX_ERROR)
18471840
return NULL;
@@ -1878,6 +1871,28 @@ _PyDict_Pop(PyObject *dict, PyObject *key, PyObject *deflt)
18781871
return old_value;
18791872
}
18801873

1874+
PyObject *
1875+
_PyDict_Pop(PyObject *dict, PyObject *key, PyObject *deflt)
1876+
{
1877+
Py_hash_t hash;
1878+
1879+
if (((PyDictObject *)dict)->ma_used == 0) {
1880+
if (deflt) {
1881+
Py_INCREF(deflt);
1882+
return deflt;
1883+
}
1884+
_PyErr_SetKeyError(key);
1885+
return NULL;
1886+
}
1887+
if (!PyUnicode_CheckExact(key) ||
1888+
(hash = ((PyASCIIObject *) key)->hash) == -1) {
1889+
hash = PyObject_Hash(key);
1890+
if (hash == -1)
1891+
return NULL;
1892+
}
1893+
return _PyDict_Pop_KnownHash(dict, key, hash, deflt);
1894+
}
1895+
18811896
/* Internal version of dict.from_keys(). It is subclass-friendly. */
18821897
PyObject *
18831898
_PyDict_FromKeys(PyObject *cls, PyObject *iterable, PyObject *value)

0 commit comments

Comments
 (0)