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