From 4dd6f49c9a8f7147fd4feb6309235632a3261de3 Mon Sep 17 00:00:00 2001 From: maurycy <5383+maurycy@users.noreply.github.com> Date: Wed, 8 Jul 2026 21:36:56 +0200 Subject: [PATCH 01/20] let me declare single limit --- Modules/_remote_debugging/_remote_debugging.h | 1 + 1 file changed, 1 insertion(+) diff --git a/Modules/_remote_debugging/_remote_debugging.h b/Modules/_remote_debugging/_remote_debugging.h index fa37fb7b2167ecf..ea96b57314e4e5b 100644 --- a/Modules/_remote_debugging/_remote_debugging.h +++ b/Modules/_remote_debugging/_remote_debugging.h @@ -147,6 +147,7 @@ typedef enum _WIN32_THREADSTATE { #define MAX_STACK_CHUNK_SIZE (16 * 1024 * 1024) /* 16 MB max for stack chunks */ #define MAX_LONG_DIGITS 64 /* Allows values up to ~2^1920 */ #define MAX_SET_TABLE_SIZE (1 << 20) /* 1 million entries max for set iteration */ +#define MAX_FRAME_CHAIN_DEPTH (1024 + 512) /* Iteration bound for frame chain walks */ #ifndef MAX #define MAX(a, b) ((a) > (b) ? (a) : (b)) From e71aaa566e24844d6a611d72c4b03a015aba3910 Mon Sep 17 00:00:00 2001 From: maurycy <5383+maurycy@users.noreply.github.com> Date: Wed, 8 Jul 2026 21:37:13 +0200 Subject: [PATCH 02/20] use our new limit in process_frame_chain() --- Modules/_remote_debugging/frames.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Modules/_remote_debugging/frames.c b/Modules/_remote_debugging/frames.c index 46968acc6ff1feb..fde3474a0539190 100644 --- a/Modules/_remote_debugging/frames.c +++ b/Modules/_remote_debugging/frames.c @@ -305,9 +305,7 @@ process_frame_chain( uintptr_t frame_addr = ctx->frame_addr; uintptr_t prev_frame_addr = 0; uintptr_t last_frame_addr = 0; - const size_t MAX_FRAMES = 1024 + 512; size_t frame_count = 0; - assert(MAX_FRAMES > 0 && MAX_FRAMES < 10000); ctx->stopped_at_cached_frame = 0; ctx->last_frame_visited = 0; @@ -318,12 +316,12 @@ process_frame_chain( uintptr_t stackpointer = 0; last_frame_addr = frame_addr; - 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; } - assert(frame_count <= MAX_FRAMES); + assert(frame_count <= MAX_FRAME_CHAIN_DEPTH); if (ctx->chunks && ctx->chunks->count > 0) { if (parse_frame_from_chunks(unwinder, &frame, frame_addr, &next_frame_addr, &stackpointer, ctx->chunks) == 0) { From 116da45f642473a69ab724456f3496536dd5e419 Mon Sep 17 00:00:00 2001 From: maurycy <5383+maurycy@users.noreply.github.com> Date: Wed, 8 Jul 2026 21:37:25 +0200 Subject: [PATCH 03/20] add it in parse_async_frame_chain() --- Modules/_remote_debugging/asyncio.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Modules/_remote_debugging/asyncio.c b/Modules/_remote_debugging/asyncio.c index 44a9a3cbce0061a..ba2a2bc612aad40 100644 --- a/Modules/_remote_debugging/asyncio.c +++ b/Modules/_remote_debugging/asyncio.c @@ -776,7 +776,13 @@ parse_async_frame_chain( 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 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( From 3e12939b13678e46924c4590b00f00dfe1c33e74 Mon Sep 17 00:00:00 2001 From: maurycy <5383+maurycy@users.noreply.github.com> Date: Wed, 8 Jul 2026 22:01:05 +0200 Subject: [PATCH 04/20] parse_coro_chain() --- Modules/_remote_debugging/_remote_debugging.h | 3 ++- Modules/_remote_debugging/asyncio.c | 22 ++++++++++++++----- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/Modules/_remote_debugging/_remote_debugging.h b/Modules/_remote_debugging/_remote_debugging.h index ea96b57314e4e5b..17137aeec902976 100644 --- a/Modules/_remote_debugging/_remote_debugging.h +++ b/Modules/_remote_debugging/_remote_debugging.h @@ -733,7 +733,8 @@ extern int parse_task( extern int parse_coro_chain( RemoteUnwinderObject *unwinder, uintptr_t coro_address, - PyObject *render_to + PyObject *render_to, + size_t depth ); extern int parse_async_frame_chain( diff --git a/Modules/_remote_debugging/asyncio.c b/Modules/_remote_debugging/asyncio.c index ba2a2bc612aad40..4f4c129d14bbcfb 100644 --- a/Modules/_remote_debugging/asyncio.c +++ b/Modules/_remote_debugging/asyncio.c @@ -252,7 +252,8 @@ handle_yield_from_frame( RemoteUnwinderObject *unwinder, uintptr_t gi_iframe_addr, uintptr_t gen_type_addr, - PyObject *render_to + PyObject *render_to, + size_t depth ) { // Read the entire interpreter frame at once char iframe[SIZEOF_INTERP_FRAME]; @@ -309,7 +310,8 @@ 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); + err = parse_coro_chain(unwinder, gi_await_addr, render_to, + depth + 1); if (err) { set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to parse coroutine chain in yield_from"); return -1; @@ -325,10 +327,19 @@ int parse_coro_chain( RemoteUnwinderObject *unwinder, uintptr_t coro_address, - PyObject *render_to + PyObject *render_to, + size_t depth ) { assert((void*)coro_address != NULL); + 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; + } + // Read the entire generator object at once char gen_object[SIZEOF_GEN_OBJ]; int err = _Py_RemoteDebug_PagedReadRemoteMemory( @@ -371,7 +382,8 @@ parse_coro_chain( Py_DECREF(name); if (frame_state == FRAME_SUSPENDED_YIELD_FROM) { - return handle_yield_from_frame(unwinder, gi_iframe_addr, gen_type_addr, render_to); + return handle_yield_from_frame(unwinder, gi_iframe_addr, gen_type_addr, + render_to, depth); } return 0; @@ -417,7 +429,7 @@ create_task_result( coro_addr = GET_MEMBER_NO_TAG(uintptr_t, task_obj, unwinder->async_debug_offsets.asyncio_task_object.task_coro); if ((void*)coro_addr != NULL) { - if (parse_coro_chain(unwinder, coro_addr, call_stack) < 0) { + if (parse_coro_chain(unwinder, coro_addr, call_stack, 0) < 0) { set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to parse coroutine chain"); goto error; } From c46810c2f24a060948c6f7666a618c8ca5d018cc Mon Sep 17 00:00:00 2001 From: maurycy <5383+maurycy@users.noreply.github.com> Date: Wed, 8 Jul 2026 22:18:07 +0200 Subject: [PATCH 05/20] NEWS --- .../next/Library/2026-07-08-22-18-04.gh-issue-153364.JBFHEg.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-07-08-22-18-04.gh-issue-153364.JBFHEg.rst 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 000000000000000..496886ac6cc819e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-08-22-18-04.gh-issue-153364.JBFHEg.rst @@ -0,0 +1,2 @@ +Add limits to frame and coroutine-chain walks in the Tachyon sampling +profiler. This avoids potential hangs. Patch by Maurycy Pawłowski-Wieroński. From dd6e19a851391edf9f2a3d3d7d9e08df768b2ffe Mon Sep 17 00:00:00 2001 From: maurycy <5383+maurycy@users.noreply.github.com> Date: Wed, 8 Jul 2026 23:07:55 +0200 Subject: [PATCH 06/20] async in the message? --- 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 4f4c129d14bbcfb..aeeccd3d67d61d8 100644 --- a/Modules/_remote_debugging/asyncio.c +++ b/Modules/_remote_debugging/asyncio.c @@ -791,7 +791,7 @@ parse_async_frame_chain( 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 stack frames (possible infinite loop)"); + 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; } From 0972e00fd527f424fb6206b36b768ff71018c50a Mon Sep 17 00:00:00 2001 From: maurycy <5383+maurycy@users.noreply.github.com> Date: Wed, 8 Jul 2026 23:10:41 +0200 Subject: [PATCH 07/20] test --- Lib/test/test_external_inspection.py | 188 ++++++++++++++++++++++----- 1 file changed, 154 insertions(+), 34 deletions(-) diff --git a/Lib/test/test_external_inspection.py b/Lib/test/test_external_inspection.py index 6b1529aa173f01c..5018e6f5eb60b12 100644 --- a/Lib/test/test_external_inspection.py +++ b/Lib/test/test_external_inspection.py @@ -339,6 +339,40 @@ def _run_script_and_get_trace( finally: _cleanup_sockets(client_socket, server_socket) + @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(cache_frames=True): + return RemoteUnwinder( + p.pid, all_threads=True, cache_frames=cache_frames + ) + + yield p, client_socket, make_unwinder + finally: + _cleanup_sockets(client_socket, server_socket) + def _find_frame_in_trace(self, stack_trace, predicate): """ Find a frame matching predicate in stack trace. @@ -2927,40 +2961,6 @@ class TestFrameCaching(RemoteInspectionTestBase): All tests verify cache reuse via object identity checks (assertIs). """ - @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(cache_frames=True): - return RemoteUnwinder( - p.pid, all_threads=True, cache_frames=cache_frames - ) - - yield p, client_socket, make_unwinder - finally: - _cleanup_sockets(client_socket, server_socket) - def _get_frames_with_retry(self, unwinder, required_funcs): """Get frames containing required_funcs, with retry for transient errors.""" for _ in range(MAX_TRIES): @@ -3804,5 +3804,125 @@ def test_get_stats_disabled_raises(self): client_socket.sendall(b"done") +@requires_remote_subprocess_debugging() +class TestFrameChainLimits(RemoteInspectionTestBase): + """Frame chain walks abort instead of looping/overflowing on deep chains.""" + + CHAIN_DEPTH = 1024 + 512 + 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 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.CHAIN_DEPTH * 2}) + + def recurse(n): + if n <= 0: + sock.sendall(b"ready") + sock.recv(16) + return + recurse(n - 1) + + recurse({self.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_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.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.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.CHAIN_DEPTH * 2}) + + async def chain(n): + if n <= 0: + sock.sendall(b"ready") + await asyncio.sleep(10_000) + return + await chain(n - 1) + + asyncio.run(chain({self.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_all_awaited_by(), + "Too many coroutine frames", + ) + + if __name__ == "__main__": unittest.main() From 3c5f78be31cfc3cd0b909f9e8b92088bf33c549a Mon Sep 17 00:00:00 2001 From: maurycy <5383+maurycy@users.noreply.github.com> Date: Wed, 8 Jul 2026 23:36:27 +0200 Subject: [PATCH 08/20] no race --- Lib/test/test_external_inspection.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_external_inspection.py b/Lib/test/test_external_inspection.py index 5018e6f5eb60b12..98bc668754a1e88 100644 --- a/Lib/test/test_external_inspection.py +++ b/Lib/test/test_external_inspection.py @@ -3909,12 +3909,17 @@ def test_get_all_awaited_by_deep_coro_chain_aborts(self): async def chain(n): if n <= 0: - sock.sendall(b"ready") await asyncio.sleep(10_000) return await chain(n - 1) - asyncio.run(chain({self.CHAIN_DEPTH})) + async def main(): + task = asyncio.create_task(chain({self.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") From 78c18f95c6dd9d5eba9c3844b1a93f69ab548338 Mon Sep 17 00:00:00 2001 From: maurycy <5383+maurycy@users.noreply.github.com> Date: Fri, 10 Jul 2026 21:25:34 +0200 Subject: [PATCH 09/20] process_task_awaited_by --- Modules/_remote_debugging/_remote_debugging.h | 12 +++-- Modules/_remote_debugging/asyncio.c | 51 ++++++++++++++----- 2 files changed, 45 insertions(+), 18 deletions(-) diff --git a/Modules/_remote_debugging/_remote_debugging.h b/Modules/_remote_debugging/_remote_debugging.h index 17137aeec902976..cc7c548ba2103f3 100644 --- a/Modules/_remote_debugging/_remote_debugging.h +++ b/Modules/_remote_debugging/_remote_debugging.h @@ -482,7 +482,8 @@ typedef int (*thread_processor_func)( typedef int (*set_entry_processor_func)( RemoteUnwinderObject *unwinder, uintptr_t key_addr, - void *context + void *context, + size_t depth ); typedef int (*interpreter_processor_func)( @@ -749,7 +750,8 @@ extern int iterate_set_entries( RemoteUnwinderObject *unwinder, uintptr_t set_addr, set_entry_processor_func processor, - void *context + void *context, + size_t depth ); /* Task awaited_by processing */ @@ -757,7 +759,8 @@ extern int process_task_awaited_by( RemoteUnwinderObject *unwinder, uintptr_t task_address, set_entry_processor_func processor, - void *context + void *context, + size_t depth ); extern int process_single_task_node( @@ -770,7 +773,8 @@ extern int process_single_task_node( extern int process_task_and_waiters( RemoteUnwinderObject *unwinder, uintptr_t task_addr, - PyObject *result + PyObject *result, + size_t depth ); extern int find_running_task_in_thread( diff --git a/Modules/_remote_debugging/asyncio.c b/Modules/_remote_debugging/asyncio.c index aeeccd3d67d61d8..58eb43d2c6d5341 100644 --- a/Modules/_remote_debugging/asyncio.c +++ b/Modules/_remote_debugging/asyncio.c @@ -121,7 +121,8 @@ iterate_set_entries( RemoteUnwinderObject *unwinder, uintptr_t set_addr, set_entry_processor_func processor, - void *context + void *context, + size_t depth ) { char set_object[SIZEOF_SET_OBJ]; if (_Py_RemoteDebug_PagedReadRemoteMemory(&unwinder->handle, set_addr, @@ -161,7 +162,7 @@ iterate_set_entries( if (ref_cnt) { // Process this valid set entry - if (processor(unwinder, key_addr, context) < 0) { + if (processor(unwinder, key_addr, context, depth) < 0) { return -1; } els++; @@ -526,15 +527,22 @@ parse_task( * ============================================================================ */ // Forward declaration for mutual recursion -static int process_waiter_task(RemoteUnwinderObject *unwinder, uintptr_t key_addr, void *context); +static int process_waiter_task( + RemoteUnwinderObject *unwinder, + uintptr_t key_addr, + void *context, + size_t depth +); // Processor function for parsing tasks in sets static int process_task_parser( RemoteUnwinderObject *unwinder, uintptr_t key_addr, - void *context + void *context, + size_t depth ) { + (void)depth; PyObject *awaited_by = (PyObject *)context; return parse_task(unwinder, key_addr, awaited_by); } @@ -545,7 +553,8 @@ parse_task_awaited_by( uintptr_t task_address, PyObject *awaited_by ) { - return process_task_awaited_by(unwinder, task_address, process_task_parser, awaited_by); + return process_task_awaited_by( + unwinder, task_address, process_task_parser, awaited_by, 0); } int @@ -553,7 +562,8 @@ process_task_awaited_by( RemoteUnwinderObject *unwinder, uintptr_t task_address, set_entry_processor_func processor, - void *context + void *context, + size_t depth ) { // Read the entire TaskObj at once char task_obj[SIZEOF_TASK_OBJ]; @@ -572,10 +582,11 @@ 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, processor, context, depth); } else { // Single task waiting - return processor(unwinder, task_ab_addr, context); + return processor(unwinder, task_ab_addr, context, depth); } } @@ -674,15 +685,25 @@ int process_task_and_waiters( RemoteUnwinderObject *unwinder, uintptr_t task_addr, - PyObject *result + PyObject *result, + size_t depth ) { + if (depth >= MAX_FRAME_CHAIN_DEPTH) { + PyErr_SetString(PyExc_RuntimeError, + "Too many task waiters (possible infinite loop)"); + set_exception_cause(unwinder, PyExc_RuntimeError, + "Task waiter chain depth limit exceeded"); + return -1; + } + // First, add this task to the result if (process_single_task_node(unwinder, task_addr, 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 process_task_awaited_by( + unwinder, task_addr, process_waiter_task, result, depth + 1); } // Processor function for task waiters @@ -690,10 +711,10 @@ static int process_waiter_task( RemoteUnwinderObject *unwinder, uintptr_t key_addr, - void *context + void *context, + size_t depth ) { - PyObject *result = (PyObject *)context; - return process_task_and_waiters(unwinder, key_addr, result); + return process_task_and_waiters(unwinder, key_addr, (PyObject *)context, depth); } /* ============================================================================ @@ -996,7 +1017,9 @@ 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_awaited_by( + unwinder, running_task_addr, process_waiter_task, result, 1) < 0) + { return -1; } From 21d764ef751592db91e6519ec2cfdfd1884cdcca Mon Sep 17 00:00:00 2001 From: maurycy <5383+maurycy@users.noreply.github.com> Date: Fri, 10 Jul 2026 21:25:43 +0200 Subject: [PATCH 10/20] process_task_awaited_by limit test --- Lib/test/test_external_inspection.py | 30 ++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/Lib/test/test_external_inspection.py b/Lib/test/test_external_inspection.py index 98bc668754a1e88..0ae523782b78928 100644 --- a/Lib/test/test_external_inspection.py +++ b/Lib/test/test_external_inspection.py @@ -3863,6 +3863,36 @@ def recurse(n): ) 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.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_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, From 000287efb4aea797b8a3e25badc49d5e7b0b2591 Mon Sep 17 00:00:00 2001 From: maurycy <5383+maurycy@users.noreply.github.com> Date: Fri, 10 Jul 2026 21:26:19 +0200 Subject: [PATCH 11/20] NEWS --- .../Library/2026-07-08-22-18-04.gh-issue-153364.JBFHEg.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 index 496886ac6cc819e..dfc05a5d428ceca 100644 --- 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 @@ -1,2 +1,3 @@ -Add limits to frame and coroutine-chain walks in the Tachyon sampling -profiler. This avoids potential hangs. Patch by Maurycy Pawłowski-Wieroński. +Add limits to frame, coroutine and task-waiter walks in the Tachyon sampling +profiler. This avoids potential hangs and stack overflows. Patch by Maurycy +Pawłowski-Wieroński. From 353f815e63e3c7c48a02b5647af01e6b915ce1d7 Mon Sep 17 00:00:00 2001 From: maurycy <5383+maurycy@users.noreply.github.com> Date: Fri, 10 Jul 2026 22:09:09 +0200 Subject: [PATCH 12/20] MAX_TASK_WAITER_CHAIN_DEPTH --- Modules/_remote_debugging/_remote_debugging.h | 1 + Modules/_remote_debugging/asyncio.c | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/_remote_debugging/_remote_debugging.h b/Modules/_remote_debugging/_remote_debugging.h index cc7c548ba2103f3..b8872755ea8c106 100644 --- a/Modules/_remote_debugging/_remote_debugging.h +++ b/Modules/_remote_debugging/_remote_debugging.h @@ -148,6 +148,7 @@ typedef enum _WIN32_THREADSTATE { #define MAX_LONG_DIGITS 64 /* Allows values up to ~2^1920 */ #define MAX_SET_TABLE_SIZE (1 << 20) /* 1 million entries max for set iteration */ #define MAX_FRAME_CHAIN_DEPTH (1024 + 512) /* Iteration bound for frame chain walks */ +#define MAX_TASK_WAITER_CHAIN_DEPTH 512 #ifndef MAX #define MAX(a, b) ((a) > (b) ? (a) : (b)) diff --git a/Modules/_remote_debugging/asyncio.c b/Modules/_remote_debugging/asyncio.c index 58eb43d2c6d5341..352edb8cb982d3e 100644 --- a/Modules/_remote_debugging/asyncio.c +++ b/Modules/_remote_debugging/asyncio.c @@ -688,7 +688,7 @@ process_task_and_waiters( PyObject *result, size_t depth ) { - if (depth >= MAX_FRAME_CHAIN_DEPTH) { + if (depth >= MAX_TASK_WAITER_CHAIN_DEPTH) { PyErr_SetString(PyExc_RuntimeError, "Too many task waiters (possible infinite loop)"); set_exception_cause(unwinder, PyExc_RuntimeError, From f66866b073851dfc47e825dccd1d1e1057050323 Mon Sep 17 00:00:00 2001 From: maurycy <5383+maurycy@users.noreply.github.com> Date: Fri, 10 Jul 2026 22:39:03 +0200 Subject: [PATCH 13/20] TASK_WAITER_CHAIN_DEPTH in test --- Lib/test/test_external_inspection.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_external_inspection.py b/Lib/test/test_external_inspection.py index 0ae523782b78928..be965938a534d8f 100644 --- a/Lib/test/test_external_inspection.py +++ b/Lib/test/test_external_inspection.py @@ -3809,6 +3809,7 @@ class TestFrameChainLimits(RemoteInspectionTestBase): """Frame chain walks abort instead of looping/overflowing on deep chains.""" CHAIN_DEPTH = 1024 + 512 + 1 + TASK_WAITER_CHAIN_DEPTH = 512 + 1 def _assert_unwinder_limit_error(self, unwind, expected_substring): """Call unwind() until it raises the frame chain limit error. @@ -3883,7 +3884,7 @@ async def chain(n): task = asyncio.create_task(chain(n - 1)) await task - asyncio.run(chain({self.CHAIN_DEPTH})) + asyncio.run(chain({self.TASK_WAITER_CHAIN_DEPTH})) """ with self._target_process(script_body) as (p, client_socket, _): _wait_for_signal(client_socket, b"ready") From c9f1ebdd29c58bed29103f01804ab6374011e189 Mon Sep 17 00:00:00 2001 From: maurycy <5383+maurycy@users.noreply.github.com> Date: Fri, 10 Jul 2026 23:05:41 +0200 Subject: [PATCH 14/20] TASK_WAITER_CHAIN_DEPTH 256 --- Lib/test/test_external_inspection.py | 3 ++- Modules/_remote_debugging/_remote_debugging.h | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_external_inspection.py b/Lib/test/test_external_inspection.py index be965938a534d8f..a66a0b4e83bc703 100644 --- a/Lib/test/test_external_inspection.py +++ b/Lib/test/test_external_inspection.py @@ -3809,7 +3809,8 @@ class TestFrameChainLimits(RemoteInspectionTestBase): """Frame chain walks abort instead of looping/overflowing on deep chains.""" CHAIN_DEPTH = 1024 + 512 + 1 - TASK_WAITER_CHAIN_DEPTH = 512 + 1 + + TASK_WAITER_CHAIN_DEPTH = 256 + 1 def _assert_unwinder_limit_error(self, unwind, expected_substring): """Call unwind() until it raises the frame chain limit error. diff --git a/Modules/_remote_debugging/_remote_debugging.h b/Modules/_remote_debugging/_remote_debugging.h index b8872755ea8c106..1f190e595e74270 100644 --- a/Modules/_remote_debugging/_remote_debugging.h +++ b/Modules/_remote_debugging/_remote_debugging.h @@ -148,7 +148,7 @@ typedef enum _WIN32_THREADSTATE { #define MAX_LONG_DIGITS 64 /* Allows values up to ~2^1920 */ #define MAX_SET_TABLE_SIZE (1 << 20) /* 1 million entries max for set iteration */ #define MAX_FRAME_CHAIN_DEPTH (1024 + 512) /* Iteration bound for frame chain walks */ -#define MAX_TASK_WAITER_CHAIN_DEPTH 512 +#define MAX_TASK_WAITER_CHAIN_DEPTH 256 #ifndef MAX #define MAX(a, b) ((a) > (b) ? (a) : (b)) From 7236ca27e57d5851b405fa47f5a72663b6493f60 Mon Sep 17 00:00:00 2001 From: maurycy <5383+maurycy@users.noreply.github.com> Date: Sat, 11 Jul 2026 20:54:56 +0200 Subject: [PATCH 15/20] prevent the drift with the comment --- Lib/test/test_external_inspection.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/test/test_external_inspection.py b/Lib/test/test_external_inspection.py index a66a0b4e83bc703..739e9f0f3e48023 100644 --- a/Lib/test/test_external_inspection.py +++ b/Lib/test/test_external_inspection.py @@ -3808,6 +3808,8 @@ def test_get_stats_disabled_raises(self): 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_CHAIN_DEPTH from _remote_debugging.h) CHAIN_DEPTH = 1024 + 512 + 1 TASK_WAITER_CHAIN_DEPTH = 256 + 1 From 35fc6c61c505509e8bfbffca5bb5216d1f3d32ff Mon Sep 17 00:00:00 2001 From: maurycy <5383+maurycy@users.noreply.github.com> Date: Sat, 11 Jul 2026 20:58:46 +0200 Subject: [PATCH 16/20] better naming, better style --- Lib/test/test_external_inspection.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Lib/test/test_external_inspection.py b/Lib/test/test_external_inspection.py index 739e9f0f3e48023..2aef1d2c4537a57 100644 --- a/Lib/test/test_external_inspection.py +++ b/Lib/test/test_external_inspection.py @@ -3810,8 +3810,7 @@ class TestFrameChainLimits(RemoteInspectionTestBase): # Limits plus one, to exceed them (must match MAX_FRAME_CHAIN_DEPTH / # MAX_TASK_WAITER_CHAIN_DEPTH from _remote_debugging.h) - CHAIN_DEPTH = 1024 + 512 + 1 - + FRAME_CHAIN_DEPTH = 1024 + 512 + 1 TASK_WAITER_CHAIN_DEPTH = 256 + 1 def _assert_unwinder_limit_error(self, unwind, expected_substring): @@ -3848,7 +3847,7 @@ def test_get_stack_trace_deep_frame_chain_aborts(self): synchronous stack walk instead of walking it indefinitely.""" script_body = f"""\ import sys - sys.setrecursionlimit({self.CHAIN_DEPTH * 2}) + sys.setrecursionlimit({self.FRAME_CHAIN_DEPTH * 2}) def recurse(n): if n <= 0: @@ -3857,7 +3856,7 @@ def recurse(n): return recurse(n - 1) - recurse({self.CHAIN_DEPTH}) + recurse({self.FRAME_CHAIN_DEPTH}) """ with self._target_process(script_body) as (p, client_socket, _): _wait_for_signal(client_socket, b"ready") @@ -3907,7 +3906,7 @@ def test_get_async_stack_trace_deep_frame_chain_aborts(self): stack walk instead of walking it indefinitely.""" script_body = f"""\ import sys, asyncio - sys.setrecursionlimit({self.CHAIN_DEPTH * 2}) + sys.setrecursionlimit({self.FRAME_CHAIN_DEPTH * 2}) def recurse(n): if n <= 0: @@ -3917,7 +3916,7 @@ def recurse(n): recurse(n - 1) async def deep(): - recurse({self.CHAIN_DEPTH}) + recurse({self.FRAME_CHAIN_DEPTH}) asyncio.run(deep()) """ @@ -3939,7 +3938,7 @@ def test_get_all_awaited_by_deep_coro_chain_aborts(self): the walk instead of overflowing the C stack.""" script_body = f"""\ import sys, asyncio - sys.setrecursionlimit({self.CHAIN_DEPTH * 2}) + sys.setrecursionlimit({self.FRAME_CHAIN_DEPTH * 2}) async def chain(n): if n <= 0: @@ -3948,7 +3947,7 @@ async def chain(n): await chain(n - 1) async def main(): - task = asyncio.create_task(chain({self.CHAIN_DEPTH})) + task = asyncio.create_task(chain({self.FRAME_CHAIN_DEPTH})) await asyncio.sleep(0) sock.sendall(b"ready") await task From 10149e98acc67d77919b0b716e147dc56981256b Mon Sep 17 00:00:00 2001 From: maurycy <5383+maurycy@users.noreply.github.com> Date: Sat, 11 Jul 2026 21:10:09 +0200 Subject: [PATCH 17/20] MAX_TASK_WAITER_CHAIN_DEPTH comment --- Modules/_remote_debugging/_remote_debugging.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/_remote_debugging/_remote_debugging.h b/Modules/_remote_debugging/_remote_debugging.h index 1f190e595e74270..8a0cca92441fcd4 100644 --- a/Modules/_remote_debugging/_remote_debugging.h +++ b/Modules/_remote_debugging/_remote_debugging.h @@ -148,7 +148,7 @@ typedef enum _WIN32_THREADSTATE { #define MAX_LONG_DIGITS 64 /* Allows values up to ~2^1920 */ #define MAX_SET_TABLE_SIZE (1 << 20) /* 1 million entries max for set iteration */ #define MAX_FRAME_CHAIN_DEPTH (1024 + 512) /* Iteration bound for frame chain walks */ -#define MAX_TASK_WAITER_CHAIN_DEPTH 256 +#define MAX_TASK_WAITER_CHAIN_DEPTH 256 /* Task waiter walk recursion cap: 256 * SIZEOF_TASK_OBJ ~= 1 MiB */ #ifndef MAX #define MAX(a, b) ((a) > (b) ? (a) : (b)) From ef5156320460ab0784f2b630d36651a08cd2a1f6 Mon Sep 17 00:00:00 2001 From: maurycy <5383+maurycy@users.noreply.github.com> Date: Sun, 12 Jul 2026 22:30:39 +0200 Subject: [PATCH 18/20] task-waiter iterative bfs walk --- Lib/test/test_external_inspection.py | 6 +- Modules/_remote_debugging/_remote_debugging.h | 34 +---- Modules/_remote_debugging/asyncio.c | 120 ++++++------------ 3 files changed, 40 insertions(+), 120 deletions(-) diff --git a/Lib/test/test_external_inspection.py b/Lib/test/test_external_inspection.py index 2aef1d2c4537a57..6439bc5c29539bc 100644 --- a/Lib/test/test_external_inspection.py +++ b/Lib/test/test_external_inspection.py @@ -3809,9 +3809,9 @@ 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_CHAIN_DEPTH from _remote_debugging.h) + # MAX_TASK_WAITER_WALK_TASKS from _remote_debugging.h) FRAME_CHAIN_DEPTH = 1024 + 512 + 1 - TASK_WAITER_CHAIN_DEPTH = 256 + 1 + TASK_WAITER_WALK_TASKS = 2**16 + 1 def _assert_unwinder_limit_error(self, unwind, expected_substring): """Call unwind() until it raises the frame chain limit error. @@ -3886,7 +3886,7 @@ async def chain(n): task = asyncio.create_task(chain(n - 1)) await task - asyncio.run(chain({self.TASK_WAITER_CHAIN_DEPTH})) + 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") diff --git a/Modules/_remote_debugging/_remote_debugging.h b/Modules/_remote_debugging/_remote_debugging.h index 8a0cca92441fcd4..37ff5a29f261a6d 100644 --- a/Modules/_remote_debugging/_remote_debugging.h +++ b/Modules/_remote_debugging/_remote_debugging.h @@ -148,7 +148,7 @@ typedef enum _WIN32_THREADSTATE { #define MAX_LONG_DIGITS 64 /* Allows values up to ~2^1920 */ #define MAX_SET_TABLE_SIZE (1 << 20) /* 1 million entries max for set iteration */ #define MAX_FRAME_CHAIN_DEPTH (1024 + 512) /* Iteration bound for frame chain walks */ -#define MAX_TASK_WAITER_CHAIN_DEPTH 256 /* Task waiter walk recursion cap: 256 * SIZEOF_TASK_OBJ ~= 1 MiB */ +#define MAX_TASK_WAITER_WALK_TASKS (1 << 16) /* Total-task bound for waiter walks */ #ifndef MAX #define MAX(a, b) ((a) > (b) ? (a) : (b)) @@ -480,13 +480,6 @@ typedef int (*thread_processor_func)( void *context ); -typedef int (*set_entry_processor_func)( - RemoteUnwinderObject *unwinder, - uintptr_t key_addr, - void *context, - size_t depth -); - typedef int (*interpreter_processor_func)( RuntimeOffsets *offsets, uintptr_t interpreter_state_addr, @@ -746,24 +739,6 @@ extern int parse_async_frame_chain( uintptr_t running_task_code_obj ); -/* Set iteration */ -extern int iterate_set_entries( - RemoteUnwinderObject *unwinder, - uintptr_t set_addr, - set_entry_processor_func processor, - void *context, - size_t depth -); - -/* Task awaited_by processing */ -extern int process_task_awaited_by( - RemoteUnwinderObject *unwinder, - uintptr_t task_address, - set_entry_processor_func processor, - void *context, - size_t depth -); - extern int process_single_task_node( RemoteUnwinderObject *unwinder, uintptr_t task_addr, @@ -771,13 +746,6 @@ extern int process_single_task_node( PyObject *result ); -extern int process_task_and_waiters( - RemoteUnwinderObject *unwinder, - uintptr_t task_addr, - PyObject *result, - size_t depth -); - extern int find_running_task_in_thread( RemoteUnwinderObject *unwinder, uintptr_t thread_state_addr, diff --git a/Modules/_remote_debugging/asyncio.c b/Modules/_remote_debugging/asyncio.c index 352edb8cb982d3e..4ee93aec55681e4 100644 --- a/Modules/_remote_debugging/asyncio.c +++ b/Modules/_remote_debugging/asyncio.c @@ -116,13 +116,11 @@ ensure_async_debug_offsets(RemoteUnwinderObject *unwinder) * SET ITERATION FUNCTIONS * ============================================================================ */ -int +static int iterate_set_entries( RemoteUnwinderObject *unwinder, uintptr_t set_addr, - set_entry_processor_func processor, - void *context, - size_t depth + PyObject *awaited_by ) { char set_object[SIZEOF_SET_OBJ]; if (_Py_RemoteDebug_PagedReadRemoteMemory(&unwinder->handle, set_addr, @@ -154,19 +152,10 @@ iterate_set_entries( } 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"); + 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, depth) < 0) { - return -1; - } - els++; - } + els++; } table_ptr += sizeof(void*) * 2; i++; @@ -526,44 +515,11 @@ parse_task( * TASK AWAITED_BY PROCESSING * ============================================================================ */ -// Forward declaration for mutual recursion -static int process_waiter_task( - RemoteUnwinderObject *unwinder, - uintptr_t key_addr, - void *context, - size_t depth -); - -// Processor function for parsing tasks in sets -static int -process_task_parser( - RemoteUnwinderObject *unwinder, - uintptr_t key_addr, - void *context, - size_t depth -) { - (void)depth; - PyObject *awaited_by = (PyObject *)context; - return parse_task(unwinder, key_addr, awaited_by); -} - 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, 0); -} - -int -process_task_awaited_by( - RemoteUnwinderObject *unwinder, - uintptr_t task_address, - set_entry_processor_func processor, - void *context, - size_t depth ) { // Read the entire TaskObj at once char task_obj[SIZEOF_TASK_OBJ]; @@ -582,11 +538,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, depth); + return iterate_set_entries(unwinder, task_ab_addr, awaited_by); } else { // Single task waiting - return processor(unwinder, task_ab_addr, context, depth); + return parse_task(unwinder, task_ab_addr, awaited_by); } } @@ -681,40 +636,39 @@ process_single_task_node( return -1; } -int -process_task_and_waiters( +static int +process_task_waiters( RemoteUnwinderObject *unwinder, - uintptr_t task_addr, - PyObject *result, - size_t depth + PyObject *result ) { - if (depth >= MAX_TASK_WAITER_CHAIN_DEPTH) { - PyErr_SetString(PyExc_RuntimeError, - "Too many task waiters (possible infinite loop)"); - set_exception_cause(unwinder, PyExc_RuntimeError, - "Task waiter chain depth limit exceeded"); - return -1; - } - - // 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); + 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, depth + 1); -} - -// Processor function for task waiters -static int -process_waiter_task( - RemoteUnwinderObject *unwinder, - uintptr_t key_addr, - void *context, - size_t depth -) { - return process_task_and_waiters(unwinder, key_addr, (PyObject *)context, depth); + return 0; } /* ============================================================================ @@ -1017,9 +971,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, 1) < 0) - { + if (process_task_waiters(unwinder, result) < 0) { return -1; } From b5022ba82443cc608030c5730bd1965d30178e97 Mon Sep 17 00:00:00 2001 From: maurycy <5383+maurycy@users.noreply.github.com> Date: Mon, 13 Jul 2026 09:30:12 +0200 Subject: [PATCH 19/20] iterative coro-walk --- Modules/_remote_debugging/_remote_debugging.h | 7 -- Modules/_remote_debugging/asyncio.c | 113 +++++++++--------- 2 files changed, 57 insertions(+), 63 deletions(-) diff --git a/Modules/_remote_debugging/_remote_debugging.h b/Modules/_remote_debugging/_remote_debugging.h index 37ff5a29f261a6d..115de983854c0af 100644 --- a/Modules/_remote_debugging/_remote_debugging.h +++ b/Modules/_remote_debugging/_remote_debugging.h @@ -725,13 +725,6 @@ extern int parse_task( PyObject *render_to ); -extern int parse_coro_chain( - RemoteUnwinderObject *unwinder, - uintptr_t coro_address, - PyObject *render_to, - size_t depth -); - extern int parse_async_frame_chain( RemoteUnwinderObject *unwinder, PyObject *calls, diff --git a/Modules/_remote_debugging/asyncio.c b/Modules/_remote_debugging/asyncio.c index 4ee93aec55681e4..63c53fe58775569 100644 --- a/Modules/_remote_debugging/asyncio.c +++ b/Modules/_remote_debugging/asyncio.c @@ -238,13 +238,14 @@ parse_task_name( * ============================================================================ */ 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, - size_t depth + uintptr_t *next_coro ) { + *next_coro = 0; + // Read the entire interpreter frame at once char iframe[SIZEOF_INTERP_FRAME]; int err = _Py_RemoteDebug_PagedReadRemoteMemory( @@ -300,12 +301,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, - depth + 1); - if (err) { - set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to parse coroutine chain in yield_from"); - return -1; - } + *next_coro = gi_await_addr; } } } @@ -313,67 +309,72 @@ handle_yield_from_frame( return 0; } -int +static int parse_coro_chain( RemoteUnwinderObject *unwinder, uintptr_t coro_address, - PyObject *render_to, - size_t depth + PyObject *render_to ) { assert((void*)coro_address != NULL); - 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; - } + 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; + } - // 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; - } + // 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; + } - 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; - } + 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; + } - uintptr_t gen_type_addr = GET_MEMBER(uintptr_t, gen_object, unwinder->debug_offsets.pyobject.ob_type); + uintptr_t gen_type_addr = GET_MEMBER(uintptr_t, gen_object, unwinder->debug_offsets.pyobject.ob_type); - PyObject* name = NULL; + PyObject* name = NULL; - // 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; - } + // 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 (!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, depth); + 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; @@ -419,7 +420,7 @@ create_task_result( coro_addr = GET_MEMBER_NO_TAG(uintptr_t, task_obj, unwinder->async_debug_offsets.asyncio_task_object.task_coro); if ((void*)coro_addr != NULL) { - if (parse_coro_chain(unwinder, coro_addr, call_stack, 0) < 0) { + if (parse_coro_chain(unwinder, coro_addr, call_stack) < 0) { set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to parse coroutine chain"); goto error; } From 73b8674214b69ce0463b23ba63e7d48e353e69b5 Mon Sep 17 00:00:00 2001 From: maurycy <5383+maurycy@users.noreply.github.com> Date: Mon, 13 Jul 2026 09:40:17 +0200 Subject: [PATCH 20/20] nicer news --- .../Library/2026-07-08-22-18-04.gh-issue-153364.JBFHEg.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 index dfc05a5d428ceca..546cd9e1c884dee 100644 --- 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 @@ -1,3 +1,2 @@ -Add limits to frame, coroutine and task-waiter walks in the Tachyon sampling -profiler. This avoids potential hangs and stack overflows. Patch by Maurycy -Pawłowski-Wieroński. +Make frame, coroutine and task-waiter walks iterative and bounded, avoiding +potential hangs and stack overflows. Patch by Maurycy Pawłowski-Wieroński.