JIT (bug): GC deadlock — method stays partially interruptible after lowering unrolls the loop's only GC safepoint (Span.CopyTo)
- 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
Assessment
This issue has not been assessed yet.