Repository navigation
[Interpreter] Raise a pending thread abort again after a catch handler - #135520
pcshrosbree wants to merge 2 commits into
Conversation
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
|
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. |
|
Tagging subscribers to this area: @JulieLeeMSFT, @BrzVlad, @janvorli |
| @@ -0,0 +1,15 @@ | |||
| <Project Sdk="Microsoft.NET.Sdk"> | |||
There was a problem hiding this comment.
This should be <CLRTestPriority>1</CLRTestPriority> test
It aborts threads in a few scenarios and is a niche case.
| INSTALL_UNWIND_AND_CONTINUE_HANDLER; | ||
|
|
||
| if (resumedAfterCatch) | ||
| { |
There was a problem hiding this comment.
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
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 theResumeAfterCatchExceptionhandler ofInterpExecMethod, and it had two problems there:COMPlusCheckForAbortwithout setting upThread::m_OSContext, whichCOMPlusCheckForAbortleaves to its caller.Thread::ReadyForAsyncExceptionasserts that the context points at managed code and walks the stack from it.EECodeManager::ResumeAfterCatchsets it up for compiled code. Now the interpreter fills it from the interpreter frame at the resume IP, withInterpreterFrame::SetContextToInterpMethodContextFrame.catchblock. When a frame of the sameInterpExecMethodcatches that abort, theResumeAfterCatchExceptionthat resumes it is thrown while the catch block is still active, so thetryblock does not catch it (Checked: assertpFrame->PtrNextFrame() == NULLin exceptionhandling.cpp). Now the catch block sets a flag and the check runs at the top of thetryblock.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
nopin front of such a try block.Tests:
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.CancelInTryAndExitCatchNormallyis enabled again.CancelItselfOutsideOfTryCatchFinallystays disabled: underDOTNET_InterpMode=1it fails for a different reason, an abort that compiled code raises again throughThrowControlForThreadand 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):
DOTNET_InterpMode1/2/3, with and withoutTieredCompilation=0ReadyForAsyncExceptionControlledExecutionTests(9 tests), JITControlledExecutionTests(9 tests),DOTNET_InterpMode=1On windows-x64 I also ran
ControlledExecutionTestsone test per process underDOTNET_InterpMode2 and 3, with a local workaround for #135535, which otherwise stops the tests during discovery. On main,CancelInTryAndExitCatchNormallyhits the same assert. With this PR, all 9 pass.The new test also passes under the JIT with
TieredCompilation=0,JITMinOpts,JitStress1 and 2,ReadyToRun=0, andGCStress0x3 and 0xC. Each part of the change was taken out in turn and the test fails under the interpreter every time: without them_OSContextsetup it hits the assert above, without recording the resume IP it hangs, and with the check left in the catch block it hits thePtrNextFrameassert.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.