Skip to content

Commit bad4fd3

Browse files
committed
pythongh-125984: fix use-after-free on fut->fut_{callback,context}0 due to an evil loop.__getattribute__ (python#126003)
1 parent 353994f commit bad4fd3

File tree

3 files changed

+87
-8
lines changed

3 files changed

+87
-8
lines changed

Lib/test/test_asyncio/test_futures.py

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,25 @@ def last_cb():
3232
pass
3333

3434

35+
class ReachableCode(Exception):
36+
"""Exception to raise to indicate that some code was reached.
37+
38+
Use this exception if using mocks is not a good alternative.
39+
"""
40+
41+
42+
class SimpleEvilEventLoop(asyncio.base_events.BaseEventLoop):
43+
"""Base class for UAF and other evil stuff requiring an evil event loop."""
44+
45+
def get_debug(self): # to suppress tracebacks
46+
return False
47+
48+
def __del__(self):
49+
# Automatically close the evil event loop to avoid warnings.
50+
if not self.is_closed() and not self.is_running():
51+
self.close()
52+
53+
3554
class DuckFuture:
3655
# Class that does not inherit from Future but aims to be duck-type
3756
# compatible with it.
@@ -948,6 +967,7 @@ def __eq__(self, other):
948967
fut.remove_done_callback(evil())
949968

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

953973
pad = lambda: ...
@@ -962,9 +982,8 @@ def evil_call_soon(*args, **kwargs):
962982
else:
963983
called_on_fut_callback0 = True
964984

965-
fake_event_loop = lambda: ...
985+
fake_event_loop = SimpleEvilEventLoop()
966986
fake_event_loop.call_soon = evil_call_soon
967-
fake_event_loop.get_debug = lambda: False # suppress traceback
968987

969988
with mock.patch.object(self, 'loop', fake_event_loop):
970989
fut = self._new_future()
@@ -980,6 +999,56 @@ def evil_call_soon(*args, **kwargs):
980999
# returns an empty list but the C implementation returns None.
9811000
self.assertIn(fut._callbacks, (None, []))
9821001

1002+
def test_use_after_free_on_fut_callback_0_with_evil__getattribute__(self):
1003+
# see: https://github.com/python/cpython/issues/125984
1004+
1005+
class EvilEventLoop(SimpleEvilEventLoop):
1006+
def call_soon(self, *args, **kwargs):
1007+
super().call_soon(*args, **kwargs)
1008+
raise ReachableCode
1009+
1010+
def __getattribute__(self, name):
1011+
nonlocal fut_callback_0
1012+
if name == 'call_soon':
1013+
fut.remove_done_callback(fut_callback_0)
1014+
del fut_callback_0
1015+
return object.__getattribute__(self, name)
1016+
1017+
evil_loop = EvilEventLoop()
1018+
with mock.patch.object(self, 'loop', evil_loop):
1019+
fut = self._new_future()
1020+
self.assertIs(fut.get_loop(), evil_loop)
1021+
1022+
fut_callback_0 = lambda: ...
1023+
fut.add_done_callback(fut_callback_0)
1024+
self.assertRaises(ReachableCode, fut.set_result, "boom")
1025+
1026+
def test_use_after_free_on_fut_context_0_with_evil__getattribute__(self):
1027+
# see: https://github.com/python/cpython/issues/125984
1028+
1029+
class EvilEventLoop(SimpleEvilEventLoop):
1030+
def call_soon(self, *args, **kwargs):
1031+
super().call_soon(*args, **kwargs)
1032+
raise ReachableCode
1033+
1034+
def __getattribute__(self, name):
1035+
if name == 'call_soon':
1036+
# resets the future's event loop
1037+
fut.__init__(loop=SimpleEvilEventLoop())
1038+
return object.__getattribute__(self, name)
1039+
1040+
evil_loop = EvilEventLoop()
1041+
with mock.patch.object(self, 'loop', evil_loop):
1042+
fut = self._new_future()
1043+
self.assertIs(fut.get_loop(), evil_loop)
1044+
1045+
fut_callback_0 = mock.Mock()
1046+
fut_context_0 = mock.Mock()
1047+
fut.add_done_callback(fut_callback_0, context=fut_context_0)
1048+
del fut_context_0
1049+
del fut_callback_0
1050+
self.assertRaises(ReachableCode, fut.set_result, "boom")
1051+
9831052

9841053
@unittest.skipUnless(hasattr(futures, '_CFuture'),
9851054
'requires the C _asyncio module')
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix use-after-free crashes on :class:`asyncio.Future` objects for which the
2+
underlying event loop implements an evil :meth:`~object.__getattribute__`.
3+
Reported by Nico-Posada. Patch by Bénédikt Tran.

Modules/_asynciomodule.c

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -409,12 +409,19 @@ future_schedule_callbacks(asyncio_state *state, FutureObj *fut)
409409
if (fut->fut_callback0 != NULL) {
410410
/* There's a 1st callback */
411411

412-
int ret = call_soon(state,
413-
fut->fut_loop, fut->fut_callback0,
414-
(PyObject *)fut, fut->fut_context0);
415-
416-
Py_CLEAR(fut->fut_callback0);
417-
Py_CLEAR(fut->fut_context0);
412+
// Beware: An evil call_soon could alter fut_callback0 or fut_context0.
413+
// Since we are anyway clearing them after the call, whether call_soon
414+
// succeeds or not, the idea is to transfer ownership so that external
415+
// code is not able to alter them during the call.
416+
PyObject *fut_callback0 = fut->fut_callback0;
417+
fut->fut_callback0 = NULL;
418+
PyObject *fut_context0 = fut->fut_context0;
419+
fut->fut_context0 = NULL;
420+
421+
int ret = call_soon(state, fut->fut_loop, fut_callback0,
422+
(PyObject *)fut, fut_context0);
423+
Py_CLEAR(fut_callback0);
424+
Py_CLEAR(fut_context0);
418425
if (ret) {
419426
/* If an error occurs in pure-Python implementation,
420427
all callbacks are cleared. */

0 commit comments

Comments
 (0)