Skip to content

Fix cDAC stack walker infinite loop on InlinedCallFrame with unreadable managed code - #127044

Open
steveisok wants to merge 1 commit into
dotnet:mainfrom
steveisok:steve/fix-cdac-stackwalk-inlinedcallframe-loop
Open

Fix cDAC stack walker infinite loop on InlinedCallFrame with unreadable managed code#127044
steveisok wants to merge 1 commit into
dotnet:mainfrom
steveisok:steve/fix-cdac-stackwalk-inlinedcallframe-loop

Conversation

@steveisok

Copy link
Copy Markdown
Member

Note

This PR was AI/Copilot-assisted.

Summary

Fix an infinite loop in the cDAC stack walker when processing an InlinedCallFrame with an active P/Invoke call whose CallerReturnAddress is not in a known managed code range.

Problem

When the cDAC stack walker encounters an InlinedCallFrame with an active call (i.e., CallerReturnAddress != null), it:

  1. Sets the context IP to CallerReturnAddress via UpdateContextFromFrame
  2. Checks IsInlineCallFrameWithActiveCall() → true → does not advance the frame iterator
  3. Calls UpdateState() which checks IsManaged(CallerReturnAddress)

If IsManaged() returns false (e.g., when analyzing a partial/mini dump that does not include JIT code heaps), the walker stays in SW_FRAME state and re-processes the same InlinedCallFrame indefinitely.

This was discovered while investigating in-process mini core dumps on mobile platforms, where the dump captures the runtime DATA segment (cDAC descriptor, thread store, frame chains, stacks) but not the JIT code heap regions.

Fix

After UpdateContextFromFrame updates the context for an active InlinedCallFrame, check whether the caller IP is recognized as managed code. If it is not, advance past the frame — the same behavior as for inactive InlinedCallFrames. This only affects scenarios where managed code ranges are unreadable; in normal operation IsManaged() returns true and the existing behavior is preserved.

Testing

  • Verified the fix resolves the infinite loop on a Mach-O ARM64 mini dump with 5 managed threads
  • Existing cDAC contract tests pass (43/43)
  • Stack walk dump tests are conditional on debuggee availability and are not affected by this change

…JIT code

When an InlinedCallFrame has an active P/Invoke call, the walker sets
the context IP to CallerReturnAddress and does not advance the frame
iterator (expecting the next iteration to process the managed caller).
If IsManaged() returns false (e.g. partial dump without JIT code
heaps), the walker stays in SW_FRAME state and re-processes the same
InlinedCallFrame indefinitely.

Fix: when the InlinedCallFrame has an active call but the caller IP
is not in a known managed code range, advance past the frame. This
only affects scenarios where JIT code ranges are unreadable.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@steveisok

Copy link
Copy Markdown
Member Author

This may never actually happen outside of my hacks. Figured I would push it up just in case.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag
See info in area-owners.md if you want to be subscribed.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes an infinite loop in the cDAC StackWalk contract when encountering an InlinedCallFrame with an active call whose CallerReturnAddress cannot be classified as managed code (e.g., partial dumps missing JIT/code range data).

Changes:

  • In SW_FRAME state, when the current frame is an active InlinedCallFrame and the updated context IP is not recognized as managed, advance the frame iterator to avoid re-processing the same frame indefinitely.

Comment on lines +100 to +108
else if (!IsManaged(handle.Context.InstructionPointer, out _))
{
// The InlinedCallFrame has an active call but the caller's IP is not
// in a known managed code range (e.g. partial dump without JIT code
// heaps). Advance past the frame to prevent an infinite loop — without
// managed code range data the walker would repeatedly re-process the
// same InlinedCallFrame.
handle.FrameIter.Next();
}

Copilot AI Apr 17, 2026

Copy link

Choose a reason for hiding this comment

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

The new edge-case behavior (advancing past an active InlinedCallFrame when the caller IP is not recognized as managed) isn’t covered by automated tests. To prevent regressions (and to ensure the infinite-loop scenario is caught), please add a targeted test that simulates an InlinedCallFrame with an active call where ExecutionManager cannot resolve the caller IP (GetCodeBlockHandle returns null) and verifies CreateStackWalk terminates / advances frames.

Copilot generated this review using guidance from repository custom instructions.
@github-actions

github-actions Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Workflow state for the Holistic Review Orchestrator.

{
  "version": 5,
  "last_dispatched_commit": "520f689f0ba2e21c4a2c81852836ecb1982389bc",
  "last_dispatched_base_ref": "main",
  "last_dispatched_base_sha": "704613644c8c641d76d95c68328f0fe403a08116",
  "last_reviewed_commit": "520f689f0ba2e21c4a2c81852836ecb1982389bc",
  "last_reviewed_base_ref": "main",
  "last_reviewed_base_sha": "704613644c8c641d76d95c68328f0fe403a08116",
  "last_recorded_worker_run_id": "29674626633",
  "review_attempt_commit": "",
  "review_attempt_base_ref": "",
  "review_attempt_count": 0,
  "max_review_attempts": 5,
  "review_history_format": "holistic-review-disclosure-v1",
  "review_history": [
    {
      "commit": "520f689f0ba2e21c4a2c81852836ecb1982389bc",
      "review_id": 4730178434
    }
  ]
}

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Holistic Review

Motivation: The cDAC stack walker can enter an infinite loop when it encounters an InlinedCallFrame with an active P/Invoke call (CallerReturnAddress != null) whose caller IP is not in a known managed code range. In that case Next keeps the walker in SW_FRAME state without advancing the frame iterator, and UpdateState re-sets SW_FRAME because IsManaged is false, so the same frame is reprocessed forever. This is reachable with partial/mini core dumps (e.g. mobile in-process dumps) that capture the DATA segment but omit JIT code heaps. The motivation is real and confirmed by reading Next/UpdateState.

Approach: Add an else if (!IsManaged(handle.Context.InstructionPointer, out _)) branch that advances the frame iterator (FrameIter.Next()) — the same action taken for inactive InlinedCallFrames — when the active-call caller IP is not recognized as managed. In the normal case IsManaged returns true, the branch is skipped, and existing behavior (leave the iterator in place so the managed caller is processed on the next iteration as SW_FRAMELESS) is preserved. The fix is minimal, targeted, and consistent with the surrounding code paths.

Summary: ✅ LGTM. The change correctly breaks the infinite loop by treating an unresolvable active InlinedCallFrame caller the same as an inactive one — a reasonable graceful-degradation for incomplete dumps. Behavior in the normal (full-dump) case is unchanged because the new branch is only reached when IsManaged is false, which does not occur when JIT code ranges are readable.

Detailed Findings

  • 💡 Minor (non-blocking): IsManaged(handle.Context.InstructionPointer, out _) is now evaluated at line 100 and again in UpdateState at line 127 for this path. IsManaged performs an ExecutionManager.GetCodeBlockHandle lookup, so this is a small redundant lookup. Given that this branch only runs in the degraded (missing-code-range) scenario and stack walks are not hot paths, this is not worth restructuring; noting only for awareness.
  • 💡 Test coverage: No automated test accompanies the fix. This is understandable — reproducing the loop requires a dump missing JIT code heaps, and the PR notes the existing stack-walk dump tests are conditional on debuggee availability. If a synthetic MockTarget-style unit test could construct an InlinedCallFrame with an active call and an unmapped caller IP, it would guard against regressions; consider it as a follow-up rather than a blocker.

Note

This review was generated by this repository's Holistic Review agentic workflow to complement the built-in Copilot review.

Generated by Holistic Review · 57.4 AIC · ⌖ 10.6 AIC · ⊞ 10K

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants