Skip to content

Commit f72c393

Browse files
committed
Address reviews and improve coverage.
1 parent f7b6730 commit f72c393

File tree

3 files changed

+111
-63
lines changed

3 files changed

+111
-63
lines changed

Lib/asyncio/futures.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,10 +234,11 @@ def remove_done_callback(self, fn):
234234
235235
Returns the number of callbacks removed.
236236
"""
237+
count = len(self._callbacks)
237238
filtered_callbacks = [(f, ctx)
238239
for (f, ctx) in self._callbacks
239240
if f != fn]
240-
removed_count = len(self._callbacks) - len(filtered_callbacks)
241+
removed_count = count - len(filtered_callbacks)
241242
if removed_count:
242243
self._callbacks[:] = filtered_callbacks
243244
return removed_count

Lib/test/test_asyncio/test_futures.py

Lines changed: 101 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -929,50 +929,92 @@ def __eq__(self, other):
929929

930930
fut.remove_done_callback(evil())
931931

932+
# Sanity checks for callback tuples corruption and Use-After-Free.
933+
# Special thanks to Nico-Posada for the original PoCs and ideas.
934+
# See https://github.com/python/cpython/issues/125789.
935+
932936
def test_schedule_callbacks_list_mutation_3(self):
933-
raise NotImplemented
937+
raise NotImplementedError
934938

935939
def _test_schedule_callbacks_list_mutation_3(self, exc_type, exc_text=None):
936-
# see https://github.com/python/cpython/issues/125789 for details
937-
fut = self._new_future()
938-
939940
class evil:
940941
def __eq__(self, other):
941942
global mem
942943
mem = other
943944
return False
944945

945946
cb_pad = lambda: ...
947+
948+
fut = self._new_future()
946949
fut.add_done_callback(cb_pad) # sets fut->fut_callback0
947-
fut.add_done_callback(evil()) # sets first item in fut->fut_callbacks
948-
# Consume fut->fut_callback0 callback but checks the remaining callbacks,
949-
# thereby invoking evil.__eq__().
950-
fut.remove_done_callback(cb_pad)
950+
fut.add_done_callback(evil()) # sets fut->fut_callbacks[0]
951+
# Consume fut->fut_callback0 callback and set it to NULL before
952+
# checking fut->fut_callbacks, thereby invoking evil.__eq__().
953+
removed = fut.remove_done_callback(cb_pad)
954+
self.assertEqual(removed, 1)
951955
self.assertIs(mem, cb_pad)
952-
953-
fake = (
954-
(0).to_bytes(8, 'little') +
955-
id(bytearray).to_bytes(8, 'little') +
956-
(2 ** 63 - 1).to_bytes(8, 'little') +
957-
(0).to_bytes(24, 'little')
958-
)
959-
960-
i2f = lambda num: 5e-324 * num
961-
fut._callbacks[0] = complex(0, i2f(id(fake) + bytes.__basicsize__ - 1))
962-
963-
# We want to call once again evil.__eq__() to set 'mem' to our
964-
# malicious bytearray. However, since we manually modified the
965-
# callbacks list, we will not be able to by-pass the checks.
956+
# Set the malicious callback tuple and trigger a type check on it
957+
# by re-invoking evil.__eq__() through remove_done_callback().
958+
fut._callbacks[0] = complex(0, 0)
966959
if exc_text is None:
967960
self.assertRaises(exc_type, fut.remove_done_callback, evil())
968961
else:
969-
self.assertRaisesRegex(exc_type, exc_text, fut.remove_done_callback, evil())
970-
962+
self.assertRaisesRegex(exc_type, exc_text,
963+
fut.remove_done_callback, evil())
971964
self.assertIs(mem, cb_pad)
972965

973-
# Use-After-Free sanity checks.
974-
# Credits to Nico-Posada for the PoCs.
975-
# See https://github.com/python/cpython/pull/125833.
966+
def _evil_call_soon_event_loop(self, evil_call_soon):
967+
fake_event_loop = lambda: ...
968+
fake_event_loop.call_soon = evil_call_soon
969+
fake_event_loop.get_debug = lambda: False # suppress traceback
970+
return fake_event_loop
971+
972+
def test_evil_call_soon_list_mutation_1(self):
973+
def evil_call_soon(*args, **kwargs):
974+
fut._callbacks.clear()
975+
976+
loop = self._evil_call_soon_event_loop(evil_call_soon)
977+
with mock.patch.object(self, 'loop', loop):
978+
fut = self._new_future()
979+
self.assertIs(fut.get_loop(), loop)
980+
981+
cb_pad = lambda: ...
982+
fut.add_done_callback(cb_pad)
983+
fut.add_done_callback(None)
984+
fut.add_done_callback(None)
985+
986+
removed = fut.remove_done_callback(cb_pad)
987+
self.assertEqual(removed, 1)
988+
self.assertEqual(len(fut._callbacks), 2)
989+
fut.set_result("boom")
990+
# When there are no more callbacks, the Python implementation
991+
# returns an empty list but the C implementation returns None.
992+
self.assertIn(fut._callbacks, (None, []))
993+
994+
def test_evil_call_soon_list_mutation_2(self):
995+
raise NotImplementedError
996+
997+
def _test_evil_call_soon_list_mutation_2(self, exc_type, exc_text=None):
998+
def evil_call_soon(*args, **kwargs):
999+
fut._callbacks[1] = complex(0, 0)
1000+
1001+
loop = self._evil_call_soon_event_loop(evil_call_soon)
1002+
with mock.patch.object(self, 'loop', loop):
1003+
fut = self._new_future()
1004+
self.assertIs(fut.get_loop(), loop)
1005+
1006+
cb_pad = lambda: ...
1007+
fut.add_done_callback(cb_pad)
1008+
fut.add_done_callback(None)
1009+
fut.add_done_callback(None)
1010+
removed = fut.remove_done_callback(cb_pad)
1011+
self.assertEqual(removed, 1)
1012+
self.assertEqual(len(fut._callbacks), 2)
1013+
# The evil 'call_soon' is executed by calling set_result().
1014+
if exc_text is None:
1015+
self.assertRaises(exc_type, fut.set_result, "boom")
1016+
else:
1017+
self.assertRaisesRegex(exc_type, exc_text, fut.set_result, "boom")
9761018

9771019
def test_use_after_free_fixed_1(self):
9781020
fut = self._new_future()
@@ -990,11 +1032,17 @@ def __eq__(self, value):
9901032
cb_pad = lambda: ...
9911033
fut.add_done_callback(cb_pad) # sets fut->fut_callback0
9921034
fut.add_done_callback(setup()) # sets fut->fut_callbacks[0]
993-
# removes callback from fut->fut_callback0 setting it to NULL
994-
fut.remove_done_callback(cb_pad)
995-
fut.remove_done_callback(evil())
1035+
removed = fut.remove_done_callback(cb_pad)
1036+
self.assertEqual(removed, 1)
1037+
1038+
# This triggers evil.__eq__(), thereby clearing fut->fut_callbacks
1039+
# but we will still hold a reference to fut->fut_callbacks[0] until
1040+
# it is no more needed.
1041+
removed = fut.remove_done_callback(evil())
1042+
self.assertEqual(removed, 0)
9961043

9971044
def test_use_after_free_fixed_2(self):
1045+
asserter = self
9981046
fut = self._new_future()
9991047

10001048
class cb_pad:
@@ -1003,11 +1051,13 @@ def __eq__(self, other):
10031051

10041052
class evil(cb_pad):
10051053
def __eq__(self, other):
1006-
fut.remove_done_callback(None)
1054+
removed = fut.remove_done_callback(None)
1055+
asserter.assertEqual(removed, 1)
10071056
return NotImplemented
10081057

10091058
fut.add_done_callback(cb_pad())
1010-
fut.remove_done_callback(evil())
1059+
removed = fut.remove_done_callback(evil())
1060+
self.assertEqual(removed, 1)
10111061

10121062

10131063
@unittest.skipUnless(hasattr(futures, '_CFuture'),
@@ -1019,7 +1069,12 @@ def _new_future(self):
10191069
return futures._CFuture(loop=self.loop)
10201070

10211071
def test_schedule_callbacks_list_mutation_3(self):
1022-
super()._test_schedule_callbacks_list_mutation_3(RuntimeError, 'corrupted')
1072+
errmsg = 'corrupted callback tuple'
1073+
super()._test_schedule_callbacks_list_mutation_3(RuntimeError, errmsg)
1074+
1075+
def test_evil_call_soon_list_mutation_2(self):
1076+
errmsg = 'corrupted callback tuple'
1077+
super()._test_evil_call_soon_list_mutation_2(RuntimeError, errmsg)
10231078

10241079

10251080
@unittest.skipUnless(hasattr(futures, '_CFuture'),
@@ -1033,7 +1088,12 @@ class CSubFuture(futures._CFuture):
10331088
return CSubFuture(loop=self.loop)
10341089

10351090
def test_schedule_callbacks_list_mutation_3(self):
1036-
super()._test_schedule_callbacks_list_mutation_3(RuntimeError, 'corrupted')
1091+
errmsg = 'corrupted callback tuple'
1092+
super()._test_schedule_callbacks_list_mutation_3(RuntimeError, errmsg)
1093+
1094+
def test_evil_call_soon_list_mutation_2(self):
1095+
errmsg = 'corrupted callback tuple'
1096+
super()._test_evil_call_soon_list_mutation_2(RuntimeError, errmsg)
10371097

10381098

10391099
class PyFutureDoneCallbackTests(BaseFutureDoneCallbackTests,
@@ -1045,6 +1105,13 @@ def _new_future(self):
10451105
def test_schedule_callbacks_list_mutation_3(self):
10461106
super()._test_schedule_callbacks_list_mutation_3(TypeError)
10471107

1108+
def test_evil_call_soon_list_mutation_2(self):
1109+
# For this test, the Python implementation raises an IndexError
1110+
# because the attribute fut._callbacks is set to an empty list
1111+
# *before* invoking the callbacks, while the C implementation
1112+
# does not make a temporary copy of the list of callbacks.
1113+
super()._test_evil_call_soon_list_mutation_2(IndexError)
1114+
10481115

10491116
class BaseFutureInheritanceTests:
10501117

Modules/_asynciomodule.c

Lines changed: 8 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -434,11 +434,6 @@ future_schedule_callbacks(asyncio_state *state, FutureObj *fut)
434434
return 0;
435435
}
436436

437-
if (!PyList_CheckExact(fut->fut_callbacks)) {
438-
PyErr_SetString(PyExc_RuntimeError, "corrupted callbacks list");
439-
return -1;
440-
}
441-
442437
len = PyList_GET_SIZE(fut->fut_callbacks);
443438
if (len == 0) {
444439
/* The list of callbacks was empty; clear it and return. */
@@ -448,17 +443,12 @@ future_schedule_callbacks(asyncio_state *state, FutureObj *fut)
448443

449444
// Beware: An evil 'call_soon' could change fut_callbacks or its items
450445
// (see https://github.com/python/cpython/issues/125789 for details).
451-
for (i = 0; fut->fut_callbacks != NULL; i++) {
452-
if (!PyList_CheckExact(fut->fut_callbacks)) {
453-
PyErr_SetString(PyExc_RuntimeError, "corrupted callbacks list");
454-
return -1;
455-
}
456-
if (i >= PyList_GET_SIZE(fut->fut_callbacks)) {
457-
break; // done
458-
}
446+
for (i = 0;
447+
fut->fut_callbacks != NULL && i < PyList_GET_SIZE(fut->fut_callbacks);
448+
i++) {
459449
PyObject *cb_tup = PyList_GET_ITEM(fut->fut_callbacks, i);
460450
if (!PyTuple_CheckExact(cb_tup) || PyTuple_GET_SIZE(cb_tup) != 2) {
461-
PyErr_SetString(PyExc_RuntimeError, "corrupted callback tuple");
451+
PyErr_SetString(PyExc_RuntimeError, "corrupted callback tuple?");
462452
return -1;
463453
}
464454
PyObject *cb = PyTuple_GET_ITEM(cb_tup, 0);
@@ -1057,11 +1047,6 @@ _asyncio_Future_remove_done_callback_impl(FutureObj *self, PyTypeObject *cls,
10571047
return PyLong_FromSsize_t(cleared_callback0);
10581048
}
10591049

1060-
if (!PyList_CheckExact(self->fut_callbacks)) {
1061-
PyErr_SetString(PyExc_RuntimeError, "corrupted callbacks list");
1062-
return NULL;
1063-
}
1064-
10651050
len = PyList_GET_SIZE(self->fut_callbacks);
10661051
if (len == 0) {
10671052
Py_CLEAR(self->fut_callbacks);
@@ -1080,7 +1065,7 @@ _asyncio_Future_remove_done_callback_impl(FutureObj *self, PyTypeObject *cls,
10801065
Py_INCREF(cb_tup);
10811066
PyObject *cb = PyTuple_GET_ITEM(cb_tup, 0);
10821067
int cmp = PyObject_RichCompareBool(cb, fn, Py_EQ);
1083-
Py_XDECREF(cb_tup);
1068+
Py_DECREF(cb_tup);
10841069
if (cmp == -1) {
10851070
return NULL;
10861071
}
@@ -1101,14 +1086,9 @@ _asyncio_Future_remove_done_callback_impl(FutureObj *self, PyTypeObject *cls,
11011086
// Beware: An evil PyObject_RichCompareBool could change fut_callbacks
11021087
// or its items (see https://github.com/python/cpython/issues/97592 or
11031088
// https://github.com/python/cpython/issues/125789 for details).
1104-
for (i = 0; self->fut_callbacks != NULL; i++) {
1105-
if (!PyList_CheckExact(self->fut_callbacks)) {
1106-
PyErr_SetString(PyExc_RuntimeError, "corrupted callbacks list");
1107-
goto fail;
1108-
}
1109-
if (i >= PyList_GET_SIZE(self->fut_callbacks)) {
1110-
break; // done
1111-
}
1089+
for (i = 0;
1090+
self->fut_callbacks != NULL && i < PyList_GET_SIZE(self->fut_callbacks);
1091+
i++) {
11121092
PyObject *cb_tup = PyList_GET_ITEM(self->fut_callbacks, i);
11131093
if (!PyTuple_CheckExact(cb_tup) || PyTuple_GET_SIZE(cb_tup) != 2) {
11141094
PyErr_SetString(PyExc_RuntimeError, "corrupted callback tuple");

0 commit comments

Comments
 (0)