Skip to content

Use-after-free in builtin_delattr_impl and builtin_setattr_impl via re-entrant __hash__ #142731

@jackfromeast

Description

@jackfromeast

What happened?

delattr(obj, name) and setattr(obj, name) ends up in PyObject_SetAttr(..., value=NULL), which calls the generic setter that deletes from obj.__dict__ via _PyDict_SetItem_LockHeld(value=NULL). If name is a str subclass with a re-entrant __hash__, hashing name runs user code that swaps out and frees the old __dict__. The deletion then continues using the stale dict pointer in _PyDict_DelItem_KnownHash_LockHeld, leading to a heap-use-after-free.

Proof of Concept:

PoC

class Evil(str):
    def __hash__(self):
        old = target.__dict__
        target.__dict__ = {}
        del old
        dicts = []
        for i in range(100):
            dicts.append({f"key{i}": f"value{i}"})
        return 0


class Victim:
    pass


target = Victim()
delattr(target, Evil("marker"))
import gc

class Evil(str):
    def __hash__(self):
        old = target.__dict__
        target.__dict__ = {}
        del old
        gc.collect()
        return 0

class Victim:
    pass

target = Victim()
setattr(target, Evil("marker"))

Related Code Snippet

Details
int
_PyDict_SetItem_LockHeld(PyDictObject *dict, PyObject *name, PyObject *value)
{
    if (value == NULL) {
		// Bug: Trigger the override __hash__ method that free the dict
        Py_hash_t hash = _PyObject_HashFast(name);
        if (hash == -1) {
            dict_unhashable_type(name);
            return -1;
        }
        // Access the freed dict buffer.
        return _PyDict_DelItem_KnownHash_LockHeld((PyObject *)dict, name, hash);
    } else {
        return setitem_lock_held(dict, name, value);
    }
}

int
_PyDict_DelItem_KnownHash_LockHeld(PyObject *op, PyObject *key, Py_hash_t hash)
{
    Py_ssize_t ix;
    PyDictObject *mp;
    PyObject *old_value;

    if (!PyDict_Check(op)) {
        PyErr_BadInternalCall();
        return -1;
    }

    ASSERT_DICT_LOCKED(op);

    assert(key);
    assert(hash != -1);
    mp = (PyDictObject *)op;
    // Leak the op/mp->me_key pointer back
    ix = _Py_dict_lookup(mp, key, hash, &old_value);
    if (ix == DKIX_ERROR)
        return -1;
    if (ix == DKIX_EMPTY || old_value == NULL) {
        _PyErr_SetKeyError(key);
        return -1;
    }

    PyInterpreterState *interp = _PyInterpreterState_GET();
    _PyDict_NotifyEvent(interp, PyDict_EVENT_DELETED, mp, key, NULL);
    delitem_common(mp, hash, ix, old_value);
    return 0;
}

Affected Versions:

Details
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

Sanitizer Output:

Details
=================================================================
==1553427==ERROR: AddressSanitizer: heap-use-after-free on address 0x5080000a5748 at pc 0x63af6ffd3df9 bp 0x7fff69a92e30 sp 0x7fff69a92e20
READ of size 8 at 0x5080000a5748 thread T0
    #0 0x63af6ffd3df8 in _Py_TYPE Include/object.h:277
    #1 0x63af6ffd3df8 in _PyDict_DelItem_KnownHash_LockHeld Objects/dictobject.c:2829
    #2 0x63af6ffd4001 in _PyDict_SetItem_LockHeld Objects/dictobject.c:6914
    #3 0x63af6ffd7cc4 in store_instance_attr_dict Objects/dictobject.c:7032
    #4 0x63af6ffd7d56 in _PyObject_StoreInstanceAttribute Objects/dictobject.c:7053
    #5 0x63af6fffe5e7 in _PyObject_GenericSetAttrWithDict Objects/object.c:1969
    #6 0x63af6fffe85e in PyObject_GenericSetAttr Objects/object.c:2031
    #7 0x63af6fffac11 in PyObject_SetAttr Objects/object.c:1476
    #8 0x63af6fffb35a in PyObject_DelAttr Objects/object.c:1512
    #9 0x63af7019c7c6 in builtin_delattr_impl Python/bltinmodule.c:1747
    #10 0x63af7019c841 in builtin_delattr Python/clinic/bltinmodule.c.h:716
    #11 0x63af6ffed364 in cfunction_vectorcall_FASTCALL Objects/methodobject.c:449
    #12 0x63af6ff3ae7f in _PyObject_VectorcallTstate Include/internal/pycore_call.h:169
    #13 0x63af6ff3af72 in PyObject_Vectorcall Objects/call.c:327
    #14 0x63af701b9056 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:1620
    #15 0x63af701fce54 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
    #16 0x63af701fd148 in _PyEval_Vector Python/ceval.c:2001
    #17 0x63af701fd3f8 in PyEval_EvalCode Python/ceval.c:884
    #18 0x63af702f4507 in run_eval_code_obj Python/pythonrun.c:1365
    #19 0x63af702f4723 in run_mod Python/pythonrun.c:1459
    #20 0x63af702f557a in pyrun_file Python/pythonrun.c:1293
    #21 0x63af702f8220 in _PyRun_SimpleFileObject Python/pythonrun.c:521
    #22 0x63af702f84f6 in _PyRun_AnyFileObject Python/pythonrun.c:81
    #23 0x63af7034974d in pymain_run_file_obj Modules/main.c:410
    #24 0x63af703499b4 in pymain_run_file Modules/main.c:429
    #25 0x63af7034b1b2 in pymain_run_python Modules/main.c:691
    #26 0x63af7034b842 in Py_RunMain Modules/main.c:772
    #27 0x63af7034ba2e in pymain_main Modules/main.c:802
    #28 0x63af7034bdb3 in Py_BytesMain Modules/main.c:826
    #29 0x63af6fdcf645 in main Programs/python.c:15
    #30 0x73d687c2a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #31 0x73d687c2a28a in __libc_start_main_impl ../csu/libc-start.c:360
    #32 0x63af6fdcf574 in _start (/home/jackfromeast/Desktop/entropy/tasks/grammar-afl++-latest/targets/cpython/python+0x2dd574) (BuildId: ff3dc40ea460bd4beb2c3a72283cca525b319bf0)

0x5080000a5748 is located 40 bytes inside of 88-byte region [0x5080000a5720,0x5080000a5778)
freed by thread T0 here:
    #0 0x73d6880fc4d8 in free ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:52
    #1 0x63af7000196d in _PyMem_RawFree Objects/obmalloc.c:91
    #2 0x63af70003cd9 in _PyMem_DebugRawFree Objects/obmalloc.c:2955
    #3 0x63af70003d1a in _PyMem_DebugFree Objects/obmalloc.c:3100
    #4 0x63af7002c06c in PyObject_Free Objects/obmalloc.c:1522
    #5 0x63af7026acf7 in PyObject_GC_Del Python/gc.c:2435
    #6 0x63af6fffa4cc in free_object Objects/object.c:916
    #7 0x63af6fff5e1e in clear_freelist Objects/object.c:902
    #8 0x63af6fff71ba in _PyObject_ClearFreeLists Objects/object.c:933
    #9 0x63af7026af8d in _PyGC_ClearAllFreeLists Python/gc_gil.c:14
    #10 0x63af70268da2 in gc_collect_full Python/gc.c:1735
    #11 0x63af7026a045 in _PyGC_Collect Python/gc.c:2096
    #12 0x63af7034d0f0 in gc_collect_impl Modules/gcmodule.c:93
    #13 0x63af7034d268 in gc_collect Modules/clinic/gcmodule.c.h:143
    #14 0x63af701bda49 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:2361
    #15 0x63af701fce54 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
    #16 0x63af701fd148 in _PyEval_Vector Python/ceval.c:2001
    #17 0x63af6ff3a9b8 in _PyFunction_Vectorcall Objects/call.c:413
    #18 0x63af6ff3ae7f in _PyObject_VectorcallTstate Include/internal/pycore_call.h:169
    #19 0x63af6ff3b03f in PyObject_CallOneArg Objects/call.c:395
    #20 0x63af7004d648 in call_unbound_noarg Objects/typeobject.c:3040
    #21 0x63af70068fa0 in maybe_call_special_no_args Objects/typeobject.c:3153
    #22 0x63af700695e4 in slot_tp_hash Objects/typeobject.c:10564
    #23 0x63af6fff73d6 in PyObject_Hash Objects/object.c:1157
    #24 0x63af6ffc4dda in _PyObject_HashFast Include/internal/pycore_object.h:872
    #25 0x63af6ffd3fed in _PyDict_SetItem_LockHeld Objects/dictobject.c:6909
    #26 0x63af6ffd7cc4 in store_instance_attr_dict Objects/dictobject.c:7032
    #27 0x63af6ffd7d56 in _PyObject_StoreInstanceAttribute Objects/dictobject.c:7053
    #28 0x63af6fffe5e7 in _PyObject_GenericSetAttrWithDict Objects/object.c:1969
    #29 0x63af6fffe85e in PyObject_GenericSetAttr Objects/object.c:2031

previously allocated by thread T0 here:
    #0 0x73d6880fd9c7 in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
    #1 0x63af70002284 in _PyMem_RawMalloc Objects/obmalloc.c:63
    #2 0x63af70001655 in _PyMem_DebugRawAlloc Objects/obmalloc.c:2887
    #3 0x63af700016bd in _PyMem_DebugRawMalloc Objects/obmalloc.c:2920
    #4 0x63af70002f3b in _PyMem_DebugMalloc Objects/obmalloc.c:3085
    #5 0x63af7002bf28 in PyObject_Malloc Objects/obmalloc.c:1493
    #6 0x63af7026a734 in _PyObject_MallocWithType Include/internal/pycore_object_alloc.h:46
    #7 0x63af7026a734 in gc_alloc Python/gc.c:2327
    #8 0x63af7026a88a in _PyObject_GC_New Python/gc.c:2347
    #9 0x63af6ffc8cad in new_dict Objects/dictobject.c:875
    #10 0x63af6ffca396 in PyDict_New Objects/dictobject.c:973
    #11 0x63af6ffca3e5 in dict_new_presized Objects/dictobject.c:2203
    #12 0x63af6ffd2877 in _PyDict_FromItems Objects/dictobject.c:2244
    #13 0x63af701b6e63 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:1272
    #14 0x63af701fce54 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
    #15 0x63af701fd148 in _PyEval_Vector Python/ceval.c:2001
    #16 0x63af6ff3a9b8 in _PyFunction_Vectorcall Objects/call.c:413
    #17 0x63af6ff3ae7f in _PyObject_VectorcallTstate Include/internal/pycore_call.h:169
    #18 0x63af6ff3cbc0 in object_vacall Objects/call.c:819
    #19 0x63af6ff3ce3e in PyObject_CallMethodObjArgs Objects/call.c:886
    #20 0x63af70289cc9 in import_find_and_load Python/import.c:3701
    #21 0x63af70290c01 in PyImport_ImportModuleLevelObject Python/import.c:3783
    #22 0x63af701ac4a3 in _PyEval_ImportName Python/ceval.c:3017
    #23 0x63af701d51b0 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:6219
    #24 0x63af701fce54 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
    #25 0x63af701fd148 in _PyEval_Vector Python/ceval.c:2001
    #26 0x63af701fd3f8 in PyEval_EvalCode Python/ceval.c:884
    #27 0x63af7019b94a in builtin_exec_impl Python/bltinmodule.c:1180
    #28 0x63af7019bc4c in builtin_exec Python/clinic/bltinmodule.c.h:571
    #29 0x63af6ffed123 in cfunction_vectorcall_FASTCALL_KEYWORDS Objects/methodobject.c:465
    #30 0x63af6ff3e1d2 in _PyVectorcall_Call Objects/call.c:273

SUMMARY: AddressSanitizer: heap-use-after-free Include/object.h:277 in _Py_TYPE
Shadow bytes around the buggy address:
  0x5080000a5480: fa fa fa fa 00 00 00 00 00 00 00 00 00 00 00 fa
  0x5080000a5500: fa fa fa fa 00 00 00 00 00 00 00 00 00 00 00 fa
  0x5080000a5580: fa fa fa fa 00 00 00 00 00 00 00 00 00 00 00 fa
  0x5080000a5600: fa fa fa fa 00 00 00 00 00 00 00 00 00 00 00 00
  0x5080000a5680: fa fa fa fa fd fd fd fd fd fd fd fd fd fd fd fa
=>0x5080000a5700: fa fa fa fa fd fd fd fd fd[fd]fd fd fd fd fd fa
  0x5080000a5780: fa fa fa fa fd fd fd fd fd fd fd fd fd fd fd fa
  0x5080000a5800: fa fa fa fa fd fd fd fd fd fd fd fd fd fd fd fa
  0x5080000a5880: fa fa fa fa fd fd fd fd fd fd fd fd fd fd fd fa
  0x5080000a5900: fa fa fa fa fd fd fd fd fd fd fd fd fd fd fd fa
  0x5080000a5980: fa fa fa fa 00 00 00 00 00 00 00 00 00 00 02 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
==1553427==ABORTING

Metadata

Metadata

Assignees

No one assigned

    Labels

    interpreter-core(Objects, Python, Grammar, and Parser dirs)type-crashA hard crash of the interpreter, possibly with a core dump

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions