Fix cDAC stack walker infinite loop on InlinedCallFrame with unreadable managed code - #127044
Fix cDAC stack walker infinite loop on InlinedCallFrame with unreadable managed code#127044steveisok wants to merge 1 commit into
Conversation
…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>
|
This may never actually happen outside of my hacks. Figured I would push it up just in case. |
|
Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag |
There was a problem hiding this comment.
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_FRAMEstate, when the current frame is an activeInlinedCallFrameand the updated context IP is not recognized as managed, advance the frame iterator to avoid re-processing the same frame indefinitely.
| 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(); | ||
| } |
There was a problem hiding this comment.
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.
|
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
}
]
} |
There was a problem hiding this comment.
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 inUpdateStateat line 127 for this path.IsManagedperforms anExecutionManager.GetCodeBlockHandlelookup, 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 anInlinedCallFramewith 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
Note
This PR was AI/Copilot-assisted.
Summary
Fix an infinite loop in the cDAC stack walker when processing an
InlinedCallFramewith an active P/Invoke call whoseCallerReturnAddressis not in a known managed code range.Problem
When the cDAC stack walker encounters an
InlinedCallFramewith an active call (i.e.,CallerReturnAddress != null), it:CallerReturnAddressviaUpdateContextFromFrameIsInlineCallFrameWithActiveCall()→ true → does not advance the frame iteratorUpdateState()which checksIsManaged(CallerReturnAddress)If
IsManaged()returns false (e.g., when analyzing a partial/mini dump that does not include JIT code heaps), the walker stays inSW_FRAMEstate and re-processes the sameInlinedCallFrameindefinitely.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
UpdateContextFromFrameupdates the context for an activeInlinedCallFrame, check whether the caller IP is recognized as managed code. If it is not, advance past the frame — the same behavior as for inactiveInlinedCallFrames. This only affects scenarios where managed code ranges are unreadable; in normal operationIsManaged()returns true and the existing behavior is preserved.Testing