https://github.com/python/cpython/commit/756319503f9692842ab5e471b44bae632efa518d
commit: 756319503f9692842ab5e471b44bae632efa518d
branch: main
author: Dmitry Spikhalsky <[email protected]>
committer: kumaraditya303 <[email protected]>
date: 2026-10-29T22:30:36+05:30
summary:
gh-158159: Fix use-after-free in asyncio Task.get_name() (#158180)
files:
A Misc/NEWS.d/next/Library/2026-09-25-17-04-45.gh-issue-158159.lbEalG.rst
M Lib/test/test_asyncio/test_tasks.py
M Modules/_asynciomodule.c
diff --git a/Lib/test/test_asyncio/test_tasks.py
b/Lib/test/test_asyncio/test_tasks.py
index 7641c361f9666e..d4d3863923e79e 100644
--- a/Lib/test/test_asyncio/test_tasks.py
+++ b/Lib/test/test_asyncio/test_tasks.py
@@ -416,6 +416,52 @@ async def notmuch():
self.assertEqual(t.get_name(), '123456789')
self.loop.run_until_complete(t)
+ # The free-threaded GC publishes allocation counts in batches, so the
+ # collection cannot be scheduled to run inside get_name().
+ @support.requires_gil_enabled("needs precise GC scheduling")
+ def test_task_get_name_reentered_during_formatting(self):
+ # gh-158159: formatting the lazy default name can run the GC, and a
+ # finalizer may call get_name() on the same task, replacing the name
+ # the outer call is still formatting.
+ loop = self.loop
+ observed = {}
+ keep = []
+ task = None
+
+ class Reenter:
+ def __init__(self):
+ self.cycle = self
+
+ def __del__(self):
+ observed['inner'] = task.get_name()
+ # Reuse the memory of a counter that was freed too early.
+ keep.extend(10**9 + i for i in range(8))
+
+ async def notmuch():
+ pass
+
+ async def scenario():
+ gc.collect()
+ Reenter()
+ old_threshold = gc.get_threshold()
+ gc.set_threshold(1)
+ try:
+ scratch = [[0], [0], [0]] # schedule a collection
+ observed['outer'] = task.get_name()
+ finally:
+ gc.set_threshold(*old_threshold)
+ del scratch
+
+ # Names are only freeable once the counter is past the small ints.
+ for _ in range(1100):
+ loop.run_until_complete(self.new_task(loop, notmuch(), name=None))
+ task = self.new_task(loop, scenario(), name=None)
+ loop.run_until_complete(task)
+
+ self.assertRegex(observed['outer'], r'^Task-\d+$')
+ self.assertEqual(observed['outer'], observed['inner'])
+ self.assertEqual(task.get_name(), observed['outer'])
+
def test_task_repr_name_not_str(self):
async def notmuch():
return 123
diff --git
a/Misc/NEWS.d/next/Library/2026-09-25-17-04-45.gh-issue-158159.lbEalG.rst
b/Misc/NEWS.d/next/Library/2026-09-25-17-04-45.gh-issue-158159.lbEalG.rst
new file mode 100644
index 00000000000000..a0d365111d92ff
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-09-25-17-04-45.gh-issue-158159.lbEalG.rst
@@ -0,0 +1,3 @@
+Fix a use-after-free in :meth:`asyncio.Task.get_name` when formatting the
+default task name triggers a garbage collection whose finalizers call
+:meth:`~asyncio.Task.get_name` on the same task.
diff --git a/Modules/_asynciomodule.c b/Modules/_asynciomodule.c
index 73404cb75b5d1f..e3dd9aa9bdb115 100644
--- a/Modules/_asynciomodule.c
+++ b/Modules/_asynciomodule.c
@@ -2803,7 +2803,12 @@ _asyncio_Task_get_name_impl(TaskObj *self)
{
if (self->task_name) {
if (PyLong_CheckExact(self->task_name)) {
- PyObject *name = PyUnicode_FromFormat("Task-%S", self->task_name);
+ // Formatting can run arbitrary code (e.g. a GC finalizer that logs
+ // and re-enters get_name()), which may replace self->task_name.
+ // Hold a strong reference to the counter while formatting it.
+ PyObject *counter = Py_NewRef(self->task_name);
+ PyObject *name = PyUnicode_FromFormat("Task-%S", counter);
+ Py_DECREF(counter);
if (name == NULL) {
return NULL;
}
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]