https://github.com/python/cpython/commit/b12e0b9a185b0cc776c7e1cd219fc32a16146716
commit: b12e0b9a185b0cc776c7e1cd219fc32a16146716
branch: 3.14
author: Miss Islington (bot) <[email protected]>
committer: StanFromIreland <[email protected]>
date: 2026-11-06T17:31:55Z
summary:

[3.14] gh-142782: Compare `zoneinfo` strong cache keys by string contents 
(GH-161463) (#161506)

(cherry picked from commit 0e885d9b53038cc2513baed0d179d6246ba6e2c9)

Co-authored-by: tonghuaroot (童话) <[email protected]>
Co-authored-by: Stan Ulbrych <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-11-06-00-00-00.gh-issue-142782.qU7nE4.rst
M Lib/test/test_zoneinfo/test_zoneinfo.py
M Modules/_zoneinfo.c

diff --git a/Lib/test/test_zoneinfo/test_zoneinfo.py 
b/Lib/test/test_zoneinfo/test_zoneinfo.py
index 128224c0d929c3e..92b8e529db13a2d 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 b488d6dacef1b0b..a5c8c182dc68fac 100644
--- a/Modules/_zoneinfo.c
+++ b/Modules/_zoneinfo.c
@@ -2435,11 +2435,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;
         }
 

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to