dotnet / dotnet/runtime

JIT (bug): GC deadlock — method stays partially interruptible after lowering unrolls the loop's only GC safepoint (Span.CopyTo)

Open
#133,822 1 comment 1 reaction 2 assignees Claimed by @EgorBo View on GitHub
area-CodeGen-coreclr
Dominant language
C#
Stars
18.3k
Forks
5.6k
PR merge metrics
PR metrics pending

Description

Interruptibility is decided in `fgSetBlockOrder` (pre-lowering) while the loop still contains a `SpanHelpers.Memmove` call; `Lowering::LowerCallMemmove` then unrolls that call away, leaving a safepoint-free loop in a partially interruptible method, so GC suspension hangs forever.

### Minimal Repro

```csharp
using System;
using System.Runtime.CompilerServices;
using System.Threading;

class Program
{
static volatile bool s_stop;

[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
static void Test(byte[] a, byte[] b)
{
while (!s_stop)
b.AsSpan(0, 32).CopyTo(a.AsSpan(0, 32));
}

static void Main()
{
byte[] a = new byte[64], b = new byte[64];
new Thread(() => Test(a, b)) { IsBackground = true }.Start();
Thread.Sleep(500);
GC.Collect();
s_stop = true;
Console.WriteLine("Done");
}
}
```

### Expected

```
Done
```

### Actual

No output — the process hangs forever inside `GC.Collect()`, because the background thread spins in a loop with no GC poll and can never be suspended.

The generated code is marked `; partially interruptible`, and the loop body contains only `vmovdqu` load/store pairs plus the backedge compare — no call, no GC poll.

### Notes

- Control experiment: with a 4096-byte copy (arrays `byte[8192]`) the size is too large for `LowerCallMemmove` to unroll, the `Memmove` call and its safepoint survive, and the program prints `Done` immediately.
- The same applies to lowering's unrolling of `SequenceEqual` / `Fill` / `ClearWithoutReferences`, and to assertion-prop removing a zero-length `Memmove`: any loop whose only call is a constant-size span helper is affected — easy to hit accidentally in spin/polling loops.
- Possible fixes: re-run the loop-safepoint analysis (or insert a backedge GC poll) after lowering deletes a call, or force full interruptibility when the removed call was the loop's only safepoint.
- Not a regression: also reproduces on released .NET 10.0.12.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.