Crash report
What happened?
_asyncio.Task.get_name() formats the lazy default name through a borrowed reference:
if (PyLong_CheckExact(self->task_name)) {
PyObject *name = PyUnicode_FromFormat("Task-%S", self->task_name);
...
Py_SETREF(self->task_name, name);
}
PyUnicode_FromFormat allocates, and that allocation can run the cyclic GC. If the collection finalizes a pending task, Task.__del__ logs Task was destroyed but it is pending!. Since 3.12, logging.LogRecord fills taskName by calling asyncio.current_task().get_name(). That is the same task, so get_name() re-enters, formats the name, and Py_SETREFs it. The PyLong the outer call is still formatting is freed.
Names above Task-256 are not immortal, so any long-running asyncio program that logs can hit this. The trigger is an abandoned pending task being collected while some task reads its name for the first time. In a large test suite this showed up as rare segfaults whose top frame is a plain Python call to get_name(). They were hard to attribute until we noticed every crash was on that line.
Reproducer (run with PYTHONMALLOC=debug to make the use-after-free deterministic; without it, it only crashes when the freed block happens to be reused mid-format):
import asyncio
import gc
import logging
logging.basicConfig()
async def wait_forever(fut):
await fut
async def scenario():
loop = asyncio.get_running_loop()
current = asyncio.current_task()
gc.collect()
# A pending task reachable only through a task <-> future cycle; its
# finalizer logs "Task was destroyed but it is pending!".
orphan = loop.create_task(wait_forever(loop.create_future()))
await asyncio.sleep(0)
del orphan
gc.set_threshold(1)
scratch = [[0], [0], [0]] # makes the next allocation run the GC
current.get_name() # formats the lazy name; the GC runs inside that
gc.set_threshold(700)
del scratch
async def main():
# Names up to Task-256 are immortal small ints and cannot be freed.
for _ in range(300):
await asyncio.create_task(asyncio.sleep(0))
await asyncio.create_task(scenario())
asyncio.run(main())
print("no crash")
$ PYTHONMALLOC=debug python3.12 -X faulthandler repro.py
ERROR:asyncio:Task was destroyed but it is pending!
task: <Task pending name='Task-303' coro=<wait_forever() done, ...>>
Fatal Python error: Segmentation fault
Current thread 0x... (most recent call first):
File "repro.py", line 23 in scenario
...
Results:
- 3.12.13 and 3.13.13 crash.
- On 3.14.7 the re-entry still happens: the finalizer's
LogRecord gets taskName='Task-302' from the nested get_name(). This reproducer just doesn't observe the freed read there. The code is unchanged on main.
- Setting
logging.logAsyncioTasks = False avoids it, which is our workaround for now.
A possible fix is to hold a strong reference while formatting, and keep whatever name a nested call may already have stored:
if (PyLong_CheckExact(self->task_name)) {
PyObject *counter = Py_NewRef(self->task_name);
PyObject *name = PyUnicode_FromFormat("Task-%S", counter);
Py_DECREF(counter);
if (name == NULL) {
return NULL;
}
if (PyLong_CheckExact(self->task_name)) {
Py_SETREF(self->task_name, name);
}
else {
Py_DECREF(name);
}
}
return Py_NewRef(self->task_name);
CPython versions tested on:
3.12, 3.13, 3.14
Operating systems tested on:
Linux, macOS
Output from running 'python -VV' on the command line:
Python 3.12.13 (main, Apr 7 2026, 21:09:58) [Clang 22.1.1 ]
Linked PRs
Crash report
What happened?
_asyncio.Task.get_name()formats the lazy default name through a borrowed reference:PyUnicode_FromFormatallocates, and that allocation can run the cyclic GC. If the collection finalizes a pending task,Task.__del__logsTask was destroyed but it is pending!. Since 3.12,logging.LogRecordfillstaskNameby callingasyncio.current_task().get_name(). That is the same task, soget_name()re-enters, formats the name, andPy_SETREFs it. ThePyLongthe outer call is still formatting is freed.Names above
Task-256are not immortal, so any long-running asyncio program that logs can hit this. The trigger is an abandoned pending task being collected while some task reads its name for the first time. In a large test suite this showed up as rare segfaults whose top frame is a plain Python call toget_name(). They were hard to attribute until we noticed every crash was on that line.Reproducer (run with
PYTHONMALLOC=debugto make the use-after-free deterministic; without it, it only crashes when the freed block happens to be reused mid-format):Results:
LogRecordgetstaskName='Task-302'from the nestedget_name(). This reproducer just doesn't observe the freed read there. The code is unchanged onmain.logging.logAsyncioTasks = Falseavoids it, which is our workaround for now.A possible fix is to hold a strong reference while formatting, and keep whatever name a nested call may already have stored:
CPython versions tested on:
3.12, 3.13, 3.14
Operating systems tested on:
Linux, macOS
Output from running 'python -VV' on the command line:
Python 3.12.13 (main, Apr 7 2026, 21:09:58) [Clang 22.1.1 ]
Linked PRs