From 47126a74e1b625424253ecbff86e7f1933cea5db Mon Sep 17 00:00:00 2001 From: Timofey Ivankov Date: Wed, 30 Sep 2026 20:10:43 +0300 Subject: [PATCH 1/2] gh-158522: Fix truncated stack for a task whose coroutine recurses --- Lib/test/test_external_inspection.py | 15 +++++++++ ...-09-30-20-09-46.gh-issue-158522.L6j7tU.rst | 2 ++ Modules/_remote_debugging/_remote_debugging.h | 6 ++-- Modules/_remote_debugging/asyncio.c | 33 +++++++------------ 4 files changed, 31 insertions(+), 25 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-09-30-20-09-46.gh-issue-158522.L6j7tU.rst diff --git a/Lib/test/test_external_inspection.py b/Lib/test/test_external_inspection.py index c83d2cb2abeac8..f86a9d4d01bc32 100644 --- a/Lib/test/test_external_inspection.py +++ b/Lib/test/test_external_inspection.py @@ -458,6 +458,21 @@ 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_recursive_coroutine_stack_is_not_truncated(self): + # gh-158522 + async def rec(n): + if n: + return await rec(n - 1) + return [ + frame.funcname.rpartition(".")[2] + for task in RemoteUnwinder(os.getpid()).get_async_stack_trace()[0].awaited_by + for coro in task.coroutine_stack + for frame in coro.call_stack + ] + + self.assertEqual(asyncio.run(rec(3)), ["rec"] * 4) + @skip_if_not_supported @unittest.skipIf( sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, diff --git a/Misc/NEWS.d/next/Library/2026-09-30-20-09-46.gh-issue-158522.L6j7tU.rst b/Misc/NEWS.d/next/Library/2026-09-30-20-09-46.gh-issue-158522.L6j7tU.rst new file mode 100644 index 00000000000000..da315f59408083 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-30-20-09-46.gh-issue-158522.L6j7tU.rst @@ -0,0 +1,2 @@ +Fix the sampling profiler showing a single frame for a task whose coroutine +recurses. Patch by Timofei Ivankov. diff --git a/Modules/_remote_debugging/_remote_debugging.h b/Modules/_remote_debugging/_remote_debugging.h index fa37fb7b2167ec..6fc468a84f512f 100644 --- a/Modules/_remote_debugging/_remote_debugging.h +++ b/Modules/_remote_debugging/_remote_debugging.h @@ -739,7 +739,7 @@ extern int parse_async_frame_chain( RemoteUnwinderObject *unwinder, PyObject *calls, uintptr_t address_of_thread, - uintptr_t running_task_code_obj + uintptr_t running_task_frame ); /* Set iteration */ @@ -777,10 +777,10 @@ extern int find_running_task_in_thread( uintptr_t *running_task_addr ); -extern int get_task_code_object( +extern int get_task_frame( RemoteUnwinderObject *unwinder, uintptr_t task_addr, - uintptr_t *code_obj_addr + uintptr_t *frame_addr ); extern int append_awaited_by( diff --git a/Modules/_remote_debugging/asyncio.c b/Modules/_remote_debugging/asyncio.c index 44a9a3cbce0061..eed98bb2b5d26f 100644 --- a/Modules/_remote_debugging/asyncio.c +++ b/Modules/_remote_debugging/asyncio.c @@ -724,7 +724,7 @@ find_running_task_in_thread( } int -get_task_code_object(RemoteUnwinderObject *unwinder, uintptr_t task_addr, uintptr_t *code_obj_addr) { +get_task_frame(RemoteUnwinderObject *unwinder, uintptr_t task_addr, uintptr_t *frame_addr) { uintptr_t running_coro_addr = 0; if(read_py_ptr( @@ -741,21 +741,7 @@ get_task_code_object(RemoteUnwinderObject *unwinder, uintptr_t task_addr, uintpt return -1; } - // note: genobject's gi_iframe is an embedded struct so the address to - // the offset leads directly to its first field: f_executable - if (read_py_ptr( - unwinder, - running_coro_addr + (uintptr_t)unwinder->debug_offsets.gen_object.gi_iframe, code_obj_addr) < 0) { - set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read running task code object"); - return -1; - } - - if (*code_obj_addr == 0) { - PyErr_SetString(PyExc_RuntimeError, "Running task code object is NULL"); - set_exception_cause(unwinder, PyExc_RuntimeError, "Running task code object address is NULL"); - return -1; - } - + *frame_addr = running_coro_addr + (uintptr_t)unwinder->debug_offsets.gen_object.gi_iframe; return 0; } @@ -768,7 +754,7 @@ parse_async_frame_chain( RemoteUnwinderObject *unwinder, PyObject *calls, uintptr_t address_of_thread, - uintptr_t running_task_code_obj + uintptr_t running_task_frame ) { uintptr_t address_of_current_frame; if (find_running_frame(unwinder, address_of_thread, &address_of_current_frame) < 0) { @@ -779,6 +765,8 @@ parse_async_frame_chain( while ((void*)address_of_current_frame != NULL) { PyObject* frame_info = NULL; uintptr_t address_of_code_object; + + uintptr_t this_frame = address_of_current_frame; int res = parse_frame_object( unwinder, &frame_info, @@ -803,8 +791,9 @@ parse_async_frame_chain( } Py_DECREF(frame_info); - - if (address_of_code_object == running_task_code_obj) { + + // gh-158522: compare frame instead of code-object + if (this_frame == running_task_frame) { break; } } @@ -940,8 +929,8 @@ process_running_task_chain( uintptr_t thread_state_addr, PyObject *result ) { - uintptr_t running_task_code_obj = 0; - if(get_task_code_object(unwinder, running_task_addr, &running_task_code_obj) < 0) { + uintptr_t running_task_frame = 0; + if(get_task_frame(unwinder, running_task_addr, &running_task_frame) < 0) { return -1; } @@ -973,7 +962,7 @@ process_running_task_chain( } // Add the chain from the current frame to this task - if (parse_async_frame_chain(unwinder, frame_chain, thread_state_addr, running_task_code_obj) < 0) { + if (parse_async_frame_chain(unwinder, frame_chain, thread_state_addr, running_task_frame) < 0) { return -1; } From a0b0fa874a348672844ad9dc41da5453d13499f5 Mon Sep 17 00:00:00 2001 From: Timofey Ivankov Date: Thu, 1 Oct 2026 00:20:37 +0300 Subject: [PATCH 2/2] fix lint --- Modules/_remote_debugging/asyncio.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/_remote_debugging/asyncio.c b/Modules/_remote_debugging/asyncio.c index eed98bb2b5d26f..71870ff6e45733 100644 --- a/Modules/_remote_debugging/asyncio.c +++ b/Modules/_remote_debugging/asyncio.c @@ -791,7 +791,7 @@ parse_async_frame_chain( } Py_DECREF(frame_info); - + // gh-158522: compare frame instead of code-object if (this_frame == running_task_frame) { break;