Skip to content

Code executor: catastrophic O(n^2) regex backtracking in extract_code_and_truncate_content hangs on large delimiter-free responses #5992

Closed

Description

@RexLeee

Version: google-adk 1.28.0 (the regex is unchanged on the 2.x line as well)

What happens

When an LlmAgent has a code_executor, _CodeExecutionResponseProcessor runs CodeExecutionUtils.extract_code_and_truncate_content on every model response. The regex (in google/adk/code_executors/code_execution_utils.py):

pattern = re.compile(
    rf'(?P<prefix>.*?)({leading_delimiter_pattern})(?P<code>.*?)({trailing_delimiter_pattern})(?P<suffix>.*?)$'.encode(),
    re.DOTALL,
)
pattern_match = pattern.search(response_text.encode())

is run via search() on the full joined response text. When the text contains no leading delimiter (e.g. a large structured/JSON response returned by a before_model_callback short-circuit, which still flows through the response post-processors), every start position rescans the whole string looking for the delimiter → O(n²). On a few-hundred-KB response this stalls for tens of minutes — effectively a hang, and no per-turn timeout catches it because it is a synchronous CPU/regex call inside the worker thread.

Reproduce

  1. Attach any BaseCodeExecutor (e.g. UnsafeLocalCodeExecutor) to an LlmAgent.
  2. Have the model (or a before_model_callback) emit a single text part of ~300KB that contains no ```python / ```tool_code fence (e.g. a large Plotly figure JSON).
  3. The post-processor hangs in pattern.search while extracting code.

Suggested fix

Before compiling/searching, short-circuit when no leading delimiter substring is present — the regex can only return None in that case, so this is behavior-preserving and makes the no-match path O(n):

text_parts = [p for p in content.parts if p.text]
if text_parts:
    response_text = '\n'.join(p.text for p in text_parts)
    if not any(d[0] in response_text for d in code_block_delimiters):
        return None  # no opening delimiter -> no code block

(The default delimiters '```tool_code\n' and '```python\n' are literal substrings, so a plain in check is sufficient.)

Activity

  1. added
    tools[Component] This issue is related to tools
    on Jun 6, 2026
  2. added theissue type on Jun 6, 2026
  3. surajksharma07 commented on Jun 8, 2026

    @surajksharma07
    Collaborator

    /adk-issue-analyze

  4. adk-bot commented on Jun 8, 2026

    @adk-bot
    Collaborator

    I will begin by reading the instructions for the adk-issue-analyze skill, as it directly corresponds to the /adk-issue-analyze command you issued. This will guide us in performing a structured analysis of issue #5992.I will fetch the details of GitHub issue #5992 using the fetch_github_issue tool.I will search for code_execution_utils.py in the workspace to locate the file described in the issue.I will read the contents of src/google/adk/code_executors/code_execution_utils.py where the regex backtracking issue is reported to occur.I will run a gh command to search for any pull requests in google/adk-python that reference this issue (5992).I see there are two open PRs related to this issue: PR #6002 and PR #6012. I will fetch the details of both PRs using the fetch_github_pr tool. Let's inspect PR #6002 first.I will fetch the details of the other pull request, #6012, to see if it is a duplicate, a revision, or has an alternative approach.I will run a clean gh pr diff command to view the code changes implemented in PR #6012. This will let me verify how the developer chose to fix the bug.I will view the test_code_execution.py file to see the testing context and check where this test fits.I will perform a search for regex usage in other execution files to see if similar $O(N^2)$ backtracking patterns may occur elsewhere.# GitHub Issue #5992 Analysis: Code executor: catastrophic O(n^2) regex backtracking in extract_code_and_truncate_content hangs on large delimiter-free responses

    Executive Summary

    1. What is broken? When an LlmAgent utilizes a code_executor, the response processor runs the utility method extract_code_and_truncate_content on every response. This function searches the text using a regular expression with multiple greedy/lazy wildcards (.*?). If a large input (such as 100KB+ JSON/data payloads) contains no opening delimiter, the regex engine falls into catastrophic $O(n^2)$ backtracking, pinning the CPU at 100% and freezing the synchronous execution thread.
    2. Is there a linked PR that fixes this issue? Yes, PR #6012 is an open pull request that completely resolves the issue. (An earlier PR #6002 is also open but superseded by fix: skip code regex without delimiters #6012).
    3. Recommendation: Should Fix (High Priority) — Unbounded regex backtracking leads to direct CPU exhaustion/lockups that bypass user-configured timeouts and block agent threads, creating a severe reliability hazard. PR #6012 implements a highly elegant, behavior-preserving $O(n)$ string search fast-path. It should be approved and merged immediately.
    Detailed Analysis

    1. Root Cause Analysis ("What is broken?")

    1. The Bug Location: In code_execution_utils.py:L145-L160, the extraction utility runs the compiled regex below:
      pattern = re.compile(
          (
              rf'(?P<prefix>.*?)({leading_delimiter_pattern})(?P<code>.*?)({trailing_delimiter_pattern})(?P<suffix>.*?)$'
          ).encode(),
          re.DOTALL,
      )
      pattern_match = pattern.search(response_text.encode())
    2. The Backtracking Mechanism: When matching against a document of size $N$ that contains no opening delimiter (e.g. leading_delimiter_pattern is not matched):
      • The engine starts scanning from index 0. The lazy dot-star (?P<prefix>.*?) tries to expand but the engine has to match the delimiter. It fails.
      • The engine then moves the starting pointer to position 1, scans to the end, fails again.
      • It continues testing all possible partition combinations starting from 2, 3, ..., up to $N$.
      • This leads to $O(N^2)$ traversal paths. On modern machines, a few hundreds of KB of text can delay the CPU for tens of minutes or hours, effectively freezing execution.

    2. Existing Pull Requests ("Is there a linked PR that fixes this issue?")

    • Linked PR: Pull Request #6012 - fix: skip code regex without delimiters (OPEN)
    • PR URL: fix: skip code regex without delimiters #6012
    • Analysis:
      • The Fix: The developer adds an inline fast-path substring search before constructing or compiling the regular expression:
        if not any(d[0] in response_text for d in code_block_delimiters):
          return
        This uses Python's optimized Boyer-Moore/Horspool C implementation for substring checks, running on average in $O(N)$ with negligible overhead. If no opening sequence exists, it returns None immediately, completely bypassing the regex engine.
      • Testing: Includes a highly precise regression test in test_code_execution.py tracking mocks of re.compile. It makes sure that re.compile is never called if a payload is delimiter-free, preventing subsequent regressions.
      • It cleanly replaces PR #6002, which had minor styling and test placement discrepancies.

    3. Recommendation

    • Recommendation: Should Fix (High Priority)
    • Rationale:
      • User/Architectural Impact: Thread-blocking CPU loops are critical reliability issues. Whenever an agent processes output files, plots, JSON figures, or callbacks, it runs into this hang, preventing execution from completing.
      • Quality & Style Alignment: This fix aligns with Python best practices. It minimizes complexity and maintains full backward compatibility for successful code block detections without altering the parsing syntax. No clean workarounds exist since the post-processing is triggered automatically by active code executors on standard message pipelines.

    Turn Summary

    I have successfully retrieved isttue #5992 and identified the root cause of the regular expression backtracking within extract_code_and_truncate_content. After checking all related PRs on Github, I confirmed that PR #6012 resolves this elegantly with robust regression testing. It is ready for approval and merging. No manual changes are required under this read-only workflow.

  5. surajksharma07 commented on Jun 9, 2026

    @surajksharma07
    Collaborator

    @RexLeee this is a real backtracking trap: re.search() with a lazy .*? prefix tries every partition of a no-delimiter string, and on 300 KB that's effectively unbounded CPU time.

    A quick local patch worth trying: right after response_text = '\n'.join(...) in extract_code_and_truncate_content, insert if not any(d[0] in response_text for d in code_block_delimiters): return before the re.compile call.

    Could you verify this resolves the hang on your payload — and also confirm normal code-block extraction still works correctly on a fenced response?

    Once you're happy it holds up, it'd be great to have that confirmed before the linked PR lands!

  6. added
    request clarification[Status] The maintainer need clarification or more information from the author
    on Jun 9, 2026
  7. RexLeee commented on Jun 9, 2026

    @RexLeee
    Author

    Thanks @surajksharma07 — confirmed on both counts. ✅

    I'd already been running exactly this guard in production since hitting the issue, so I can speak to it directly:

    1. Resolves the hang. The one-liner you suggested

    if not any(d[0] in response_text for d in code_block_delimiters):
        return

    is the same condition I shipped. On a ~500 KB delimiter-free payload (a large Plotly chart JSON emitted via our agent's direct-output path) extract_code_and_truncate_content stalled the whole turn for ~36 minutes; with the fast-path substring check it returns None in well under a second. It runs on the exact same response_text the regex would scan, so for the delimiter-free case it's behaviorally identical to the old path (which could only ever return None there) — just O(n) instead of O(n²).

    2. Normal extraction still works. Fenced responses (```python … ```, ```tool_code … ```) still extract correctly — I have a regression test asserting both the delimiter-free fast-return and that a normal fenced block is still parsed unchanged.

    One implementation note in case it's useful: I deliver it as a monkeypatch over the whole method (we vendor ADK and want it trivially removable once this lands), so mine adds an extra guard to skip the short-circuit when a part carries executable_code. PR #6012 doesn't need that because it places the check after the executable_code early-return — which is the cleaner spot for the in-tree fix.

    +1 to merge #6012 as-is. Thanks for picking this up!

  8. surajksharma07 commented on Jun 9, 2026

    @surajksharma07
    Collaborator

    That production number — 36 minutes on a 500 KB Plotly JSON — is exactly the kind of data that makes the severity undeniable, so thanks for sharing that directly.

    Your note on positioning is worth calling out: PR #6012 placing the guard after the executable_code early-return is the cleaner design precisely because it avoids the extra conditional your monkeypatch needs — the check only runs on text-based responses, which is all it ever needs to protect.

    Everything is confirmed at this point — the hang is reproducible, the fix is behaviour-preserving, regression-tested, and scoped correctly in #6012.

    Marking this confirmed and flagging #6012 for reviewer attention — appreciate the thorough follow-up, @RexLeee!

    Team is looking more into it.
    Thanks!

  9. self-assigned this
    on Jun 17, 2026
  10. GWeale commented on Jun 17, 2026

    @GWeale
    Collaborator

    Fixed on main via #6118 (commit 910e1c1): the O(n^2) backtracking regex in extract_code_and_truncate_content was replaced with a linear scan, so the ReDoS hang on large delimiter-free responses is gone. Thanks for the careful writeup. Closing as fixed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

request clarification[Status] The maintainer need clarification or more information from the authortools[Component] This issue is related to tools

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions