Skip to content

Crash: UAF in task_call_step_soon in _asynciomodule.c (with admittedly ridiculous setup) #126080

@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

    pFad - Phonifier reborn

    Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

    Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


    Alternative Proxies:

    Alternative Proxy

    pFad Proxy

    pFad v3 Proxy

    pFad v4 Proxy