Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
Nico-Posada opened this issue Oct 28, 2024 · 4 comments
Assignees
Labels
3.12 bugs and security fixes 3.13 bugs and security fixes 3.14 new features, bugs and security fixes extension-modules C modules in the Modules dir topic-asyncio type-crash A hard crash of the interpreter, possibly with a core dump

Comments

@Nico-Posada
Copy link
Contributor

Nico-Posada commented Oct 28, 2024

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

@Nico-Posada Nico-Posada added the type-crash A hard crash of the interpreter, possibly with a core dump label Oct 28, 2024
@picnixz picnixz added topic-asyncio extension-modules C modules in the Modules dir 3.12 bugs and security fixes 3.13 bugs and security fixes 3.14 new features, bugs and security fixes labels Oct 28, 2024
@picnixz
Copy link
Contributor

picnixz commented Oct 28, 2024

I'll create a PR tomorrow and try to find an easier way to trigger the bug but thanks as always! I forgot about checking UAF for the Task object. (If you want to make the PR, just tell me btw).

@Nico-Posada
Copy link
Contributor Author

For now, I'm looking into a potential second UAF in Task, but if I have time later today I'll make the PR!

@1st1
Copy link
Member

1st1 commented Oct 30, 2024

My personal position on this is that we shouldn't fix extremely contrived ways of crashing C Tasks and Futures. We by all means should fix potential crashers that users can hit with regular code, but making code completely 100% safe for all far fetched hypothetical unreal-world scenarios wasn't ever a goal in CPython (AFAIK).

-1.

1st1 pushed a commit that referenced this issue Oct 31, 2024
…ue to an evil `loop.__getattribute__` (#126120)
miss-islington pushed a commit to miss-islington/cpython that referenced this issue Oct 31, 2024
…oon` due to an evil `loop.__getattribute__` (pythonGH-126120)

(cherry picked from commit 0e86655)

Co-authored-by: Bénédikt Tran <[email protected]>
miss-islington pushed a commit to miss-islington/cpython that referenced this issue Oct 31, 2024
…oon` due to an evil `loop.__getattribute__` (pythonGH-126120)

(cherry picked from commit 0e86655)

Co-authored-by: Bénédikt Tran <[email protected]>
willingc pushed a commit that referenced this issue Oct 31, 2024
…soon` due to an evil `loop.__getattribute__` (GH-126120) (#126250)

gh-126080: fix UAF on `task->task_context` in `task_call_step_soon` due to an evil `loop.__getattribute__` (GH-126120)
(cherry picked from commit 0e86655)

Co-authored-by: Bénédikt Tran <[email protected]>
willingc pushed a commit that referenced this issue Oct 31, 2024
…soon` due to an evil `loop.__getattribute__` (GH-126120) (#126251)

gh-126080: fix UAF on `task->task_context` in `task_call_step_soon` due to an evil `loop.__getattribute__` (GH-126120)
(cherry picked from commit 0e86655)

Co-authored-by: Bénédikt Tran <[email protected]>
@picnixz
Copy link
Contributor

picnixz commented Oct 31, 2024

Closing since completed and backported. Thanks for the report Nico!

@picnixz picnixz closed this as completed Oct 31, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3.12 bugs and security fixes 3.13 bugs and security fixes 3.14 new features, bugs and security fixes extension-modules C modules in the Modules dir topic-asyncio type-crash A hard crash of the interpreter, possibly with a core dump
Projects
Status: Done
Development

No branches or pull requests

3 participants