diff --git a/Lib/test/test_zoneinfo/test_zoneinfo.py b/Lib/test/test_zoneinfo/test_zoneinfo.py index 41ba92342ff2cbd..659af400e71994e 100644 --- a/Lib/test/test_zoneinfo/test_zoneinfo.py +++ b/Lib/test/test_zoneinfo/test_zoneinfo.py @@ -1664,6 +1664,28 @@ class EvilZoneInfo(self.klass): self.assertEqual(str(zone2), key) self.assertIsNot(zone2, zone1) + def test_strong_cache_reentrant_clear(self): + # gh-142782: a key whose __eq__ clears the cache during a strong-cache + # lookup must not cause a use-after-free. + klass = self.klass + + class Stringy(str): + def __new__(cls, value): + self = super().__new__(cls, value) + self.cleared = False + return self + def __eq__(self, other): + if not self.cleared: + self.cleared = True + klass.clear_cache(only_keys=(self,)) + return super().__eq__(other) + __hash__ = str.__hash__ + + zone1 = klass(Stringy("America/Los_Angeles")) + zone2 = klass(Stringy("America/Los_Angeles")) + self.assertEqual(str(zone1), "America/Los_Angeles") + self.assertEqual(str(zone2), "America/Los_Angeles") + class CZoneInfoCacheTest(ZoneInfoCacheTest): module = c_zoneinfo diff --git a/Misc/NEWS.d/next/Library/2026-10-15-09-30-00.gh-issue-142782.z1Uaf7.rst b/Misc/NEWS.d/next/Library/2026-10-15-09-30-00.gh-issue-142782.z1Uaf7.rst new file mode 100644 index 000000000000000..d68f9f5a5b8903b --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-15-09-30-00.gh-issue-142782.z1Uaf7.rst @@ -0,0 +1,2 @@ +Fix a use-after-free in :class:`zoneinfo.ZoneInfo` when a key's ``__eq__`` +clears the cache during a strong-cache lookup. diff --git a/Modules/_zoneinfo.c b/Modules/_zoneinfo.c index 46ab3499e003542..eb59090e584258e 100644 --- a/Modules/_zoneinfo.c +++ b/Modules/_zoneinfo.c @@ -103,6 +103,8 @@ typedef struct { PyObject *TIMEDELTA_CACHE; PyObject *ZONEINFO_WEAK_CACHE; StrongCacheNode *ZONEINFO_STRONG_CACHE; + // Bumped on every strong-cache mutation (gh-142782). + size_t strong_cache_version; _ttinfo NO_TTINFO; } zoneinfo_state; @@ -2434,19 +2436,28 @@ remove_from_strong_cache(zoneinfo_state *state, StrongCacheNode *node) * root may be NULL, indicating an empty cache. */ static StrongCacheNode * -find_in_strong_cache(const StrongCacheNode *const root, PyObject *const key) +find_in_strong_cache(zoneinfo_state *state, PyObject *const key) { - const StrongCacheNode *node = root; + // A key's __eq__ can re-enter and clear the cache during the comparison, + // freeing nodes; treat such a mutation as a miss (gh-142782). + size_t version = state->strong_cache_version; + const StrongCacheNode *node = state->ZONEINFO_STRONG_CACHE; while (node != NULL) { - int rv = PyObject_RichCompareBool(key, node->key, Py_EQ); + PyObject *node_key = Py_NewRef(node->key); + const StrongCacheNode *next = node->next; + int rv = PyObject_RichCompareBool(key, node_key, Py_EQ); + Py_DECREF(node_key); if (rv < 0) { return NULL; } + if (state->strong_cache_version != version) { + return NULL; + } if (rv) { return (StrongCacheNode *)node; } - node = node->next; + node = next; } return NULL; @@ -2465,12 +2476,12 @@ eject_from_strong_cache(zoneinfo_state *state, const PyTypeObject *const type, } _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(state->ZoneInfoType); - StrongCacheNode *cache = state->ZONEINFO_STRONG_CACHE; - StrongCacheNode *node = find_in_strong_cache(cache, key); + StrongCacheNode *node = find_in_strong_cache(state, key); if (node != NULL) { remove_from_strong_cache(state, node); strong_cache_node_free(node); + state->strong_cache_version++; } else if (PyErr_Occurred()) { return -1; @@ -2522,8 +2533,7 @@ zone_from_strong_cache(zoneinfo_state *state, const PyTypeObject *const type, } _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(state->ZoneInfoType); - StrongCacheNode *cache = state->ZONEINFO_STRONG_CACHE; - StrongCacheNode *node = find_in_strong_cache(cache, key); + StrongCacheNode *node = find_in_strong_cache(state, key); if (node != NULL) { StrongCacheNode **root = &(state->ZONEINFO_STRONG_CACHE); @@ -2555,6 +2565,7 @@ update_strong_cache(zoneinfo_state *state, const PyTypeObject *const type, } StrongCacheNode **root = &(state->ZONEINFO_STRONG_CACHE); move_strong_cache_node_to_front(state, root, new_node); + state->strong_cache_version++; StrongCacheNode *node = new_node->next; for (size_t i = 1; i < ZONEINFO_STRONG_CACHE_MAX_SIZE; ++i) { @@ -2587,6 +2598,7 @@ clear_strong_cache(zoneinfo_state *state, const PyTypeObject *const type) strong_cache_free(state->ZONEINFO_STRONG_CACHE); state->ZONEINFO_STRONG_CACHE = NULL; + state->strong_cache_version++; } static PyObject *