Skip to content

JIT: (bug) loop left partially interruptible after morph removes the stelem.ref call (GC suspension hang) #134894

Description

@EgorBo

When a non-inlined CastHelpers.StelemRef user call storing null is replaced by a plain STOREIND in morph, the block keeps a stale BBF_GC_SAFE_POINT flag. A call-free loop is then emitted partially interruptible and GC suspension hangs forever.

Minimal Repro

using System;
using System.Runtime.CompilerServices;
using System.Threading;

public class Program
{
    static volatile bool s_started;
    static int s_x;

    [MethodImpl(MethodImplOptions.AggressiveInlining)] static void F6() { s_x++; }
    [MethodImpl(MethodImplOptions.AggressiveInlining)] static void F5() { F6(); F6(); F6(); F6(); }
    [MethodImpl(MethodImplOptions.AggressiveInlining)] static void F4() { F5(); F5(); F5(); F5(); }
    [MethodImpl(MethodImplOptions.AggressiveInlining)] static void F3() { F4(); F4(); F4(); F4(); }
    [MethodImpl(MethodImplOptions.AggressiveInlining)] static void F2() { F3(); F3(); F3(); F3(); }
    static void F1() => F2(); // exhausts the inline budget, so StelemRef can't be inlined

    [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
    static void Test(object[] arr, int i)
    {
        F1();
        s_started = true;
        while (true)
        {
            object o = null;
            arr[i] = o;
        }
    }

    static void Main()
    {
        var t = new Thread(() => Test(new string[10], 1)) { IsBackground = true };
        t.Start();
        while (!s_started) { }
        Thread.Sleep(100);
        GC.Collect();
        Console.WriteLine("Done");
    }
}

Expected

Done

Actual

The process hangs in GC.Collect(). Test is reported as ; partially interruptible while its loop contains no call.

Regression?

Yes. .NET 8, 9 and 10 print Done; .NET 11 RC2 and main hang.

Notes

fgMorphCall sets BBF_GC_SAFE_POINT while the call is still CT_USER_FUNC, then the "stelem of null" optimization replaces the call with a STOREIND. The flag is never cleared, so fgHasCycleWithoutGCSafePoint thinks the loop has a safe point.
Became reachable in .NET 11 when stelem.ref started being imported as a user call to CastHelpers.StelemRef.

Activity

  1. added this to the 12.0.0 milestone on Sep 29, 2026
  2. added
    area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI
    on Sep 29, 2026
  3. dotnet-policy-service commented on Sep 29, 2026

    @dotnet-policy-service
    Contributor

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

  4. added a commit that references this issue on Oct 1, 2026
    12955c8
  5. self-assigned this
    on Oct 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions