Skip to content

Commit bd39dc7

Browse files
authored
SNOW-698526: Python connector 2.8.2 infinite loop in OCSP cache validation (#1348)
1 parent 290d4cb commit bd39dc7

File tree

5 files changed

+34
-4
lines changed

5 files changed

+34
-4
lines changed

DESCRIPTION.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Source code is also available at: https://github.com/snowflakedb/snowflake-conne
1111
- v2.8.3(Unreleased)
1212
- Bumped cryptography dependency from <39.0.0 to <41.0.0
1313
- Fixed a bug where the permission of the file downloaded via GET command is changed
14+
- Fixed a bug where expired OCSP response cache caused infinite recursion during cache loading
1415

1516
- v2.8.2(November 18,2022)
1617

src/snowflake/connector/cache.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ def __init__(
5858
self._cache: dict[K, CacheEntry[V]] = {}
5959
self._lock = Lock()
6060
self._reset_telemetry()
61+
# aliasing _getitem to unify the api with SFDictFileCache
62+
self._getitem_non_locking = self._getitem
6163

6264
def __len__(self) -> int:
6365
with self._lock:
@@ -403,16 +405,20 @@ def __init__(
403405
if os.path.exists(self.file_path):
404406
self._load()
405407

406-
def _getitem(
408+
def _getitem_non_locking(
407409
self,
408410
k: K,
409411
*,
410412
should_record_hits: bool = True,
411413
) -> V:
412-
"""Non-locking version of __getitem__.
414+
"""Non-locking version of __getitem__ of SFDictFileCache.
413415
414416
This should only be used by internal functions when already
415417
holding self._lock.
418+
419+
Note that we do not overwrite _getitem because _getitem is used by
420+
self._load to clear in-memory expired caches. Overwriting would cause
421+
infinite recursive call.
416422
"""
417423
if k not in self._cache:
418424
loaded = self._load_if_should()

src/snowflake/connector/ocsp_snowflake.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -708,7 +708,7 @@ def find_cache(
708708
ocsp_response_validation_result = (
709709
OCSP_RESPONSE_VALIDATION_CACHE[cache_key]
710710
if lock_cache
711-
else OCSP_RESPONSE_VALIDATION_CACHE._getitem(cache_key)
711+
else OCSP_RESPONSE_VALIDATION_CACHE._getitem_non_locking(cache_key)
712712
)
713713
try:
714714
# is_valid_time can raise exception if the cache

src/snowflake/connector/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# Update this for the versions
22
# Don't change the forth version number from None
3-
VERSION = (2, 8, 2, None)
3+
VERSION = (2, 8, 3, None)

test/unit/test_cache.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ def test_get(self):
104104
c._entry_lifetime = NO_LIFETIME
105105
c["d"] = 4
106106
assert c.get("d") is None
107+
assert c._getitem_non_locking(1) == "a"
107108

108109
def test_items(self):
109110
c = cache.SFDictCache()
@@ -405,3 +406,25 @@ def test_clear(self, tmpdir):
405406
os.unlink(cache_path)
406407
c["a"] = 1
407408
assert os.path.exists(cache_path)
409+
410+
def test_load_with_expired_entries(self, tmpdir):
411+
# Test: https://snowflakecomputing.atlassian.net/browse/SNOW-698526
412+
413+
# create cache first
414+
cache_path = os.path.join(tmpdir, "cache.txt")
415+
c1 = cache.SFDictFileCache(file_path=cache_path)
416+
c1["a"] = 1
417+
c1._save()
418+
419+
# load cache
420+
c2 = cache.SFDictFileCache(
421+
file_path=cache_path, entry_lifetime=int(NO_LIFETIME.total_seconds())
422+
)
423+
c2["b"] = 2
424+
c2["c"] = 3
425+
# cache will expire immediately due to the NO_LIFETIME setting
426+
# when loading again, clearing cache logic will be triggered
427+
# load will trigger clear expired entries, and then further call _getitem
428+
c2._load()
429+
430+
assert len(c2) == 1 and c2["a"] == 1 and c2._getitem_non_locking("a") == 1

0 commit comments

Comments
 (0)