Skip to content

Commit 083d9d0

Browse files
committed
Fix for Ctrl-C hang on Python 3.14 (leaked cache locks)
1 parent a01582b commit 083d9d0

3 files changed

Lines changed: 46 additions & 6 deletions

File tree

‎lib/core/datatype.py‎

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,40 @@
99
import threading
1010

1111
from collections import OrderedDict
12+
from thirdparty import six
1213
from thirdparty.six.moves import collections_abc as _collections
1314

15+
class NoLock(object):
16+
"""
17+
No-op stand-in for a lock, used where the guarded operations are already atomic on their own
18+
19+
>>> with NoLock():
20+
... True
21+
True
22+
"""
23+
24+
def __enter__(self):
25+
return self
26+
27+
def __exit__(self, *args):
28+
return False
29+
30+
def cacheLock(reentrant=False):
31+
"""
32+
Returns a lock for guarding the (OrderedDict based) caches. Only Python 2 needs a real one, as its
33+
OrderedDict is implemented in Python (#5619), while on Python 3 each operation is atomic. Moreover,
34+
since Python 3.14 a Ctrl-C landing right after a lock got acquired by the 'with' statement leaves it
35+
acquired forever (i.e. '__exit__' never gets called), hanging every thread that touches the cache
36+
37+
>>> isinstance(cacheLock(), NoLock) == six.PY3
38+
True
39+
"""
40+
41+
if six.PY2:
42+
return threading.RLock() if reentrant else threading.Lock()
43+
else:
44+
return NoLock()
45+
1446
class AttribDict(dict):
1547
"""
1648
This class defines the dictionary with added capability to access members as attributes
@@ -133,7 +165,7 @@ class LRUDict(object):
133165
def __init__(self, capacity):
134166
self.capacity = capacity
135167
self.cache = OrderedDict()
136-
self.__lock = threading.Lock()
168+
self.__lock = cacheLock()
137169

138170
def __len__(self):
139171
return len(self.cache)
@@ -159,7 +191,10 @@ def __setitem__(self, key, value):
159191
self.cache.pop(key)
160192
except KeyError:
161193
if len(self.cache) >= self.capacity:
162-
self.cache.popitem(last=False)
194+
try:
195+
self.cache.popitem(last=False)
196+
except KeyError: # Note: concurrently emptied by another thread (unlocked on Python 3)
197+
pass
163198
self.cache[key] = value
164199

165200
def set(self, key, value):

‎lib/core/decorators.py‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import threading
1010

1111
from lib.core.datatype import LRUDict
12+
from lib.core.datatype import cacheLock
1213
from lib.core.settings import MAX_CACHE_ITEMS
1314
from lib.core.threads import getCurrentThreadData
1415

@@ -40,7 +41,7 @@ def cachedmethod(f):
4041
"""
4142

4243
_cache[f] = LRUDict(capacity=MAX_CACHE_ITEMS)
43-
_method_locks[f] = threading.RLock()
44+
_method_locks[f] = cacheLock(reentrant=True)
4445

4546
def _freeze(val):
4647
if isinstance(val, (list, set, tuple)):
@@ -60,8 +61,10 @@ def _f(*args, **kwargs):
6061
key = args
6162

6263
with lock:
63-
if key in cache:
64+
try:
6465
return cache[key]
66+
except KeyError: # Note: single lookup, as an entry can get evicted in between (unlocked on Python 3)
67+
pass
6568

6669
except TypeError:
6770
# Note: fallback (slow-path) for unhashable arguments
@@ -72,8 +75,10 @@ def _f(*args, **kwargs):
7275
key = (_FROZEN_KEY_MARKER, _freeze(args))
7376

7477
with lock:
75-
if key in cache:
78+
try:
7679
return cache[key]
80+
except KeyError:
81+
pass
7782
except TypeError:
7883
# Note: genuinely uncacheable arguments; skip caching altogether
7984
return f(*args, **kwargs)

‎lib/core/settings.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from thirdparty import six
2020

2121
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
22-
VERSION = "1.10.10.24"
22+
VERSION = "1.10.10.25"
2323
TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
2424
TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34}
2525
VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE)

0 commit comments

Comments
 (0)