https://github.com/python/cpython/commit/f6608160082e89e9aace8d70486162101b09aa49
commit: f6608160082e89e9aace8d70486162101b09aa49
branch: 3.14
author: Pablo Galindo Salgado <[email protected]>
committer: pablogsal <[email protected]>
date: 2026-10-05T00:37:18Z
summary:

[3.14] gh-153364: Make frame, coroutine, and task-waiter chain walks iterative 
and bounded (GH-153365) (#158814)

gh-153364: Make frame, coroutine, and task-waiter chain walks iterative and 
bounded (#153365)

* let me declare single limit

* use our new limit in process_frame_chain()

* add it in parse_async_frame_chain()

* parse_coro_chain()

* NEWS

* async in the message?

* test

* no race

* process_task_awaited_by

* process_task_awaited_by limit test

* NEWS

* MAX_TASK_WAITER_CHAIN_DEPTH

* TASK_WAITER_CHAIN_DEPTH in test

* TASK_WAITER_CHAIN_DEPTH 256

* prevent the drift with the comment

* better naming, better style

* MAX_TASK_WAITER_CHAIN_DEPTH comment

* task-waiter iterative bfs walk

* iterative coro-walk

* nicer news

* 1 << 14

* comment

* unused read_Py_ssize_t

* fix tombstones

* simplify

* correct msg

* better test

* news for tombstones

* left-over from when testing buggy version

* redundant new line

(cherry picked from commit e0861c6ae70c7e6f16c28399bd2b28c521e11e84)

Co-authored-by: Maurycy Pawłowski-Wieroński <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-07-08-22-18-04.gh-issue-153364.JBFHEg.rst
M Lib/test/test_external_inspection.py
M Modules/_remote_debugging_module.c

diff --git a/Lib/test/test_external_inspection.py 
b/Lib/test/test_external_inspection.py
index 766dfce64872bc..a4fed09034cc23 100644
--- a/Lib/test/test_external_inspection.py
+++ b/Lib/test/test_external_inspection.py
@@ -1,4 +1,5 @@
 import unittest
+from contextlib import contextmanager
 import asyncio
 import os
 import textwrap
@@ -1437,5 +1438,379 @@ def lines(u, expected_count):
         )
 
 
+
+
+TRANSIENT_ERRORS = (OSError, RuntimeError, UnicodeDecodeError)
+
+def _create_server_socket(port, backlog=1):
+    """Create and configure a server socket for test communication."""
+    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+    server_socket.bind(("localhost", port))
+    server_socket.settimeout(SHORT_TIMEOUT)
+    server_socket.listen(backlog)
+    return server_socket
+
+
+def _wait_for_signal(sock, expected_signals, timeout=SHORT_TIMEOUT):
+    """
+    Wait for expected signal(s) from a socket with proper timeout and EOF 
handling.
+
+    Args:
+        sock: Connected socket to read from
+        expected_signals: Single bytes object or list of bytes objects to wait 
for
+        timeout: Socket timeout in seconds
+
+    Returns:
+        bytes: Complete accumulated response buffer
+
+    Raises:
+        RuntimeError: If connection closed before signal received or timeout
+    """
+    if isinstance(expected_signals, bytes):
+        expected_signals = [expected_signals]
+
+    sock.settimeout(timeout)
+    buffer = b""
+
+    while True:
+        # Check if all expected signals are in buffer
+        if all(sig in buffer for sig in expected_signals):
+            return buffer
+
+        try:
+            chunk = sock.recv(4096)
+            if not chunk:
+                # EOF - connection closed
+                raise RuntimeError(
+                    f"Connection closed before receiving expected signals. "
+                    f"Expected: {expected_signals}, Got: {buffer[-200:]!r}"
+                )
+            buffer += chunk
+        except socket.timeout:
+            raise RuntimeError(
+                f"Timeout waiting for signals. "
+                f"Expected: {expected_signals}, Got: {buffer[-200:]!r}"
+            )
+
+
+@contextmanager
+def _managed_subprocess(args, timeout=SHORT_TIMEOUT):
+    """
+    Context manager for subprocess lifecycle management.
+
+    Ensures process is properly terminated and cleaned up even on exceptions.
+    Uses graceful termination first, then forceful kill if needed.
+    """
+    p = subprocess.Popen(args)
+    try:
+        yield p
+    finally:
+        try:
+            p.terminate()
+            try:
+                p.wait(timeout=timeout)
+            except subprocess.TimeoutExpired:
+                p.kill()
+                try:
+                    p.wait(timeout=timeout)
+                except subprocess.TimeoutExpired:
+                    pass  # Process refuses to die, nothing more we can do
+        except OSError:
+            pass  # Process already dead
+
+
+def _cleanup_sockets(*sockets):
+    """Safely close multiple sockets, ignoring errors."""
+    for sock in sockets:
+        if sock is not None:
+            try:
+                sock.close()
+            except OSError:
+                pass
+
+
+
+class RemoteInspectionTestBase(unittest.TestCase):
+    @contextmanager
+    def _target_process(self, script_body):
+        """Context manager for running a target process with socket sync."""
+        port = find_unused_port()
+        script = f"""\
+import socket
+sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+sock.connect(('localhost', {port}))
+{textwrap.dedent(script_body)}
+"""
+
+        with os_helper.temp_dir() as work_dir:
+            script_dir = os.path.join(work_dir, "script_pkg")
+            os.mkdir(script_dir)
+
+            server_socket = _create_server_socket(port)
+            script_name = _make_test_script(script_dir, "script", script)
+            client_socket = None
+
+            try:
+                with _managed_subprocess([sys.executable, script_name]) as p:
+                    client_socket, _ = server_socket.accept()
+                    server_socket.close()
+                    server_socket = None
+
+                    def make_unwinder():
+                        try:
+                            return RemoteUnwinder(p.pid, all_threads=True)
+                        except PermissionError:
+                            self.skipTest("Insufficient permissions to read 
the stack trace")
+
+                    yield p, client_socket, make_unwinder
+            finally:
+                _cleanup_sockets(client_socket, server_socket)
+
+
+    def _get_task_id_map(self, stack_trace):
+        """Create task_id -> task mapping from async stack trace."""
+        return {task.task_id: task for task in stack_trace[0].awaited_by}
+
+
+    def _get_awaited_by_relationships(self, stack_trace):
+        """Extract task name to awaited_by set mapping."""
+        id_to_task = self._get_task_id_map(stack_trace)
+        return {
+            task.task_name: set(
+                id_to_task[awaited.task_name].task_name
+                for awaited in task.awaited_by
+            )
+            for task in stack_trace[0].awaited_by
+        }
+
+
+
+class TestFrameChainLimits(RemoteInspectionTestBase):
+    """Frame chain walks abort instead of looping/overflowing on deep 
chains."""
+
+    # Limits plus one, to exceed them (must match MAX_FRAME_CHAIN_DEPTH /
+    # MAX_TASK_WAITER_WALK_TASKS from _remote_debugging_module.c)
+    FRAME_CHAIN_DEPTH = 1024 + 1
+    TASK_WAITER_WALK_TASKS = 2**14 + 1
+
+    def _assert_unwinder_limit_error(self, unwind, expected_substring):
+        """Call unwind() until it raises the frame chain limit error.
+
+        unwind must construct the RemoteUnwinder and call it, so that
+        transient RuntimeErrors from either step are retried; a successful
+        call means the limit never triggered and fails immediately.
+        """
+        last_error = None
+        for _ in busy_retry(SHORT_TIMEOUT, error=False):
+            try:
+                unwind()
+            except PermissionError:
+                self.skipTest("Insufficient permissions to read the stack 
trace")
+            except TRANSIENT_ERRORS as e:
+                if expected_substring in str(e):
+                    return
+                last_error = e
+                continue
+            self.fail(
+                "frame chain limit did not trigger; call returned a result"
+            )
+        self.fail(
+            f"frame chain limit never raised; last transient error: "
+            f"{last_error!r}"
+        )
+
+    @skip_if_not_supported
+    @unittest.skipIf(
+        sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
+        "Test only runs on Linux with process_vm_readv support",
+    )
+    def test_get_stack_trace_deep_frame_chain_aborts(self):
+        """Test that a frame chain deeper than the limit aborts the
+        synchronous stack walk instead of walking it indefinitely."""
+        script_body = f"""\
+            import sys
+            sys.setrecursionlimit({self.FRAME_CHAIN_DEPTH * 2})
+
+            def recurse(n):
+                if n <= 0:
+                    sock.sendall(b"ready")
+                    sock.recv(16)
+                    return
+                recurse(n - 1)
+
+            recurse({self.FRAME_CHAIN_DEPTH})
+            """
+        with self._target_process(script_body) as (p, client_socket, _):
+            _wait_for_signal(client_socket, b"ready")
+            self._assert_unwinder_limit_error(
+                lambda: RemoteUnwinder(p.pid).get_stack_trace(),
+                "Too many stack frames",
+            )
+            client_socket.sendall(b"done")
+
+    @skip_if_not_supported
+    @unittest.skipIf(
+        sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
+        "Test only runs on Linux with process_vm_readv support",
+    )
+    def test_get_async_stack_trace_deep_task_waiter_chain_aborts(self):
+        """Test that a task waiter chain deeper than the limit aborts
+        the walk instead of overflowing the C stack."""
+        script_body = f"""\
+            import asyncio
+
+            async def chain(n):
+                if n <= 0:
+                    sock.sendall(b"ready")
+                    sock.recv(16)
+                    return
+
+                task = asyncio.create_task(chain(n - 1))
+                await task
+
+            asyncio.run(chain({self.TASK_WAITER_WALK_TASKS}))
+            """
+        with self._target_process(script_body) as (p, client_socket, _):
+            _wait_for_signal(client_socket, b"ready")
+            self._assert_unwinder_limit_error(
+                lambda: RemoteUnwinder(p.pid).get_async_stack_trace(),
+                "Too many task waiters",
+            )
+            client_socket.sendall(b"done")
+
+    @skip_if_not_supported
+    @unittest.skipIf(
+        sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
+        "Test only runs on Linux with process_vm_readv support",
+    )
+    def test_get_async_stack_trace_deep_frame_chain_aborts(self):
+        """Test that a frame chain deeper than the limit aborts the async
+        stack walk instead of walking it indefinitely."""
+        script_body = f"""\
+            import sys, asyncio
+            sys.setrecursionlimit({self.FRAME_CHAIN_DEPTH * 2})
+
+            def recurse(n):
+                if n <= 0:
+                    sock.sendall(b"ready")
+                    sock.recv(16)
+                    return
+                recurse(n - 1)
+
+            async def deep():
+                recurse({self.FRAME_CHAIN_DEPTH})
+
+            asyncio.run(deep())
+            """
+        with self._target_process(script_body) as (p, client_socket, _):
+            _wait_for_signal(client_socket, b"ready")
+            self._assert_unwinder_limit_error(
+                lambda: RemoteUnwinder(p.pid).get_async_stack_trace(),
+                "Too many async stack frames",
+            )
+            client_socket.sendall(b"done")
+
+    @skip_if_not_supported
+    @unittest.skipIf(
+        sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
+        "Test only runs on Linux with process_vm_readv support",
+    )
+    def test_get_all_awaited_by_deep_coro_chain_aborts(self):
+        """Test that a coroutine await chain deeper than the limit aborts
+        the walk instead of overflowing the C stack."""
+        script_body = f"""\
+            import sys, asyncio
+            sys.setrecursionlimit({self.FRAME_CHAIN_DEPTH * 2})
+
+            async def chain(n):
+                if n <= 0:
+                    await asyncio.sleep(10_000)
+                    return
+                await chain(n - 1)
+
+            async def main():
+                task = asyncio.create_task(chain({self.FRAME_CHAIN_DEPTH}))
+                await asyncio.sleep(0)
+                sock.sendall(b"ready")
+                await task
+
+            asyncio.run(main())
+            """
+        with self._target_process(script_body) as (p, client_socket, _):
+            _wait_for_signal(client_socket, b"ready")
+            self._assert_unwinder_limit_error(
+                lambda: RemoteUnwinder(p.pid).get_all_awaited_by(),
+                "Too many coroutine frames",
+            )
+
+
+    @skip_if_not_supported
+    @unittest.skipIf(
+        sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
+        "Test only runs on Linux with process_vm_readv support",
+    )
+    def test_async_awaited_by_skips_set_tombstones(self):
+        script_body = """\
+            import asyncio
+
+            class RemovedTask(asyncio.Task):
+                def __hash__(self):
+                    return 0
+
+            class RemainingTask(asyncio.Task):
+                def __hash__(self):
+                    return 1
+
+            async def main():
+                victim = asyncio.current_task()
+                victim.set_name("victim")
+                removed = RemovedTask(
+                    asyncio.sleep(10_000), name="removed"
+                )
+                remaining = RemainingTask(
+                    asyncio.sleep(10_000), name="remaining"
+                )
+
+                asyncio.future_add_to_awaited_by(victim, removed)
+                asyncio.future_add_to_awaited_by(victim, remaining)
+
+                # Removing hash 0 leaves a dummy in slot 0 before the only
+                # active entry in slot 1. It must not count toward the set's
+                # used entries.
+                asyncio.future_discard_from_awaited_by(victim, removed)
+
+                sock.sendall(b"ready")
+                sock.recv(16)
+
+            asyncio.run(main())
+            """
+
+        with self._target_process(script_body) as (
+            _,
+            client_socket,
+            make_unwinder,
+        ):
+            _wait_for_signal(client_socket, b"ready")
+
+            for method_name in (
+                "get_async_stack_trace",
+                "get_all_awaited_by",
+            ):
+                with self.subTest(method=method_name):
+                    unwinder = make_unwinder()
+                    stack_trace = getattr(unwinder, method_name)()
+                    relationships = self._get_awaited_by_relationships(
+                        stack_trace
+                    )
+                    self.assertEqual(
+                        relationships["victim"],
+                        {"remaining"},
+                    )
+
+            client_socket.sendall(b"done")
+
+
+
 if __name__ == "__main__":
     unittest.main()
diff --git 
a/Misc/NEWS.d/next/Library/2026-07-08-22-18-04.gh-issue-153364.JBFHEg.rst 
b/Misc/NEWS.d/next/Library/2026-07-08-22-18-04.gh-issue-153364.JBFHEg.rst
new file mode 100644
index 00000000000000..b60a75b1c59738
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-07-08-22-18-04.gh-issue-153364.JBFHEg.rst
@@ -0,0 +1,3 @@
+Make frame, coroutine and task-waiter walks iterative and bounded, avoiding
+potential hangs and stack overflows. Fix asyncio task inspection when
+awaited-by sets contain removed entries. Patch by Maurycy Pawłowski-Wieroński.
diff --git a/Modules/_remote_debugging_module.c 
b/Modules/_remote_debugging_module.c
index 3c0841c2a307ed..3fa3232c355ae3 100644
--- a/Modules/_remote_debugging_module.c
+++ b/Modules/_remote_debugging_module.c
@@ -83,6 +83,8 @@
 #define INTERP_STATE_BUFFER_SIZE MAX(INTERP_STATE_MIN_SIZE, 256)
 #define MAX_STACK_CHUNK_SIZE (16 * 1024 * 1024)  /* 16 MB max for stack chunks 
*/
 #define MAX_SET_TABLE_SIZE (1 << 20)  /* 1 million entries max for set 
iteration */
+#define MAX_FRAME_CHAIN_DEPTH 1024  /* Iteration bound for frame chain walks */
+#define MAX_TASK_WAITER_WALK_TASKS (1 << 14)  /* Total-task bound for waiter 
walks */
 #define MAX_LONG_DIGITS 64  /* Allows values up to ~2^1920 */
 
 
@@ -754,11 +756,6 @@ typedef int (*thread_processor_func)(
     void *context
 );
 
-typedef int (*set_entry_processor_func)(
-    RemoteUnwinderObject *unwinder,
-    uintptr_t key_addr,
-    void *context
-);
 
 
 static int
@@ -793,10 +790,9 @@ parse_async_frame_chain(
 );
 
 static int read_py_ptr(RemoteUnwinderObject *unwinder, uintptr_t address, 
uintptr_t *ptr_addr);
-static int read_Py_ssize_t(RemoteUnwinderObject *unwinder, uintptr_t address, 
Py_ssize_t *size);
 
-static int process_task_and_waiters(RemoteUnwinderObject *unwinder, uintptr_t 
task_addr, PyObject *result);
-static int process_task_awaited_by(RemoteUnwinderObject *unwinder, uintptr_t 
task_address, set_entry_processor_func processor, void *context);
+static int process_task_waiters(RemoteUnwinderObject *unwinder, PyObject 
*result);
+static int parse_task_awaited_by(RemoteUnwinderObject *unwinder, uintptr_t 
task_address, PyObject *awaited_by);
 static int find_running_task_in_thread(RemoteUnwinderObject *unwinder, 
uintptr_t thread_state_addr, uintptr_t *running_task_addr);
 static int get_task_code_object(RemoteUnwinderObject *unwinder, uintptr_t 
task_addr, uintptr_t *code_obj_addr);
 static int append_awaited_by(RemoteUnwinderObject *unwinder, unsigned long 
tid, uintptr_t head_addr, PyObject *result);
@@ -977,8 +973,7 @@ static int
 iterate_set_entries(
     RemoteUnwinderObject *unwinder,
     uintptr_t set_addr,
-    set_entry_processor_func processor,
-    void *context
+    PyObject *awaited_by
 ) {
     char set_object[SIZEOF_SET_OBJ];
     if (_Py_RemoteDebug_PagedReadRemoteMemory(&unwinder->handle, set_addr,
@@ -990,6 +985,8 @@ iterate_set_entries(
     Py_ssize_t num_els = GET_MEMBER(Py_ssize_t, set_object, 
unwinder->debug_offsets.set_object.used);
     Py_ssize_t mask = GET_MEMBER(Py_ssize_t, set_object, 
unwinder->debug_offsets.set_object.mask);
     uintptr_t table_ptr = GET_MEMBER(uintptr_t, set_object, 
unwinder->debug_offsets.set_object.table);
+
+    // Validate mask and num_els to prevent huge loop iterations from garbage 
data
     if (mask < 0 || mask >= MAX_SET_TABLE_SIZE || num_els < 0 || num_els > 
mask + 1) {
         PyErr_SetString(PyExc_RuntimeError, "Invalid set object (corrupted 
remote memory)");
         set_exception_cause(unwinder, PyExc_RuntimeError,
@@ -1001,56 +998,28 @@ iterate_set_entries(
     Py_ssize_t i = 0;
     Py_ssize_t els = 0;
     while (i < set_len && els < num_els) {
-        uintptr_t key_addr;
-        if (read_py_ptr(unwinder, table_ptr, &key_addr) < 0) {
-            set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read 
set entry key");
+        setentry entry;
+        if (_Py_RemoteDebug_PagedReadRemoteMemory(
+                &unwinder->handle, table_ptr, sizeof(entry), &entry) < 0)
+        {
+            set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read 
set entry");
             return -1;
         }
 
-        if ((void*)key_addr != NULL) {
-            Py_ssize_t ref_cnt;
-            if (read_Py_ssize_t(unwinder, table_ptr, &ref_cnt) < 0) {
-                set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to 
read set entry ref count");
+        uintptr_t key_addr = (uintptr_t)entry.key;
+        if (key_addr != 0 && entry.hash != -1) {
+            if (parse_task(unwinder, key_addr, awaited_by) < 0) {
                 return -1;
             }
-
-            if (ref_cnt) {
-                // Process this valid set entry
-                if (processor(unwinder, key_addr, context) < 0) {
-                    return -1;
-                }
-                els++;
-            }
+            els++;
         }
-        table_ptr += sizeof(void*) * 2;
+        table_ptr += sizeof(entry);
         i++;
     }
 
     return 0;
 }
 
-// Processor function for task waiters
-static int
-process_waiter_task(
-    RemoteUnwinderObject *unwinder,
-    uintptr_t key_addr,
-    void *context
-) {
-    PyObject *result = (PyObject *)context;
-    return process_task_and_waiters(unwinder, key_addr, result);
-}
-
-// Processor function for parsing tasks in sets
-static int
-process_task_parser(
-    RemoteUnwinderObject *unwinder,
-    uintptr_t key_addr,
-    void *context
-) {
-    PyObject *awaited_by = (PyObject *)context;
-    return parse_task(unwinder, key_addr, awaited_by);
-}
-
 /* ============================================================================
  * MEMORY READING FUNCTIONS
  * 
============================================================================ */
@@ -1068,7 +1037,6 @@ read_##type_name(RemoteUnwinderObject *unwinder, 
uintptr_t address, c_type *resu
 }
 
 DEFINE_MEMORY_READER(ptr, uintptr_t, "Failed to read pointer from remote 
memory")
-DEFINE_MEMORY_READER(Py_ssize_t, Py_ssize_t, "Failed to read Py_ssize_t from 
remote memory")
 DEFINE_MEMORY_READER(char, char, "Failed to read char from remote memory")
 
 static int
@@ -1456,21 +1424,16 @@ parse_task_name(
     );
 }
 
-static int parse_task_awaited_by(
-    RemoteUnwinderObject *unwinder,
-    uintptr_t task_address,
-    PyObject *awaited_by
-) {
-    return process_task_awaited_by(unwinder, task_address, 
process_task_parser, awaited_by);
-}
 
 static int
-handle_yield_from_frame(
+get_awaited_coro_address(
     RemoteUnwinderObject *unwinder,
     uintptr_t gi_iframe_addr,
     uintptr_t gen_type_addr,
-    PyObject *render_to
+    uintptr_t *next_coro
 ) {
+    *next_coro = 0;
+
     // Read the entire interpreter frame at once
     char iframe[SIZEOF_INTERP_FRAME];
     int err = _Py_RemoteDebug_PagedReadRemoteMemory(
@@ -1526,11 +1489,7 @@ handle_yield_from_frame(
                    doesn't match the type of whatever it points to
                    in its cr_await.
                 */
-                err = parse_coro_chain(unwinder, gi_await_addr, render_to);
-                if (err) {
-                    set_exception_cause(unwinder, PyExc_RuntimeError, "Failed 
to parse coroutine chain in yield_from");
-                    return -1;
-                }
+                *next_coro = gi_await_addr;
             }
         }
     }
@@ -1546,49 +1505,64 @@ parse_coro_chain(
 ) {
     assert((void*)coro_address != NULL);
 
-    // Read the entire generator object at once
-    char gen_object[SIZEOF_GEN_OBJ];
-    int err = _Py_RemoteDebug_PagedReadRemoteMemory(
-        &unwinder->handle,
-        coro_address,
-        SIZEOF_GEN_OBJ,
-        gen_object);
-    if (err < 0) {
-        set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read 
generator object in coro chain");
-        return -1;
-    }
+    for (size_t depth = 0; (void*)coro_address != NULL; depth++) {
+        if (depth >= MAX_FRAME_CHAIN_DEPTH) {
+            PyErr_SetString(PyExc_RuntimeError,
+                "Too many coroutine frames (possible infinite loop)");
+            set_exception_cause(unwinder, PyExc_RuntimeError,
+                "Coroutine chain depth limit exceeded");
+            return -1;
+        }
 
-    int8_t frame_state = GET_MEMBER(int8_t, gen_object, 
unwinder->debug_offsets.gen_object.gi_frame_state);
-    if (frame_state == FRAME_CLEARED) {
-        return 0;
-    }
+        // Read the entire generator object at once
+        char gen_object[SIZEOF_GEN_OBJ];
+        int err = _Py_RemoteDebug_PagedReadRemoteMemory(
+            &unwinder->handle,
+            coro_address,
+            SIZEOF_GEN_OBJ,
+            gen_object);
+        if (err < 0) {
+            set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read 
generator object in coro chain");
+            return -1;
+        }
 
-    uintptr_t gen_type_addr = GET_MEMBER(uintptr_t, gen_object, 
unwinder->debug_offsets.pyobject.ob_type);
+        int8_t frame_state = GET_MEMBER(int8_t, gen_object, 
unwinder->debug_offsets.gen_object.gi_frame_state);
+        if (frame_state == FRAME_CLEARED) {
+            return 0;
+        }
 
-    PyObject* name = NULL;
+        uintptr_t gen_type_addr = GET_MEMBER(uintptr_t, gen_object, 
unwinder->debug_offsets.pyobject.ob_type);
 
-    // Parse the previous frame using the gi_iframe from local copy
-    uintptr_t prev_frame;
-    uintptr_t gi_iframe_addr = coro_address + 
(uintptr_t)unwinder->debug_offsets.gen_object.gi_iframe;
-    uintptr_t address_of_code_object = 0;
-    if (parse_frame_object(unwinder, &name, gi_iframe_addr, 
&address_of_code_object, &prev_frame) < 0) {
-        set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to parse 
frame object in coro chain");
-        return -1;
-    }
+        PyObject* name = NULL;
 
-    if (!name) {
-        return 0;
-    }
+        // Parse the previous frame using the gi_iframe from local copy
+        uintptr_t prev_frame;
+        uintptr_t gi_iframe_addr = coro_address + 
(uintptr_t)unwinder->debug_offsets.gen_object.gi_iframe;
+        uintptr_t address_of_code_object = 0;
+        if (parse_frame_object(unwinder, &name, gi_iframe_addr, 
&address_of_code_object, &prev_frame) < 0) {
+            set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to parse 
frame object in coro chain");
+            return -1;
+        }
+
+        if (!name) {
+            return 0;
+        }
 
-    if (PyList_Append(render_to, name)) {
+        if (PyList_Append(render_to, name)) {
+            Py_DECREF(name);
+            set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to 
append frame to coro chain");
+            return -1;
+        }
         Py_DECREF(name);
-        set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to append 
frame to coro chain");
-        return -1;
-    }
-    Py_DECREF(name);
 
-    if (frame_state == FRAME_SUSPENDED_YIELD_FROM) {
-        return handle_yield_from_frame(unwinder, gi_iframe_addr, 
gen_type_addr, render_to);
+        if (frame_state != FRAME_SUSPENDED_YIELD_FROM) {
+            return 0;
+        }
+
+        if (get_awaited_coro_address(unwinder, gi_iframe_addr, gen_type_addr,
+                                     &coro_address) < 0) {
+            return -1;
+        }
     }
 
     return 0;
@@ -1828,11 +1802,10 @@ process_thread_for_awaited_by(
 
 // Generic function to process task awaited_by
 static int
-process_task_awaited_by(
+parse_task_awaited_by(
     RemoteUnwinderObject *unwinder,
     uintptr_t task_address,
-    set_entry_processor_func processor,
-    void *context
+    PyObject *awaited_by
 ) {
     // Read the entire TaskObj at once
     char task_obj[SIZEOF_TASK_OBJ];
@@ -1851,10 +1824,10 @@ process_task_awaited_by(
     char awaited_by_is_a_set = GET_MEMBER(char, task_obj, 
unwinder->async_debug_offsets.asyncio_task_object.task_awaited_by_is_set);
 
     if (awaited_by_is_a_set) {
-        return iterate_set_entries(unwinder, task_ab_addr, processor, context);
+        return iterate_set_entries(unwinder, task_ab_addr, awaited_by);
     } else {
         // Single task waiting
-        return processor(unwinder, task_ab_addr, context);
+        return parse_task(unwinder, task_ab_addr, awaited_by);
     }
 }
 
@@ -1903,7 +1876,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, result) < 0) {
         return -1;
     }
 
@@ -1972,18 +1945,39 @@ process_thread_for_async_stack_trace(
 }
 
 static int
-process_task_and_waiters(
+process_task_waiters(
     RemoteUnwinderObject *unwinder,
-    uintptr_t task_addr,
     PyObject *result
 ) {
-    // First, add this task to the result
-    if (process_single_task_node(unwinder, task_addr, NULL, result) < 0) {
-        return -1;
+    for (Py_ssize_t i = 0; i < PyList_GET_SIZE(result); i++) {
+        PyObject *task_info = PyList_GET_ITEM(result, i);
+        PyObject *waiters = PyStructSequence_GET_ITEM(task_info, 3);
+        for (Py_ssize_t j = 0; j < PyList_GET_SIZE(waiters); j++) {
+            if (PyList_GET_SIZE(result) >= MAX_TASK_WAITER_WALK_TASKS) {
+                PyErr_SetString(PyExc_RuntimeError,
+                    "Too many task waiters (possible infinite loop)");
+                set_exception_cause(unwinder, PyExc_RuntimeError,
+                    "Task waiter walk size limit exceeded");
+                return -1;
+            }
+            PyObject *waiter = PyList_GET_ITEM(waiters, j);
+            // CoroInfo item 1 holds the waiter task address stored by 
parse_task().
+            PyObject *task_id = PyStructSequence_GET_ITEM(waiter, 1);
+            void *task_ptr = PyLong_AsVoidPtr(task_id);
+            if (task_ptr == NULL && PyErr_Occurred()) {
+                set_exception_cause(unwinder, PyExc_RuntimeError,
+                                    "Failed to parse waiter task ID");
+                return -1;
+            }
+            if (process_single_task_node(
+                    unwinder, (uintptr_t)task_ptr, NULL, result) < 0)
+            {
+                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);
+    return 0;
 }
 
 static int
@@ -2796,7 +2790,13 @@ static int
         return -1;
     }
 
+    size_t frame_count = 0;
     while ((void*)address_of_current_frame != NULL) {
+        if (++frame_count > MAX_FRAME_CHAIN_DEPTH) {
+            PyErr_SetString(PyExc_RuntimeError, "Too many async stack frames 
(possible infinite loop)");
+            set_exception_cause(unwinder, PyExc_RuntimeError, "Async frame 
chain iteration limit exceeded");
+            return -1;
+        }
         PyObject* frame_info = NULL;
         uintptr_t address_of_code_object;
         int res = parse_frame_object(
@@ -2949,14 +2949,13 @@ process_frame_chain(
 ) {
     uintptr_t frame_addr = initial_frame_addr;
     uintptr_t prev_frame_addr = 0;
-    const size_t MAX_FRAMES = 1024;
     size_t frame_count = 0;
 
     while ((void*)frame_addr != NULL) {
         PyObject *frame = NULL;
         uintptr_t next_frame_addr = 0;
 
-        if (++frame_count > MAX_FRAMES) {
+        if (++frame_count > MAX_FRAME_CHAIN_DEPTH) {
             PyErr_SetString(PyExc_RuntimeError, "Too many stack frames 
(possible infinite loop)");
             set_exception_cause(unwinder, PyExc_RuntimeError, "Frame chain 
iteration limit exceeded");
             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