Skip to content

Commit f6682fb

Browse files
miss-islingtonpicnixzasvetlov
authored
[3.12] gh-125969: fix OOB in future_schedule_callbacks due to an evil call_soon (GH-125970) (#125992)
gh-125969: fix OOB in `future_schedule_callbacks` due to an evil `call_soon` (GH-125970) (cherry picked from commit c5b99f5) Co-authored-by: Bénédikt Tran <[email protected]> Co-authored-by: Andrew Svetlov <[email protected]>
1 parent 42927f7 commit f6682fb

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
@@ -936,6 +936,39 @@ def __eq__(self, other):
936936

937937
fut.remove_done_callback(evil())
938938

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

940973
@unittest.skipUnless(hasattr(futures, '_CFuture'),
941974
'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
@@ -430,9 +430,6 @@ future_ensure_alive(FutureObj *fut)
430430
static int
431431
future_schedule_callbacks(asyncio_state *state, FutureObj *fut)
432432
{
433-
Py_ssize_t len;
434-
Py_ssize_t i;
435-
436433
if (fut->fut_callback0 != NULL) {
437434
/* There's a 1st callback */
438435

@@ -458,27 +455,25 @@ future_schedule_callbacks(asyncio_state *state, FutureObj *fut)
458455
return 0;
459456
}
460457

461-
len = PyList_GET_SIZE(fut->fut_callbacks);
462-
if (len == 0) {
463-
/* The list of callbacks was empty; clear it and return. */
464-
Py_CLEAR(fut->fut_callbacks);
465-
return 0;
466-
}
467-
468-
for (i = 0; i < len; i++) {
469-
PyObject *cb_tup = PyList_GET_ITEM(fut->fut_callbacks, i);
458+
// Beware: An evil call_soon could change fut->fut_callbacks.
459+
// The idea is to transfer the ownership of the callbacks list
460+
// so that external code is not able to mutate the list during
461+
// the iteration.
462+
PyObject *callbacks = fut->fut_callbacks;
463+
fut->fut_callbacks = NULL;
464+
Py_ssize_t n = PyList_GET_SIZE(callbacks);
465+
for (Py_ssize_t i = 0; i < n; i++) {
466+
assert(PyList_GET_SIZE(callbacks) == n);
467+
PyObject *cb_tup = PyList_GET_ITEM(callbacks, i);
470468
PyObject *cb = PyTuple_GET_ITEM(cb_tup, 0);
471469
PyObject *ctx = PyTuple_GET_ITEM(cb_tup, 1);
472470

473471
if (call_soon(state, fut->fut_loop, cb, (PyObject *)fut, ctx)) {
474-
/* If an error occurs in pure-Python implementation,
475-
all callbacks are cleared. */
476-
Py_CLEAR(fut->fut_callbacks);
472+
Py_DECREF(callbacks);
477473
return -1;
478474
}
479475
}
480-
481-
Py_CLEAR(fut->fut_callbacks);
476+
Py_DECREF(callbacks);
482477
return 0;
483478
}
484479

0 commit comments

Comments
 (0)