Skip to content

UAF in asyncio.Future when removing a callback while fut->fut_callbacks length is 1 #126405

Closed
@Nico-Posada

Description

@Nico-Posada

Crash report

What happened?

This bug boils down to a missing incref on the callback before calling PyObject_RichCompareBool. Although we can't directly control the callbacks list with fut._callbacks ever since it was made to return a copy every time, we can still modify it by removing/adding callbacks in evil __eq__ functions.

The bug is here

if (len == 1) {
PyObject *cb_tup = PyList_GET_ITEM(self->fut_callbacks, 0);
int cmp = PyObject_RichCompareBool(
PyTuple_GET_ITEM(cb_tup, 0), fn, Py_EQ);

PoC

import asyncio

fut = asyncio.Future()

class ConfirmUAF:
    def __eq__(self, other):
        print("!!!! How did we get here !!!!")

class Base:
    def __eq__(self, other):
        print("in tracker eq", self, other)
        return other != pad

    def __del__(self):
        # to see when objects are being deleted
        print("deleting", self)

class Evil(Base):
    def __eq__(self, other):
        global _no_del
        print("in evil eq", self, other)
        fut.remove_done_callback(Base())
        old_id = id(other)
        del other
        _no_del = ConfirmUAF()
        new_id = id(_no_del)

        # if these two are the same, you'll end up in the ConfirmUAF.__eq__ func
        # if not, you'll probably just crash
        print(f"{old_id = :#x} {new_id = :#x}")
        return NotImplemented


pad = ...
fut.add_done_callback(pad)
fut.add_done_callback(Base())
assert fut.remove_done_callback(pad) == 1

print("starting bug")
fut.remove_done_callback(Evil())

Output

in tracker eq <__main__.Base object at 0x7f8cac015d40> Ellipsis
starting bug
in evil eq <__main__.Evil object at 0x7f8cac015ea0> <__main__.Base object at 0x7f8cac015d40>
in tracker eq <__main__.Base object at 0x7f8cac015d40> <__main__.Base object at 0x7f8cac016000>
in tracker eq <__main__.Base object at 0x7f8cac016000> Ellipsis
deleting <__main__.Base object at 0x7f8cac016000>
deleting <__main__.Base object at 0x7f8cac015d40>
old_id = 0x7f8cac015d40 new_id = 0x7f8cac015d40
!!!! How did we get here !!!!
deleting <__main__.Evil object at 0x7f8cac015ea0>

CPython versions tested on:

3.13, 3.14

Operating systems tested on:

Linux, Windows

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

Python 3.14.0a1+ (heads/main:85799f1ffd, Nov 4 2024, 11:28:25) [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