Skip to content

UAF when using a malicious __getattribute__ when calling a class's cancel function in task_step_handle_result_impl in _asynciomodule.c #126138

Closed
@Nico-Posada

Description

@Nico-Posada

Crash report

What happened?

This is the bug I mentioned I was looking into in #126080 (comment), but it's the same as all the ones that came before this.

PyObject *r;
int is_true;
r = PyObject_CallMethodOneArg(result, &_Py_ID(cancel),
task->task_cancel_msg);

task->task_cancel_msg is missing an incref before usage so we can use a malicious __getattribute__ function in our class to free it before it gets sent to our cancel function.

PoC

import asyncio
import types

async def evil_coroutine():
    @types.coroutine
    def sync_generator():
        # ensure to keep obj alive after the first send() call
        global evil
        while 1:
            yield evil
    await sync_generator()

class Loop:
    is_running = staticmethod(lambda: True)
    get_debug = staticmethod(lambda: False)
         
class Evil:
    _asyncio_future_blocking = True
    get_loop = staticmethod(lambda: normal_loop)

    def add_done_callback(self, callback, *args, **kwargs):
        # sets task_cancel_msg to our victim object which will be deleted
        asyncio.Task.cancel(task, to_uaf)
    
    def cancel(self, msg):
        # if hasn't crashed at this point, you'll see its the same object that was just deleted
        print("in cancel", hex(id(msg)))

    def __getattribute__(self, name):
        global to_uaf
        if name == "cancel":
            class Break:
                def __str__(self):
                    raise RuntimeError("break")

            # at this point, our obj to uaf only has 2 refs, `to_uaf` and `task->task_cancel_msg`. Doing a partial task init will clear
            # fut->fut_cancel_msg (same thing as task_cancel_msg, it's just been cast to a fut obj), and then we can just `del to_uaf` to free
            # the object before it gets sent to our `cancel` func
            try:
                task.__init__(coro, loop=normal_loop, name=Break())
            except Exception as e:
                assert type(e) == RuntimeError and e.args[0] == "break"

            del to_uaf
            # to_uaf has now been deleted, but it will still be sent to our `cancel` func

        return object.__getattribute__(self, name)

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

to_uaf = DelTracker()
normal_loop = Loop()
coro = evil_coroutine()
evil = Evil()

task = asyncio.Task.__new__(asyncio.Task)
task.__init__(coro, loop=normal_loop, name="init", eager_start=True)

Output

deleting 0x7f7a49cf9940
in cancel 0x7f7a49cf9940
Segmentation fault

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