Skip to content

[3.13] gh-125984: fix use-after-free on fut->fut_{callback,context}0 due to an evil loop.__getattribute__ (GH-126003) #126043

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

Merged
merged 1 commit into from
Oct 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 71 additions & 2 deletions Lib/test/test_asyncio/test_futures.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,25 @@ def last_cb():
pass


class ReachableCode(Exception):
"""Exception to raise to indicate that some code was reached.

Use this exception if using mocks is not a good alternative.
"""


class SimpleEvilEventLoop(asyncio.base_events.BaseEventLoop):
"""Base class for UAF and other evil stuff requiring an evil event loop."""

def get_debug(self): # to suppress tracebacks
return False

def __del__(self):
# Automatically close the evil event loop to avoid warnings.
if not self.is_closed() and not self.is_running():
self.close()


class DuckFuture:
# Class that does not inherit from Future but aims to be duck-type
# compatible with it.
Expand Down Expand Up @@ -956,6 +975,7 @@ def __eq__(self, other):
fut.remove_done_callback(evil())

def test_evil_call_soon_list_mutation(self):
# see: https://github.com/python/cpython/issues/125969
called_on_fut_callback0 = False

pad = lambda: ...
Expand All @@ -970,9 +990,8 @@ def evil_call_soon(*args, **kwargs):
else:
called_on_fut_callback0 = True

fake_event_loop = lambda: ...
fake_event_loop = SimpleEvilEventLoop()
fake_event_loop.call_soon = evil_call_soon
fake_event_loop.get_debug = lambda: False # suppress traceback

with mock.patch.object(self, 'loop', fake_event_loop):
fut = self._new_future()
Expand All @@ -988,6 +1007,56 @@ def evil_call_soon(*args, **kwargs):
# returns an empty list but the C implementation returns None.
self.assertIn(fut._callbacks, (None, []))

def test_use_after_free_on_fut_callback_0_with_evil__getattribute__(self):
# see: https://github.com/python/cpython/issues/125984

class EvilEventLoop(SimpleEvilEventLoop):
def call_soon(self, *args, **kwargs):
super().call_soon(*args, **kwargs)
raise ReachableCode

def __getattribute__(self, name):
nonlocal fut_callback_0
if name == 'call_soon':
fut.remove_done_callback(fut_callback_0)
del fut_callback_0
return object.__getattribute__(self, name)

evil_loop = EvilEventLoop()
with mock.patch.object(self, 'loop', evil_loop):
fut = self._new_future()
self.assertIs(fut.get_loop(), evil_loop)

fut_callback_0 = lambda: ...
fut.add_done_callback(fut_callback_0)
self.assertRaises(ReachableCode, fut.set_result, "boom")

def test_use_after_free_on_fut_context_0_with_evil__getattribute__(self):
# see: https://github.com/python/cpython/issues/125984

class EvilEventLoop(SimpleEvilEventLoop):
def call_soon(self, *args, **kwargs):
super().call_soon(*args, **kwargs)
raise ReachableCode

def __getattribute__(self, name):
if name == 'call_soon':
# resets the future's event loop
fut.__init__(loop=SimpleEvilEventLoop())
return object.__getattribute__(self, name)

evil_loop = EvilEventLoop()
with mock.patch.object(self, 'loop', evil_loop):
fut = self._new_future()
self.assertIs(fut.get_loop(), evil_loop)

fut_callback_0 = mock.Mock()
fut_context_0 = mock.Mock()
fut.add_done_callback(fut_callback_0, context=fut_context_0)
del fut_context_0
del fut_callback_0
self.assertRaises(ReachableCode, fut.set_result, "boom")


@unittest.skipUnless(hasattr(futures, '_CFuture'),
'requires the C _asyncio module')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix use-after-free crashes on :class:`asyncio.Future` objects for which the
underlying event loop implements an evil :meth:`~object.__getattribute__`.
Reported by Nico-Posada. Patch by Bénédikt Tran.
19 changes: 13 additions & 6 deletions Modules/_asynciomodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -355,12 +355,19 @@ future_schedule_callbacks(asyncio_state *state, FutureObj *fut)
if (fut->fut_callback0 != NULL) {
/* There's a 1st callback */

int ret = call_soon(state,
fut->fut_loop, fut->fut_callback0,
(PyObject *)fut, fut->fut_context0);

Py_CLEAR(fut->fut_callback0);
Py_CLEAR(fut->fut_context0);
// Beware: An evil call_soon could alter fut_callback0 or fut_context0.
// Since we are anyway clearing them after the call, whether call_soon
// succeeds or not, the idea is to transfer ownership so that external
// code is not able to alter them during the call.
PyObject *fut_callback0 = fut->fut_callback0;
fut->fut_callback0 = NULL;
PyObject *fut_context0 = fut->fut_context0;
fut->fut_context0 = NULL;

int ret = call_soon(state, fut->fut_loop, fut_callback0,
(PyObject *)fut, fut_context0);
Py_CLEAR(fut_callback0);
Py_CLEAR(fut_context0);
if (ret) {
/* If an error occurs in pure-Python implementation,
all callbacks are cleared. */
Expand Down
Loading