Skip to content

Commit bb63e8c

Browse files
committed
gh-159041: Fix asyncio task tracking during thread state cleanup
Move lingering asyncio tasks to the interpreter only after all finalizer-capable thread-state cleanup has completed. Also transfer tasks created by finalizers during interpreter shutdown. Add regression tests for both thread-state and interpreter cleanup paths.
1 parent 5a22a62 commit bb63e8c

3 files changed

Lines changed: 85 additions & 7 deletions

File tree

‎Lib/test/test_asyncio/test_tasks.py‎

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Tests for tasks.py."""
22

3+
import _thread
34
import collections
45
import contextlib
56
import contextvars
@@ -20,6 +21,7 @@
2021
from asyncio import tasks
2122
from test.test_asyncio import utils as test_utils
2223
from test import support
24+
from test.support import threading_helper
2325
from test.support.script_helper import assert_python_ok
2426

2527

@@ -3095,6 +3097,68 @@ class CTask_CFuture_Tests(BaseTaskTests, SetMethodsTest,
30953097
all_tasks = getattr(tasks, '_c_all_tasks', None)
30963098
current_task = staticmethod(getattr(tasks, '_c_current_task', None))
30973099

3100+
@threading_helper.requires_working_threading()
3101+
def test_task_created_during_thread_state_clear(self):
3102+
loop = self.loop
3103+
task = None
3104+
var = contextvars.ContextVar('var')
3105+
task_context = contextvars.Context()
3106+
3107+
async def noop():
3108+
pass
3109+
3110+
class CreatesTaskOnClear:
3111+
def __del__(self):
3112+
nonlocal task
3113+
task = loop.create_task(noop(), context=task_context)
3114+
3115+
def create_finalizer():
3116+
var.set(CreatesTaskOnClear())
3117+
3118+
handle = _thread.start_joinable_thread(create_finalizer)
3119+
handle.join(support.SHORT_TIMEOUT)
3120+
self.assertTrue(handle.is_done())
3121+
self.assertIsNotNone(task)
3122+
3123+
try:
3124+
self.assertEqual(self.all_tasks(loop), {task})
3125+
finally:
3126+
loop.run_until_complete(task)
3127+
3128+
def test_task_created_during_interpreter_clear(self):
3129+
code = """if 1:
3130+
import asyncio
3131+
import contextvars
3132+
import warnings
3133+
3134+
async def noop():
3135+
pass
3136+
3137+
class Loop:
3138+
def get_debug(self):
3139+
return False
3140+
3141+
def call_soon(self, callback, *args, context=None):
3142+
self.callback = callback
3143+
3144+
class CreatesTaskOnClear:
3145+
def __init__(self):
3146+
self.task_type = asyncio.Task
3147+
self.coro = noop()
3148+
self.loop = Loop()
3149+
self.context = contextvars.Context()
3150+
3151+
def __del__(self):
3152+
self.task_type(
3153+
self.coro, loop=self.loop, context=self.context)
3154+
3155+
# Leave the only reference to the filters list in the warnings
3156+
# state, which is cleared after the final garbage collection.
3157+
warnings.filters.append(CreatesTaskOnClear())
3158+
del warnings.filters
3159+
"""
3160+
assert_python_ok('-c', code)
3161+
30983162
def test_del__log_destroy_pending_segfault(self):
30993163
async def coro():
31003164
pass
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix a use-after-free in :mod:`asyncio` task bookkeeping when a finalizer
2+
creates a task while its thread state is being cleared.

‎Python/pystate.c‎

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -812,6 +812,16 @@ common_constants_clear(PyInterpreterState *interp)
812812
}
813813

814814

815+
static void
816+
move_asyncio_tasks_to_interpreter(PyThreadState *tstate)
817+
{
818+
PyMutex_Lock(&tstate->interp->asyncio_tasks_lock);
819+
llist_concat(&tstate->interp->asyncio_tasks_head,
820+
&((_PyThreadStateImpl *)tstate)->asyncio_tasks_head);
821+
PyMutex_Unlock(&tstate->interp->asyncio_tasks_lock);
822+
}
823+
824+
815825
static void
816826
interpreter_clear(PyInterpreterState *interp, PyThreadState *tstate)
817827
{
@@ -953,6 +963,10 @@ interpreter_clear(PyInterpreterState *interp, PyThreadState *tstate)
953963
if (tstate->interp == interp) {
954964
/* We are now safe to fix tstate->_status.cleared. */
955965
// XXX Do this (much) earlier?
966+
// Finalizers above may have registered new asyncio tasks.
967+
move_asyncio_tasks_to_interpreter(tstate);
968+
assert(llist_empty(
969+
&((_PyThreadStateImpl *)tstate)->asyncio_tasks_head));
956970
tstate->_status.cleared = 1;
957971
}
958972

@@ -1837,13 +1851,6 @@ PyThreadState_Clear(PyThreadState *tstate)
18371851
Py_CLEAR(((_PyThreadStateImpl *)tstate)->asyncio_running_task);
18381852

18391853

1840-
PyMutex_Lock(&tstate->interp->asyncio_tasks_lock);
1841-
// merge any lingering tasks from thread state to interpreter's
1842-
// tasks list
1843-
llist_concat(&tstate->interp->asyncio_tasks_head,
1844-
&((_PyThreadStateImpl *)tstate)->asyncio_tasks_head);
1845-
PyMutex_Unlock(&tstate->interp->asyncio_tasks_lock);
1846-
18471854
Py_CLEAR(tstate->dict);
18481855
Py_CLEAR(tstate->async_exc);
18491856

@@ -1910,6 +1917,11 @@ PyThreadState_Clear(PyThreadState *tstate)
19101917
_PyJit_TracerFree((_PyThreadStateImpl *)tstate);
19111918
#endif
19121919

1920+
// Merge any lingering tasks from the thread state to the interpreter's
1921+
// tasks list. This must happen after all cleanup which can run finalizers,
1922+
// since those finalizers may create and register new tasks.
1923+
move_asyncio_tasks_to_interpreter(tstate);
1924+
19131925
tstate->_status.cleared = 1;
19141926

19151927
// XXX Call _PyThreadStateSwap(runtime, NULL) here if "current".

0 commit comments

Comments
 (0)