Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions Lib/test/test_asyncio/test_tasks.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Tests for tasks.py."""

import _thread
import collections
import contextlib
import contextvars
Expand All @@ -20,6 +21,7 @@
from asyncio import tasks
from test.test_asyncio import utils as test_utils
from test import support
from test.support import threading_helper
from test.support.script_helper import assert_python_ok


Expand Down Expand Up @@ -3095,6 +3097,73 @@ class CTask_CFuture_Tests(BaseTaskTests, SetMethodsTest,
all_tasks = getattr(tasks, '_c_all_tasks', None)
current_task = staticmethod(getattr(tasks, '_c_current_task', None))

@threading_helper.requires_working_threading()
def test_task_created_during_thread_state_clear(self):
loop = self.loop
task = None
var = contextvars.ContextVar('var')
# Avoid recreating the thread state's context after it is cleared.
task_context = contextvars.Context()

async def noop():
pass

class CreatesTaskOnClear:
def __del__(self):
nonlocal task
task = loop.create_task(noop(), context=task_context)

def create_finalizer():
# gh-159041: Run the finalizer when PyThreadState_Clear()
# clears the thread's context.
var.set(CreatesTaskOnClear())

# threading.Thread owns its Context, so use _thread to make the
# Context lifetime match the thread state's lifetime.
handle = _thread.start_joinable_thread(create_finalizer)
handle.join(support.SHORT_TIMEOUT)
self.assertTrue(handle.is_done(), 'thread failed to exit')
self.assertIsNotNone(task)

try:
self.assertEqual(self.all_tasks(loop), {task})
finally:
loop.run_until_complete(task)

def test_task_created_during_interpreter_clear(self):
code = """if 1:
import asyncio
import contextvars
import warnings

async def noop():
pass

class Loop:
def get_debug(self):
return False

def call_soon(self, callback, *args, context=None):
self.callback = callback

class CreatesTaskOnClear:
def __init__(self):
self.task_type = asyncio.Task
self.coro = noop()
self.loop = Loop()
self.context = contextvars.Context()

def __del__(self):
self.task_type(
self.coro, loop=self.loop, context=self.context)

# Leave the only reference to the filters list in the warnings
# state, which is cleared after the final garbage collection.
warnings.filters.append(CreatesTaskOnClear())
del warnings.filters
"""
assert_python_ok('-c', code)

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,2 @@
Fix a use-after-free in :mod:`asyncio` task bookkeeping when a finalizer
creates a task while its thread state is being cleared.
32 changes: 25 additions & 7 deletions Python/pystate.c
Original file line number Diff line number Diff line change
Expand Up @@ -812,6 +812,21 @@ common_constants_clear(PyInterpreterState *interp)
}


static void
move_asyncio_tasks_to_interpreter(PyThreadState *tstate)
{
_PyThreadStateImpl *tstate_impl = (_PyThreadStateImpl *)tstate;
if (llist_empty(&tstate_impl->asyncio_tasks_head)) {
return;
}

PyMutex_Lock(&tstate->interp->asyncio_tasks_lock);
llist_concat(&tstate->interp->asyncio_tasks_head,
&tstate_impl->asyncio_tasks_head);
PyMutex_Unlock(&tstate->interp->asyncio_tasks_lock);
}


static void
interpreter_clear(PyInterpreterState *interp, PyThreadState *tstate)
{
Expand Down Expand Up @@ -951,6 +966,9 @@ interpreter_clear(PyInterpreterState *interp, PyThreadState *tstate)
#endif

if (tstate->interp == interp) {
// gh-159041: Finalizers above may have registered new asyncio tasks.
move_asyncio_tasks_to_interpreter(tstate);

/* We are now safe to fix tstate->_status.cleared. */
// XXX Do this (much) earlier?
tstate->_status.cleared = 1;
Expand Down Expand Up @@ -1837,13 +1855,6 @@ PyThreadState_Clear(PyThreadState *tstate)
Py_CLEAR(((_PyThreadStateImpl *)tstate)->asyncio_running_task);


PyMutex_Lock(&tstate->interp->asyncio_tasks_lock);
// merge any lingering tasks from thread state to interpreter's
// tasks list
llist_concat(&tstate->interp->asyncio_tasks_head,
&((_PyThreadStateImpl *)tstate)->asyncio_tasks_head);
PyMutex_Unlock(&tstate->interp->asyncio_tasks_lock);

Py_CLEAR(tstate->dict);
Py_CLEAR(tstate->async_exc);

Expand Down Expand Up @@ -1910,6 +1921,11 @@ PyThreadState_Clear(PyThreadState *tstate)
_PyJit_TracerFree((_PyThreadStateImpl *)tstate);
#endif

// Merge any lingering tasks from the thread state to the interpreter's
// tasks list. This must happen after all cleanup which can run finalizers,
// since those finalizers may create and register new tasks.
move_asyncio_tasks_to_interpreter(tstate);

tstate->_status.cleared = 1;

// XXX Call _PyThreadStateSwap(runtime, NULL) here if "current".
Expand All @@ -1924,6 +1940,8 @@ static void
tstate_delete_common(PyThreadState *tstate, int release_gil)
{
assert(tstate->_status.cleared && !tstate->_status.finalized);
assert(llist_empty(
&((_PyThreadStateImpl *)tstate)->asyncio_tasks_head));
tstate_verify_not_active(tstate);
assert(!_PyThreadState_IsRunningMain(tstate));

Expand Down
Loading