Skip to content

Use-after-free in asyncio Task deallocation via re-registering task in call_exception_handler #142556

@jackfromeast

Description

@jackfromeast

What happened?

During Task teardown TaskObj_dealloc() calls the finalizer (TaskObj_finalize) which invokes the loop's call_exception_handler (user code). If that handler re-registers the same Task (e.g. calls _asyncio._register_task(context["task"])), the runtime may add the freed task pointer into a registry while tp_free is still running, causing a heap use-after-free when the registry later touches the pointer.

Proof of Concept:

import _asyncio

class EvilLoop:
    def get_debug(self):
        return False

    def call_exception_handler(self, context):
        _asyncio._register_task(context["task"])

async def coro():
    pass

loop = EvilLoop()
task = _asyncio.Task(coro(), loop=loop)

Affected Versions:

Python Version Status Exit Code
Python 3.9.24+ (heads/3.9:9c4638d, Oct 17 2025, 11:19:30) Exception 1
Python 3.10.19+ (heads/3.10:0142619, Oct 17 2025, 11:20:05) [GCC 13.3.0] Exception 1
Python 3.11.14+ (heads/3.11:88f3f5b, Oct 17 2025, 11:20:44) [GCC 13.3.0] Exception 1
Python 3.12.12+ (heads/3.12:8cb2092, Oct 17 2025, 11:21:35) [GCC 13.3.0] Exception 1
Python 3.13.9+ (heads/3.13:0760a57, Oct 17 2025, 11:22:25) [GCC 13.3.0] Exception 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

Vulnerable Code Snippet

static void
TaskObj_dealloc(PyObject *self)
{
    _PyObject_ResurrectStart(self);
    // Unregister the task here so that even if any subclass of Task
    // which doesn't end up calling TaskObj_finalize not crashes.
    unregister_task((TaskObj *)self);

    PyObject_CallFinalizer(self); // Call TaskObj_finalize

    if (_PyObject_ResurrectEnd(self)) {
        return;
    }

    PyTypeObject *tp = Py_TYPE(self);
    PyObject_GC_UnTrack(self);

    PyObject_ClearWeakRefs(self);

    (void)TaskObj_clear(self);
    tp->tp_free(self);
    Py_DECREF(tp);
}

static void
TaskObj_finalize(PyObject *op)
{
    TaskObj *task = (TaskObj*)op;
    PyObject *context;
    PyObject *message = NULL;
    PyObject *func;

    if (task->task_state != STATE_PENDING || !task->task_log_destroy_pending) {
        goto done;
    }

    /* Save the current exception, if any. */
    PyObject *exc = PyErr_GetRaisedException();

    context = PyDict_New();
    if (context == NULL) {
        goto finally;
    }

    message = PyUnicode_FromString("Task was destroyed but it is pending!");
    if (message == NULL) {
        goto finally;
    }

    if (PyDict_SetItem(context, &_Py_ID(message), message) < 0 ||
        PyDict_SetItem(context, &_Py_ID(task), (PyObject*)task) < 0)
    {
        goto finally;
    }

    if (task->task_source_tb != NULL) {
        if (PyDict_SetItem(context, &_Py_ID(source_traceback),
                              task->task_source_tb) < 0)
        {
            goto finally;
        }
    }
	   
	// Bug: In the call_exception_handler, we re-register the task so that the registry holds its pointer
	// While later, the tp->tp_free(self); in TaskObj_dealloc, the task will be freed and any access to the pointer will cause UAF.
	// When task has ended, the call_exception_handler method will be invoked.
    func = PyObject_GetAttr(task->task_loop, &_Py_ID(call_exception_handler));
    if (func != NULL) {
        PyObject *res = PyObject_CallOneArg(func, context);
        if (res == NULL) {
            PyErr_FormatUnraisable("Exception ignored while calling asyncio "
                                   "function %R", func);
        }
        else {
            Py_DECREF(res);
        }
        Py_DECREF(func);
    }

finally:
    Py_XDECREF(context);
    Py_XDECREF(message);

    /* Restore the saved exception. */
    PyErr_SetRaisedException(exc);

done:
    FutureObj_finalize((PyObject*)task);
}

Sanitizer

=================================================================
==1485200==ERROR: AddressSanitizer: heap-use-after-free on address 0x5150000657d0 at pc 0x62a676285a06 bp 0x7ffe931f2c30 sp 0x7ffe931f2c20
WRITE of size 8 at 0x5150000657d0 thread T0
    #0 0x62a676285a05 in llist_concat Include/internal/pycore_llist.h:95
    #1 0x62a676285a05 in PyThreadState_Clear Python/pystate.c:1687
    #2 0x62a676285bb1 in interpreter_clear Python/pystate.c:746
    #3 0x62a676286636 in _PyInterpreterState_Clear Python/pystate.c:904
    #4 0x62a67627cb73 in finalize_interp_clear Python/pylifecycle.c:1920
    #5 0x62a67627d669 in _Py_Finalize Python/pylifecycle.c:2337
    #6 0x62a67627d6f2 in Py_FinalizeEx Python/pylifecycle.c:2378
    #7 0x62a6762df847 in Py_RunMain Modules/main.c:774
    #8 0x62a6762dfa2e in pymain_main Modules/main.c:802
    #9 0x62a6762dfdb3 in Py_BytesMain Modules/main.c:826
    #10 0x62a675d63645 in main Programs/python.c:15
    #11 0x7405bae2a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #12 0x7405bae2a28a in __libc_start_main_impl ../csu/libc-start.c:360
    #13 0x62a675d63574 in _start (/home/jackfromeast/Desktop/entropy/tasks/grammar-afl++-latest/targets/cpython/python+0x2dd574) (BuildId: ff3dc40ea460bd4beb2c3a72283cca525b319bf0)

0x5150000657d0 is located 208 bytes inside of 504-byte region [0x515000065700,0x5150000658f8)
freed by thread T0 here:
    #0 0x7405bb2fc4d8 in free ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:52
    #1 0x62a675f9596d in _PyMem_RawFree Objects/obmalloc.c:91
    #2 0x62a675f97cd9 in _PyMem_DebugRawFree Objects/obmalloc.c:2955
    #3 0x62a675f97d1a in _PyMem_DebugFree Objects/obmalloc.c:3100
    #4 0x62a675fc006c in PyObject_Free Objects/obmalloc.c:1522
    #5 0x62a6761fecf7 in PyObject_GC_Del Python/gc.c:2435
    #6 0x62a6762eef87 in TaskObj_dealloc Modules/_asynciomodule.c:3010
    #7 0x62a675f8c481 in _Py_Dealloc Objects/object.c:3200
    #8 0x62a675fdd81c in Py_DECREF Include/refcount.h:401
    #9 0x62a675ff5646 in type_call Objects/typeobject.c:2463
    #10 0x62a675ecec71 in _PyObject_MakeTpCall Objects/call.c:242
    #11 0x62a675ecef19 in _PyObject_VectorcallTstate Include/internal/pycore_call.h:167
    #12 0x62a675ecef72 in PyObject_Vectorcall Objects/call.c:327
    #13 0x62a676154c60 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:2920
    #14 0x62a676190e54 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
    #15 0x62a676191148 in _PyEval_Vector Python/ceval.c:2001
    #16 0x62a6761913f8 in PyEval_EvalCode Python/ceval.c:884
    #17 0x62a676288507 in run_eval_code_obj Python/pythonrun.c:1365
    #18 0x62a676288723 in run_mod Python/pythonrun.c:1459
    #19 0x62a67628957a in pyrun_file Python/pythonrun.c:1293
    #20 0x62a67628c220 in _PyRun_SimpleFileObject Python/pythonrun.c:521
    #21 0x62a67628c4f6 in _PyRun_AnyFileObject Python/pythonrun.c:81
    #22 0x62a6762dd74d in pymain_run_file_obj Modules/main.c:410
    #23 0x62a6762dd9b4 in pymain_run_file Modules/main.c:429
    #24 0x62a6762df1b2 in pymain_run_python Modules/main.c:691
    #25 0x62a6762df842 in Py_RunMain Modules/main.c:772
    #26 0x62a6762dfa2e in pymain_main Modules/main.c:802
    #27 0x62a6762dfdb3 in Py_BytesMain Modules/main.c:826
    #28 0x62a675d63645 in main Programs/python.c:15
    #29 0x7405bae2a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58

previously allocated by thread T0 here:
    #0 0x7405bb2fd9c7 in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
    #1 0x62a675f96284 in _PyMem_RawMalloc Objects/obmalloc.c:63
    #2 0x62a675f95655 in _PyMem_DebugRawAlloc Objects/obmalloc.c:2887
    #3 0x62a675f956bd in _PyMem_DebugRawMalloc Objects/obmalloc.c:2920
    #4 0x62a675f96f3b in _PyMem_DebugMalloc Objects/obmalloc.c:3085
    #5 0x62a675fbff28 in PyObject_Malloc Objects/obmalloc.c:1493
    #6 0x62a675ff203b in _PyObject_MallocWithType Include/internal/pycore_object_alloc.h:46
    #7 0x62a675ff203b in _PyType_AllocNoTrack Objects/typeobject.c:2504
    #8 0x62a675ff21c7 in PyType_GenericAlloc Objects/typeobject.c:2535
    #9 0x62a675fd9dd9 in PyType_GenericNew Objects/typeobject.c:2549
    #10 0x62a675ff5346 in type_call Objects/typeobject.c:2448
    #11 0x62a675ecec71 in _PyObject_MakeTpCall Objects/call.c:242
    #12 0x62a675ecef19 in _PyObject_VectorcallTstate Include/internal/pycore_call.h:167
    #13 0x62a675ecef72 in PyObject_Vectorcall Objects/call.c:327
    #14 0x62a676154c60 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:2920
    #15 0x62a676190e54 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
    #16 0x62a676191148 in _PyEval_Vector Python/ceval.c:2001
    #17 0x62a6761913f8 in PyEval_EvalCode Python/ceval.c:884
    #18 0x62a676288507 in run_eval_code_obj Python/pythonrun.c:1365
    #19 0x62a676288723 in run_mod Python/pythonrun.c:1459
    #20 0x62a67628957a in pyrun_file Python/pythonrun.c:1293
    #21 0x62a67628c220 in _PyRun_SimpleFileObject Python/pythonrun.c:521
    #22 0x62a67628c4f6 in _PyRun_AnyFileObject Python/pythonrun.c:81
    #23 0x62a6762dd74d in pymain_run_file_obj Modules/main.c:410
    #24 0x62a6762dd9b4 in pymain_run_file Modules/main.c:429
    #25 0x62a6762df1b2 in pymain_run_python Modules/main.c:691
    #26 0x62a6762df842 in Py_RunMain Modules/main.c:772
    #27 0x62a6762dfa2e in pymain_main Modules/main.c:802
    #28 0x62a6762dfdb3 in Py_BytesMain Modules/main.c:826
    #29 0x62a675d63645 in main Programs/python.c:15
    #30 0x7405bae2a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58

SUMMARY: AddressSanitizer: heap-use-after-free Include/internal/pycore_llist.h:95 in llist_concat
Shadow bytes around the buggy address:
  0x515000065500: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x515000065580: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x515000065600: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fa fa
  0x515000065680: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x515000065700: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
=>0x515000065780: fd fd fd fd fd fd fd fd fd fd[fd]fd fd fd fd fd
  0x515000065800: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x515000065880: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fa
  0x515000065900: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x515000065980: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x515000065a00: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
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
==1485200==ABORTING

Linked PRs

Metadata

Metadata

Assignees

No one assigned

    Labels

    3.14bugs and security fixes3.15new features, bugs and security fixestopic-asynciotype-crashA hard crash of the interpreter, possibly with a core dump

    Projects

    Status

    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions