From f3ad5d19bc1f99b0c314a94a3a946c9a8ec38b5d Mon Sep 17 00:00:00 2001 From: Timofey Ivankov Date: Sun, 4 Oct 2026 02:06:12 +0300 Subject: [PATCH] gh-158688: Fix crash when dumping a deep chain of awaiting tasks --- Lib/test/test_external_inspection.py | 30 ++++++ ...-10-04-02-05-43.gh-issue-158688.X8kcFc.rst | 2 + Modules/_remote_debugging/_remote_debugging.h | 6 -- Modules/_remote_debugging/asyncio.c | 95 +++++++++++++++---- 4 files changed, 108 insertions(+), 25 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-10-04-02-05-43.gh-issue-158688.X8kcFc.rst diff --git a/Lib/test/test_external_inspection.py b/Lib/test/test_external_inspection.py index c83d2cb2abeac81..21a1b49981de03b 100644 --- a/Lib/test/test_external_inspection.py +++ b/Lib/test/test_external_inspection.py @@ -458,6 +458,36 @@ async def main(): self.assertIn(main_name, names) self.assertEqual([len(n) for n in names if n.startswith("x")], [255]) + @skip_if_not_supported + def test_deep_awaiter_chain_is_walked_depth_first(self): + # gh-158688 + depth = 20000 + + async def root(): + await asyncio.sleep(0) + return [ + task.task_name + for info in RemoteUnwinder(os.getpid()).get_async_stack_trace() + for task in info.awaited_by + ] + + async def link(awaited): + return await awaited + + async def main(): + first = asyncio.create_task(root(), name="root") + top = first + for i in range(depth): + top = asyncio.create_task(link(top), name=f"a{i}") + asyncio.create_task(link(first), name="b") + return (await top), asyncio.current_task().get_name() + + names, main_name = asyncio.run(main()) + self.assertEqual(len(names), depth + 3) + head = names.index("a0") + self.assertEqual(names[head:head + 2], ["a0", "a1"]) + self.assertEqual(names[head + depth], main_name) + @skip_if_not_supported @unittest.skipIf( sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, diff --git a/Misc/NEWS.d/next/Library/2026-10-04-02-05-43.gh-issue-158688.X8kcFc.rst b/Misc/NEWS.d/next/Library/2026-10-04-02-05-43.gh-issue-158688.X8kcFc.rst new file mode 100644 index 000000000000000..10357b889a39861 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-04-02-05-43.gh-issue-158688.X8kcFc.rst @@ -0,0 +1,2 @@ +Fix a crash in the sampling profiler when dumping a program with a deep +chain of awaiting tasks. Patch by Timofei Ivankov. diff --git a/Modules/_remote_debugging/_remote_debugging.h b/Modules/_remote_debugging/_remote_debugging.h index fa37fb7b2167ecf..7dbf6244212d155 100644 --- a/Modules/_remote_debugging/_remote_debugging.h +++ b/Modules/_remote_debugging/_remote_debugging.h @@ -765,12 +765,6 @@ extern int process_single_task_node( PyObject *result ); -extern int process_task_and_waiters( - RemoteUnwinderObject *unwinder, - uintptr_t task_addr, - PyObject *result -); - extern int find_running_task_in_thread( RemoteUnwinderObject *unwinder, uintptr_t thread_state_addr, diff --git a/Modules/_remote_debugging/asyncio.c b/Modules/_remote_debugging/asyncio.c index 44a9a3cbce0061a..2a1384186c79918 100644 --- a/Modules/_remote_debugging/asyncio.c +++ b/Modules/_remote_debugging/asyncio.c @@ -513,9 +513,6 @@ parse_task( * TASK AWAITED_BY PROCESSING * ============================================================================ */ -// Forward declaration for mutual recursion -static int process_waiter_task(RemoteUnwinderObject *unwinder, uintptr_t key_addr, void *context); - // Processor function for parsing tasks in sets static int process_task_parser( @@ -658,30 +655,90 @@ process_single_task_node( return -1; } -int -process_task_and_waiters( +// Processor function for task waiters +static int +queue_waiter_task( RemoteUnwinderObject *unwinder, - uintptr_t task_addr, - PyObject *result + uintptr_t key_addr, + void *context ) { - // First, add this task to the result - if (process_single_task_node(unwinder, task_addr, NULL, result) < 0) { + PyObject *queue = (PyObject *)context; + PyObject *addr = PyLong_FromUnsignedLongLong(key_addr); + if (addr == NULL) { + set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create task address"); return -1; } - - // Now find all tasks that are waiting for this task and process them - return process_task_awaited_by(unwinder, task_addr, process_waiter_task, result); + int res = PyList_Append(queue, addr); + Py_DECREF(addr); + if (res < 0) { + set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to queue waiter task"); + } + return res; } -// Processor function for task waiters +// gh-158688: Add every task that transitively waits for task_addr, already in the result static int -process_waiter_task( +process_task_waiters( RemoteUnwinderObject *unwinder, - uintptr_t key_addr, - void *context + uintptr_t task_addr, + PyObject *result ) { - PyObject *result = (PyObject *)context; - return process_task_and_waiters(unwinder, key_addr, result); + PyObject *queue = PyList_New(0); + PyObject *seen = PySet_New(NULL); + PyObject *waiters = PyList_New(0); + int res = -1; + + if (queue == NULL || seen == NULL || waiters == NULL) { + set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create task queue"); + goto done; + } + + int seeded = queue_waiter_task(unwinder, task_addr, queue); + if (seeded < 0) { + goto done; + } + + for (Py_ssize_t i = 0; i < PyList_GET_SIZE(queue); i++) { + PyObject *addr = PyList_GET_ITEM(queue, i); + Py_ssize_t seen_count = PySet_GET_SIZE(seen); + + int marked = PySet_Add(seen, addr); + if (marked < 0) { + set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to mark task as seen"); + goto done; + } + if (PySet_GET_SIZE(seen) == seen_count) { + // already visited + continue; + } + + uintptr_t waiter_addr = (uintptr_t)PyLong_AsUnsignedLongLong(addr); + if (i > 0) { + int added = process_single_task_node(unwinder, waiter_addr, NULL, result); + if (added < 0) { + goto done; + } + } + + int queued = process_task_awaited_by(unwinder, waiter_addr, queue_waiter_task, waiters); + if (queued < 0) { + goto done; + } + + // Visit the waiters before the rest of the queue, depth first + int spliced = PyList_SetSlice(queue, i + 1, i + 1, waiters); + int cleared = PyList_SetSlice(waiters, 0, PyList_GET_SIZE(waiters), NULL); + if (spliced < 0 || cleared < 0) { + goto done; + } + } + res = 0; + +done: + Py_XDECREF(queue); + Py_XDECREF(seen); + Py_XDECREF(waiters); + return res; } /* ============================================================================ @@ -978,7 +1035,7 @@ process_running_task_chain( } // Now find all tasks that are waiting for this task and process them - if (process_task_awaited_by(unwinder, running_task_addr, process_waiter_task, result) < 0) { + if (process_task_waiters(unwinder, running_task_addr, result) < 0) { return -1; }