Skip to content
Open
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
22 changes: 22 additions & 0 deletions Lib/test/test_zoneinfo/test_zoneinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
28 changes: 20 additions & 8 deletions Modules/_zoneinfo.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 *
Expand Down
Loading