Skip to content

Commit 4947080

Browse files
committed
gh-159041: Fix asyncio task tracking during thread state clear
1 parent 77f7b96 commit 4947080

3 files changed

Lines changed: 74 additions & 7 deletions

File tree

‎Lib/test/test_asyncio/test_free_threading.py‎

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import asyncio
2+
import contextvars
3+
import _thread
24
import threading
35
import unittest
46
from threading import Thread
@@ -232,6 +234,68 @@ def factory(self, loop, coro, **kwargs):
232234
@unittest.skipUnless(hasattr(asyncio.tasks, "_c_all_tasks"), "requires _asyncio")
233235
class TestCFreeThreading(TestFreeThreading, TestCase):
234236

237+
def test_task_created_during_thread_state_clear_is_tracked(self):
238+
"""Tasks created by a thread-state finalizer must not be orphaned."""
239+
loop = asyncio.new_event_loop()
240+
loop_ready = threading.Event()
241+
task_started = threading.Event()
242+
task_finished = threading.Event()
243+
thread_done = threading.Event()
244+
blocker = []
245+
246+
async def blocked():
247+
task_started.set()
248+
await blocker[0]
249+
task_finished.set()
250+
251+
def initialize_loop():
252+
blocker.append(loop.create_future())
253+
loop_ready.set()
254+
255+
def run_loop():
256+
asyncio.set_event_loop(loop)
257+
loop.call_soon(initialize_loop)
258+
try:
259+
loop.run_forever()
260+
finally:
261+
loop.close()
262+
263+
loop_thread = Thread(target=run_loop)
264+
loop_thread.start()
265+
try:
266+
self.assertTrue(loop_ready.wait(10))
267+
268+
created = []
269+
270+
class CreatesTaskOnClear:
271+
def __del__(self):
272+
task = loop.create_task(blocked())
273+
created.append(task)
274+
# loop.create_task() does not wake a loop running in
275+
# another thread.
276+
loop.call_soon_threadsafe(lambda: None)
277+
278+
var = contextvars.ContextVar("var")
279+
280+
def worker():
281+
var.set(CreatesTaskOnClear())
282+
thread_done.set()
283+
284+
_thread.start_new_thread(worker, ())
285+
self.assertTrue(thread_done.wait(10))
286+
287+
self.assertTrue(task_started.wait(10))
288+
self.assertEqual(len(created), 1)
289+
self.assertIn(created[0], asyncio.all_tasks(loop))
290+
finally:
291+
if blocker:
292+
loop.call_soon_threadsafe(blocker[0].set_result, None)
293+
if task_started.is_set():
294+
self.assertTrue(task_finished.wait(10))
295+
loop.call_soon_threadsafe(loop.stop)
296+
loop_thread.join(10)
297+
self.assertFalse(loop_thread.is_alive())
298+
235299
def setUp(self):
236300
self._old_current_task = asyncio.current_task
237301
asyncio.current_task = asyncio.tasks.current_task = asyncio.tasks._c_current_task
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 the free-threaded build when an :mod:`asyncio` task is
2+
created by a thread-state finalizer.

‎Python/pystate.c‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1837,13 +1837,6 @@ PyThreadState_Clear(PyThreadState *tstate)
18371837
Py_CLEAR(((_PyThreadStateImpl *)tstate)->asyncio_running_task);
18381838

18391839

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-
18471840
Py_CLEAR(tstate->dict);
18481841
Py_CLEAR(tstate->async_exc);
18491842

@@ -1877,6 +1870,14 @@ PyThreadState_Clear(PyThreadState *tstate)
18771870
// Finalizers above may resolve imports and create this set.
18781871
Py_CLEAR(((_PyThreadStateImpl *)tstate)->lazy_imports);
18791872

1873+
// Finalizers run by the Py_CLEAR() calls above can create asyncio tasks.
1874+
// Merge the task list only after all of those finalizers have run, so
1875+
// those tasks are not left linked to this thread state when it is freed.
1876+
PyMutex_Lock(&tstate->interp->asyncio_tasks_lock);
1877+
llist_concat(&tstate->interp->asyncio_tasks_head,
1878+
&((_PyThreadStateImpl *)tstate)->asyncio_tasks_head);
1879+
PyMutex_Unlock(&tstate->interp->asyncio_tasks_lock);
1880+
18801881
#ifdef Py_GIL_DISABLED
18811882
// Each thread should clear own freelists in free-threading builds.
18821883
struct _Py_freelists *freelists = _Py_freelists_GET();

0 commit comments

Comments
 (0)