dotnet / dotnet/runtime

JIT: (bug) assertion prop retargets a small-typed `LCL_VAR` to an `int` local, emitting a truncated compare (wrong result)

Open
#133,859 1 comment 0 reactions 0 assignees View on GitHub
area-CodeGen-coreclr
Dominant language
C#
Stars
18.3k
Forks
5.6k
PR merge metrics
PR metrics pending

Description

The VN relop based copy assertion prop path in `optAssertionProp_RelOp` retargets `op1`'s lclNum/ssaNum to `op2`'s local but leaves `op1`'s node type alone, producing `NE(LCL_VAR ubyte V03, LCL_VAR int V03)`. The mismatched types stop the self-compare from folding, and codegen emits a narrow load for one side, so the compare yields the wrong answer.

### Minimal Repro

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

public static class Program
{
[MethodImpl(MethodImplOptions.NoInlining)]
static void Consume(int x) { }

[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
public static int Test(byte b, int[] arr, int x)
{
b = (byte)x;
int len = arr.Length;
int r = 0;
try
{
if (b != len)
{
Consume(b);
Consume(len);
r = (b == len) ? 1 : 0;
}
else
{
r = 3;
}
if (x == 12345)
{
len = 7;
}
}
catch
{
r = len;
}
return r;
}

public static int Main()
{
Console.WriteLine(Test(0, new int[300], 44));
return 100;
}
}
```

`b` is `44`, `len` is `300`; the `b != len` branch is taken, so `(b == len)` is statically `false` and `r` must be `0`.

### Expected

```
0
```

### Actual

```
1
```

Disasm (`DOTNET_JitDisasm=Test`), `V03` is `int loc0`:

```
movzx rax, byte ptr [rbp-0x0C] ; <=== narrow load of the TYP_INT local V03 (300 -> 44)
mov ecx, dword ptr [rbp-0x0C] ; <=== full load of the same local (300)
cmp eax, ecx
setne al
```

### Notes

- Malformed tree after assertion prop: `NE(LCL_VAR ubyte V03, LCL_VAR int V03)` — `GenTree::Compare` won't fold identical operands whose types differ, so the node reaches codegen.
- Fix direction: in the local/local copy path also fix up `op1`'s type (e.g. `op1->ChangeType(op2->TypeGet())`), or bail out when the types differ.
- The `try`/`catch` and second store to `len` only force `V03` to be `do-not-enreg` so the narrow load becomes a visible memory access; the malformed IR is produced without them too.
- Reproduces in Release and Checked corerun, with and without tiering; no assert fires. Does not reproduce on .NET 10.0.12, so this looks `main`-only (the `optAssertionProp_RelOp` code is old, so some upstream change now lets the small-typed `LCL_VAR` reach assertion prop uncast).

Contributor guide

Open the contributing guide

Research direction

Start at optAssertionProp_RelOp and reproduce the issue with the provided Test method, checking the malformed NE(LCL_VAR ubyte V03, LCL_VAR int V03) tree. Inspect the local/local copy assertion path and verify the fix prevents the narrow load and produces the expected output 0 in Release and Checked corerun.

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
Active
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.