@@ -339,6 +339,40 @@ def _run_script_and_get_trace(
339339 finally :
340340 _cleanup_sockets (client_socket , server_socket )
341341
342+ @contextmanager
343+ def _target_process (self , script_body ):
344+ """Context manager for running a target process with socket sync."""
345+ port = find_unused_port ()
346+ script = f"""\
347+ import socket
348+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
349+ sock.connect(('localhost', { port } ))
350+ { textwrap .dedent (script_body )}
351+ """
352+
353+ with os_helper .temp_dir () as work_dir :
354+ script_dir = os .path .join (work_dir , "script_pkg" )
355+ os .mkdir (script_dir )
356+
357+ server_socket = _create_server_socket (port )
358+ script_name = _make_test_script (script_dir , "script" , script )
359+ client_socket = None
360+
361+ try :
362+ with _managed_subprocess ([sys .executable , script_name ]) as p :
363+ client_socket , _ = server_socket .accept ()
364+ server_socket .close ()
365+ server_socket = None
366+
367+ def make_unwinder (cache_frames = True ):
368+ return RemoteUnwinder (
369+ p .pid , all_threads = True , cache_frames = cache_frames
370+ )
371+
372+ yield p , client_socket , make_unwinder
373+ finally :
374+ _cleanup_sockets (client_socket , server_socket )
375+
342376 def _find_frame_in_trace (self , stack_trace , predicate ):
343377 """
344378 Find a frame matching predicate in stack trace.
@@ -2927,40 +2961,6 @@ class TestFrameCaching(RemoteInspectionTestBase):
29272961 All tests verify cache reuse via object identity checks (assertIs).
29282962 """
29292963
2930- @contextmanager
2931- def _target_process (self , script_body ):
2932- """Context manager for running a target process with socket sync."""
2933- port = find_unused_port ()
2934- script = f"""\
2935- import socket
2936- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
2937- sock.connect(('localhost', { port } ))
2938- { textwrap .dedent (script_body )}
2939- """
2940-
2941- with os_helper .temp_dir () as work_dir :
2942- script_dir = os .path .join (work_dir , "script_pkg" )
2943- os .mkdir (script_dir )
2944-
2945- server_socket = _create_server_socket (port )
2946- script_name = _make_test_script (script_dir , "script" , script )
2947- client_socket = None
2948-
2949- try :
2950- with _managed_subprocess ([sys .executable , script_name ]) as p :
2951- client_socket , _ = server_socket .accept ()
2952- server_socket .close ()
2953- server_socket = None
2954-
2955- def make_unwinder (cache_frames = True ):
2956- return RemoteUnwinder (
2957- p .pid , all_threads = True , cache_frames = cache_frames
2958- )
2959-
2960- yield p , client_socket , make_unwinder
2961- finally :
2962- _cleanup_sockets (client_socket , server_socket )
2963-
29642964 def _get_frames_with_retry (self , unwinder , required_funcs ):
29652965 """Get frames containing required_funcs, with retry for transient errors."""
29662966 for _ in range (MAX_TRIES ):
@@ -3804,5 +3804,125 @@ def test_get_stats_disabled_raises(self):
38043804 client_socket .sendall (b"done" )
38053805
38063806
3807+ @requires_remote_subprocess_debugging ()
3808+ class TestFrameChainLimits (RemoteInspectionTestBase ):
3809+ """Frame chain walks abort instead of looping/overflowing on deep chains."""
3810+
3811+ CHAIN_DEPTH = 1024 + 512 + 1
3812+
3813+ def _assert_unwinder_limit_error (self , unwind , expected_substring ):
3814+ """Call unwind() until it raises the frame chain limit error.
3815+
3816+ unwind must construct the RemoteUnwinder and call it, so that
3817+ transient RuntimeErrors from either step are retried; a successful
3818+ call means the limit never triggered and fails immediately.
3819+ """
3820+ last_error = None
3821+ for _ in busy_retry (SHORT_TIMEOUT , error = False ):
3822+ try :
3823+ unwind ()
3824+ except TRANSIENT_ERRORS as e :
3825+ if expected_substring in str (e ):
3826+ return
3827+ last_error = e
3828+ continue
3829+ self .fail (
3830+ "frame chain limit did not trigger; call returned a result"
3831+ )
3832+ self .fail (
3833+ f"frame chain limit never raised; last transient error: "
3834+ f"{ last_error !r} "
3835+ )
3836+
3837+ @skip_if_not_supported
3838+ @unittest .skipIf (
3839+ sys .platform == "linux" and not PROCESS_VM_READV_SUPPORTED ,
3840+ "Test only runs on Linux with process_vm_readv support" ,
3841+ )
3842+ def test_get_stack_trace_deep_frame_chain_aborts (self ):
3843+ """Test that a frame chain deeper than the limit aborts the
3844+ synchronous stack walk instead of walking it indefinitely."""
3845+ script_body = f"""\
3846+ import sys
3847+ sys.setrecursionlimit({ self .CHAIN_DEPTH * 2 } )
3848+
3849+ def recurse(n):
3850+ if n <= 0:
3851+ sock.sendall(b"ready")
3852+ sock.recv(16)
3853+ return
3854+ recurse(n - 1)
3855+
3856+ recurse({ self .CHAIN_DEPTH } )
3857+ """
3858+ with self ._target_process (script_body ) as (p , client_socket , _ ):
3859+ _wait_for_signal (client_socket , b"ready" )
3860+ self ._assert_unwinder_limit_error (
3861+ lambda : RemoteUnwinder (p .pid ).get_stack_trace (),
3862+ "Too many stack frames" ,
3863+ )
3864+ client_socket .sendall (b"done" )
3865+
3866+ @skip_if_not_supported
3867+ @unittest .skipIf (
3868+ sys .platform == "linux" and not PROCESS_VM_READV_SUPPORTED ,
3869+ "Test only runs on Linux with process_vm_readv support" ,
3870+ )
3871+ def test_get_async_stack_trace_deep_frame_chain_aborts (self ):
3872+ """Test that a frame chain deeper than the limit aborts the async
3873+ stack walk instead of walking it indefinitely."""
3874+ script_body = f"""\
3875+ import sys, asyncio
3876+ sys.setrecursionlimit({ self .CHAIN_DEPTH * 2 } )
3877+
3878+ def recurse(n):
3879+ if n <= 0:
3880+ sock.sendall(b"ready")
3881+ sock.recv(16)
3882+ return
3883+ recurse(n - 1)
3884+
3885+ async def deep():
3886+ recurse({ self .CHAIN_DEPTH } )
3887+
3888+ asyncio.run(deep())
3889+ """
3890+ with self ._target_process (script_body ) as (p , client_socket , _ ):
3891+ _wait_for_signal (client_socket , b"ready" )
3892+ self ._assert_unwinder_limit_error (
3893+ lambda : RemoteUnwinder (p .pid ).get_async_stack_trace (),
3894+ "Too many async stack frames" ,
3895+ )
3896+ client_socket .sendall (b"done" )
3897+
3898+ @skip_if_not_supported
3899+ @unittest .skipIf (
3900+ sys .platform == "linux" and not PROCESS_VM_READV_SUPPORTED ,
3901+ "Test only runs on Linux with process_vm_readv support" ,
3902+ )
3903+ def test_get_all_awaited_by_deep_coro_chain_aborts (self ):
3904+ """Test that a coroutine await chain deeper than the limit aborts
3905+ the walk instead of overflowing the C stack."""
3906+ script_body = f"""\
3907+ import sys, asyncio
3908+ sys.setrecursionlimit({ self .CHAIN_DEPTH * 2 } )
3909+
3910+ async def chain(n):
3911+ if n <= 0:
3912+ sock.sendall(b"ready")
3913+ await asyncio.sleep(10_000)
3914+ return
3915+ await chain(n - 1)
3916+
3917+ asyncio.run(chain({ self .CHAIN_DEPTH } ))
3918+ """
3919+ with self ._target_process (script_body ) as (p , client_socket , _ ):
3920+ _wait_for_signal (client_socket , b"ready" )
3921+ self ._assert_unwinder_limit_error (
3922+ lambda : RemoteUnwinder (p .pid ).get_all_awaited_by (),
3923+ "Too many coroutine frames" ,
3924+ )
3925+
3926+
38073927if __name__ == "__main__" :
38083928 unittest .main ()
0 commit comments