Skip to content

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

Description

@pcshrosbree

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

  1. added
    area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI
    on Oct 9, 2026
  2. dotnet-policy-service commented on Oct 9, 2026

    @dotnet-policy-service
    Contributor

    Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
    See info in area-owners.md if you want to be subscribed.

  3. dotnet-policy-service commented on Oct 9, 2026

    @dotnet-policy-service
    Contributor

    Tagging subscribers to this area: @JulieLeeMSFT, @BrzVlad, @janvorli
    See info in area-owners.md if you want to be subscribed.

  4. BrzVlad commented on Oct 9, 2026

    @BrzVlad
    Member

    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.

  5. pcshrosbree commented on Oct 9, 2026

    @pcshrosbree
    ContributorAuthor

    Fair question. ControlledExecution.Run is obsolete, but the debugger's func-eval abort also goes through Thread::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_OSContext from the interpreter frame at the resume ip, as EECodeManager::ResumeAfterCatch does for JIT frames, and re-enable the two ControlledExecutionTests commented 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).

  6. BrzVlad commented on Oct 9, 2026

    @BrzVlad
    Member

    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

  7. jkotas commented on Oct 9, 2026

    @jkotas
    Member

    the API in question is obsolete

    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.

    a test using the obsolete API

    We have number of tests for this API in this repo. I do not see a problem with adding another one.

  8. added a commit that references this issue on Oct 14, 2026
    4f40519
  9. pcshrosbree commented on Oct 15, 2026

    @pcshrosbree
    ContributorAuthor

    @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=1 still 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?

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions