Skip to content

Fix interpreter SetIP breakpoint handling - #131784

Open
matouskozak wants to merge 1 commit into
dotnet:mainfrom
matouskozak:matouskozak-fix-interpreter-setip
Open

Fix interpreter SetIP breakpoint handling#131784
matouskozak wants to merge 1 commit into
dotnet:mainfrom
matouskozak:matouskozak-fix-interpreter-setip

Conversation

@matouskozak

@matouskozak matouskozak commented Aug 4, 2026

Copy link
Copy Markdown
Member

Summary

  • Preserve the destination breakpoint bypass across SetIP context mutation and consume the matching bypass before debugger notification, allowing the saved opcode to execute exactly once.
  • Emit interpreter IL/native mappings for catch and filter entries whose exception object makes the IL stack non-empty, using an invalid source type rather than omitting the entry.

Validation

  • macOS arm64 directly fixed Stepping.SetIPTest and Stepping.SetIPTestwithUnvaildPos (2/2).

Note

This pull request description was generated by GitHub Copilot.

Preserve destination breakpoint bypasses across SetIP and emit debugger mappings for catch and filter entries with an exception on the evaluation stack.

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

Copilot-Session: 285cb880-7c44-45a2-a8bb-61eb764db71c
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @BrzVlad, @janvorli
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

Updates CoreCLR interpreter debugging behavior to correctly honor breakpoint bypass state across SetIP (so the destination’s saved opcode executes exactly once), and improves debugger mapping emission for catch/filter entries where the IL stack is intentionally non-empty (exception object present).

Changes:

  • Preserve a destination breakpoint bypass across SetIP context mutation inside InterpBreakpoint, restoring it only when the new IP matches the saved bypass address.
  • Consume a matching breakpoint bypass before notifying the debugger (pre-callback) in the INTOP_BREAKPOINT dispatch path to avoid redundant callbacks and ensure single-step execution of the saved opcode.
  • Emit IL->native mapping entries for filter/catch funclet entry IL offsets even when the IL stack isn’t empty, tagging them with ICorDebugInfo::SOURCE_TYPE_INVALID instead of omitting the mapping.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
src/coreclr/vm/interpexec.cpp Adjusts interpreter breakpoint/bypass flow to correctly preserve and consume bypasses around debugger SetIP and callbacks.
src/coreclr/interpreter/compiler.cpp Emits IL->native mappings for filter/catch entrypoints with non-empty IL stack using SOURCE_TYPE_INVALID rather than skipping the entry.

@matouskozak
matouskozak marked this pull request as ready for review August 6, 2026 09:46
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@matouskozak
matouskozak requested a review from tommcdon August 6, 2026 09:47
// created for the destination before resuming at the new context.
if ((GetIP(&ctx) != (PCODE)ip) || (GetSP(&ctx) != (DWORD64)pFrame))
{
if (GetIP(&ctx) == (PCODE)savedBypassAddress)

@BrzVlad BrzVlad Aug 6, 2026

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.

In which scenario does this happen ? Say the debugger wants to continue execution at a different ip. Then it would just set the ip to the new location and it just starts executing from there. What would be the point of setting a bypass. If the target instruction is a breakpoint, sounds like it should just execute it normally.

Maybe the debugger still wants to execute the original instruction that was trapped, before dispatching to the new location ? In that case, isn't savedBypassAddress pointing to the original breakpoint location ? Or is savedBypassAddress always pointing to the new target with the opcode from the other trapping instruction, suggesting that the debugger has enough information to always set the bypass to the right values. In which case, why are we doing this equality check, should we just set the bypass unconditionally, unifying with the code below ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

In which scenario does this happen ?

This is for the case when SetIP moves execution to a place that already has a breakpoint. After debugger resumes we would hit the breakpoint again.

If the target instruction is a breakpoint, sounds like it should just execute it normally.

I'm not sure we can because we need to exit through ThrowResumeAfterCatchException to move to the new IP, thus skipping the regular breakpoint bypass path.

Maybe the debugger still wants to execute the original instruction that was trapped, before dispatching to the new location ?

I'm not sure that's what debugger want, I would expect that if execution is changed via SetIP, we just move to the new location.

should we just set the bypass unconditionally, unifying with the code below ?

We only need the bypass in case we go to breakpoint with SetIP other case will execute normally.

InterpBreakpoint(ip, pFrame, stack, pInterpreterFrame);

int32_t bypassOpcode = 0;
if (pThreadContext->HasBypass(ip, &bypassOpcode))

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.

It is not clear to me why we want to execute the bypass byte code. Do we do that for JITted code too? I would expect that in native code, if I set a breakpoint at say a division instruction, we would not execute that instruction until we resume after breakpoint. Why would the interpreter be different?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This bypass dispatch was intended for a case where SetIP redirects to a location with another breakpoint. In that case, when we resume execution via ThrowResumeAfterCatchException, we would hit the breakpoint at SetIP location, bypassing it here we can dispatch to the underlying opcode instead of hitting another breakpoint.

I thought this would be safe, as I don't expect we would have more than one bypass set at given ip and so if there is a bypass already set when we hit INTOP_BREAKPOINT it must have come from SetIP.

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.

4 participants