Fix for Ctrl-C hang on Python 3.14 (leaked cache locks)

This commit is contained in:
Miroslav Štampar
2026-10-27 13:11:04 +01:00
parent a01582b83f
commit 083d9d0e6f
3 changed files with 46 additions and 6 deletions
+37 -2
View File
@@ -9,8 +9,40 @@ import copy
import threading
from collections import OrderedDict
from thirdparty import six
from thirdparty.six.moves import collections_abc as _collections
class NoLock(object):
"""
No-op stand-in for a lock, used where the guarded operations are already atomic on their own
>>> with NoLock():
... True
True
"""
def __enter__(self):
return self
def __exit__(self, *args):
return False
def cacheLock(reentrant=False):
"""
Returns a lock for guarding the (OrderedDict based) caches. Only Python 2 needs a real one, as its
OrderedDict is implemented in Python (#5619), while on Python 3 each operation is atomic. Moreover,
since Python 3.14 a Ctrl-C landing right after a lock got acquired by the 'with' statement leaves it
acquired forever (i.e. '__exit__' never gets called), hanging every thread that touches the cache
>>> isinstance(cacheLock(), NoLock) == six.PY3
True
"""
if six.PY2:
return threading.RLock() if reentrant else threading.Lock()
else:
return NoLock()
class AttribDict(dict):
"""
This class defines the dictionary with added capability to access members as attributes
@@ -133,7 +165,7 @@ class LRUDict(object):
def __init__(self, capacity):
self.capacity = capacity
self.cache = OrderedDict()
self.__lock = threading.Lock()
self.__lock = cacheLock()
def __len__(self):
return len(self.cache)
@@ -159,7 +191,10 @@ class LRUDict(object):
self.cache.pop(key)
except KeyError:
if len(self.cache) >= self.capacity:
self.cache.popitem(last=False)
try:
self.cache.popitem(last=False)
except KeyError: # Note: concurrently emptied by another thread (unlocked on Python 3)
pass
self.cache[key] = value
def set(self, key, value):
+8 -3
View File
@@ -9,6 +9,7 @@ import functools
import threading
from lib.core.datatype import LRUDict
from lib.core.datatype import cacheLock
from lib.core.settings import MAX_CACHE_ITEMS
from lib.core.threads import getCurrentThreadData
@@ -40,7 +41,7 @@ def cachedmethod(f):
"""
_cache[f] = LRUDict(capacity=MAX_CACHE_ITEMS)
_method_locks[f] = threading.RLock()
_method_locks[f] = cacheLock(reentrant=True)
def _freeze(val):
if isinstance(val, (list, set, tuple)):
@@ -60,8 +61,10 @@ def cachedmethod(f):
key = args
with lock:
if key in cache:
try:
return cache[key]
except KeyError: # Note: single lookup, as an entry can get evicted in between (unlocked on Python 3)
pass
except TypeError:
# Note: fallback (slow-path) for unhashable arguments
@@ -72,8 +75,10 @@ def cachedmethod(f):
key = (_FROZEN_KEY_MARKER, _freeze(args))
with lock:
if key in cache:
try:
return cache[key]
except KeyError:
pass
except TypeError:
# Note: genuinely uncacheable arguments; skip caching altogether
return f(*args, **kwargs)
+1 -1
View File
@@ -19,7 +19,7 @@ from lib.core.enums import OS
from thirdparty import six
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
VERSION = "1.10.10.24"
VERSION = "1.10.10.25"
TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34}
VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE)