GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — AsyncIO thread-safety issue in Python 3.14's free-threading · Issue #159449 · python/cpython

AsyncIO thread-safety issue in Python 3.14's free-threading #159449

New issue
Copy link
New issue
Copy link
Closed
Closed
AsyncIO thread-safety issue in Python 3.14's free-threading#159449
Copy link
Labels
stdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directorytopic-asynciotopic-free-threadingtype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error

Description

@jblespiau
jblespiau
opened on Oct 15, 2026
Issue body actions

Bug report

When a short-lived thread exits while some of its async tasks are still unfinished, Python moves those leftover tasks onto a shared global list; however, because Linux reuses the OS thread IDs of exited threads for newly created threads, a later thread can be mistaken for the original creator of those tasks and remove them from the shared list without locking, which corrupts the list and causes the next thread cleaning up after an async run to segfault.

Found and fixed by a colleague.

Reproducer:

"""Reproduces a free-threaded CPython 3.14 SIGSEGV in `_asyncio_all_tasks`.

Spawner threads repeatedly start a short-lived worker thread that cancels the
asyncio Tasks left behind by its predecessor, creates a new batch of pending
Tasks, and exits. When a thread exits with pending Tasks, PyThreadState_Clear()
moves them onto the interpreter-wide `interp->asyncio_tasks_head` list. glibc
hands the exited thread's pthread_t to the next thread, so the successor has the
same `_Py_ThreadId()` as the creator of those Tasks. unregister_task() then
takes
its lock-free fast path and unlinks them from the shared list without stopping
the world, and concurrent unlinks corrupt the list.

Runner threads call asyncio.run(). Its shutdown calls asyncio.all_tasks() after
the loop stopped running, which takes the stop-the-world slow path in
_asyncio_all_tasks() and walks the corrupted list until it dereferences NULL.

If the runtime is not affected, the binary runs for --duration_secs and exits 0.
"""

import asyncio
import collections
from collections.abc import Sequence
import sys
import sysconfig
import threading
from typing import Any

from absl import app
from absl import flags
from absl import logging

_DURATION_SECS = flags.DEFINE_float(
    "duration_secs", 30.0, "How long to run before declaring a pass."
)
_SPAWNER_THREADS = flags.DEFINE_integer(
    "spawner_threads", 6, "Threads that churn short-lived Task-owning threads."
)
_RUNNER_THREADS = flags.DEFINE_integer(
    "runner_threads", 2, "Threads that call asyncio.run() in a loop."
)
_TASKS_PER_THREAD = flags.DEFINE_integer(
    "tasks_per_thread", 32, "Pending Tasks each short-lived thread leaves."
)

_TaskBatch = list[asyncio.Task[Any]]


class _InertLoop:
  """Minimal event loop stand-in for constructing `asyncio.Task` objects.

  `call_soon` drops the Task's first step, so a Task stays pending without a
  real event loop holding a reference to it or opening file descriptors.
  """

  def get_debug(self) -> bool:
    return False

  def call_soon(self, *args: object, **kwargs: object) -> None:
    del args, kwargs  # Unused.


_INERT_LOOP = _InertLoop()

# Keeps recently cancelled Tasks alive so their memory is not reused while a
# corrupted list may still point at them. This turns a nondeterministic
# use-after-free into the deterministic NULL dereference seen in production.
_RECENTLY_CANCELLED: collections.deque[_TaskBatch] = collections.deque(
    maxlen=4096
)


async def _noop() -> None:
  pass


def _create_pending_tasks(count: int) -> _TaskBatch:
  """Creates `count` Tasks that stay pending until they are cancelled."""
  tasks = []
  for _ in range(count):
    coro = _noop()
    task = asyncio.Task(coro, loop=_INERT_LOOP)  # type: ignore[arg-type]
    # The Task never runs: close its coroutine to avoid "never awaited", and do
    # not warn about Tasks that are still pending when the process exits.
    coro.close()
    setattr(task, "_log_destroy_pending", False)
    tasks.append(task)
  return tasks


def _short_lived_worker(
    handoff: list[_TaskBatch], tasks_per_thread: int
) -> None:
  """Cancels the predecessor's Tasks, then exits leaving new pending Tasks."""
  while handoff:
    tasks = handoff.pop()
    for task in tasks:
      # Future.cancel() completes the Task immediately, which unregisters it
      # from the asyncio task list (Task.cancel() would only request it).
      asyncio.Future.cancel(task)
    _RECENTLY_CANCELLED.append(tasks)
  handoff.append(_create_pending_tasks(tasks_per_thread))


def _spawner_loop(stop: threading.Event, tasks_per_thread: int) -> None:
  """Starts and joins short-lived worker threads until `stop` is set."""
  handoff: list[_TaskBatch] = []
  while not stop.is_set():
    worker = threading.Thread(
        target=_short_lived_worker, args=(handoff, tasks_per_thread)
    )
    worker.start()
    worker.join()


def _runner_loop(stop: threading.Event) -> None:
  """Runs short asyncio.run() calls until `stop` is set."""
  while not stop.is_set():
    asyncio.run(_noop())


def main(argv: Sequence[str]) -> None:
  if len(argv) > 1:
    raise app.UsageError("Too many command-line arguments.")
  if not sysconfig.get_config_var("Py_GIL_DISABLED"):
    raise app.UsageError("Needs a free-threaded Python build; see README.md.")
  if sys._is_gil_enabled():  # pylint: disable=protected-access
    raise app.UsageError("The GIL is enabled; run with PYTHON_GIL=0.")
  logging.info("Running on Python %s with the GIL disabled.", sys.version)

  stop = threading.Event()
  threads = [
      threading.Thread(
          target=_spawner_loop, args=(stop, _TASKS_PER_THREAD.value)
      )
      for _ in range(_SPAWNER_THREADS.value)
  ]
  threads.extend(
      threading.Thread(target=_runner_loop, args=(stop,))
      for _ in range(_RUNNER_THREADS.value)
  )
  for thread in threads:
    thread.start()
  stop.wait(_DURATION_SECS.value)
  stop.set()
  for thread in threads:
    thread.join()
  logging.info("PASS: no crash after %.1f seconds.", _DURATION_SECS.value)


if __name__ == "__main__":
  app.run(main)

I am sending a PR separately.

CPython versions tested on:

3.14

Operating systems tested on:

Linux

Linked PRs

  • gh-159449 - Fix asyncio task unregister race 3.14 #159450
  • gh-159449 - Fix a free-threaded asyncio race that corrupts the task lists #159451
  • gh-159449: Fix a free-threaded asyncio race that corrupts the task lists #159483
  • [3.15] gh-159449: Fix free-threaded asyncio race that corrupts the task lists (GH-159483) #161379
  • [3.14] gh-159449: Fix free-threaded asyncio race that corrupts the task lists (GH-159483) #161380

Activity

  1. jblespiau
    added
    type-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
    on Oct 15, 2026
  2. aisk
    added
    topic-asyncio
    stdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directory
    on Oct 15, 2026
  3. github-project-automation
    added this to asyncioon Oct 15, 2026
  4. github-project-automation
    moved this to Todo in asyncioon Oct 15, 2026
  5. aisk
    added
    topic-free-threading
    on Oct 15, 2026
  6. AnnaAr321 commented on Oct 15, 2026

    @AnnaAr321
    AnnaAr321
    on Oct 15, 2026
    Contributor
    More actions

    This also reproduces in the Python 3.15 free-threaded build

  7. AnnaAr321 commented on Oct 15, 2026

    @AnnaAr321
    AnnaAr321
    on Oct 15, 2026
    Contributor
    More actions

    The failure log for the reproduction code from the issue description:

    assert.h assertion failed at Modules/_asynciomodule.c:4080 in int add_tasks_llist(struct llist_node *, PyListObject *): task->task_state == STATE_PENDING
    *** Check failure stack trace: ***                                                                                          
        @     0x55d3f448b239  absl::log_internal::LogMessage::SendToLog()                                                       
        @     0x55d3f448b1c7  absl::log_internal::LogMessage::Flush()                                                           
        @     0x55d3f43c5d34  __assert_fail                                                                                     
        @     0x55d3f46833e3  add_tasks_llist                                                                                   
        @     0x55d3f4681e88  _asyncio_all_tasks                                                                                
        @     0x55d3f4a866dd  cfunction_vectorcall_FASTCALL_KEYWORDS                                                            
        @     0x55d3f4a05e7a  _PyObject_VectorcallTstate                                                                        
        @     0x55d3f4bd3696  _PyEval_EvalFrameDefault                                                                          
        @     0x55d3f4bcc88c  _PyEval_Vector                                                                                    
        @     0x55d3f4a08a34  _PyObject_VectorcallPrepend                                                                       
        @     0x55d3f4c26ba1  context_run                                                                                       
        @     0x55d3f4bdd6c5  _PyEval_EvalFrameDefault                                                                          
        @     0x55d3f4bcc88c  _PyEval_Vector                                                                                    
        @     0x55d3f4a08a34  _PyObject_VectorcallPrepend                                                                       
        @     0x55d3f4603195  thread_run                                                                                        
        @     0x55d3f4cb226e  pythread_wrapper                                                                                  
        @     0x7fa8e9b440bf  start_thread                                                                                      
        @     0x7fa8e9ab467f  __clone
    
  8. kevinprimeraquecreo-stack
    added this to Project_VKon Oct 18, 2026
  9. github-project-automation
    moved this to Todo in Project_VKon Oct 18, 2026
  10. kevinprimeraquecreo-stack
    added this to Project_VKon Oct 18, 2026
  11. jblespiau
    added a commit that references this issue on Nov 4, 2026

    pythongh-159449: Fix a free-threaded asyncio race that corrupts the t…

    60d8cac
  12. nascheme commented on Nov 4, 2026

    @nascheme
    nascheme
    on Nov 4, 2026
    Member
    More actions

    Ping @kumaraditya303

  13. kumaraditya303
    added 3 commits that reference this issue on Nov 5, 2026

    pythongh-159449: Fix a free-threaded asyncio race that corrupts the t…

    8e75acc

    gh-159449: Fix free-threaded asyncio race that corrupts the task lists (

    aaaf61c

    [3.14] pythongh-159449: Fix free-threaded asyncio race that corrupts …

    abee7e4
  14. kumaraditya303
    closed this as completedon Nov 5, 2026
  15. github-project-automation
    moved this from Todo to Done in asyncioon Nov 5, 2026
  16. kumaraditya303
    added 2 commits that reference this issue on Nov 5, 2026

    [3.15] gh-159449: Fix free-threaded asyncio race that corrupts the ta…

    3619efa

    [3.14] gh-159449: Fix free-threaded asyncio race that corrupts the ta…

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    stdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directorytopic-asynciotopic-free-threadingtype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error

    Projects

    • asyncio
      Status
      Done
      Show more project fields

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    You are visitor 01402503 — GeezerVison does not store the pages you look up.
    Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.