Skip to content

Commit

Permalink
gh-125984: fix use-after-free on fut->fut_{callback,context}0 due t…
Browse files Browse the repository at this point in the history
…o an evil `loop.__getattribute__` (GH-126003)

(cherry picked from commit f819d43)

Co-authored-by: Bénédikt Tran <[email protected]>
  • Loading branch information
picnixz authored and miss-islington committed Oct 27, 2024
1 parent 67b2701 commit acad5a1
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 8 deletions.
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 @@ -31,6 +31,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 @@ -937,6 +956,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 @@ -951,9 +971,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 @@ -969,6 +988,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 @@ -433,12 +433,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

0 comments on commit acad5a1

Please sign in to comment.