python / python/cpython

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

Ouverte
#142,782 2 commentaires 0 réactions 0 personnes assignées Voir sur GitHub

Personne n'a encore pris cette issue.

extension-modules type-crash
Langage dominant
Python
Étoiles
77.2k
Forks
35.9k
Métriques de merge des PR
Métriques de PR en attente

Description

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

Guide de contribution

Ouvrir le guide de contribution

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

Piste de recherche

Commencez par Modules/_zoneinfo.c, en particulier find_in_strong_cache(), zone_from_strong_cache() et ZoneInfo.clear_cache() ; exécutez la preuve de concept fournie avec EvilStr et ASan sur les versions affectées. C’est terminé lorsque la comparaison réentrante ne permet plus d’accéder à un StrongCacheNode libéré et que la preuve de concept se termine sans rapport d’use-after-free d’ASan.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Évaluation

Stack technique
c, python
Domaine
security
Type d'issue
Bug
Difficulté
4/5
Temps estimé
3-5 jours
Activité
À l'abandon
Clarté
Clairement spécifiée
Accessibilité débutants
35/100

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.