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

Bugs in asyncio.Future.remove_done_callback() cause segfault. #97592

Open
xiaxinmeng opened this issue Sep 27, 2022 · 4 comments
Open

Bugs in asyncio.Future.remove_done_callback() cause segfault. #97592

xiaxinmeng opened this issue Sep 27, 2022 · 4 comments
Assignees
Labels
3.8 3.9 3.10 3.11 3.12 expert-asyncio type-crash A hard crash of the interpreter, possibly with a core dump

Comments

@xiaxinmeng
Copy link

xiaxinmeng commented Sep 27, 2022

Crash report

The following example triggers a segfault on the latest stable Python3.8.14. I think there might be a bug in asyncio.Future.remove_done_callback().

import asyncio
fut = asyncio.Future()
fut.add_done_callback(str)
for str in range(63):
    fut.add_done_callback(id)

class evil:
    def __eq__(self, other):
        fut.remove_done_callback(other)
fut.remove_done_callback(evil())

Error messages

Segmentation fault (core dumped)

Your environment

  • CPython versions tested on: Python 3.8.14, Python 3.9.0, main branch 68c46ae
  • Operating system and architecture: [GCC 7.5.0] on linux
@xiaxinmeng xiaxinmeng added the type-crash A hard crash of the interpreter, possibly with a core dump label Sep 27, 2022
@gvanrossum
Copy link
Member

gvanrossum commented Sep 27, 2022

Yeah, I can still repro this on 3.11. It doesn't crash when the pure-Python asyncio implementation is used (sys.modules['_asyncio'] = None) so this appears to be crashing in _asyncio.

Someone needs to look into the code there and see what is happening.

@CharlieZhao95
Copy link
Contributor

CharlieZhao95 commented Sep 28, 2022

I simply debugged with the following code:

import asyncio

async def crash():
    fut = asyncio.Future()
    fut.add_done_callback(str)
    for _ in range(10):
        fut.add_done_callback(id)

    class Evil:
        def __eq__(self, other):
            fut.remove_done_callback(other)

    fut.remove_done_callback(Evil())
    asyncio.create_task(set_after(fut, 1, '... world'))
    print('hello ...')
    print(await fut)


if __name__ == "__main__":
    asyncio.run(crash())

The core dump seems to happen in the _asyncio_Future_remove_done_callback function of _asynciomodule.c.
At some point, the self->fut_callbacks is set to null, which causes crashing. In my tests it seems to be related to Py_CLEAR(self->fut_callbacks);

static PyObject *
_asyncio_Future_remove_done_callback(FutureObj *self, PyObject *fn)
{
...
    // The `self->fut_callbacks` here is a nullptr, which causes the core dump
    for (i = 0; i < PyList_GET_SIZE(self->fut_callbacks); i++) {
        int ret;
        PyObject *item = PyList_GET_ITEM(self->fut_callbacks, i);
        Py_INCREF(item);
        ret = PyObject_RichCompareBool(PyTuple_GET_ITEM(item, 0), fn, Py_EQ);
        if (ret == 0) {
            if (j < len) {
                PyList_SET_ITEM(newlist, j, item);
                j++;
                continue;
            }
            ret = PyList_Append(newlist, item);
        }
        Py_DECREF(item);
        if (ret < 0) {
            goto fail;
        }
    }

    if (j == 0) {
        Py_CLEAR(self->fut_callbacks);
        Py_DECREF(newlist);
        return PyLong_FromSsize_t(len + cleared_callback0);
    }
}

@gvanrossum
Copy link
Member

gvanrossum commented Sep 28, 2022

Thanks, that's good research! The cause is now clear: the PyObject_RichCompareBool call invokes the evil class's __eq__ method, which calls the same function and ends up clearing fut_callbacks.

The fix would seem to require some check for whether fut_callbacks is NULL before doing anything further to it. This seems a little tricky, we need to check in the for loop header, but also in the code (that you snipped) below that assigns newlist to a slice of fut_callbacks.

Do you want to create the PR to get full credit?

gvanrossum added a commit to gvanrossum/cpython that referenced this issue Sep 28, 2022
@CharlieZhao95
Copy link
Contributor

CharlieZhao95 commented Sep 29, 2022

Thanks for your guidance! I notice that you have created a commit, I wonder if it would be better to add a test case that can trigger the crash :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3.8 3.9 3.10 3.11 3.12 expert-asyncio type-crash A hard crash of the interpreter, possibly with a core dump
Projects
Status: In Progress
Development

No branches or pull requests

4 participants