Skip to content

Commit 05302c0

Browse files
deadlovelllmaurycy
andauthored
gh-158522: Fix truncated stack for a task whose coroutine recurses (#158526)
* gh-158522: Fix truncated stack for a task whose coroutine recurses * fix lint * Apply suggestion from @maurycy Co-authored-by: Maurycy Pawłowski-Wieroński <maurycy@maurycy.com> * Update Misc/NEWS.d/next/Library/2026-09-30-20-09-46.gh-issue-158522.L6j7tU.rst Co-authored-by: Maurycy Pawłowski-Wieroński <maurycy@maurycy.com> * Fix line length in the new test --------- Co-authored-by: Maurycy Pawłowski-Wieroński <maurycy@maurycy.com>
1 parent 14641b0 commit 05302c0

4 files changed

Lines changed: 32 additions & 24 deletions

File tree

‎Lib/test/test_external_inspection.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,22 @@ async def main():
492492
self.assertIn(main_name, names)
493493
self.assertEqual([len(n) for n in names if n.startswith("x")], [255])
494494

495+
@skip_if_not_supported
496+
def test_recursive_coroutine_stack_is_not_truncated(self):
497+
# gh-158522
498+
async def rec(n):
499+
if n:
500+
return await rec(n - 1)
501+
return [
502+
frame.funcname.rpartition(".")[2]
503+
for task in RemoteUnwinder(
504+
os.getpid()).get_async_stack_trace()[0].awaited_by
505+
for coro in task.coroutine_stack
506+
for frame in coro.call_stack
507+
]
508+
509+
self.assertEqual(asyncio.run(rec(3)), ["rec"] * 4)
510+
495511
@skip_if_not_supported
496512
@unittest.skipIf(
497513
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix :mod:`profiling.sampling` showing a single frame for a task whose
2+
coroutine recurses. Patch by Timofei Ivankov.

‎Modules/_remote_debugging/_remote_debugging.h‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -729,7 +729,7 @@ extern int parse_async_frame_chain(
729729
RemoteUnwinderObject *unwinder,
730730
PyObject *calls,
731731
uintptr_t address_of_thread,
732-
uintptr_t running_task_code_obj
732+
uintptr_t running_task_frame
733733
);
734734

735735
extern int process_single_task_node(
@@ -745,10 +745,10 @@ extern int find_running_task_in_thread(
745745
uintptr_t *running_task_addr
746746
);
747747

748-
extern int get_task_code_object(
748+
extern int get_task_frame(
749749
RemoteUnwinderObject *unwinder,
750750
uintptr_t task_addr,
751-
uintptr_t *code_obj_addr
751+
uintptr_t *frame_addr
752752
);
753753

754754
extern int append_awaited_by(

‎Modules/_remote_debugging/asyncio.c‎

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -716,7 +716,7 @@ find_running_task_in_thread(
716716
}
717717

718718
int
719-
get_task_code_object(RemoteUnwinderObject *unwinder, uintptr_t task_addr, uintptr_t *code_obj_addr) {
719+
get_task_frame(RemoteUnwinderObject *unwinder, uintptr_t task_addr, uintptr_t *frame_addr) {
720720
uintptr_t running_coro_addr = 0;
721721

722722
if(read_py_ptr(
@@ -733,21 +733,7 @@ get_task_code_object(RemoteUnwinderObject *unwinder, uintptr_t task_addr, uintpt
733733
return -1;
734734
}
735735

736-
// note: genobject's gi_iframe is an embedded struct so the address to
737-
// the offset leads directly to its first field: f_executable
738-
if (read_py_ptr(
739-
unwinder,
740-
running_coro_addr + (uintptr_t)unwinder->debug_offsets.gen_object.gi_iframe, code_obj_addr) < 0) {
741-
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read running task code object");
742-
return -1;
743-
}
744-
745-
if (*code_obj_addr == 0) {
746-
PyErr_SetString(PyExc_RuntimeError, "Running task code object is NULL");
747-
set_exception_cause(unwinder, PyExc_RuntimeError, "Running task code object address is NULL");
748-
return -1;
749-
}
750-
736+
*frame_addr = running_coro_addr + (uintptr_t)unwinder->debug_offsets.gen_object.gi_iframe;
751737
return 0;
752738
}
753739

@@ -760,7 +746,7 @@ parse_async_frame_chain(
760746
RemoteUnwinderObject *unwinder,
761747
PyObject *calls,
762748
uintptr_t address_of_thread,
763-
uintptr_t running_task_code_obj
749+
uintptr_t running_task_frame
764750
) {
765751
uintptr_t address_of_current_frame;
766752
if (find_running_frame(unwinder, address_of_thread, &address_of_current_frame) < 0) {
@@ -777,6 +763,8 @@ parse_async_frame_chain(
777763
}
778764
PyObject* frame_info = NULL;
779765
uintptr_t address_of_code_object;
766+
767+
uintptr_t this_frame = address_of_current_frame;
780768
int res = parse_frame_object(
781769
unwinder,
782770
&frame_info,
@@ -802,7 +790,9 @@ parse_async_frame_chain(
802790

803791
Py_DECREF(frame_info);
804792

805-
if (address_of_code_object == running_task_code_obj) {
793+
// Stop at the task's own frame. Code objects are shared by
794+
// recursive calls, so they cannot identify it.
795+
if (this_frame == running_task_frame) {
806796
break;
807797
}
808798
}
@@ -938,8 +928,8 @@ process_running_task_chain(
938928
uintptr_t thread_state_addr,
939929
PyObject *result
940930
) {
941-
uintptr_t running_task_code_obj = 0;
942-
if(get_task_code_object(unwinder, running_task_addr, &running_task_code_obj) < 0) {
931+
uintptr_t running_task_frame = 0;
932+
if(get_task_frame(unwinder, running_task_addr, &running_task_frame) < 0) {
943933
return -1;
944934
}
945935

@@ -971,7 +961,7 @@ process_running_task_chain(
971961
}
972962

973963
// Add the chain from the current frame to this task
974-
if (parse_async_frame_chain(unwinder, frame_chain, thread_state_addr, running_task_code_obj) < 0) {
964+
if (parse_async_frame_chain(unwinder, frame_chain, thread_state_addr, running_task_frame) < 0) {
975965
return -1;
976966
}
977967

0 commit comments

Comments
 (0)