diff --git a/Lib/test/test_zoneinfo/test_zoneinfo.py b/Lib/test/test_zoneinfo/test_zoneinfo.py index d4b3cd36e6d3293..6656d7c1dc2cd67 100644 --- a/Lib/test/test_zoneinfo/test_zoneinfo.py +++ b/Lib/test/test_zoneinfo/test_zoneinfo.py @@ -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): @@ -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): diff --git a/Misc/NEWS.d/next/Library/2026-11-06-00-00-00.gh-issue-142782.qU7nE4.rst b/Misc/NEWS.d/next/Library/2026-11-06-00-00-00.gh-issue-142782.qU7nE4.rst new file mode 100644 index 000000000000000..4bccc120ab8d33f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-11-06-00-00-00.gh-issue-142782.qU7nE4.rst @@ -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. diff --git a/Modules/_zoneinfo.c b/Modules/_zoneinfo.c index 45f65b9d30b6374..3a87440c63e8d24 100644 --- a/Modules/_zoneinfo.c +++ b/Modules/_zoneinfo.c @@ -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; }