Skip to content

Commit c5b99f5

Browse files
picnixzasvetlov
andauthored
gh-125969: fix OOB in future_schedule_callbacks due to an evil call_soon (#125970)
Co-authored-by: Andrew Svetlov <[email protected]>
1 parent 1384409 commit c5b99f5

File tree

3 files changed

+47
-17
lines changed

3 files changed

+47
-17
lines changed

Lib/test/test_asyncio/test_futures.py

+33
Original file line numberDiff line numberDiff line change
@@ -947,6 +947,39 @@ def __eq__(self, other):
947947

948948
fut.remove_done_callback(evil())
949949

950+
def test_evil_call_soon_list_mutation(self):
951+
called_on_fut_callback0 = False
952+
953+
pad = lambda: ...
954+
955+
def evil_call_soon(*args, **kwargs):
956+
nonlocal called_on_fut_callback0
957+
if called_on_fut_callback0:
958+
# Called when handling fut->fut_callbacks[0]
959+
# and mutates the length fut->fut_callbacks.
960+
fut.remove_done_callback(int)
961+
fut.remove_done_callback(pad)
962+
else:
963+
called_on_fut_callback0 = True
964+
965+
fake_event_loop = lambda: ...
966+
fake_event_loop.call_soon = evil_call_soon
967+
fake_event_loop.get_debug = lambda: False # suppress traceback
968+
969+
with mock.patch.object(self, 'loop', fake_event_loop):
970+
fut = self._new_future()
971+
self.assertIs(fut.get_loop(), fake_event_loop)
972+
973+
fut.add_done_callback(str) # sets fut->fut_callback0
974+
fut.add_done_callback(int) # sets fut->fut_callbacks[0]
975+
fut.add_done_callback(pad) # sets fut->fut_callbacks[1]
976+
fut.add_done_callback(pad) # sets fut->fut_callbacks[2]
977+
fut.set_result("boom")
978+
979+
# When there are no more callbacks, the Python implementation
980+
# returns an empty list but the C implementation returns None.
981+
self.assertIn(fut._callbacks, (None, []))
982+
950983

951984
@unittest.skipUnless(hasattr(futures, '_CFuture'),
952985
'requires the C _asyncio module')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix an out-of-bounds crash when an evil :meth:`asyncio.loop.call_soon`
2+
mutates the length of the internal callbacks list. Patch by Bénédikt Tran.

Modules/_asynciomodule.c

+12-17
Original file line numberDiff line numberDiff line change
@@ -406,9 +406,6 @@ future_ensure_alive(FutureObj *fut)
406406
static int
407407
future_schedule_callbacks(asyncio_state *state, FutureObj *fut)
408408
{
409-
Py_ssize_t len;
410-
Py_ssize_t i;
411-
412409
if (fut->fut_callback0 != NULL) {
413410
/* There's a 1st callback */
414411

@@ -434,27 +431,25 @@ future_schedule_callbacks(asyncio_state *state, FutureObj *fut)
434431
return 0;
435432
}
436433

437-
len = PyList_GET_SIZE(fut->fut_callbacks);
438-
if (len == 0) {
439-
/* The list of callbacks was empty; clear it and return. */
440-
Py_CLEAR(fut->fut_callbacks);
441-
return 0;
442-
}
443-
444-
for (i = 0; i < len; i++) {
445-
PyObject *cb_tup = PyList_GET_ITEM(fut->fut_callbacks, i);
434+
// Beware: An evil call_soon could change fut->fut_callbacks.
435+
// The idea is to transfer the ownership of the callbacks list
436+
// so that external code is not able to mutate the list during
437+
// the iteration.
438+
PyObject *callbacks = fut->fut_callbacks;
439+
fut->fut_callbacks = NULL;
440+
Py_ssize_t n = PyList_GET_SIZE(callbacks);
441+
for (Py_ssize_t i = 0; i < n; i++) {
442+
assert(PyList_GET_SIZE(callbacks) == n);
443+
PyObject *cb_tup = PyList_GET_ITEM(callbacks, i);
446444
PyObject *cb = PyTuple_GET_ITEM(cb_tup, 0);
447445
PyObject *ctx = PyTuple_GET_ITEM(cb_tup, 1);
448446

449447
if (call_soon(state, fut->fut_loop, cb, (PyObject *)fut, ctx)) {
450-
/* If an error occurs in pure-Python implementation,
451-
all callbacks are cleared. */
452-
Py_CLEAR(fut->fut_callbacks);
448+
Py_DECREF(callbacks);
453449
return -1;
454450
}
455451
}
456-
457-
Py_CLEAR(fut->fut_callbacks);
452+
Py_DECREF(callbacks);
458453
return 0;
459454
}
460455

0 commit comments

Comments
 (0)