Skip to content
This repository was archived by the owner on Feb 13, 2025. It is now read-only.

Commit 6779652

Browse files
Issue python#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.
1 parent 9b8dcc6 commit 6779652

File tree

5 files changed

+79
-30
lines changed

5 files changed

+79
-30
lines changed

Include/dictobject.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ PyAPI_FUNC(int) _PyDict_HasOnlyStringKeys(PyObject *mp);
102102
Py_ssize_t _PyDict_KeysSize(PyDictKeysObject *keys);
103103
Py_ssize_t _PyDict_SizeOf(PyDictObject *);
104104
PyObject *_PyDict_Pop(PyDictObject *, PyObject *, PyObject *);
105+
PyObject *_PyDict_Pop_KnownHash(PyDictObject *, PyObject *, Py_hash_t, PyObject *);
105106
PyObject *_PyDict_FromKeys(PyObject *, PyObject *, PyObject *);
106107
#define _PyDict_HasSplitTable(d) ((d)->ma_values != NULL)
107108

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
try:
@@ -1364,6 +1365,20 @@ def test():
13641365
pause.reset()
13651366
self.assertEqual(f.cache_info(), (0, (i+1)*n, m*n, i+1))
13661367

1368+
@unittest.skipUnless(threading, 'This test requires threading.')
1369+
def test_lru_cache_threaded3(self):
1370+
@self.module.lru_cache(maxsize=2)
1371+
def f(x):
1372+
time.sleep(.01)
1373+
return 3 * x
1374+
def test(i, x):
1375+
with self.subTest(thread=i):
1376+
self.assertEqual(f(x), 3 * x, i)
1377+
threads = [threading.Thread(target=test, args=(i, v))
1378+
for i, v in enumerate([1, 2, 2, 3, 2])]
1379+
with support.start_threads(threads):
1380+
pass
1381+
13671382
def test_need_for_rlock(self):
13681383
# This will deadlock on an LRU cache that uses a regular lock
13691384

Misc/NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ Core and Builtins
1313
Library
1414
-------
1515

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

Modules/_functoolsmodule.c

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

14761476
/* Internal version of dict.pop(). */
14771477
PyObject *
1478-
_PyDict_Pop(PyDictObject *mp, PyObject *key, PyObject *deflt)
1478+
_PyDict_Pop_KnownHash(PyDictObject *mp, PyObject *key, Py_hash_t hash, PyObject *deflt)
14791479
{
1480-
Py_hash_t hash;
14811480
PyObject *old_value, *old_key;
14821481
PyDictKeyEntry *ep;
14831482
PyObject **value_addr;
@@ -1490,12 +1489,6 @@ _PyDict_Pop(PyDictObject *mp, PyObject *key, PyObject *deflt)
14901489
_PyErr_SetKeyError(key);
14911490
return NULL;
14921491
}
1493-
if (!PyUnicode_CheckExact(key) ||
1494-
(hash = ((PyASCIIObject *) key)->hash) == -1) {
1495-
hash = PyObject_Hash(key);
1496-
if (hash == -1)
1497-
return NULL;
1498-
}
14991492
ep = (mp->ma_keys->dk_lookup)(mp, key, hash, &value_addr);
15001493
if (ep == NULL)
15011494
return NULL;
@@ -1520,6 +1513,28 @@ _PyDict_Pop(PyDictObject *mp, PyObject *key, PyObject *deflt)
15201513
return old_value;
15211514
}
15221515

1516+
PyObject *
1517+
_PyDict_Pop(PyDictObject *mp, PyObject *key, PyObject *deflt)
1518+
{
1519+
Py_hash_t hash;
1520+
1521+
if (mp->ma_used == 0) {
1522+
if (deflt) {
1523+
Py_INCREF(deflt);
1524+
return deflt;
1525+
}
1526+
_PyErr_SetKeyError(key);
1527+
return NULL;
1528+
}
1529+
if (!PyUnicode_CheckExact(key) ||
1530+
(hash = ((PyASCIIObject *) key)->hash) == -1) {
1531+
hash = PyObject_Hash(key);
1532+
if (hash == -1)
1533+
return NULL;
1534+
}
1535+
return _PyDict_Pop_KnownHash(mp, key, hash, deflt);
1536+
}
1537+
15231538
/* Internal version of dict.from_keys(). It is subclass-friendly. */
15241539
PyObject *
15251540
_PyDict_FromKeys(PyObject *cls, PyObject *iterable, PyObject *value)

0 commit comments

Comments
 (0)