https://github.com/python/cpython/commit/a904b397ed324e404a8abd682b056c146cb48263
commit: a904b397ed324e404a8abd682b056c146cb48263
branch: 3.15
author: Pablo Galindo Salgado <[email protected]>
committer: pablogsal <[email protected]>
date: 2026-10-05T17:18:26+01:00
summary:

[3.15] gh-158522: Fix truncated stack for a task whose coroutine recurses 
(GH-158526) (#158870)

Co-authored-by: Timofei Ivankov <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-09-30-20-09-46.gh-issue-158522.L6j7tU.rst
M Lib/test/test_external_inspection.py
M Modules/_remote_debugging/_remote_debugging.h
M Modules/_remote_debugging/asyncio.c

diff --git a/Lib/test/test_external_inspection.py 
b/Lib/test/test_external_inspection.py
index fa37629c7a9a1b..eaec2943ecbaaf 100644
--- a/Lib/test/test_external_inspection.py
+++ b/Lib/test/test_external_inspection.py
@@ -492,6 +492,22 @@ 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..30c2df4bacd4a1
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-09-30-20-09-46.gh-issue-158522.L6j7tU.rst
@@ -0,0 +1,2 @@
+Fix :mod:`profiling.sampling` 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 c57b30533ce1c1..79fa4a92e745bd 100644
--- a/Modules/_remote_debugging/_remote_debugging.h
+++ b/Modules/_remote_debugging/_remote_debugging.h
@@ -729,7 +729,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
 );
 
 extern int process_single_task_node(
@@ -745,10 +745,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 aa3f5b638931f8..c8d79774a47a73 100644
--- a/Modules/_remote_debugging/asyncio.c
+++ b/Modules/_remote_debugging/asyncio.c
@@ -716,7 +716,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(
@@ -733,21 +733,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;
 }
 
@@ -760,7 +746,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) {
@@ -777,6 +763,8 @@ parse_async_frame_chain(
         }
         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,
@@ -802,7 +790,9 @@ parse_async_frame_chain(
 
         Py_DECREF(frame_info);
 
-        if (address_of_code_object == running_task_code_obj) {
+        // Stop at the task's own frame. Code objects are shared by
+        // recursive calls, so they cannot identify it.
+        if (this_frame == running_task_frame) {
             break;
         }
     }
@@ -938,8 +928,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;
     }
 
@@ -971,7 +961,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;
     }
 

_______________________________________________
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