Commit 67796521 authored by Serhiy Storchaka's avatar Serhiy Storchaka

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.
parent 9b8dcc6b
...@@ -102,6 +102,7 @@ PyAPI_FUNC(int) _PyDict_HasOnlyStringKeys(PyObject *mp); ...@@ -102,6 +102,7 @@ PyAPI_FUNC(int) _PyDict_HasOnlyStringKeys(PyObject *mp);
Py_ssize_t _PyDict_KeysSize(PyDictKeysObject *keys); Py_ssize_t _PyDict_KeysSize(PyDictKeysObject *keys);
Py_ssize_t _PyDict_SizeOf(PyDictObject *); Py_ssize_t _PyDict_SizeOf(PyDictObject *);
PyObject *_PyDict_Pop(PyDictObject *, PyObject *, PyObject *); PyObject *_PyDict_Pop(PyDictObject *, PyObject *, PyObject *);
PyObject *_PyDict_Pop_KnownHash(PyDictObject *, PyObject *, Py_hash_t, PyObject *);
PyObject *_PyDict_FromKeys(PyObject *, PyObject *, PyObject *); PyObject *_PyDict_FromKeys(PyObject *, PyObject *, PyObject *);
#define _PyDict_HasSplitTable(d) ((d)->ma_values != NULL) #define _PyDict_HasSplitTable(d) ((d)->ma_values != NULL)
......
...@@ -7,6 +7,7 @@ import pickle ...@@ -7,6 +7,7 @@ import pickle
from random import choice from random import choice
import sys import sys
from test import support from test import support
import time
import unittest import unittest
from weakref import proxy from weakref import proxy
try: try:
...@@ -1364,6 +1365,20 @@ class TestLRU: ...@@ -1364,6 +1365,20 @@ class TestLRU:
pause.reset() pause.reset()
self.assertEqual(f.cache_info(), (0, (i+1)*n, m*n, i+1)) self.assertEqual(f.cache_info(), (0, (i+1)*n, m*n, i+1))
@unittest.skipUnless(threading, 'This test requires threading.')
def test_lru_cache_threaded3(self):
@self.module.lru_cache(maxsize=2)
def f(x):
time.sleep(.01)
return 3 * x
def test(i, x):
with self.subTest(thread=i):
self.assertEqual(f(x), 3 * x, i)
threads = [threading.Thread(target=test, args=(i, v))
for i, v in enumerate([1, 2, 2, 3, 2])]
with support.start_threads(threads):
pass
def test_need_for_rlock(self): def test_need_for_rlock(self):
# This will deadlock on an LRU cache that uses a regular lock # This will deadlock on an LRU cache that uses a regular lock
......
...@@ -13,6 +13,10 @@ Core and Builtins ...@@ -13,6 +13,10 @@ Core and Builtins
Library Library
------- -------
- 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.
- Issue #29142: In urllib.request, suffixes in no_proxy environment variable with - Issue #29142: In urllib.request, suffixes in no_proxy environment variable with
leading dots could match related hostnames again (e.g. .b.c matches a.b.c). leading dots could match related hostnames again (e.g. .b.c matches a.b.c).
Patch by Milan Oberkirch. Patch by Milan Oberkirch.
......
...@@ -864,20 +864,33 @@ bounded_lru_cache_wrapper(lru_cache_object *self, PyObject *args, PyObject *kwds ...@@ -864,20 +864,33 @@ bounded_lru_cache_wrapper(lru_cache_object *self, PyObject *args, PyObject *kwds
} }
if (self->full && self->root.next != &self->root) { if (self->full && self->root.next != &self->root) {
/* Use the oldest item to store the new key and result. */ /* Use the oldest item to store the new key and result. */
PyObject *oldkey, *oldresult; PyObject *oldkey, *oldresult, *popresult;
/* Extricate the oldest item. */ /* Extricate the oldest item. */
link = self->root.next; link = self->root.next;
lru_cache_extricate_link(link); lru_cache_extricate_link(link);
/* Remove it from the cache. /* Remove it from the cache.
The cache dict holds one reference to the link, The cache dict holds one reference to the link,
and the linked list holds yet one reference to it. */ and the linked list holds yet one reference to it. */
if (_PyDict_DelItem_KnownHash(self->cache, link->key, popresult = _PyDict_Pop_KnownHash((PyDictObject *)self->cache,
link->hash) < 0) { link->key, link->hash,
Py_None);
if (popresult == Py_None) {
/* Getting here means that this same key was added to the
cache while the lock was released. Since the link
update is already done, we need only return the
computed result and update the count of misses. */
Py_DECREF(popresult);
Py_DECREF(link);
Py_DECREF(key);
}
else if (popresult == NULL) {
lru_cache_append_link(self, link); lru_cache_append_link(self, link);
Py_DECREF(key); Py_DECREF(key);
Py_DECREF(result); Py_DECREF(result);
return NULL; return NULL;
} }
else {
Py_DECREF(popresult);
/* Keep a reference to the old key and old result to /* Keep a reference to the old key and old result to
prevent their ref counts from going to zero during the prevent their ref counts from going to zero during the
update. That will prevent potentially arbitrary object update. That will prevent potentially arbitrary object
...@@ -900,6 +913,7 @@ bounded_lru_cache_wrapper(lru_cache_object *self, PyObject *args, PyObject *kwds ...@@ -900,6 +913,7 @@ bounded_lru_cache_wrapper(lru_cache_object *self, PyObject *args, PyObject *kwds
Py_INCREF(result); /* for return */ Py_INCREF(result); /* for return */
Py_DECREF(oldkey); Py_DECREF(oldkey);
Py_DECREF(oldresult); Py_DECREF(oldresult);
}
} else { } else {
/* Put result in a new link at the front of the queue. */ /* Put result in a new link at the front of the queue. */
link = (lru_list_elem *)PyObject_GC_New(lru_list_elem, link = (lru_list_elem *)PyObject_GC_New(lru_list_elem,
......
...@@ -1475,9 +1475,8 @@ _PyDict_Next(PyObject *op, Py_ssize_t *ppos, PyObject **pkey, ...@@ -1475,9 +1475,8 @@ _PyDict_Next(PyObject *op, Py_ssize_t *ppos, PyObject **pkey,
/* Internal version of dict.pop(). */ /* Internal version of dict.pop(). */
PyObject * PyObject *
_PyDict_Pop(PyDictObject *mp, PyObject *key, PyObject *deflt) _PyDict_Pop_KnownHash(PyDictObject *mp, PyObject *key, Py_hash_t hash, PyObject *deflt)
{ {
Py_hash_t hash;
PyObject *old_value, *old_key; PyObject *old_value, *old_key;
PyDictKeyEntry *ep; PyDictKeyEntry *ep;
PyObject **value_addr; PyObject **value_addr;
...@@ -1490,12 +1489,6 @@ _PyDict_Pop(PyDictObject *mp, PyObject *key, PyObject *deflt) ...@@ -1490,12 +1489,6 @@ _PyDict_Pop(PyDictObject *mp, PyObject *key, PyObject *deflt)
_PyErr_SetKeyError(key); _PyErr_SetKeyError(key);
return NULL; return NULL;
} }
if (!PyUnicode_CheckExact(key) ||
(hash = ((PyASCIIObject *) key)->hash) == -1) {
hash = PyObject_Hash(key);
if (hash == -1)
return NULL;
}
ep = (mp->ma_keys->dk_lookup)(mp, key, hash, &value_addr); ep = (mp->ma_keys->dk_lookup)(mp, key, hash, &value_addr);
if (ep == NULL) if (ep == NULL)
return NULL; return NULL;
...@@ -1520,6 +1513,28 @@ _PyDict_Pop(PyDictObject *mp, PyObject *key, PyObject *deflt) ...@@ -1520,6 +1513,28 @@ _PyDict_Pop(PyDictObject *mp, PyObject *key, PyObject *deflt)
return old_value; return old_value;
} }
PyObject *
_PyDict_Pop(PyDictObject *mp, PyObject *key, PyObject *deflt)
{
Py_hash_t hash;
if (mp->ma_used == 0) {
if (deflt) {
Py_INCREF(deflt);
return deflt;
}
_PyErr_SetKeyError(key);
return NULL;
}
if (!PyUnicode_CheckExact(key) ||
(hash = ((PyASCIIObject *) key)->hash) == -1) {
hash = PyObject_Hash(key);
if (hash == -1)
return NULL;
}
return _PyDict_Pop_KnownHash(mp, key, hash, deflt);
}
/* Internal version of dict.from_keys(). It is subclass-friendly. */ /* Internal version of dict.from_keys(). It is subclass-friendly. */
PyObject * PyObject *
_PyDict_FromKeys(PyObject *cls, PyObject *iterable, PyObject *value) _PyDict_FromKeys(PyObject *cls, PyObject *iterable, PyObject *value)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment