Skip to content

Checked runtime asserts in CallCatchFunclet on Windows x64 with shadow stacks when a thread abort raised at the end of a catch is caught in the same method #135530

Closed

Description

@pcshrosbree

Description

On windows-x64 with CET shadow stacks enabled, a Checked runtime asserts in CallCatchFunclet (exceptionhandling.cpp) when a ThreadAbortException that the runtime raises again at the end of a catch handler is caught by another handler of the same method:

Assert failure: targetSSP == 0 || (pHandlerIP != NULL) && (exInfo->m_frameIter.m_crawl.GetCodeManager() == ExecutionManager::GetInterpreterCodeManager()) || (*(size_t*)(targetSSP-8) == exInfo->m_frameIter.m_crawl.GetRegisterSet()->ControlPC)
    File: src\coreclr\vm\exceptionhandling.cpp:3099

This is JIT-compiled code; the interpreter is not involved. linux-x64 and osx-arm64 run the same program correctly.

Reproduction

#pragma warning disable SYSLIB0046
using System;
using System.Runtime;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;

static class Program
{
    static volatile bool s_ready;
    static volatile int s_counter;

    [MethodImpl(MethodImplOptions.NoInlining)]
    static void RunInfiniteLoop()
    {
        while (true)
        {
            if ((++s_counter & 0xfffff) == 0)
                Thread.Sleep(0);
        }
    }

    [MethodImpl(MethodImplOptions.NoInlining)] static void Nop() { }

    [MethodImpl(MethodImplOptions.NoInlining)]
    static void NestedCatch()
    {
        try
        {
            Nop();
            try { s_ready = true; RunInfiniteLoop(); }
            catch (ThreadAbortException) { Console.WriteLine("inner catch"); }
            // the abort is raised again here, inside the outer try
            Nop();
        }
        catch (ThreadAbortException) { Console.WriteLine("outer catch"); }
    }

    static int Main()
    {
        var cts = new CancellationTokenSource();
        Task.Run(() => { while (!s_ready) Thread.Sleep(10); cts.Cancel(); });
        try { ControlledExecution.Run(NestedCatch, cts.Token); }
        catch (OperationCanceledException) { Console.WriteLine("OperationCanceledException"); }
        return 100;
    }
}

Expected (and what linux-x64 and osx-arm64 print): inner catch, outer catch, OperationCanceledException. windows-x64 Checked (CET enabled; corerun.exe is CET compatible): the assert above, on main at 6ff62b1. The same happens when the catch is followed by a try block that catches the abort.

Analysis

EECodeManager::ResumeAfterCatch sends the thread to THROW_CONTROL_FOR_THREAD_FUNCTION, and RedirectForThrowControl records the SSP at the point where the thread was redirected in the FaultingExceptionFrame (ThrowControlForThread, pfef->SetSSP(ssp)). The frame being resumed was interrupted there, not stopped at a call, so no shadow stack slot holds its ControlPC. That makes the third clause of the assert false whenever the catch handler is in that same frame. The SSP itself is the right one to resume with: it points at the frame's return address.

With the assert also accepting a frame that has faulted ((pHandlerIP != NULL) && exInfo->m_frameIter.m_crawl.HasFaulted()), the program above and the "try block at the resume point" case print the expected output on windows-x64 Checked with CET, with no shadow stack violation. Release builds do not have the assert.

Note

This issue was prepared with AI assistance (Anthropic Claude).

Activity

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

    @dotnet-policy-service
    Contributor

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

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

    @dotnet-policy-service
    Contributor

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

  3. added a commit that references this issue on Oct 14, 2026
    4f40519
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

    • Status
      No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions