python / python/cpython

Use-after-free in `_PyEval_LoadName` via re-entrant frame locals lookup

Open
#143,236 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

interpreter-core type-crash
Dominant language
Python
Stars
77.2k
Forks
35.9k
PR merge metrics
PR metrics pending

Description

What happened?

_PyEval_LoadName consults frame.f_locals through PyMapping_GetOptionalItem, but it does so with the locals proxy still live. A crafted key can run frame.clear() inside its __eq__, freeing the locals dict while the lookup proceeds, so the name resolution continues to read the already-freed keys table and causes a use-after-free.

Proof of Concept:

def get_frame():
    try: raise RuntimeError
    except RuntimeError as e: return e.__traceback__.tb_frame

frame = get_frame()
proxy = frame.f_locals

class Fuse(str):
    armed = False
    __hash__ = str.__hash__
    def __eq__(self, other):
        if not Fuse.armed and other == "boom":
            Fuse.armed = True
            Fuse.frame.clear()
        return True

Fuse.frame = frame
proxy[Fuse("boom")] = 0
exec("boom", {}, proxy)

Affected Versions

Python Version Status Exit Code
Python 3.9.24+ (heads/3.9:111bbc15b26, Oct 28 2025, 16:51:20) OK 0
Python 3.10.19+ (heads/3.10:014261980b1, Oct 28 2025, 16:52:08) [Clang 18.1.3 (1ubuntu1)] OK 0
Python 3.11.14+ (heads/3.11:88f3f5b5f11, Oct 28 2025, 16:53:08) [Clang 18.1.3 (1ubuntu1)] OK 0
Python 3.12.12+ (heads/3.12:8cb2092bd8c, Oct 28 2025, 16:54:14) [Clang 18.1.3 (1ubuntu1)] OK 0
Python 3.13.9+ (heads/3.13:9c8eade20c6, Oct 28 2025, 16:55:18) [Clang 18.1.3 (1ubuntu1)] ASAN 1
Python 3.14.0+ (heads/3.14:2e216728038, Oct 28 2025, 16:56:16) [Clang 18.1.3 (1ubuntu1)] ASAN 1
Python 3.15.0a1+ (heads/main:f5394c257ce, Oct 28 2025, 19:29:54) [GCC 13.3.0] ASAN 1

Vulnerable Code

PyObject *
_PyEval_LoadName(PyThreadState *tstate, _PyInterpreterFrame *frame, PyObject *name)
{

    PyObject *value;
    if (frame->f_locals == NULL) {
        _PyErr_SetString(tstate, PyExc_SystemError,
                            "no locals found");
        return NULL;
    }

    /* Trigger the __eq__ which clear the frame->f_locals */
    if (PyMapping_GetOptionalItem(frame->f_locals, name, &value) < 0) {
        return NULL;
    }
    if (value != NULL) {
        return value;
    }

    if (PyDict_GetItemRef(frame->f_globals, name, &value) < 0) {
        return NULL;
    }
    if (value != NULL) {
        return value;
    }
    if (PyMapping_GetOptionalItem(frame->f_builtins, name, &value) < 0) {
        return NULL;
    }
    if (value == NULL) {
        _PyEval_FormatExcCheckArg(
                    tstate, PyExc_NameError,
                    NAME_ERROR_MSG, name);
    }
    return value;
}

Sanitizer Output

=================================================================
==2195217==ERROR: AddressSanitizer: heap-use-after-free on address 0x50e00000deb0 at pc 0x59ec04e2e8cf bp 0x7ffecc9b6f20 sp 0x7ffecc9b6f10
READ of size 8 at 0x50e00000deb0 thread T0
    #0 0x59ec04e2e8ce in compare_generic Objects/dictobject.c:1115
    #1 0x59ec04e2e8ce in do_lookup Objects/dictobject.c:1010
    #2 0x59ec04e2e8ce in dictkeys_generic_lookup Objects/dictobject.c:1129
    #3 0x59ec04e2e8ce in _Py_dict_lookup Objects/dictobject.c:1295
    #4 0x59ec04e3865e in _PyDict_GetItemRef_KnownHash Objects/dictobject.c:2421
    #5 0x59ec04e3865e in PyDict_GetItemRef Objects/dictobject.c:2456
    #6 0x59ec04dd2260 in framelocalsproxy_getitem Objects/frameobject.c:206
    #7 0x59ec04d0eeba in PyObject_GetItem Objects/abstract.c:163
    #8 0x59ec04d0eeba in PyMapping_GetOptionalItem Objects/abstract.c:215
    #9 0x59ec050db89b in _PyEval_LoadName Python/ceval.c:3536
    #10 0x59ec04c04c5e in _PyEval_EvalFrameDefault Python/generated_cases.c.h:9348
    #11 0x59ec050dcad6 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
    #12 0x59ec050dcad6 in _PyEval_Vector Python/ceval.c:2001
    #13 0x59ec050dcad6 in PyEval_EvalCode Python/ceval.c:884
    #14 0x59ec0522216e in run_eval_code_obj Python/pythonrun.c:1365
    #15 0x59ec0522216e in run_mod Python/pythonrun.c:1459
    #16 0x59ec05228670 in _PyRun_StringFlagsWithName Python/pythonrun.c:1259
    #17 0x59ec05228670 in PyRun_StringFlags Python/pythonrun.c:1271
    #18 0x59ec050c6b09 in builtin_exec_impl Python/bltinmodule.c:1209
    #19 0x59ec050c6b09 in builtin_exec Python/clinic/bltinmodule.c.h:571
    #20 0x59ec04d5e3e7 in _PyObject_VectorcallTstate Include/internal/pycore_call.h:169
    #21 0x59ec04d5e3e7 in PyObject_Vectorcall Objects/call.c:327
    #22 0x59ec04c125a2 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:1620
    #23 0x59ec050dcad6 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
    #24 0x59ec050dcad6 in _PyEval_Vector Python/ceval.c:2001
    #25 0x59ec050dcad6 in PyEval_EvalCode Python/ceval.c:884
    #26 0x59ec0522216e in run_eval_code_obj Python/pythonrun.c:1365
    #27 0x59ec0522216e in run_mod Python/pythonrun.c:1459
    #28 0x59ec05226e17 in pyrun_file Python/pythonrun.c:1293
    #29 0x59ec05226e17 in _PyRun_SimpleFileObject Python/pythonrun.c:521
    #30 0x59ec0522793c in _PyRun_AnyFileObject Python/pythonrun.c:81
    #31 0x59ec0529ae3c in pymain_run_file_obj Modules/main.c:410
    #32 0x59ec0529ae3c in pymain_run_file Modules/main.c:429
    #33 0x59ec0529ae3c in pymain_run_python Modules/main.c:691
    #34 0x59ec0529c71e in Py_RunMain Modules/main.c:772
    #35 0x59ec0529c71e in pymain_main Modules/main.c:802
    #36 0x59ec0529c71e in Py_BytesMain Modules/main.c:826
    #37 0x7d3ef602a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #38 0x7d3ef602a28a in __libc_start_main_impl ../csu/libc-start.c:360
    #39 0x59ec04c36634 in _start (/home/jackfromeast/Desktop/entropy/targets/grammar-afl++-latest/targets/cpython/python+0x206634) (BuildId: 4d105290d0ad566a4d6f4f7b2f05fbc9e317b533)

0x50e00000deb0 is located 48 bytes inside of 160-byte region [0x50e00000de80,0x50e00000df20)
freed by thread T0 here:
    #0 0x7d3ef64fc4d8 in free ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:52
    #1 0x59ec04e2b3df in free_keys_object Objects/dictobject.c:823
    #2 0x59ec04e2b3df in dictkeys_decref Objects/dictobject.c:473
    #3 0x59ec04e2b3df in dictkeys_decref Objects/dictobject.c:446
    #4 0x59ec04e2b3df in dict_dealloc Objects/dictobject.c:3354
    #5 0x59ec04e2b3df in dict_dealloc Objects/dictobject.c:3328
    #6 0x59ec04e7b1d8 in _Py_Dealloc Objects/object.c:3200
    #7 0x59ec04dcc7a4 in Py_DECREF Include/refcount.h:418
    #8 0x59ec04dcc7a4 in frame_tp_clear Objects/frameobject.c:1985
    #9 0x59ec04dcc8aa in frame_clear_impl Objects/frameobject.c:2028
    #10 0x59ec04dcc8aa in frame_clear Objects/clinic/frameobject.c.h:407
    #11 0x59ec04d5e3e7 in _PyObject_VectorcallTstate Include/internal/pycore_call.h:169
    #12 0x59ec04d5e3e7 in PyObject_Vectorcall Objects/call.c:327
    #13 0x59ec04c125a2 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:1620
    #14 0x59ec050dd2a5 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
    #15 0x59ec050dd2a5 in _PyEval_Vector Python/ceval.c:2001
    #16 0x59ec04f105e2 in _PyObject_VectorcallTstate Include/internal/pycore_call.h:169
    #17 0x59ec04f105e2 in vectorcall_unbound Objects/typeobject.c:3033
    #18 0x59ec04f105e2 in maybe_call_special_one_arg Objects/typeobject.c:3175
    #19 0x59ec04f105e2 in _PyObject_MaybeCallSpecialOneArg Objects/typeobject.c:3190
    #20 0x59ec04f105e2 in slot_tp_richcompare Objects/typeobject.c:10729
    #21 0x59ec04e822af in do_richcompare Objects/object.c:1059
    #22 0x59ec04e822af in PyObject_RichCompare Objects/object.c:1108
    #23 0x59ec04e822af in PyObject_RichCompareBool Objects/object.c:1130
    #24 0x59ec04e2e373 in compare_generic Objects/dictobject.c:1110
    #25 0x59ec04e2e373 in do_lookup Objects/dictobject.c:1010
    #26 0x59ec04e2e373 in dictkeys_generic_lookup Objects/dictobject.c:1129
    #27 0x59ec04e2e373 in _Py_dict_lookup Objects/dictobject.c:1295
    #28 0x59ec04e3865e in _PyDict_GetItemRef_KnownHash Objects/dictobject.c:2421
    #29 0x59ec04e3865e in PyDict_GetItemRef Objects/dictobject.c:2456
    #30 0x59ec04dd2260 in framelocalsproxy_getitem Objects/frameobject.c:206
    #31 0x59ec04d0eeba in PyObject_GetItem Objects/abstract.c:163
    #32 0x59ec04d0eeba in PyMapping_GetOptionalItem Objects/abstract.c:215
    #33 0x59ec050db89b in _PyEval_LoadName Python/ceval.c:3536
    #34 0x59ec04c04c5e in _PyEval_EvalFrameDefault Python/generated_cases.c.h:9348
    #35 0x59ec050dcad6 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
    #36 0x59ec050dcad6 in _PyEval_Vector Python/ceval.c:2001
    #37 0x59ec050dcad6 in PyEval_EvalCode Python/ceval.c:884
    #38 0x59ec0522216e in run_eval_code_obj Python/pythonrun.c:1365
    #39 0x59ec0522216e in run_mod Python/pythonrun.c:1459
    #40 0x59ec05228670 in _PyRun_StringFlagsWithName Python/pythonrun.c:1259
    #41 0x59ec05228670 in PyRun_StringFlags Python/pythonrun.c:1271
    #42 0x59ec050c6b09 in builtin_exec_impl Python/bltinmodule.c:1209
    #43 0x59ec050c6b09 in builtin_exec Python/clinic/bltinmodule.c.h:571
    #44 0x59ec04d5e3e7 in _PyObject_VectorcallTstate Include/internal/pycore_call.h:169
    #45 0x59ec04d5e3e7 in PyObject_Vectorcall Objects/call.c:327
    #46 0x59ec04c125a2 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:1620
    #47 0x59ec050dcad6 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
    #48 0x59ec050dcad6 in _PyEval_Vector Python/ceval.c:2001
    #49 0x59ec050dcad6 in PyEval_EvalCode Python/ceval.c:884
    #50 0x59ec0522216e in run_eval_code_obj Python/pythonrun.c:1365
    #51 0x59ec0522216e in run_mod Python/pythonrun.c:1459
    #52 0x59ec05226e17 in pyrun_file Python/pythonrun.c:1293
    #53 0x59ec05226e17 in _PyRun_SimpleFileObject Python/pythonrun.c:521
    #54 0x59ec0522793c in _PyRun_AnyFileObject Python/pythonrun.c:81
    #55 0x59ec0529ae3c in pymain_run_file_obj Modules/main.c:410
    #56 0x59ec0529ae3c in pymain_run_file Modules/main.c:429
    #57 0x59ec0529ae3c in pymain_run_python Modules/main.c:691
    #58 0x59ec0529c71e in Py_RunMain Modules/main.c:772
    #59 0x59ec0529c71e in pymain_main Modules/main.c:802
    #60 0x59ec0529c71e in Py_BytesMain Modules/main.c:826
    #61 0x7d3ef602a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #62 0x7d3ef602a28a in __libc_start_main_impl ../csu/libc-start.c:360

previously allocated by thread T0 here:
    #0 0x7d3ef64fd9c7 in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
    #1 0x59ec04e27f14 in new_keys_object Objects/dictobject.c:784
    #2 0x59ec04e27f14 in insert_to_emptydict Objects/dictobject.c:1948
    #3 0x59ec04c01cbc in _PyEval_EvalFrameDefault Python/generated_cases.c.h:11245
    #4 0x59ec050dcad6 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
    #5 0x59ec050dcad6 in _PyEval_Vector Python/ceval.c:2001
    #6 0x59ec050dcad6 in PyEval_EvalCode Python/ceval.c:884
    #7 0x59ec0522216e in run_eval_code_obj Python/pythonrun.c:1365
    #8 0x59ec0522216e in run_mod Python/pythonrun.c:1459
    #9 0x59ec05226e17 in pyrun_file Python/pythonrun.c:1293
    #10 0x59ec05226e17 in _PyRun_SimpleFileObject Python/pythonrun.c:521
    #11 0x59ec0522793c in _PyRun_AnyFileObject Python/pythonrun.c:81
    #12 0x59ec0529ae3c in pymain_run_file_obj Modules/main.c:410
    #13 0x59ec0529ae3c in pymain_run_file Modules/main.c:429
    #14 0x59ec0529ae3c in pymain_run_python Modules/main.c:691
    #15 0x59ec0529c71e in Py_RunMain Modules/main.c:772
    #16 0x59ec0529c71e in pymain_main Modules/main.c:802
    #17 0x59ec0529c71e in Py_BytesMain Modules/main.c:826
    #18 0x7d3ef602a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #19 0x7d3ef602a28a in __libc_start_main_impl ../csu/libc-start.c:360
    #20 0x59ec04c36634 in _start (/home/jackfromeast/Desktop/entropy/targets/grammar-afl++-latest/targets/cpython/python+0x206634) (BuildId: 4d105290d0ad566a4d6f4f7b2f05fbc9e317b533)

SUMMARY: AddressSanitizer: heap-use-after-free Objects/dictobject.c:1115 in compare_generic
Shadow bytes around the buggy address:
  0x50e00000dc00: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x50e00000dc80: fa fa fa fa fa fa fa fa fd fd fd fd fd fd fd fd
  0x50e00000dd00: fd fd fd fd fd fd fd fd fd fd fd fa fa fa fa fa
  0x50e00000dd80: fa fa fa fa 00 00 00 00 00 00 00 00 00 00 00 00
  0x50e00000de00: 00 00 00 00 00 00 00 00 fa fa fa fa fa fa fa fa
=>0x50e00000de80: fd fd fd fd fd fd[fd]fd fd fd fd fd fd fd fd fd
  0x50e00000df00: fd fd fd fd fa fa fa fa fa fa fa fa fd fd fd fd
  0x50e00000df80: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x50e00000e000: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x50e00000e080: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x50e00000e100: 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
==2195217==ABORTING
Linked PRs
  • gh-143283

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in Python/ceval.c at _PyEval_LoadName, then trace the frame-locals lookup through Objects/frameobject.c and Objects/dictobject.c. Reproduce the supplied proof of concept with an ASAN build and add regression coverage; done means the lookup no longer produces the reported use-after-free and the relevant tests pass.

Written by the indexing model from the issue text.

Assessment

Tech stack
c, python
Domain
compilers, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.