Skip to content
4 changes: 4 additions & 0 deletions Lib/asyncio/futures.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ def __init__(self, *, loop=None):
loop object used by the future. If it's not provided, the future uses
the default event loop.
"""
if self._loop is not None:
raise RuntimeError(f"{self.__class__.__name__} object is already "
"initialized")

if loop is None:
self._loop = events.get_event_loop()
else:
Expand Down
31 changes: 4 additions & 27 deletions Lib/test/test_asyncio/test_futures.py
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,10 @@ def test_future_cancelled_exception_refcycles(self):
self.assertIsNotNone(exc)
self.assertListEqual(gc.get_referrers(exc), [])

def test_future_disallow_multiple_initialization(self):
f = self._new_future(loop=self.loop)
with self.assertRaises(RuntimeError, msg="is already initialized"):
f.__init__(loop=self.loop)

@unittest.skipUnless(hasattr(futures, '_CFuture'),
'requires the C _asyncio module')
Expand Down Expand Up @@ -1091,33 +1095,6 @@ def __getattribute__(self, name):
fut.add_done_callback(fut_callback_0)
self.assertRaises(ReachableCode, fut.set_result, "boom")

def test_use_after_free_on_fut_context_0_with_evil__getattribute__(self):
# see: https://github.com/python/cpython/issues/125984

class EvilEventLoop(SimpleEvilEventLoop):
def call_soon(self, *args, **kwargs):
super().call_soon(*args, **kwargs)
raise ReachableCode

def __getattribute__(self, name):
if name == 'call_soon':
# resets the future's event loop
fut.__init__(loop=SimpleEvilEventLoop())
return object.__getattribute__(self, name)

evil_loop = EvilEventLoop()
with mock.patch.object(self, 'loop', evil_loop):
fut = self._new_future()
self.assertIs(fut.get_loop(), evil_loop)

fut_callback_0 = mock.Mock()
fut_context_0 = mock.Mock()
fut.add_done_callback(fut_callback_0, context=fut_context_0)
del fut_context_0
del fut_callback_0
self.assertRaises(ReachableCode, fut.set_result, "boom")


@unittest.skipUnless(hasattr(futures, '_CFuture'),
'requires the C _asyncio module')
class CFutureDoneCallbackTests(BaseFutureDoneCallbackTests,
Expand Down
42 changes: 9 additions & 33 deletions Lib/test/test_asyncio/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2776,28 +2776,17 @@ def test_get_context(self):
finally:
loop.close()

def test_proper_refcounts(self):
# see: https://github.com/python/cpython/issues/126083
class Break:
def __str__(self):
raise RuntimeError("break")

obj = object()
initial_refcount = sys.getrefcount(obj)

coro = coroutine_function()
with contextlib.closing(asyncio.EventLoop()) as loop:
task = asyncio.Task.__new__(asyncio.Task)
for _ in range(5):
with self.assertRaisesRegex(RuntimeError, 'break'):
task.__init__(coro, loop=loop, context=obj, name=Break())

coro.close()
task._log_destroy_pending = False
del task
def test_task_disallow_multiple_initialization(self):
async def foo():
pass

self.assertEqual(sys.getrefcount(obj), initial_refcount)
coro = foo()
self.addCleanup(coro.close)
task = self.new_task(self.loop, coro)
task._log_destroy_pending = False

with self.assertRaises(RuntimeError, msg="is already initialized"):
task.__init__(coro, loop=self.loop)

def add_subclass_tests(cls):
BaseTask = cls.Task
Expand Down Expand Up @@ -2921,19 +2910,6 @@ class CTask_CFuture_Tests(BaseTaskTests, SetMethodsTest,
all_tasks = getattr(tasks, '_c_all_tasks', None)
current_task = staticmethod(getattr(tasks, '_c_current_task', None))

@support.refcount_test
def test_refleaks_in_task___init__(self):
gettotalrefcount = support.get_attribute(sys, 'gettotalrefcount')
async def coro():
pass
task = self.new_task(self.loop, coro())
self.loop.run_until_complete(task)
refs_before = gettotalrefcount()
for i in range(100):
task.__init__(coro(), loop=self.loop)
self.loop.run_until_complete(task)
self.assertAlmostEqual(gettotalrefcount() - refs_before, 0, delta=10)

def test_del__log_destroy_pending_segfault(self):
async def coro():
pass
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix possible crashes when initializing :class:`asyncio.Task` or :class:`asyncio.Future` multiple times.
These classes can now be initialized only once and any subsequent initialization attempt will raise a RuntimeError.
Patch by Kumar Aditya.
24 changes: 6 additions & 18 deletions Modules/_asynciomodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -498,21 +498,13 @@ future_schedule_callbacks(asyncio_state *state, FutureObj *fut)
static int
future_init(FutureObj *fut, PyObject *loop)
{
if (fut->fut_loop != NULL) {
PyErr_Format(PyExc_RuntimeError, "%T object is already initialized", fut);
return -1;
}

PyObject *res;
int is_true;

Py_CLEAR(fut->fut_loop);
Py_CLEAR(fut->fut_callback0);
Py_CLEAR(fut->fut_context0);
Py_CLEAR(fut->fut_callbacks);
Py_CLEAR(fut->fut_result);
Py_CLEAR(fut->fut_exception);
Py_CLEAR(fut->fut_exception_tb);
Py_CLEAR(fut->fut_source_tb);
Py_CLEAR(fut->fut_cancel_msg);
Py_CLEAR(fut->fut_cancelled_exc);
Py_CLEAR(fut->fut_awaited_by);

fut->fut_state = STATE_PENDING;
fut->fut_log_tb = 0;
fut->fut_blocking = 0;
Expand Down Expand Up @@ -3008,11 +3000,7 @@ task_call_step_soon(asyncio_state *state, TaskObj *task, PyObject *arg)
return -1;
}

// Beware: An evil call_soon could alter task_context.
// See: https://github.com/python/cpython/issues/126080.
PyObject *task_context = Py_NewRef(task->task_context);
int ret = call_soon(state, task->task_loop, cb, NULL, task_context);
Py_DECREF(task_context);
int ret = call_soon(state, task->task_loop, cb, NULL, task->task_context);
Py_DECREF(cb);
return ret;
}
Expand Down
Loading