Skip to content

Commit fdedb26

Browse files
[3.12] gh-125984: fix use-after-free on fut->fut_{callback,context}0 due to an evil loop.__getattribute__ (GH-126003) (#126044)
gh-125984: fix use-after-free on `fut->fut_{callback,context}0` due to an evil `loop.__getattribute__` (GH-126003) (cherry picked from commit f819d43) Co-authored-by: Bénédikt Tran <[email protected]>
1 parent 67b2701 commit fdedb26

File tree

3 files changed

+87
-8
lines changed

3 files changed

+87
-8
lines changed

Lib/test/test_asyncio/test_futures.py

+71-2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,25 @@ def last_cb():
3131
pass
3232

3333

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

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

942962
pad = lambda: ...
@@ -951,9 +971,8 @@ def evil_call_soon(*args, **kwargs):
951971
else:
952972
called_on_fut_callback0 = True
953973

954-
fake_event_loop = lambda: ...
974+
fake_event_loop = SimpleEvilEventLoop()
955975
fake_event_loop.call_soon = evil_call_soon
956-
fake_event_loop.get_debug = lambda: False # suppress traceback
957976

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

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

9731042
@unittest.skipUnless(hasattr(futures, '_CFuture'),
9741043
'requires the C _asyncio module')
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

+13-6
Original file line numberDiff line numberDiff line change
@@ -433,12 +433,19 @@ future_schedule_callbacks(asyncio_state *state, FutureObj *fut)
433433
if (fut->fut_callback0 != NULL) {
434434
/* There's a 1st callback */
435435

436-
int ret = call_soon(state,
437-
fut->fut_loop, fut->fut_callback0,
438-
(PyObject *)fut, fut->fut_context0);
439-
440-
Py_CLEAR(fut->fut_callback0);
441-
Py_CLEAR(fut->fut_context0);
436+
// Beware: An evil call_soon could alter fut_callback0 or fut_context0.
437+
// Since we are anyway clearing them after the call, whether call_soon
438+
// succeeds or not, the idea is to transfer ownership so that external
439+
// code is not able to alter them during the call.
440+
PyObject *fut_callback0 = fut->fut_callback0;
441+
fut->fut_callback0 = NULL;
442+
PyObject *fut_context0 = fut->fut_context0;
443+
fut->fut_context0 = NULL;
444+
445+
int ret = call_soon(state, fut->fut_loop, fut_callback0,
446+
(PyObject *)fut, fut_context0);
447+
Py_CLEAR(fut_callback0);
448+
Py_CLEAR(fut_context0);
442449
if (ret) {
443450
/* If an error occurs in pure-Python implementation,
444451
all callbacks are cleared. */

0 commit comments

Comments
 (0)