FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Crash: UAF in `task_call_step_soon` in `_asynciomodule.c` (with admittedly ridiculous setup) · Issue #126080 · python/cpython · GitHub

Repository navigation

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

Description

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

Activity

  1. added
    type-crashA hard crash of the interpreter, possibly with a core dump
    on Oct 28, 2024
  2. picnixz commented on Oct 28, 2024

    Member

    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).

  3. Nico-Posada commented on Oct 28, 2024

    ContributorAuthor

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

  4. self-assigned this
    on Oct 29, 2024
  5. 1st1 commented on Oct 30, 2024

    Member

    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.

  6. added a commit that references this issue on Oct 31, 2024
  7. added 2 commits that reference this issue on Oct 31, 2024
  8. added 2 commits that reference this issue on Oct 31, 2024
  9. picnixz commented on Oct 31, 2024

    Member

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

  10. moved this from Todo to Done in asyncioon Oct 31, 2024
  11. added a commit that references this issue on Dec 8, 2024
  12. added a commit that references this issue on Jan 12, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

3.12only security fixes3.13only 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


Back | FazBrowse Home | New Git URL