Skip to content

Commit 67e7b05

Browse files
committed
gh-151535: Bound _remote_debugging asyncio awaited_by graph recursion
1 parent 11f032f commit 67e7b05

4 files changed

Lines changed: 122 additions & 7 deletions

File tree

Lib/test/test_external_inspection.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1164,6 +1164,85 @@ async def main():
11641164
finally:
11651165
_cleanup_sockets(client_socket, server_socket)
11661166

1167+
@skip_if_not_supported
1168+
@unittest.skipIf(
1169+
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
1170+
"Test only runs on Linux with process_vm_readv support",
1171+
)
1172+
def test_async_deep_awaited_by_chain_is_bounded(self):
1173+
# A very deep awaited_by chain in the target (which a corrupted or
1174+
# concurrently-mutated remote process can also present as a cycle) must
1175+
# not drive unbounded C recursion in the debugger. get_async_stack_trace
1176+
# should raise instead of overflowing the stack.
1177+
depth = 2000
1178+
port = find_unused_port()
1179+
script = textwrap.dedent(
1180+
f"""\
1181+
import asyncio
1182+
import socket
1183+
import time
1184+
1185+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1186+
sock.connect(('localhost', {port}))
1187+
1188+
async def main():
1189+
started = asyncio.Event()
1190+
1191+
async def leaf():
1192+
while not started.is_set():
1193+
await asyncio.sleep(0)
1194+
end = time.time() + 10_000
1195+
while time.time() < end:
1196+
pass
1197+
1198+
leaf_t = asyncio.ensure_future(leaf())
1199+
1200+
async def waiter(child):
1201+
await child
1202+
1203+
cur = leaf_t
1204+
tasks = [cur]
1205+
for _ in range({depth}):
1206+
cur = asyncio.ensure_future(waiter(cur))
1207+
tasks.append(cur)
1208+
1209+
for _ in range(5):
1210+
await asyncio.sleep(0)
1211+
1212+
sock.sendall(b"ready")
1213+
started.set()
1214+
try:
1215+
await leaf_t
1216+
finally:
1217+
for t in tasks:
1218+
t.cancel()
1219+
1220+
asyncio.run(main())
1221+
"""
1222+
)
1223+
1224+
with os_helper.temp_dir() as work_dir:
1225+
script_dir = os.path.join(work_dir, "script_pkg")
1226+
os.mkdir(script_dir)
1227+
1228+
server_socket = _create_server_socket(port)
1229+
script_name = _make_test_script(script_dir, "script", script)
1230+
client_socket = None
1231+
try:
1232+
with _managed_subprocess([sys.executable, script_name]) as p:
1233+
client_socket, _ = server_socket.accept()
1234+
server_socket.close()
1235+
server_socket = None
1236+
1237+
_wait_for_signal(client_socket, b"ready")
1238+
1239+
unwinder = RemoteUnwinder(p.pid)
1240+
with self.assertRaises(RuntimeError) as cm:
1241+
unwinder.get_async_stack_trace()
1242+
self.assertIn("too deep or cyclic", str(cm.exception))
1243+
finally:
1244+
_cleanup_sockets(client_socket, server_socket)
1245+
11671246
@skip_if_not_supported
11681247
@unittest.skipIf(
11691248
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:meth:`!RemoteUnwinder.get_async_stack_trace` no longer crashes when the
2+
target process presents a deeply nested or concurrently-mutated ``awaited_by``
3+
graph; a :exc:`RuntimeError` is now raised instead.

Modules/_remote_debugging/_remote_debugging.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ typedef enum _WIN32_THREADSTATE {
147147
#define MAX_STACK_CHUNK_SIZE (16 * 1024 * 1024) /* 16 MB max for stack chunks */
148148
#define MAX_LONG_DIGITS 64 /* Allows values up to ~2^1920 */
149149
#define MAX_SET_TABLE_SIZE (1 << 20) /* 1 million entries max for set iteration */
150+
#define MAX_TASK_AWAITED_BY_DEPTH 1000 /* Bound recursion over the awaited_by graph */
150151

151152
#ifndef MAX
152153
#define MAX(a, b) ((a) > (b) ? (a) : (b))

Modules/_remote_debugging/asyncio.c

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,17 @@ parse_task(
516516
// Forward declaration for mutual recursion
517517
static int process_waiter_task(RemoteUnwinderObject *unwinder, uintptr_t key_addr, void *context);
518518

519+
// Carries the recursion depth so a cyclic or corrupted remote awaited_by graph
520+
// cannot drive unbounded C recursion and overflow the debugger's stack.
521+
typedef struct {
522+
PyObject *result;
523+
int depth;
524+
} waiter_context_t;
525+
526+
static int process_task_and_waiters_impl(
527+
RemoteUnwinderObject *unwinder, uintptr_t task_addr, PyObject *result,
528+
int depth);
529+
519530
// Processor function for parsing tasks in sets
520531
static int
521532
process_task_parser(
@@ -658,19 +669,30 @@ process_single_task_node(
658669
return -1;
659670
}
660671

661-
int
662-
process_task_and_waiters(
672+
static int
673+
process_task_and_waiters_impl(
663674
RemoteUnwinderObject *unwinder,
664675
uintptr_t task_addr,
665-
PyObject *result
676+
PyObject *result,
677+
int depth
666678
) {
667679
// First, add this task to the result
668680
if (process_single_task_node(unwinder, task_addr, NULL, result) < 0) {
669681
return -1;
670682
}
671683

672684
// Now find all tasks that are waiting for this task and process them
673-
return process_task_awaited_by(unwinder, task_addr, process_waiter_task, result);
685+
waiter_context_t ctx = {result, depth};
686+
return process_task_awaited_by(unwinder, task_addr, process_waiter_task, &ctx);
687+
}
688+
689+
int
690+
process_task_and_waiters(
691+
RemoteUnwinderObject *unwinder,
692+
uintptr_t task_addr,
693+
PyObject *result
694+
) {
695+
return process_task_and_waiters_impl(unwinder, task_addr, result, 0);
674696
}
675697

676698
// Processor function for task waiters
@@ -680,8 +702,17 @@ process_waiter_task(
680702
uintptr_t key_addr,
681703
void *context
682704
) {
683-
PyObject *result = (PyObject *)context;
684-
return process_task_and_waiters(unwinder, key_addr, result);
705+
waiter_context_t *ctx = (waiter_context_t *)context;
706+
if (ctx->depth >= MAX_TASK_AWAITED_BY_DEPTH) {
707+
PyErr_SetString(PyExc_RuntimeError,
708+
"Task awaited_by chain is too deep or cyclic "
709+
"(corrupted remote memory)");
710+
set_exception_cause(unwinder, PyExc_RuntimeError,
711+
"Task awaited_by recursion limit exceeded");
712+
return -1;
713+
}
714+
return process_task_and_waiters_impl(unwinder, key_addr, ctx->result,
715+
ctx->depth + 1);
685716
}
686717

687718
/* ============================================================================
@@ -978,7 +1009,8 @@ process_running_task_chain(
9781009
}
9791010

9801011
// Now find all tasks that are waiting for this task and process them
981-
if (process_task_awaited_by(unwinder, running_task_addr, process_waiter_task, result) < 0) {
1012+
waiter_context_t ctx = {result, 0};
1013+
if (process_task_awaited_by(unwinder, running_task_addr, process_waiter_task, &ctx) < 0) {
9821014
return -1;
9831015
}
9841016

0 commit comments

Comments
 (0)