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;