Skip to content

Use-after-free in zone_from_strong_cache via re-entrant ZoneInfo.clear_cache() from key __eq__ #142782

@jackfromeast

Description

@jackfromeast

What happened?

The zoneinfo_ZoneInfo_impl() looks up an entry via zone_from_strong_cache, which traverses the strong-cache list and compares keys using PyObject_RichCompareBool(key, node->key, Py_EQ). A crafted str subclass overrides __eq__ to call ZoneInfo.clear_cache(only_keys=(self,)) during the comparison, freeing the currently visited StrongCacheNode. Traversal then proceeds and moves the (now freed) node to the LRU front, triggering a heap use-after-free (ASan crash in remove_from_strong_cache).

Proof of Concept:

from zoneinfo import ZoneInfo

class EvilStr(str):
    def __new__(cls, value: str):
        obj = super().__new__(cls, value)
        obj._triggered = False
        return obj

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

    def __hash__(self):
        return super().__hash__()

key1 = EvilStr("UTC")
key2 = EvilStr("UTC")
ZoneInfo(key1)
ZoneInfo(key2)

Affected Versions:

Python Version Status Exit Code
Python 3.9.24+ (heads/3.9:9c4638d, Oct 17 2025, 11:19:30) ASAN 1
Python 3.10.19+ (heads/3.10:0142619, Oct 17 2025, 11:20:05) [GCC 13.3.0] ASAN 1
Python 3.11.14+ (heads/3.11:88f3f5b, Oct 17 2025, 11:20:44) [GCC 13.3.0] ASAN 1
Python 3.12.12+ (heads/3.12:8cb2092, Oct 17 2025, 11:21:35) [GCC 13.3.0] ASAN 1
Python 3.13.9+ (heads/3.13:0760a57, Oct 17 2025, 11:22:25) [GCC 13.3.0] ASAN 1
Python 3.14.0+ (heads/3.14:889e918, Oct 17 2025, 11:23:02) [GCC 13.3.0] ASAN 1
Python 3.15.0a1+ (heads/main:fbf0843, Oct 17 2025, 11:23:37) [GCC 13.3.0] ASAN 1

Related Code Snippet

static PyObject *
zoneinfo_ZoneInfo_impl(PyTypeObject *type, PyObject *key)
/*[clinic end generated code: output=95e61dab86bb95c3 input=ef73d7a83bf8790e]*/
{
    zoneinfo_state *state = zoneinfo_get_state_by_self(type);
    PyObject *instance = zone_from_strong_cache(state, type, key); // Trigged __eq__ method
    if (instance != NULL || PyErr_Occurred()) {
        return instance;
    }

    PyObject *weak_cache = get_weak_cache(state, type);
    instance = PyObject_CallMethod(weak_cache, "get", "O", key, Py_None);
    if (instance == NULL) {
        return NULL;
    }

    if (instance == Py_None) {
        Py_DECREF(instance);
        PyObject *tmp = zoneinfo_new_instance(state, type, key);
        if (tmp == NULL) {
            return NULL;
        }

        instance =
            PyObject_CallMethod(weak_cache, "setdefault", "OO", key, tmp);
        Py_DECREF(tmp);
        if (instance == NULL) {
            return NULL;
        }
        ((PyZoneInfo_ZoneInfo *)instance)->source = SOURCE_CACHE;
    }

    update_strong_cache(state, type, key, instance);
    return instance;
}

/* Retrieves a ZoneInfo from the strong cache if it's present.
 *
 * This function finds the ZoneInfo by key and if found will move the node to
 * the front of the LRU cache and return a new reference to it. It returns NULL
 * if the key is not in the cache.
 *
 * The strong cache is currently only implemented for the base class, so this
 * always returns a cache miss for subclasses.
 */
static PyObject *
zone_from_strong_cache(zoneinfo_state *state, const PyTypeObject *const type,
                       PyObject *const key)
{
    if (type != state->ZoneInfoType) {
        return NULL;  // Strong cache currently only implemented for base class
    }

    _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(state->ZoneInfoType);
    StrongCacheNode *cache = state->ZONEINFO_STRONG_CACHE;
    StrongCacheNode *node = find_in_strong_cache(cache, key);

    if (node != NULL) {
        StrongCacheNode **root = &(state->ZONEINFO_STRONG_CACHE);
        // Node has been accessed while it has been freed already
        move_strong_cache_node_to_front(state, root, node);
        return Py_NewRef(node->zone);
    }

    return NULL;  // Cache miss
}

/* Retrieves the node associated with a key, if it exists.
 *
 * This traverses the strong cache until it finds a matching key and returns a
 * pointer to the relevant node if found. Returns NULL if no node is found.
 *
 * root may be NULL, indicating an empty cache.
 */
static StrongCacheNode *
find_in_strong_cache(const StrongCacheNode *const root, PyObject *const key)
{
    const StrongCacheNode *node = root;
    while (node != NULL) {
		// Trigged __eq__ method where the strong cache node has been freed due to the clear
        int rv = PyObject_RichCompareBool(key, node->key, Py_EQ); 
        if (rv < 0) {
            return NULL;
        }
        if (rv) {
            return (StrongCacheNode *)node;
        }

        node = node->next;
    }

    return NULL;
}

Sanitizer Report

=================================================================
==1443306==ERROR: AddressSanitizer: heap-use-after-free on address 0x506000079ef8 at pc 0x7108671893f8 bp 0x7fff61acc1b0 sp 0x7fff61acc1a0
READ of size 8 at 0x506000079ef8 thread T0
    #0 0x7108671893f7 in remove_from_strong_cache Modules/_zoneinfo.c:2379
    #1 0x710867189444 in move_strong_cache_node_to_front Modules/_zoneinfo.c:2457
    #2 0x71086718ce56 in zone_from_strong_cache Modules/_zoneinfo.c:2492
    #3 0x71086719149b in zoneinfo_ZoneInfo_impl Modules/_zoneinfo.c:323
    #4 0x710867191710 in zoneinfo_ZoneInfo Modules/clinic/_zoneinfo.c.h:64
    #5 0x5703b3b3f346 in type_call Objects/typeobject.c:2448
    #6 0x5703b3a18c71 in _PyObject_MakeTpCall Objects/call.c:242
    #7 0x5703b3a18f19 in _PyObject_VectorcallTstate Include/internal/pycore_call.h:167
    #8 0x5703b3a18f72 in PyObject_Vectorcall Objects/call.c:327
    #9 0x5703b3c97056 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:1620
    #10 0x5703b3cdae54 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
    #11 0x5703b3cdb148 in _PyEval_Vector Python/ceval.c:2001
    #12 0x5703b3cdb3f8 in PyEval_EvalCode Python/ceval.c:884
    #13 0x5703b3dd2507 in run_eval_code_obj Python/pythonrun.c:1365
    #14 0x5703b3dd2723 in run_mod Python/pythonrun.c:1459
    #15 0x5703b3dd357a in pyrun_file Python/pythonrun.c:1293
    #16 0x5703b3dd6220 in _PyRun_SimpleFileObject Python/pythonrun.c:521
    #17 0x5703b3dd64f6 in _PyRun_AnyFileObject Python/pythonrun.c:81
    #18 0x5703b3e2774d in pymain_run_file_obj Modules/main.c:410
    #19 0x5703b3e279b4 in pymain_run_file Modules/main.c:429
    #20 0x5703b3e291b2 in pymain_run_python Modules/main.c:691
    #21 0x5703b3e29842 in Py_RunMain Modules/main.c:772
    #22 0x5703b3e29a2e in pymain_main Modules/main.c:802
    #23 0x5703b3e29db3 in Py_BytesMain Modules/main.c:826
    #24 0x5703b38ad645 in main Programs/python.c:15
    #25 0x710867c2a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #26 0x710867c2a28a in __libc_start_main_impl ../csu/libc-start.c:360
    #27 0x5703b38ad574 in _start (/home/jackfromeast/Desktop/entropy/tasks/grammar-afl++-latest/targets/cpython/python+0x2dd574) (BuildId: ff3dc40ea460bd4beb2c3a72283cca525b319bf0)

0x506000079ef8 is located 24 bytes inside of 56-byte region [0x506000079ee0,0x506000079f18)
freed by thread T0 here:
    #0 0x7108680fc4d8 in free ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:52
    #1 0x5703b3adf96d in _PyMem_RawFree Objects/obmalloc.c:91
    #2 0x5703b3ae1cd9 in _PyMem_DebugRawFree Objects/obmalloc.c:2955
    #3 0x5703b3ae1d1a in _PyMem_DebugFree Objects/obmalloc.c:3100
    #4 0x5703b3b09348 in PyMem_Free Objects/obmalloc.c:1070
    #5 0x710867190c1a in strong_cache_node_free Modules/_zoneinfo.c:2344
    #6 0x710867190c93 in eject_from_strong_cache Modules/_zoneinfo.c:2435
    #7 0x710867191079 in zoneinfo_ZoneInfo_clear_cache_impl Modules/_zoneinfo.c:524
    #8 0x7108671912f3 in zoneinfo_ZoneInfo_clear_cache Modules/clinic/_zoneinfo.c.h:255
    #9 0x5703b3acb0b6 in cfunction_vectorcall_FASTCALL_KEYWORDS_METHOD Objects/methodobject.c:481
    #10 0x5703b3a18e7f in _PyObject_VectorcallTstate Include/internal/pycore_call.h:169
    #11 0x5703b3a18f72 in PyObject_Vectorcall Objects/call.c:327
    #12 0x5703b3c9ec60 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:2920
    #13 0x5703b3cdae54 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
    #14 0x5703b3cdb148 in _PyEval_Vector Python/ceval.c:2001
    #15 0x5703b3a189b8 in _PyFunction_Vectorcall Objects/call.c:413
    #16 0x5703b3b2b56b in _PyObject_VectorcallTstate Include/internal/pycore_call.h:169
    #17 0x5703b3b479a6 in vectorcall_unbound Objects/typeobject.c:3033
    #18 0x5703b3b479a6 in maybe_call_special_one_arg Objects/typeobject.c:3175
    #19 0x5703b3b47ad3 in _PyObject_MaybeCallSpecialOneArg Objects/typeobject.c:3190
    #20 0x5703b3b47b19 in slot_tp_richcompare Objects/typeobject.c:10729
    #21 0x5703b3ad8687 in do_richcompare Objects/object.c:1059
    #22 0x5703b3ad893d in PyObject_RichCompare Objects/object.c:1108
    #23 0x5703b3ad89ad in PyObject_RichCompareBool Objects/object.c:1130
    #24 0x71086718af2a in find_in_strong_cache Modules/_zoneinfo.c:2403
    #25 0x71086718ce3f in zone_from_strong_cache Modules/_zoneinfo.c:2488
    #26 0x71086719149b in zoneinfo_ZoneInfo_impl Modules/_zoneinfo.c:323
    #27 0x710867191710 in zoneinfo_ZoneInfo Modules/clinic/_zoneinfo.c.h:64
    #28 0x5703b3b3f346 in type_call Objects/typeobject.c:2448
    #29 0x5703b3a18c71 in _PyObject_MakeTpCall Objects/call.c:242
    #30 0x5703b3a18f19 in _PyObject_VectorcallTstate Include/internal/pycore_call.h:167

previously allocated by thread T0 here:
    #0 0x7108680fd9c7 in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
    #1 0x5703b3ae0284 in _PyMem_RawMalloc Objects/obmalloc.c:63
    #2 0x5703b3adf655 in _PyMem_DebugRawAlloc Objects/obmalloc.c:2887
    #3 0x5703b3adf6bd in _PyMem_DebugRawMalloc Objects/obmalloc.c:2920
    #4 0x5703b3ae0f3b in _PyMem_DebugMalloc Objects/obmalloc.c:3085
    #5 0x5703b3b09204 in PyMem_Malloc Objects/obmalloc.c:1041
    #6 0x71086718d3c9 in strong_cache_node_new Modules/_zoneinfo.c:2324
    #7 0x7108671913ba in update_strong_cache Modules/_zoneinfo.c:2514
    #8 0x710867191514 in zoneinfo_ZoneInfo_impl Modules/_zoneinfo.c:350
    #9 0x710867191710 in zoneinfo_ZoneInfo Modules/clinic/_zoneinfo.c.h:64
    #10 0x5703b3b3f346 in type_call Objects/typeobject.c:2448
    #11 0x5703b3a18c71 in _PyObject_MakeTpCall Objects/call.c:242
    #12 0x5703b3a18f19 in _PyObject_VectorcallTstate Include/internal/pycore_call.h:167
    #13 0x5703b3a18f72 in PyObject_Vectorcall Objects/call.c:327
    #14 0x5703b3c97056 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:1620
    #15 0x5703b3cdae54 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
    #16 0x5703b3cdb148 in _PyEval_Vector Python/ceval.c:2001
    #17 0x5703b3cdb3f8 in PyEval_EvalCode Python/ceval.c:884
    #18 0x5703b3dd2507 in run_eval_code_obj Python/pythonrun.c:1365
    #19 0x5703b3dd2723 in run_mod Python/pythonrun.c:1459
    #20 0x5703b3dd357a in pyrun_file Python/pythonrun.c:1293
    #21 0x5703b3dd6220 in _PyRun_SimpleFileObject Python/pythonrun.c:521
    #22 0x5703b3dd64f6 in _PyRun_AnyFileObject Python/pythonrun.c:81
    #23 0x5703b3e2774d in pymain_run_file_obj Modules/main.c:410
    #24 0x5703b3e279b4 in pymain_run_file Modules/main.c:429
    #25 0x5703b3e291b2 in pymain_run_python Modules/main.c:691
    #26 0x5703b3e29842 in Py_RunMain Modules/main.c:772
    #27 0x5703b3e29a2e in pymain_main Modules/main.c:802
    #28 0x5703b3e29db3 in Py_BytesMain Modules/main.c:826
    #29 0x5703b38ad645 in main Programs/python.c:15

SUMMARY: AddressSanitizer: heap-use-after-free Modules/_zoneinfo.c:2379 in remove_from_strong_cache
Shadow bytes around the buggy address:
  0x506000079c00: fd fd fd fd fa fa fa fa fd fd fd fd fd fd fd fa
  0x506000079c80: fa fa fa fa fd fd fd fd fd fd fd fd fa fa fa fa
  0x506000079d00: fd fd fd fd fd fd fd fd fa fa fa fa fd fd fd fd
  0x506000079d80: fd fd fd fd fa fa fa fa fd fd fd fd fd fd fd fa
  0x506000079e00: fa fa fa fa fd fd fd fd fd fd fd fd fa fa fa fa
=>0x506000079e80: fd fd fd fd fd fd fd fd fa fa fa fa fd fd fd[fd]
  0x506000079f00: fd fd fd fa fa fa fa fa fd fd fd fd fd fd fd fd
  0x506000079f80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x50600007a000: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x50600007a080: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x50600007a100: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==1443306==ABORTING

Metadata

Metadata

Assignees

No one assigned

    Labels

    extension-modulesC modules in the Modules dirtype-crashA hard crash of the interpreter, possibly with a core dump

    Projects

    Status

    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions