Description
On osx-arm64, k % 2 for a non-negative k returns -1 when the result is stored into a newly allocated object right after another field. The JIT emits the sign fix-up cneg ..., lt before the cmp that should set its flags. cneg then reads whatever flags the allocation helper left behind.
Reproduction Steps
Repro.csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0;net9.0</TargetFrameworks>
</PropertyGroup>
</Project>
Program.cs:
using System;
using System.Runtime.InteropServices;
sealed class Item { public int A, B; }
static class Program
{
static Item[] Fill(int n, int a)
{
var items = new Item[n];
for (int k = 0; k < n; k++)
items[k] = new Item { A = a, B = k % 2 }; // k >= 0, so B must be 0 or 1
return items;
}
static int Main()
{
int wrong = 0, total = 0;
for (int i = 0; i < 5000; i++)
foreach (var item in Fill(11, i))
{
total++;
if (item.B != 0 && item.B != 1) wrong++;
}
Console.WriteLine($"{Environment.Version} {RuntimeInformation.ProcessArchitecture}: {wrong} of {total} wrong");
return wrong == 0 ? 100 : 1;
}
}
Run dotnet run -c Release -f net10.0. The bug shows with default settings, and on almost every odd k with DOTNET_TieredCompilation=0.
Expected behavior
0 of 55000 wrong
Actual behavior
| Runtime (osx-arm64) |
Default |
DOTNET_TieredCompilation=0 |
| 10.0.12 |
21067 of 55000 wrong |
24902 of 55000 wrong |
| 10.0.11 |
21067 |
24900 |
| 10.0.0 |
21067 |
24902 |
| 9.0.20 |
0 |
0 |
The wrong values are all -1 (odd k comes out negated). The count varies slightly between runs.
DOTNET_JitDisasm=Fill for the loop body on 10.0.12 (FullOpts):
G_M000_IG04:
sub x0, x21, #128
bl CORINFO_HELP_NEWSFAST
and w14, w23, #1
cneg w14, w14, lt ; reads flags left by the helper call
stp w20, w14, [x0, #0x08]
cmp w23, #0 ; the compare cneg needed, emitted after it (and dead here)
add x14, x24, x23, LSL #3
mov x15, x0
bl CORINFO_HELP_ASSIGN_REF
add w23, w23, #1
cmp w23, w19
blt G_M000_IG04
Regression?
The wrong results are new in .NET 10 (10.0.0 through 10.0.12). The misordered code is not: 9.0.20 emits the same and / cneg / stp / cmp sequence. It only gives the right answer there because its allocation helper happens to leave flags for which lt is false. So the codegen bug looks latent, and a change to the allocation helper in .NET 10 exposed it.
Known Workarounds
- Swap the two assignments (
new Item { B = k % 2, A = a }): the result is correct.
- Compute the remainder outside the initializer, or use
k & 1 when k is known to be non-negative.
Configuration
- .NET SDK 10.0.401, runtime 10.0.12 (also 10.0.11 and 10.0.0 from the official osx-arm64 tarballs)
- macOS 27.0.1 (26A434), Apple silicon (M-series), osx-arm64
- Not reproduced on linux-x64 (10.0.11)
- Not tried on linux-arm64 or win-arm64
Other information
- It needs both stores into the new object, in that order. Without the
A = a store, or with no allocation in the loop (storing into two int[]s), the result is correct. That suggests the store-pair (stp) combine is moving the cmp past the cneg.
- First seen in real code where a lambda captured locals of the method. The lambda isn't needed to reproduce it.
Filed by an AI agent (Claude Code) on behalf of Christoffer Buusmann.
Description
On osx-arm64,
k % 2for a non-negativekreturns-1when the result is stored into a newly allocated object right after another field. The JIT emits the sign fix-upcneg ..., ltbefore thecmpthat should set its flags.cnegthen reads whatever flags the allocation helper left behind.Reproduction Steps
Repro.csproj:Program.cs:Run
dotnet run -c Release -f net10.0. The bug shows with default settings, and on almost every oddkwithDOTNET_TieredCompilation=0.Expected behavior
0 of 55000 wrongActual behavior
DOTNET_TieredCompilation=0The wrong values are all
-1(oddkcomes out negated). The count varies slightly between runs.DOTNET_JitDisasm=Fillfor the loop body on 10.0.12 (FullOpts):Regression?
The wrong results are new in .NET 10 (10.0.0 through 10.0.12). The misordered code is not: 9.0.20 emits the same
and / cneg / stp / cmpsequence. It only gives the right answer there because its allocation helper happens to leave flags for whichltis false. So the codegen bug looks latent, and a change to the allocation helper in .NET 10 exposed it.Known Workarounds
new Item { B = k % 2, A = a }): the result is correct.k & 1whenkis known to be non-negative.Configuration
Other information
A = astore, or with no allocation in the loop (storing into twoint[]s), the result is correct. That suggests the store-pair (stp) combine is moving thecmppast thecneg.Filed by an AI agent (Claude Code) on behalf of Christoffer Buusmann.