8000 Crash: UAF in `task_call_step_soon` in `_asynciomodule.c` (with admittedly ridiculous setup) · Issue #126080 · python/cpython · GitHub
[go: up one dir, main page]

Skip to content
Crash: UAF in task_call_step_soon in _asynciomodule.c (with admittedly ridiculous setup) #126080
Closed
@Nico-Posada

Description

@Nico-Posada

Crash report

What happened?

This is basically an extension to #125984 but it took me a bit to get a working PoC because I have never used asyncio.Task ever.

The crash is caused because of a missing incref before calling call_soon in task_call_step_soon which allows us to corrupt task_context in an evil __getattribute__ class func before handing it off to call_soon. There's probably a much simpler way to trigger the crash but this is the only working route I found.

import asyncio
import types

@types.coroutine
def gen():
    # this just needs to stay alive after the first `send` call
    global catcher
    while True:
        yield catcher

async def coro():
    await gen()

# this class is used to help return early from the Task.__init__ function just after
# task_context gets set in the func
class EvilStr:
    def __str__(self):
        raise Exception("break")

class EvilLoop:
    def get_debug(self):
        return False
    
    def is_running(self):
        return True
    
    def call_soon(self, cb, *, context):
        # if it hasnt crashed for you at this point, you'll see this is the same obj that was just freed
        print("in call_soon", context)

    def __getattribute__(self, name):
        global ctx
        if name == "call_soon":
            try:
                # context needs to be `None` so that it uses Py_XSETREF instead of just using regular assignment
                task.__init__(co, loop=loop, context=None, name=EvilStr())
            except: pass
        
        return object.__getattribute__(self, name)
    
class TaskWakeupCatch:
    def __init__(self):
        self._asyncio_future_blocking = True
    
    def get_loop(self):
        global loop
        return loop
    
    # as far as i know, this is the only way to get access to the `task_wakeup` function
    # which is needed to abuse the UAF
    def add_done_callback(self, cb, *, context):
        global wakeup_fn
        if wakeup_fn == None:
            wakeup_fn = cb

class DelTracker:
    def __del__(self):
        print("deleting", self)

co = coro()
loop = EvilLoop()
catcher = TaskWakeupCatch()
wakeup_fn = None

task = asyncio.Task(co, loop=loop, eager_start=True, name="init")

# set ctx to any obj you want to use after free
# im using an obj that tells us when it's been freed so we can see the UAF in action
ctx = DelTracker()
try:
    # use exception trick to return early from the init func just after task_context gets set
    task.__init__(co, loop=loop, context=ctx, name=EvilStr())
except: pass
del ctx

minimal = lambda: ...
minimal.result = lambda: None # only needs to be a function that doesnt error

assert wakeup_fn is not None
wakeup_fn(minimal)

Output:

deleting <__main__.DelTracker object at 0x7f28e01d5be0>
in call_soon <__main__.DelTracker object at 0x7f28e01d5be0>
Segmentation fault

I am on a version of python that doesn't include all the recent fixes to asyncio, so just to confirm I was triggering this via task_call_step_soon I made sure to check the crash backtrace in gdb.

#0  _PyWeakref_GetWeakrefCount (obj=0x7ffff6f6dbe0) at Objects/weakrefobject.c:52
#1  _PyWeakref_GetWeakrefCount (obj=0x7ffff6f6dbe0) at Objects/weakrefobject.c:42
#2  PyObject_ClearWeakRefs (object=0x7ffff6f6dbe0) at Objects/weakrefobject.c:1018
#3  0x00005555556daf9e in subtype_dealloc (self=0x7ffff6f6dbe0) at Objects/typeobject.c:2322
#4  0x00005555557c4ec7 in Py_DECREF (op=<optimized out>) at ./Include/object.h:949
#5  Py_XDECREF (op=<optimized out>) at ./Include/object.h:1042
#6  _PyFrame_ClearLocals (frame=0x7ffff7afa0a0) at Python/frame.c:104
#7  _PyFrame_ClearExceptCode (frame=0x7ffff7afa0a0) at Python/frame.c:129
#8  0x0000555555796d47 in clear_thread_frame (frame=0x7ffff7afa0a0, tstate=0x555555adfc60 <_PyRuntime+282976>)
    at Python/ceval.c:1668
#9  _PyEval_FrameClearAndPop (tstate=0x555555adfc60 <_PyRuntime+282976>, frame=0x7ffff7afa0a0) at Python/ceval.c:1695
#10 0x00005555555db84f in _PyEval_EvalFrameDefault (tstate=0x7ffff6f6dd10, frame=0x7fffffffd680, throwflag=1437070368)
    at Python/generated_cases.c.h:5204
#11 0x0000555555644638 in _PyObject_VectorcallTstate (kwnames=0x7ffff713db10, nargsf=2, args=0x7fffffffd7f0,
    callable=0x7ffff6dc00e0, tstate=0x555555adfc60 <_PyRuntime+282976>) at ./Include/internal/pycore_call.h:168
#12 method_vectorcall (method=<optimized out>, args=0x7fffffffd7f8, nargsf=<optimized out>, kwnames=0x7ffff713db10)
    at Objects/classobject.c:62
#13 0x0000555555641743 in _PyObject_VectorcallTstate (kwnames=0x7ffff713db10, nargsf=<optimized out>,
    args=0x7fffffffd7f8, callable=0x7ffff6f588c0, tstate=0x555555adfc60 <_PyRuntime+282976>)
    at ./Include/internal/pycore_call.h:168
#14 PyObject_VectorcallMethod (name=<optimized out>, args=0x7fffffffd7f8, args@entry=0x7fffffffd7f0,
    nargsf=<optimized out>, nargsf@entry=9223372036854775810, kwnames=0x7ffff713db10) at Objects/call.c:856
#15 0x00007ffff7021504 in call_soon (ctx=<optimized out>, arg=0x0, func=0x7ffff6f5f100, loop=<optimized out>,
    state=0x7ffff7098b30) at ./Modules/_asynciomodule.c:311
#16 task_call_step_soon (state=state@entry=0x7ffff7098b30, task=task@entry=0x7ffff6f54c00, arg=arg@entry=0x7ffff7a61b40)
    at ./Modules/_asynciomodule.c:2677
#17 0x00007ffff70216a9 in task_set_error_soon (state=state@entry=0x7ffff7098b30, task=task@entry=0x7ffff6f54c00,
    et=<optimized out>, format=<optimized out>) at ./Modules/_asynciomodule.c:2703
#18 0x00007ffff7022043 in task_step_handle_result_impl (result=<optimized out>, task=0x7ffff6f54c00,
    state=0x7ffff7098b30) at ./Modules/_asynciomodule.c:3052
#19 task_step_impl (state=state@entry=0x7ffff7098b30, task=task@entry=0x7ffff6f54c00, exc=<optimized out>, exc@entry=0x0)
    at ./Modules/_asynciomodule.c:2847
#20 0x00007ffff7023327 in task_step (state=0x7ffff7098b30, task=0x7ffff6f54c00, exc=0x0)
    at ./Modules/_asynciomodule.c:3073

The fix for this is to just incref task->task_context before calling call_soon to avoid deleting it in the evil func

int ret = call_soon(state, task->task_loop, cb, NULL, task->task_context);
Py_DECREF(cb);
return ret;
}

CPython versions tested on:

3.13

Operating systems tested on:

Linux

Output from running 'python -VV' on the command line:

Python 3.13.0 (tags/v3.13.0:60403a5409f, Oct 10 2024, 09:24:12) [GCC 13.2.0]

Linked PRs

Metadata

Metadata

Assignees

Labels

3.12only security fixes3.13bugs and security fixes3.14bugs and security fixesextension-modulesC modules in the Modules dirtopic-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

    0