gh-153364: Make frame, coroutine, and task-waiter chain walks iterative and bounded#153365
gh-153364: Make frame, coroutine, and task-waiter chain walks iterative and bounded#153365maurycy wants to merge 20 commits into
Conversation
| #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 */ |
There was a problem hiding this comment.
Not the scope but maybe MAX_THREADS, MAX_STACK_CHUNKS, MAX_LINETABLE_ENTRIES or MAX_ITERATIONS are worth keeping here too
get_async_stack_trace() and parse_coro_chain()| PyObject *result, | ||
| size_t depth | ||
| ) { | ||
| if (depth >= MAX_TASK_WAITER_CHAIN_DEPTH) { |
There was a problem hiding this comment.
This can still overflow a 1 MiB stack: every recursive level keeps the 4 KiB task_obj alive, so 256 levels exhaust the stack before this check fires. We should walk the waiter graph iteratively.
There was a problem hiding this comment.
@pablogsal I've changed both to iterative.
|
A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated. Once you have made the requested changes, please leave a comment on this pull request containing the phrase |
|
I have made the requested changes; please review again. |
|
Thanks for making the requested changes! @pablogsal: please review the changes made to this pull request. |
| return -1; | ||
| } | ||
|
|
||
| if (ref_cnt) { |
There was a problem hiding this comment.
I believe this was a bug:
- this address is already read as
key_addr: https://github.com/python/cpython/pull/153365/changes#diff-d3a9f5049ca6c549a4feb928b6d06a7dbce8486745bb06f6b8254cddce207e32L150 (which is correctly named) - if we aim to filter out tombstones, then we should use
dummyorhash != -1:Line 595 in c843cb7
cpython/Include/cpython/setobject.h
Lines 5 to 22 in c843cb7
ref 1885988
| 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) { |
There was a problem hiding this comment.
Not keeping the visited set, so duplicated are counted towards. I believe that 65536 gives enough head room?
| int | ||
| process_task_and_waiters( | ||
| static int | ||
| process_task_waiters( |
There was a problem hiding this comment.
BFS. Leveraging results as the queue. Note that the current order is not documented and not relied by tests.
| void *context | ||
| ); | ||
|
|
||
| extern int process_single_task_node( |
There was a problem hiding this comment.
It's used only in the asyncio.c. Does it, and friends, need to be in the header?
The PR hardens and cleans up frame, coro and task-waiter walks a bit, by making them iterative and bounded.
I'm addressing two issues here:
process_frame_chain()already had1024 + 512limit, but the PR extracts it asMAX_FRAME_CHAIN_DEPTHand applies in theparse_coro_chain(), used by both sync and async paths, andparse_async_frame_chain(). The task-waiter walk inget_async_stack_trace()gets a separate limit (MAX_TASK_WAITER_WALK_TASKS), on the total number of tasks visited (including duplicates), since its' a graph, not a chain.SIZEOF_TASK_OBJ) 256 was enough to overflow a 1MiB stack.The obvious context here is avoiding infinite loops. Truth be told, I think that in some places torn reads were also responsible for avoiding them and exists. :-)
_remote_debugging: No frame limit inget_async_stack_trace()#153364