Skip to content

[Interpreter] Raise a pending thread abort again after a catch handler - #135520

Open
pcshrosbree wants to merge 2 commits into
dotnet:mainfrom
pcshrosbree:interp-abort-after-catch
Open

pcshrosbree wants to merge 2 commits into
dotnet:mainfrom
pcshrosbree:interp-abort-after-catch

Conversation

@pcshrosbree

@pcshrosbree pcshrosbree commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #135463.

When interpreted code catches a ThreadAbortException, the abort has to be raised again at the end of the catch handler. The interpreter does that in the ResumeAfterCatchException handler of InterpExecMethod, and it had two problems there:

  1. It called COMPlusCheckForAbort without setting up Thread::m_OSContext, which COMPlusCheckForAbort leaves to its caller. Thread::ReadyForAsyncException asserts that the context points at managed code and walks the stack from it. EECodeManager::ResumeAfterCatch sets it up for compiled code. Now the interpreter fills it from the interpreter frame at the resume IP, with InterpreterFrame::SetContextToInterpMethodContextFrame.
  2. It dispatched the abort from inside the C++ catch block. When a frame of the same InterpExecMethod catches that abort, the ResumeAfterCatchException that resumes it is thrown while the catch block is still active, so the try block does not catch it (Checked: assert pFrame->PtrNextFrame() == NULL in exceptionhandling.cpp). Now the catch block sets a flag and the check runs at the top of the try block.

The abort is still raised as a return address at the resume IP, as before, so a try block that starts right at the resume point does not cover it. That matches optimized JIT code, which resumes on a nop in front of such a try block.

Tests:

  • New src/tests/Regressions/coreclr/GitHub_135463: the abort caught in the method, caught again by its caller, and kept pending at the end of a catch inside a finally. It checks the exception and which blocks ran.
  • ControlledExecutionTests.CancelInTryAndExitCatchNormally is enabled again. CancelItselfOutsideOfTryCatchFinally stays disabled: under DOTNET_InterpMode=1 it fails for a different reason, an abort that compiled code raises again through ThrowControlForThread and that is then resumed in an interpreted frame. I'll open a separate issue for that.

Validation on linux-x64, osx-arm64 and windows-x64 (Checked):

main this PR
test135463, JIT pass pass
test135463, DOTNET_InterpMode 1/2/3, with and without TieredCompilation=0 assert in ReadyForAsyncException pass
ControlledExecutionTests (9 tests), JIT 9 pass 9 pass
ControlledExecutionTests (9 tests), DOTNET_InterpMode=1 process dies (same assert) 9 pass

On windows-x64 I also ran ControlledExecutionTests one test per process under DOTNET_InterpMode 2 and 3, with a local workaround for #135535, which otherwise stops the tests during discovery. On main, CancelInTryAndExitCatchNormally hits the same assert. With this PR, all 9 pass.

The new test also passes under the JIT with TieredCompilation=0, JITMinOpts, JitStress 1 and 2, ReadyToRun=0, and GCStress 0x3 and 0xC. Each part of the change was taken out in turn and the test fails under the interpreter every time: without the m_OSContext setup it hits the assert above, without recording the resume IP it hangs, and with the check left in the catch block it hits the PtrNextFrame assert.

Note

This PR was prepared with AI assistance (Anthropic Claude, reviewed by OpenAI Codex). The builds and runs it cites were done at the PR's base commit.

COMPlusCheckForAbort expects its caller to set up Thread::m_OSContext for
the place where the execution resumes; the interpreter did not, so
ReadyForAsyncException asserted. The abort was also dispatched from inside
the C++ catch block that handles ResumeAfterCatchException, so when a frame
of the same InterpExecMethod caught it, the next ResumeAfterCatchException
was not caught. Do the check at the top of the try block instead, with
m_OSContext describing the interpreter frame at the resume IP.

Add a regression test and enable
ControlledExecutionTests.CancelInTryAndExitCatchNormally again.

Fixes dotnet#135463
@dotnet-policy-service dotnet-policy-service Bot added the community-contribution Indicates that the PR has been added by a community member label Oct 9, 2026
@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.

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">

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 should be <CLRTestPriority>1</CLRTestPriority> test

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.

Done in 08e3dfb.

It aborts threads in a few scenarios and is a niche case.
INSTALL_UNWIND_AND_CONTINUE_HANDLER;

if (resumedAfterCatch)
{

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.

Ideally, we should have no special checks on this execution path and keep it as simple as possible, just so we can have the best perf possible, even if it is just a var init and a check for it.

In the context of this PR, isn't this implementation basically an implicit rethrow of the abort exception ? Sounds like we could avoid resuming altogether and throw the abort exception earlier, while executing the catch from EH, same as a rethrow I believe is processed. Maybe we could add a check for thread abort somewhere around INTOP_LEAVE_CATCH. cc @janvorli

This branch has not been deployed

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

Labels

area-CodeGen-Interpreter-coreclr community-contribution Indicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Interpreter] Thread abort re-raised at the end of an interpreted catch: Thread::m_OSContext is not set up

3 participants