Skip to content

gh-153364: Make frame, coroutine, and task-waiter chain walks iterative and bounded - #153365

Merged
pablogsal merged 31 commits into
python:mainfrom
maurycy:tachyon-parse_async_frame_chain-limit
Oct 4, 2026
Merged

pablogsal merged 31 commits into
python:mainfrom
maurycy:tachyon-parse_async_frame_chain-limit

Conversation

@maurycy

@maurycy maurycy commented Jul 8, 2026 •

Copy link
Copy Markdown
Contributor

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:

  1. No walk limits. The walks use pointers read from the target without any guarantees, so a torn read could stitch a cycle. Not to mention adversial targets. process_frame_chain() already had 1024 + 512 limit, but the PR extracts it as MAX_FRAME_CHAIN_DEPTH and applies in the parse_coro_chain(), used by both sync and async paths, and parse_async_frame_chain(). The task-waiter walk in get_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.
  2. Stack overflows. Both coro and task-waiter walks were recursive. With 4KB (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 early exits. :-)

Comment thread Modules/_remote_debugging/asyncio.c
#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 */

@maurycy maurycy Jul 8, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe MAX_THREADS, MAX_TLBC_SIZE, MAX_STACK_CHUNKS, MAX_LINETABLE_SIZE or MAX_ITERATIONS are worth keeping here too?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While we're at it, should MAX_TLBC_SIZE be in sync with MAX_THREADS?

@maurycy maurycy changed the title gh-153364: Add frame limit in get_async_stack_trace() and parse_coro_chain() gh-153364: Limit frame, coroutine, and task-waiter chain walks Jul 10, 2026
Comment thread Modules/_remote_debugging/asyncio.c Outdated
Comment thread Lib/test/test_external_inspection.py Outdated
Comment thread Modules/_remote_debugging/_remote_debugging.h Outdated
Comment thread Modules/_remote_debugging/asyncio.c Outdated
Comment thread Modules/_remote_debugging/asyncio.c Outdated
@bedevere-app

bedevere-app Bot commented Jul 11, 2026

Copy link
Copy Markdown

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. I will then notify any core developers who have left a review that you're ready for them to take another look at this pull request.

set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read set entry ref count");
uintptr_t key_addr = (uintptr_t)entry.key;
if (key_addr != 0 && entry.hash != -1) {
if (parse_task(unwinder, key_addr, awaited_by) < 0) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still parses the complete awaited_by set before checking the waiter limit. Can we pass the remaining budget into iterate_set_entries() and check it before parse_task()?

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) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still does not bound the total work, no? process_single_task_node() parses the complete awaited_by set via iterate_set_entries() (up to MAX_SET_TABLE_SIZE entries, each doing create_task_result() + a coro chain walk) before we get back to this check. Can we share the remaining budget with iterate_set_entries() and decrement it before every parse_task()? I am also fine doing this in a follow-up if you prefer.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pablogsal Let's do it as a follow up!

@maurycy
maurycy requested a review from pablogsal October 4, 2026 19:33

@pablogsal pablogsal left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, good work! 🚀

@pablogsal
pablogsal enabled auto-merge (squash) October 4, 2026 23:57
@pablogsal pablogsal added needs backport to 3.14 bugs and security fixes needs backport to 3.15 pre-release feature fixes, bugs and security fixes labels Oct 4, 2026
@pablogsal
pablogsal merged commit e0861c6 into python:main Oct 4, 2026
61 of 62 checks passed
@miss-islington-app

Copy link
Copy Markdown

Thanks @maurycy for the PR, and @pablogsal for merging it 🌮🎉.. I'm working now to backport this PR to: 3.14, 3.15.
🐍🍒⛏🤖

@miss-islington-app

Copy link
Copy Markdown

Sorry, @maurycy and @pablogsal, I could not cleanly backport this to 3.14 due to a conflict.

Please backport manually with cherry_picker, see the devguide for more information.

cherry_picker e0861c6ae70c7e6f16c28399bd2b28c521e11e84 3.14

@bedevere-app

bedevere-app Bot commented Oct 4, 2026

Copy link
Copy Markdown

GH-158813 is a backport of this pull request to the 3.15 branch.

@bedevere-app

bedevere-app Bot commented Oct 5, 2026

Copy link
Copy Markdown

GH-158814 is a backport of this pull request to the 3.14 branch.

@bedevere-app bedevere-app Bot removed the needs backport to 3.14 bugs and security fixes label Oct 5, 2026
pablogsal pushed a commit that referenced this pull request Oct 5, 2026
…iterative and bounded (GH-153365) (#158813)

gh-153364: Make frame, coroutine, and task-waiter chain walks iterative and bounded (GH-153365)

* let me declare single limit

* use our new limit in process_frame_chain()

* add it in parse_async_frame_chain()

* parse_coro_chain()

* NEWS

* async in the message?

* test

* no race

* process_task_awaited_by

* process_task_awaited_by limit test

* NEWS

* MAX_TASK_WAITER_CHAIN_DEPTH

* TASK_WAITER_CHAIN_DEPTH in test

* TASK_WAITER_CHAIN_DEPTH 256

* prevent the drift with the comment

* better naming, better style

* MAX_TASK_WAITER_CHAIN_DEPTH comment

* task-waiter iterative bfs walk

* iterative coro-walk

* nicer news

* 1 << 14

* comment

* unused read_Py_ssize_t

* fix tombstones

* simplify

* correct msg

* better test

* news for tombstones

* left-over from when testing buggy version

* redundant new line
(cherry picked from commit e0861c6)

Co-authored-by: Maurycy Pawłowski-Wieroński <[email protected]>
pablogsal added a commit that referenced this pull request Oct 5, 2026
…iterative and bounded (GH-153365) (#158814)

gh-153364: Make frame, coroutine, and task-waiter chain walks iterative and bounded (#153365)

* let me declare single limit

* use our new limit in process_frame_chain()

* add it in parse_async_frame_chain()

* parse_coro_chain()

* NEWS

* async in the message?

* test

* no race

* process_task_awaited_by

* process_task_awaited_by limit test

* NEWS

* MAX_TASK_WAITER_CHAIN_DEPTH

* TASK_WAITER_CHAIN_DEPTH in test

* TASK_WAITER_CHAIN_DEPTH 256

* prevent the drift with the comment

* better naming, better style

* MAX_TASK_WAITER_CHAIN_DEPTH comment

* task-waiter iterative bfs walk

* iterative coro-walk

* nicer news

* 1 << 14

* comment

* unused read_Py_ssize_t

* fix tombstones

* simplify

* correct msg

* better test

* news for tombstones

* left-over from when testing buggy version

* redundant new line

(cherry picked from commit e0861c6)

Co-authored-by: Maurycy Pawłowski-Wieroński <[email protected]>
@bedevere-bot

Copy link
Copy Markdown

⚠️⚠️⚠️ Buildbot failure ⚠️⚠️⚠️

Hi! The buildbot s390x Fedora Stable LTO + PGO 3.x (tier-3) has failed when building commit e0861c6.

What do you need to do:

  1. Don't panic.
  2. Check the buildbot page in the devguide if you don't know what the buildbots are or how they work.
  3. Go to the page of the buildbot that failed (https://buildbot.python.org/#/builders/1627/builds/3469) and take a look at the build logs.
  4. Check if the failure is related to this commit (e0861c6) or if it is a false positive.
  5. If the failure is related to this commit, please, reflect that on the issue and make a new Pull Request with a fix.

You can take a look at the buildbot page here:

https://buildbot.python.org/#/builders/1627/builds/3469

Failed tests:

  • test_faulthandler

Failed subtests:

  • test_register_max_threads - test.test_faulthandler.FaultHandlerTests.test_register_max_threads

Summary of the results of the build (if available):

==

Click to see traceback logs
TracebackThreads+0x2d6 [0x108df86]
  Binary file "/var/lib/buildbot/worker/cstratak-fedora-stable-s390x/3.x.cstratak-fedora-stable-s390x.lto-pgo/build/python" [0x1099814]
  Binary file "/var/lib/buildbot/worker/cstratak-fedora-stable-s390x/3.x.cstratak-fedora-stable-s390x.lto-pgo/build/python" [0x10998be]
  Binary file "linux-vdso.so.1", at __kernel_sigreturn+0x0 [0x3ffd7bfe4f8]
  Binary file "/lib64/libc.so.6", at +0xaf456 [0x3ff8d8af456]
  Binary file "/lib64/libc.so.6", at gsignal+0x20 [0x3ff8d8544f0]
  Binary file "/var/lib/buildbot/worker/cstratak-fedora-stable-s390x/3.x.cstratak-fedora-stable-s390x.lto-pgo/build/python" [0x10a0f3a]
  Binary file "/var/lib/buildbot/worker/cstratak-fedora-stable-s390x/3.x.cstratak-fedora-stable-s390x.lto-pgo/build/python" [0x111a3b8]
  Binary file "/var/lib/buildbot/worker/cstratak-fedora-stable-s390x/3.x.cstratak-fedora-stable-s390x.lto-pgo/build/python", at _PyEval_EvalFrameDefault+0x7b66 [0x10ff0d6]
  Binary file "/var/lib/buildbot/worker/cstratak-fedora-stable-s390x/3.x.cstratak-fedora-stable-s390x.lto-pgo/build/python", at PyEval_EvalCode+0xca [0x12395fa]
  Binary file "/var/lib/buildbot/worker/cstratak-fedora-stable-s390x/3.x.cstratak-fedora-stable-s390x.lto-pgo/build/python" [0x12a0aee]
  Binary file "/var/lib/buildbot/worker/cstratak-fedora-stable-s390x/3.x.cstratak-fedora-stable-s390x.lto-pgo/build/python" [0x12946ea]
  Binary file "/var/lib/buildbot/worker/cstratak-fedora-stable-s390x/3.x.cstratak-fedora-stable-s390x.lto-pgo/build/python" [0x12945ae]
  Binary file "/var/lib/buildbot/worker/cstratak-fedora-stable-s390x/3.x.cstratak-fedora-stable-s390x.lto-pgo/build/python", at Py_RunMain+0x322 [0x12919b2]
  Binary file "/var/lib/buildbot/worker/cstratak-fedora-stable-s390x/3.x.cstratak-fedora-stable-s390x.lto-pgo/build/python", at Py_BytesMain+0x3e [0x121aaee]
  Binary file "/lib64/libc.so.6", at +0x34aec [0x3ff8d834aec]
  Binary file "/lib64/libc.so.6", at __libc_start_main+0xa6 [0x3ff8d834c46]
  Binary file "/var/lib/buildbot/worker/cstratak-fedora-stable-s390x/3.x.cstratak-fedora-stable-s390x.lto-pgo/build/python" [0x121946a]
---


TracebackThreads+0x2d6 [0x108df86]
  Binary file "/var/lib/buildbot/worker/cstratak-fedora-stable-s390x/3.x.cstratak-fedora-stable-s390x.lto-pgo/build/python" [0x1099814]
  Binary file "/var/lib/buildbot/worker/cstratak-fedora-stable-s390x/3.x.cstratak-fedora-stable-s390x.lto-pgo/build/python" [0x10998be]
  Binary file "linux-vdso.so.1", at __kernel_sigreturn+0x0 [0x3ffebafe4f8]
  Binary file "/lib64/libc.so.6", at +0xaf456 [0x3ff94eaf456]
  Binary file "/lib64/libc.so.6", at gsignal+0x20 [0x3ff94e544f0]
  Binary file "/var/lib/buildbot/worker/cstratak-fedora-stable-s390x/3.x.cstratak-fedora-stable-s390x.lto-pgo/build/python" [0x10a0f3a]
  Binary file "/var/lib/buildbot/worker/cstratak-fedora-stable-s390x/3.x.cstratak-fedora-stable-s390x.lto-pgo/build/python" [0x111a3b8]
  Binary file "/var/lib/buildbot/worker/cstratak-fedora-stable-s390x/3.x.cstratak-fedora-stable-s390x.lto-pgo/build/python", at _PyEval_EvalFrameDefault+0x7b66 [0x10ff0d6]
  Binary file "/var/lib/buildbot/worker/cstratak-fedora-stable-s390x/3.x.cstratak-fedora-stable-s390x.lto-pgo/build/python", at PyEval_EvalCode+0xca [0x12395fa]
  Binary file "/var/lib/buildbot/worker/cstratak-fedora-stable-s390x/3.x.cstratak-fedora-stable-s390x.lto-pgo/build/python" [0x12a0aee]
  Binary file "/var/lib/buildbot/worker/cstratak-fedora-stable-s390x/3.x.cstratak-fedora-stable-s390x.lto-pgo/build/python" [0x12946ea]
  Binary file "/var/lib/buildbot/worker/cstratak-fedora-stable-s390x/3.x.cstratak-fedora-stable-s390x.lto-pgo/build/python" [0x12945ae]
  Binary file "/var/lib/buildbot/worker/cstratak-fedora-stable-s390x/3.x.cstratak-fedora-stable-s390x.lto-pgo/build/python", at Py_RunMain+0x322 [0x12919b2]
  Binary file "/var/lib/buildbot/worker/cstratak-fedora-stable-s390x/3.x.cstratak-fedora-stable-s390x.lto-pgo/build/python", at Py_BytesMain+0x3e [0x121aaee]
  Binary file "/lib64/libc.so.6", at +0x34aec [0x3ff94e34aec]
  Binary file "/lib64/libc.so.6", at __libc_start_main+0xa6 [0x3ff94e34c46]
  Binary file "/var/lib/buildbot/worker/cstratak-fedora-stable-s390x/3.x.cstratak-fedora-stable-s390x.lto-pgo/build/python" [0x121946a]
---


Traceback (most recent call last):
  File "/var/lib/buildbot/worker/cstratak-fedora-stable-s390x/3.x.cstratak-fedora-stable-s390x.lto-pgo/build/Lib/test/test_faulthandler.py", line 936, in test_register_max_threads
    proc = script_helper.assert_python_ok('-c', code)
  File "/var/lib/buildbot/worker/cstratak-fedora-stable-s390x/3.x.cstratak-fedora-stable-s390x.lto-pgo/build/Lib/test/support/script_helper.py", line 182, in assert_python_ok
    return _assert_python(True, *args, **env_vars)
  File "/var/lib/buildbot/worker/cstratak-fedora-stable-s390x/3.x.cstratak-fedora-stable-s390x.lto-pgo/build/Lib/test/support/script_helper.py", line 167, in _assert_python
    res.fail(cmd_line)
    ~~~~~~~~^^^^^^^^^^
  File "/var/lib/buildbot/worker/cstratak-fedora-stable-s390x/3.x.cstratak-fedora-stable-s390x.lto-pgo/build/Lib/test/support/script_helper.py", line 80, in fail
    raise AssertionError(f"Process return code is {exitcode}\n"
    ...<10 lines>...
                         f"---")
AssertionError: Process return code is -11 (SIGSEGV)
command line: ['/var/lib/buildbot/worker/cstratak-fedora-stable-s390x/3.x.cstratak-fedora-stable-s390x.lto-pgo/build/python', '-X', 'faulthandler', '-I', '-c', 'import faulthandler\nimport signal\nimport threading\n\nNTHREADS = 6\nCAP = 3\n\nready = threading.Barrier(NTHREADS + 1)\nstop = threading.Event()\n\ndef worker():\n    ready.wait()\n    stop.wait()\n\nthreads = [threading.Thread(target=worker) for _ in range(NTHREADS)]\nfor t in threads:\n    t.start()\nready.wait()\ntry:\n    faulthandler.register(signal.SIGUSR1, all_threads=True,\n                          max_threads=CAP)\n    signal.raise_signal(signal.SIGUSR1)\nfinally:\n    stop.set()\n    for t in threads:\n        t.join()']

@maurycy
maurycy deleted the tachyon-parse_async_frame_chain-limit branch October 5, 2026 14:50
pablogsal added a commit to pablogsal/cpython that referenced this pull request Oct 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants