Skip to content

gh-159449 - Fix a free-threaded asyncio race that corrupts the task lists - #159451

Closed
jblespiau wants to merge 2 commits into
python:3.14from
jblespiau:fix-asyncio-task-unregister-race-3.14
Closed

jblespiau wants to merge 2 commits into
python:3.14from
jblespiau:fix-asyncio-task-unregister-race-3.14

Conversation

@jblespiau

@jblespiau jblespiau commented Oct 15, 2026 •

Copy link
Copy Markdown

See #159449

When a thread exits with pending asyncio Tasks, PyThreadState_Clear() moves them onto the shared interp->asyncio_tasks_head list, but each Task still records the _Py_ThreadId() of the thread that created it. unregister_task() skips the stop-the-world pause whenever that id matches the current thread. That also happens for a later thread that reuses the exited thread's pthread_t, and for destructors that run later in PyThreadState_Clear(). The resulting unlocked llist_remove() calls on the shared list race with each other and can leave an already-removed node (next == NULL) linked into it. asyncio.all_tasks(), which asyncio.run() calls on shutdown, then crashes with SIGSEGV in _asyncio_all_tasks(). Record the unique id (PyThreadState.id) of the creating thread state instead, and only take the lock-free path while that thread state is not being cleared. Also drop the assertion in add_tasks_llist() that every linked Task is pending. A Task's state is set before unregister_task() unlinks it, and that removal can be waiting for the stop-the-world pause during which the list is walked. add_one_task() already filters out Tasks that are done.

I don't know if the bug is also present in 3.15, it's worth checking. I am hoping that given we have a reprocuder in the bug, + a bug-fix, it should be doable for the maintainers to port it.

When a thread exits with pending asyncio Tasks, PyThreadState_Clear() moves them
onto the shared interp->asyncio_tasks_head list, but each Task still records the
_Py_ThreadId() of the thread that created it. unregister_task() skips the
stop-the-world pause whenever that id matches the current thread. That also
happens for a later thread that reuses the exited thread's pthread_t, and for
destructors that run later in PyThreadState_Clear(). The resulting unlocked
llist_remove() calls on the shared list race with each other and can leave an
already-removed node (next == NULL) linked into it. asyncio.all_tasks(), which
asyncio.run() calls on shutdown, then crashes with SIGSEGV in
_asyncio_all_tasks().
Record the unique id (PyThreadState.id) of the creating thread state instead,
and only take the lock-free path while that thread state is not being cleared.
Also drop the assertion in add_tasks_llist() that every linked Task is pending.
A Task's state is set before unregister_task() unlinks it, and that removal can
be waiting for the stop-the-world pause during which the list is walked.
add_one_task() already filters out Tasks that are done.
@python-cla-bot

python-cla-bot Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

All commit authors signed the Contributor License Agreement.

CLA signed

@bedevere-app

bedevere-app Bot commented Oct 15, 2026

Copy link
Copy Markdown

Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool.

If this change has little impact on Python users, wait for a maintainer to apply the skip news label instead.

@jblespiau

Copy link
Copy Markdown
Author

I think I did it. I used blurb-it.

@jblespiau jblespiau changed the title gh-159449 - Fix a free-threaded asyncio race that corrupts the task lists [3-14] gh-159449 - Fix a free-threaded asyncio race that corrupts the task lists Oct 15, 2026
@jblespiau jblespiau changed the title [3-14] gh-159449 - Fix a free-threaded asyncio race that corrupts the task lists gh-159449 - Fix a free-threaded asyncio race that corrupts the task lists Oct 15, 2026
@jblespiau

Copy link
Copy Markdown
Author

Note that it's not as far as I am aware a backport of a fix; I haven't looked at whether the bug is still here for newer versions. So I don't think it should be titled as "[3.14] ..."

@AnnaAr321

Copy link
Copy Markdown
Contributor

This also reproduces in the Python 3.15 free-threaded build

@StanFromIreland

Copy link
Copy Markdown
Member

Please open a PR against main.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants