Skip to content

Commit dcd1db0

Browse files
committed
gh-151535: Walk awaited_by graph iteratively to avoid C stack overflow
The depth-bounded recursion still overflowed the debugger's C stack on platforms with a small default thread stack (Windows uses 1 MiB): every level keeps a SIZEOF_TASK_OBJ buffer alive in process_task_awaited_by, so the MAX_TASK_AWAITED_BY_DEPTH limit of 1000 was only reached after several MiB of stack had already been consumed, and the process aborted with a stack overflow before the limit could fire. Walk the awaited_by graph with an explicit, heap-allocated work-stack instead of mutual recursion, so the C stack depth stays constant no matter how deep the graph is. The depth limit is retained as a cycle guard for corrupted or concurrently-mutated remote memory. Also make the regression test deterministic under load: signal readiness from the leaf task itself, immediately before it busy-spins, so the observer always inspects while the full chain is built and rooted at a running task. The previous handshake was sent before the leaf started running and could race, letting the observer see a shallow graph.
1 parent 67e7b05 commit dcd1db0

3 files changed

Lines changed: 97 additions & 41 deletions

File tree

Lib/test/test_external_inspection.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1191,6 +1191,11 @@ async def main():
11911191
async def leaf():
11921192
while not started.is_set():
11931193
await asyncio.sleep(0)
1194+
# The whole awaited_by chain is built and this is now the
1195+
# running task at the bottom of it. Signal here, then
1196+
# busy-spin, so the observer inspects while the chain is
1197+
# fully present and rooted at a running task.
1198+
sock.sendall(b"ready")
11941199
end = time.time() + 10_000
11951200
while time.time() < end:
11961201
pass
@@ -1209,7 +1214,6 @@ async def waiter(child):
12091214
for _ in range(5):
12101215
await asyncio.sleep(0)
12111216
1212-
sock.sendall(b"ready")
12131217
started.set()
12141218
try:
12151219
await leaf_t

Modules/_remote_debugging/_remote_debugging.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +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 */
150+
#define MAX_TASK_AWAITED_BY_DEPTH 1000 /* Bound the awaited_by graph walk so a cycle terminates */
151151

152152
#ifndef MAX
153153
#define MAX(a, b) ((a) > (b) ? (a) : (b))

Modules/_remote_debugging/asyncio.c

Lines changed: 91 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -513,19 +513,24 @@ parse_task(
513513
* TASK AWAITED_BY PROCESSING
514514
* ============================================================================ */
515515

516-
// Forward declaration for mutual recursion
517-
static int process_waiter_task(RemoteUnwinderObject *unwinder, uintptr_t key_addr, void *context);
518-
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.
516+
// The awaited_by graph is walked with an explicit, heap-allocated work-stack
517+
// rather than C recursion. A deeply nested -- or, in a corrupted or
518+
// concurrently-mutated remote process, cyclic -- chain would otherwise drive
519+
// unbounded recursion and overflow the debugger's own C stack: each level holds
520+
// a SIZEOF_TASK_OBJ buffer (see process_task_awaited_by), so even a few hundred
521+
// levels can exhaust a 1 MB stack. MAX_TASK_AWAITED_BY_DEPTH bounds the walk so
522+
// a cycle terminates.
521523
typedef struct {
522-
PyObject *result;
524+
uintptr_t addr;
523525
int depth;
524-
} waiter_context_t;
526+
} awaited_by_entry_t;
525527

526-
static int process_task_and_waiters_impl(
527-
RemoteUnwinderObject *unwinder, uintptr_t task_addr, PyObject *result,
528-
int depth);
528+
typedef struct {
529+
awaited_by_entry_t *items;
530+
Py_ssize_t size;
531+
Py_ssize_t capacity;
532+
int current_depth;
533+
} awaited_by_stack_t;
529534

530535
// Processor function for parsing tasks in sets
531536
static int
@@ -670,49 +675,88 @@ process_single_task_node(
670675
}
671676

672677
static int
673-
process_task_and_waiters_impl(
678+
awaited_by_stack_push(
674679
RemoteUnwinderObject *unwinder,
675-
uintptr_t task_addr,
676-
PyObject *result,
680+
awaited_by_stack_t *stack,
681+
uintptr_t addr,
677682
int depth
678683
) {
679-
// First, add this task to the result
680-
if (process_single_task_node(unwinder, task_addr, NULL, result) < 0) {
681-
return -1;
684+
if (stack->size >= stack->capacity) {
685+
Py_ssize_t new_capacity = stack->capacity ? stack->capacity * 2 : 16;
686+
awaited_by_entry_t *new_items = PyMem_Realloc(
687+
stack->items, (size_t)new_capacity * sizeof(awaited_by_entry_t));
688+
if (new_items == NULL) {
689+
PyErr_NoMemory();
690+
set_exception_cause(unwinder, PyExc_MemoryError,
691+
"Failed to grow awaited_by work-stack");
692+
return -1;
693+
}
694+
stack->items = new_items;
695+
stack->capacity = new_capacity;
682696
}
683-
684-
// Now find all tasks that are waiting for this task and process them
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);
697+
stack->items[stack->size].addr = addr;
698+
stack->items[stack->size].depth = depth;
699+
stack->size++;
700+
return 0;
696701
}
697702

698-
// Processor function for task waiters
703+
// set_entry_processor_func: enqueue a task waiting on the one currently being
704+
// expanded, one level deeper. The depth bound makes a cyclic or corrupted
705+
// awaited_by graph terminate instead of looping forever.
699706
static int
700-
process_waiter_task(
707+
push_awaited_by_waiter(
701708
RemoteUnwinderObject *unwinder,
702709
uintptr_t key_addr,
703710
void *context
704711
) {
705-
waiter_context_t *ctx = (waiter_context_t *)context;
706-
if (ctx->depth >= MAX_TASK_AWAITED_BY_DEPTH) {
712+
awaited_by_stack_t *stack = (awaited_by_stack_t *)context;
713+
if (stack->current_depth >= MAX_TASK_AWAITED_BY_DEPTH) {
707714
PyErr_SetString(PyExc_RuntimeError,
708715
"Task awaited_by chain is too deep or cyclic "
709716
"(corrupted remote memory)");
710717
set_exception_cause(unwinder, PyExc_RuntimeError,
711-
"Task awaited_by recursion limit exceeded");
718+
"Task awaited_by depth limit exceeded");
712719
return -1;
713720
}
714-
return process_task_and_waiters_impl(unwinder, key_addr, ctx->result,
715-
ctx->depth + 1);
721+
return awaited_by_stack_push(unwinder, stack, key_addr,
722+
stack->current_depth + 1);
723+
}
724+
725+
// Drain the work-stack: append each task node to result, then enqueue the
726+
// tasks waiting on it. Depth-first, with no C recursion over the graph.
727+
static int
728+
drain_awaited_by_stack(
729+
RemoteUnwinderObject *unwinder,
730+
PyObject *result,
731+
awaited_by_stack_t *stack
732+
) {
733+
while (stack->size > 0) {
734+
awaited_by_entry_t entry = stack->items[--stack->size];
735+
if (process_single_task_node(unwinder, entry.addr, NULL, result) < 0) {
736+
return -1;
737+
}
738+
stack->current_depth = entry.depth;
739+
if (process_task_awaited_by(unwinder, entry.addr,
740+
push_awaited_by_waiter, stack) < 0) {
741+
return -1;
742+
}
743+
}
744+
return 0;
745+
}
746+
747+
int
748+
process_task_and_waiters(
749+
RemoteUnwinderObject *unwinder,
750+
uintptr_t task_addr,
751+
PyObject *result
752+
) {
753+
awaited_by_stack_t stack = {0};
754+
int result_code = -1;
755+
if (awaited_by_stack_push(unwinder, &stack, task_addr, 0) == 0) {
756+
result_code = drain_awaited_by_stack(unwinder, result, &stack);
757+
}
758+
PyMem_Free(stack.items);
759+
return result_code;
716760
}
717761

718762
/* ============================================================================
@@ -1008,9 +1052,17 @@ process_running_task_chain(
10081052
return -1;
10091053
}
10101054

1011-
// Now find all tasks that are waiting for this task and process them
1012-
waiter_context_t ctx = {result, 0};
1013-
if (process_task_awaited_by(unwinder, running_task_addr, process_waiter_task, &ctx) < 0) {
1055+
// Now find all tasks that are waiting for this task and process them with
1056+
// the same iterative, heap-stacked walk as process_task_and_waiters (the
1057+
// running task itself is already recorded via the frame chain above).
1058+
awaited_by_stack_t stack = {0};
1059+
int waiters_code = process_task_awaited_by(unwinder, running_task_addr,
1060+
push_awaited_by_waiter, &stack);
1061+
if (waiters_code == 0) {
1062+
waiters_code = drain_awaited_by_stack(unwinder, result, &stack);
1063+
}
1064+
PyMem_Free(stack.items);
1065+
if (waiters_code < 0) {
10141066
return -1;
10151067
}
10161068

0 commit comments

Comments
 (0)