Repository navigation
[Interpreter] Thread abort re-raised at the end of an interpreted catch: Thread::m_OSContext is not set up #135463
Description
With the CoreCLR interpreter (DOTNET_InterpMode=1), a thread abort caught by an interpreted catch cannot be re-raised at the end of that catch. A Checked runtime asserts:
ASSERT FAILED
Expression: ExecutionManager::IsManagedCode(GetIP(m_OSContext))
Location: src/coreclr/vm/threadsuspend.cpp:899
Function: ReadyForAsyncException
Repro. ControlledExecution aborts the thread when the token is cancelled:
#pragma warning disable SYSLIB0046 // ControlledExecution is obsolete
using System;
using System.Runtime;
using System.Threading;
using System.Threading.Tasks;
static class Program
{
static volatile bool s_ready;
static int s_counter;
static void Main()
{
var cts = new CancellationTokenSource();
Task.Run(() => { while (!s_ready) Thread.Sleep(10); cts.Cancel(); });
try
{
ControlledExecution.Run(() =>
{
try
{
s_ready = true;
while (true)
{
if ((++s_counter & 0xfffff) == 0)
Thread.Sleep(0);
}
}
catch
{
Console.WriteLine("caught; the abort is re-raised at the end of this catch");
}
Console.WriteLine("not reached");
}, cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("OperationCanceledException");
}
}
}| Checked, at 6ff62b1 | JIT | DOTNET_InterpMode=1 |
|---|---|---|
| linux-x64, osx-arm64, windows-x64 | caught; ..., then OperationCanceledException |
caught; ..., then the assert above |
From the source, the cause is in the ResumeAfterCatchException handler in InterpExecMethod. After a catch it calls COMPlusCheckForAbort(resumeIP) without setting up Thread::m_OSContext, which COMPlusCheckForAbort requires of its caller. The JIT path, EECodeManager::ResumeAfterCatch, calls CopyOSContext(pThread->m_OSContext, pContext) first. I have only run Checked builds; a Release build would use whatever context was last stored there.
Once that is fixed there is a second difference. The interpreter raises the abort with SetIsFaulting(false) and pFrame->ip at the resume point, so the stack walk attributes it to the instruction before the resume point. JIT code raises it through ThrowControlForThread's FaultingExceptionFrame, at the resume point itself. The difference shows when the resume point is the first instruction of another try region: with the JIT, that region's handlers see the abort.
ControlledExecutionTests.CancelInTryAndExitCatchNormally and CancelItselfOutsideOfTryCatchFinally, which cover this re-raise, have had their [ConditionalFact] commented out since e72e7c7 (#122308). The first passes with the JIT today. I plan to open a PR that fixes both points and re-enables them.
Note
This issue was drafted with AI assistance (Anthropic Claude). The runs it cites were done at commit 6ff62b1.
Activity
dotnet-policy-service commentedon Oct 9, 2026
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.
dotnet-policy-service commentedon Oct 9, 2026
Tagging subscribers to this area: @JulieLeeMSFT, @BrzVlad, @janvorli
See info in area-owners.md if you want to be subscribed.
BrzVlad commentedon Oct 9, 2026
I know we have been moving away from thread aborts with modern .NET and I see that the API in question is obsolete: https://learn.microsoft.com/en-us/dotnet/api/system.runtime.controlledexecution.run. I'm wondering whether it makes sense to fix this in the first place.
pcshrosbree commentedon Oct 9, 2026
Fair question.
ControlledExecution.Runis obsolete, but the debugger's func-eval abort also goes throughThread::UserAbort, so aborts can still reach interpreted code. #135467 (for #135465) touches abort delivery only in that it records the right ip at a safepoint, and that part is separable.I don't have a PR for this one yet, but I'd be happy to open one if you think it's worth fixing: set up
Thread::m_OSContextfrom the interpreter frame at the resume ip, asEECodeManager::ResumeAfterCatchdoes for JIT frames, and re-enable the twoControlledExecutionTestscommented out in #122308. If you'd rather not fix this, I'm happy for it to be closed.Note
This comment was prepared with AI assistance (Anthropic Claude).
BrzVlad commentedon Oct 9, 2026
That sounds like reasonable justification to give it a go. I'm not sure whether adding a test using the obsolete API would be the right way to do it. I know we have some debugger tests somewhere internally, would it make sense to add this scenario there instead ? @matouskozak @tommcdon
jkotas commentedon Oct 9, 2026
The API is marked as obsolete to give a strong warning to people that it is not meant to be used in production code. It is not really obsolete in the traditional meaning of obsolete.
This API was introduced in .NET Core relatively recently to allow REPLs and similar applications to do best-effort abort of runaway user code, similar to debugger func-eval. The implementation is shared with debugger funceval abort.
We have number of tests for this API in this repo. I do not see a problem with adding another one.
[Interpreter] Record the next instruction's ip before calling a manag…
Accept a faulted frame in the shadow stack assert of CallCatchFunclet (…
pcshrosbree commentedon Oct 15, 2026
@janvorli, this was closed when #135531 merged, because its description says "(fixes #135463)" and GitHub took that as a closing keyword. The bug is still there on main: at 2165605, a Checked linux-x64 runtime with
DOTNET_InterpMode=1still hits this assert when an abort is raised again at the end of an interpreted catch. Its fix, #135520, is still open. Could you reopen it?