You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Bugs in asyncio.Future.remove_done_callback() cause segfault. #97592
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
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.
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);
staticPyObject*_asyncio_Future_remove_done_callback(FutureObj*self, PyObject*fn)
{
...
// The `self->fut_callbacks` here is a nullptr, which causes the core dumpfor (i=0; i<PyList_GET_SIZE(self->fut_callbacks); i++) {
intret;
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);
returnPyLong_FromSsize_t(len+cleared_callback0);
}
}
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.
FWIW looking for a good place to add that test I noticed that there was a very similar test case already, fixing #73149. @xiaxinmeng did you derive your demo from that code?
FWIW looking for a good place to add that test I noticed that there was a very similar test case already, fixing #73149. @xiaxinmeng did you derive your demo from that code?
Thanks for your efforts on fixing this bug! Yes, this case is derived from that code. The original bug has been fixed and the original test case cannot crash Python anymore. So we mutate the dependency relations in the original code and found this one crashing Python again.
Activity
gvanrossum commentedon 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 commentedon Sep 28, 2022
I simply debugged with the following code:
The core dump seems to happen in the
_asyncio_Future_remove_done_callbackfunction of_asynciomodule.c.At some point, the
self->fut_callbacksis set to null, which causes crashing. In my tests it seems to be related toPy_CLEAR(self->fut_callbacks);gvanrossum commentedon Sep 28, 2022
Thanks, that's good research! The cause is now clear: the
PyObject_RichCompareBoolcall invokes theevilclass's__eq__method, which calls the same function and ends up clearingfut_callbacks.The fix would seem to require some check for whether
fut_callbacksis NULL before doing anything further to it. This seems a little tricky, we need to check in theforloop header, but also in the code (that you snipped) below that assignsnewlistto a slice offut_callbacks.Do you want to create the PR to get full credit?
pythonGH-97592: Anticipate fut_callbacks being cleared by PyObject_Ri…
CharlieZhao95 commentedon 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 :)
gvanrossum commentedon Sep 29, 2022
Yeah, I didn't get to that yet. It should be a simple case of converting your test case to a unit test.
gvanrossum commentedon Sep 29, 2022
FWIW looking for a good place to add that test I noticed that there was a very similar test case already, fixing #73149. @xiaxinmeng did you derive your demo from that code?
pythonGH-97592: Anticipate fut_callbacks being cleared by PyObject_Ri…
xiaxinmeng commentedon Sep 30, 2022
Thanks for your efforts on fixing this bug! Yes, this case is derived from that code. The original bug has been fixed and the original test case cannot crash Python anymore. So we mutate the dependency relations in the original code and found this one crashing Python again.
GH-97592: Fix crash in C remove_done_callback due to evil code (#97660)
pythonGH-97592: Fix crash in C remove_done_callback due to evil code (p…
pythonGH-97592: Fix crash in C remove_done_callback due to evil code (p…
GH-97592: Fix crash in C remove_done_callback due to evil code (GH-97660
GH-97592: Fix crash in C remove_done_callback due to evil code (GH-97660
pythonGH-97592: Fix crash in C remove_done_callback due to evil code (p…