Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 37 additions & 7 deletions Lib/test/test_external_inspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,34 @@ async def rec(n):

self.assertEqual(asyncio.run(rec(3)), ["rec"] * 4)

@skip_if_not_supported
def test_deep_awaiter_chain_is_walked_fully(self):
# gh-158688
depth = 20000

async def root():
await asyncio.sleep(0)
return [
task.task_name
for info in RemoteUnwinder(os.getpid()).get_async_stack_trace()
for task in info.awaited_by
]

async def link(awaited):
return await awaited

async def main():
first = asyncio.create_task(root(), name="root")
top = first
for i in range(depth):
top = asyncio.create_task(link(top), name=f"a{i}")
second = asyncio.create_task(link(first), name="b")
return (await top), asyncio.current_task().get_name()

names, main_name = asyncio.run(main())
self.assertEqual(len(names), depth + 3)
self.assertIn(main_name, names)

@skip_if_not_supported
@unittest.skipIf(
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
Expand Down Expand Up @@ -4550,9 +4578,9 @@ def recurse(n):
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."""
def test_get_async_stack_trace_deep_task_waiter_chain_is_walked(self):
"""Test that a task waiter chain longer than the walk used to allow
is walked to the end instead of overflowing the C stack."""
script_body = f"""\
import asyncio

Expand All @@ -4569,10 +4597,12 @@ async def chain(n):
"""
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",
)
tasks = [
task
for info in get_async_stack_trace(p.pid)
for task in info.awaited_by
]
self.assertEqual(len(tasks), self.TASK_WAITER_WALK_TASKS + 1)
client_socket.sendall(b"done")

@skip_if_not_supported
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix a crash in the sampling profiler when dumping a program with a deep
chain of awaiting tasks. Patch by Timofei Ivankov.
1 change: 0 additions & 1 deletion Modules/_remote_debugging/_remote_debugging.h
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,6 @@ 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_WALK_TASKS (1 << 14) /* Total-task bound for waiter walks */

#ifndef MAX
#define MAX(a, b) ((a) > (b) ? (a) : (b))
Expand Down
37 changes: 28 additions & 9 deletions Modules/_remote_debugging/asyncio.c
Original file line number Diff line number Diff line change
Expand Up @@ -645,35 +645,54 @@ process_task_waiters(
RemoteUnwinderObject *unwinder,
PyObject *result
) {
// gh-158688: walk each task once, not once per path to it
PyObject *seen = PySet_New(NULL);
if (seen == NULL) {
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create visited task set");
return -1;
}

for (Py_ssize_t i = 0; i < PyList_GET_SIZE(result); i++) {
PyObject *task_info = PyList_GET_ITEM(result, i);
if (PySet_Add(seen, PyStructSequence_GET_ITEM(task_info, 0)) < 0) {
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to mark task as visited");
goto error;
}
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);
Py_ssize_t seen_count = PySet_GET_SIZE(seen);
int marked = PySet_Add(seen, task_id);
if (marked < 0) {
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to mark task as visited");
goto error;
}
if (PySet_GET_SIZE(seen) == seen_count) {
// already visited
continue;
}
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;
goto error;
}
if (process_single_task_node(
unwinder, (uintptr_t)task_ptr, NULL, result) < 0)
{
return -1;
goto error;
}
}
}

Py_DECREF(seen);
return 0;

error:
Py_DECREF(seen);
return -1;
}

/* ============================================================================
Expand Down
Loading