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]

Reply via email to