Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions Lib/test/test_zoneinfo/test_zoneinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -1723,6 +1723,13 @@ def __eq__(self, other):
except CustomError:
pass

def test_clear_cache_non_string_keys(self):
la0 = self.klass("America/Los_Angeles")
self.klass.clear_cache(only_keys=[None, 1])
la1 = self.klass("America/Los_Angeles")

self.assertIs(la0, la1)

def test_weak_cache_descriptor_use_after_free(self):
class BombDescriptor:
def __get__(self, obj, owner):
Expand All @@ -1747,6 +1754,70 @@ class EvilZoneInfo(self.klass):
class CZoneInfoCacheTest(ZoneInfoCacheTest):
module = c_zoneinfo

def test_strong_cache_path_like_key(self):
# A non-str key is matched by identity, and comparing it against a
# str key must not raise TypeError.
class Key:
def __fspath__(self):
return "UTC"

def __len__(self):
return 3

def split(self, sep):
return ["UTC"]

key = Key()
original = self.klass(key)
self.assertIs(self.klass(key), original)
self.assertIsNot(self.klass("UTC"), original)
self.klass.clear_cache(only_keys=(key,))
self.assertIsNot(self.klass(key), original)

def test_strong_cache_str_subclass(self):
class Stringy(str):
def __eq__(self, other):
raise CustomError

__hash__ = str.__hash__

for cached_type, lookup_type in ((str, Stringy), (Stringy, str),
(Stringy, Stringy)):
with self.subTest(cached_type=cached_type, lookup_type=lookup_type):
self.klass.clear_cache()
original = self.klass(cached_type("America/Los_Angeles"))
self.assertIs(
self.klass(lookup_type("America/Los_Angeles")), original
)

def test_strong_cache_reentrant_clear(self):
self._test_strong_cache_reentrant_clear("lookup")

def test_strong_cache_reentrant_clear_only_keys(self):
self._test_strong_cache_reentrant_clear("clear")

def _test_strong_cache_reentrant_clear(self, operation):
klass = self.klass

class Stringy(str):
cleared = False

def __eq__(self, other):
if not self.cleared:
self.cleared = True
klass.clear_cache(only_keys=(self,))
return super().__eq__(other)

__hash__ = str.__hash__

original = klass(Stringy("America/Los_Angeles"))
key = Stringy("America/Los_Angeles")
if operation == "lookup":
self.assertIs(klass(key), original)
else:
klass.clear_cache(only_keys=(key,))
self.assertIsNot(klass("America/Los_Angeles"), original)

def test_inconsistent_weak_cache_get(self):
class Cache:
def get(self, key, default=None):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix a use-after-free in :class:`zoneinfo.ZoneInfo` when a key's ``__eq__``
clears the cache during a lookup. Strong cache keys are now compared by
string contents.
8 changes: 3 additions & 5 deletions Modules/_zoneinfo.c
Original file line number Diff line number Diff line change
Expand Up @@ -2438,11 +2438,9 @@ find_in_strong_cache(const StrongCacheNode *const root, PyObject *const key)
{
const StrongCacheNode *node = root;
while (node != NULL) {
int rv = PyObject_RichCompareBool(key, node->key, Py_EQ);
if (rv < 0) {
return NULL;
}
if (rv) {
if (key == node->key ||
(PyUnicode_Check(key) && PyUnicode_Check(node->key) &&
PyUnicode_Equal(key, node->key))) {
return (StrongCacheNode *)node;
}

Expand Down
Loading