-
-
Notifications
You must be signed in to change notification settings - Fork 31.1k
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
gh-125789: fix side-effects in asyncio
callback scheduling methods
#125833
Changes from all commits
be7cac9
ca61908
640c799
a05cde2
22b21cc
f7b6730
f72c393
378fe09
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
Mitigate interpreter crashes and state corruption due to side-effects in | ||
:meth:`asyncio.Future.remove_done_callback` or other callback scheduling | ||
methods. Patch by Bénédikt Tran. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -441,8 +441,16 @@ future_schedule_callbacks(asyncio_state *state, FutureObj *fut) | |
return 0; | ||
} | ||
|
||
for (i = 0; i < len; i++) { | ||
// Beware: An evil 'call_soon' could change fut_callbacks or its items | ||
// (see https://github.com/python/cpython/issues/125789 for details). | ||
for (i = 0; | ||
fut->fut_callbacks != NULL && i < PyList_GET_SIZE(fut->fut_callbacks); | ||
i++) { | ||
PyObject *cb_tup = PyList_GET_ITEM(fut->fut_callbacks, i); | ||
if (!PyTuple_CheckExact(cb_tup) || PyTuple_GET_SIZE(cb_tup) != 2) { | ||
PyErr_SetString(PyExc_RuntimeError, "corrupted callback tuple"); | ||
return -1; | ||
} | ||
PyObject *cb = PyTuple_GET_ITEM(cb_tup, 0); | ||
PyObject *ctx = PyTuple_GET_ITEM(cb_tup, 1); | ||
|
||
|
@@ -1017,7 +1025,13 @@ _asyncio_Future_remove_done_callback_impl(FutureObj *self, PyTypeObject *cls, | |
ENSURE_FUTURE_ALIVE(state, self) | ||
|
||
if (self->fut_callback0 != NULL) { | ||
int cmp = PyObject_RichCompareBool(self->fut_callback0, fn, Py_EQ); | ||
// Beware: An evil PyObject_RichCompareBool could change fut_callback0 | ||
// (see https://github.com/python/cpython/issues/125789 for details) | ||
// In addition, the reference to self->fut_callback0 may be cleared, | ||
// so we need to temporarily hold it explicitly. | ||
PyObject *fut_callback0 = Py_NewRef(self->fut_callback0); | ||
int cmp = PyObject_RichCompareBool(fut_callback0, fn, Py_EQ); | ||
Py_DECREF(fut_callback0); | ||
if (cmp == -1) { | ||
return NULL; | ||
} | ||
|
@@ -1041,8 +1055,17 @@ _asyncio_Future_remove_done_callback_impl(FutureObj *self, PyTypeObject *cls, | |
|
||
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); | ||
// Beware: An evil PyObject_RichCompareBool could change fut_callbacks | ||
// or its items (see https://github.com/python/cpython/issues/97592 or | ||
// https://github.com/python/cpython/issues/125789 for details). | ||
if (!PyTuple_CheckExact(cb_tup) || PyTuple_GET_SIZE(cb_tup) != 2) { | ||
PyErr_SetString(PyExc_RuntimeError, "corrupted callback tuple"); | ||
return NULL; | ||
} | ||
Py_INCREF(cb_tup); | ||
PyObject *cb = PyTuple_GET_ITEM(cb_tup, 0); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A little nitpicky, but the incref should be on |
||
int cmp = PyObject_RichCompareBool(cb, fn, Py_EQ); | ||
Py_DECREF(cb_tup); | ||
if (cmp == -1) { | ||
return NULL; | ||
} | ||
|
@@ -1060,24 +1083,29 @@ _asyncio_Future_remove_done_callback_impl(FutureObj *self, PyTypeObject *cls, | |
return NULL; | ||
} | ||
|
||
// Beware: PyObject_RichCompareBool below may change fut_callbacks. | ||
// See GH-97592. | ||
// Beware: An evil PyObject_RichCompareBool could change fut_callbacks | ||
// or its items (see https://github.com/python/cpython/issues/97592 or | ||
// https://github.com/python/cpython/issues/125789 for details). | ||
for (i = 0; | ||
self->fut_callbacks != NULL && 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); | ||
PyObject *cb_tup = PyList_GET_ITEM(self->fut_callbacks, i); | ||
if (!PyTuple_CheckExact(cb_tup) || PyTuple_GET_SIZE(cb_tup) != 2) { | ||
PyErr_SetString(PyExc_RuntimeError, "corrupted callback tuple"); | ||
goto fail; | ||
} | ||
Py_INCREF(cb_tup); | ||
PyObject *cb = PyTuple_GET_ITEM(cb_tup, 0); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same thing here, I don't think it's possible to delete |
||
int ret = PyObject_RichCompareBool(cb, fn, Py_EQ); | ||
if (ret == 0) { | ||
if (j < len) { | ||
PyList_SET_ITEM(newlist, j, item); | ||
PyList_SET_ITEM(newlist, j, cb_tup); | ||
j++; | ||
continue; | ||
} | ||
ret = PyList_Append(newlist, item); | ||
ret = PyList_Append(newlist, cb_tup); | ||
} | ||
Py_DECREF(item); | ||
Py_DECREF(cb_tup); | ||
if (ret < 0) { | ||
goto fail; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you have a test case for an evil 'call_soon'?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, you're right. I didn't test but I think we can just call
fut._callbacks.clear()
inside the callback that is being called and that would be it but I'd need to confirm tomorrow.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The callback being called won't run until the loop here is finished (and whatever is running future_schedule_callbacks returns to _run_once)
You do need an evil call_soon that either calls the callback immediately or mutates fut._callbacks itself
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, I mentioned it can be abused in my initial report but never actually followed up on it. Here's a simple crash POC
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for that Nico; I've also added a test for when the callback tuple is changed in the evil
call_soon
.