JIT: (bug) Tail-recursion-to-loop conversion drops the `callvirt` receiver null check
- Dominant language
- C#
- Stars
- 18.3k
- Forks
- 5.6k
- PR merge metrics
- PR metrics pending
Description
A `callvirt` to a non-virtual method is a direct call (`IsVirtual()` is false) but still carries
`GTF_CALL_NULLCHECK`. The recursive-fast-tailcall-to-loop transform deletes the call node without materializing
that null check, so a `callvirt` on a `null` receiver silently succeeds.
### Minimal Repro
```csharp
using System;
using System.Runtime.CompilerServices;
public class C
{
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public int Test(C o, int n, int acc)
{
if (n == 0)
return acc;
return o.Test(o, n - 1, acc + n); // callvirt on possibly-null 'o'
}
}
public class Program
{
public static void Main()
{
C c = new C();
Console.WriteLine(c.Test(c, 5, 0));
try
{
Console.WriteLine("returned " + c.Test(null, 1, 0));
}
catch (NullReferenceException)
{
Console.WriteLine("NullReferenceException");
}
}
}
```
### Expected
```
15
NullReferenceException
```
### Actual
```
15
returned 1
```
Both `this` (V00) and `o` (V01) become `zero-ref` and no null check is emitted:
```asm
G_M3746_IG02:
test r8d, r8d
je SHORT G_M3746_IG04
G_M3746_IG03:
add r9d, r8d
dec r8d
jne SHORT G_M3746_IG03
```
### Notes
`fgMorphPotentialTailCall` (`morph.cpp`) gates `fastTailCallToLoop` on `!call->IsVirtual()` but never checks
`call->NeedsNullCheck()`; `fgMorphRecursiveFastTailCallIntoLoop` then emits only the argument stores and the
back-edge. Fix: also require `!call->NeedsNullCheck()`, or materialize an explicit `GT_NULLCHECK`.
Observable with default (tiered) settings; `DOTNET_TieredCompilation=0` masks it in this repro because `Main`
then inlines one level of `Test`. Also reproduces on .NET 10.0.12.
Contributor guide
Research direction
Start in morph.cpp at fgMorphPotentialTailCall and follow fgMorphRecursiveFastTailCallIntoLoop, then run the supplied C# reproducer with default tiered settings. Compare the generated behavior for a null receiver with the expected NullReferenceException; the work is done when the recursive fast-tailcall path preserves that behavior without breaking the successful case.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 45/100