Repository navigation
Description
Description
On osx-arm64, a NativeAOT-compiled method that takes an (int Start, int End) value tuple and writes an array in a loop starting at Start crashes with EXC_BAD_ACCESS (SIGBUS).
The tuple arrives packed in one register (x1: Start in the low 32 bits, End in the high 32 bits). The loop's first store computes its address with x1 as a 64-bit index (add x14, x0, x1, LSL #3) without zero-extending Start first. So the first write lands (End << 32) * 8 bytes past the array. Later iterations are fine, because add w1, w1, #1 clears the upper half.
The same code runs correctly under the regular runtime (JIT, tiered or fully optimized). The same loop taking two int parameters is also fine under NativeAOT.
Reproduction Steps
AotTupleIndex.csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<PublishAot>true</PublishAot>
</PropertyGroup>
</Project>Program.cs:
using System.Runtime.CompilerServices;
string mode = args.Length > 0 ? args[0] : "tuple";
string[] states = ["a", "a", "b", "b", "b", "b", "b", "b"];
if (mode == "tuple")
{
Repro.FillFromTuple(states, (0, 7)); // crashes under NativeAOT on osx-arm64
}
else
{
Repro.FillFromInts(states, 0, 7); // same loop with two int parameters: fine
}
Console.WriteLine($"{mode}: {string.Join("", states)}");
static class Repro
{
[MethodImpl(MethodImplOptions.NoInlining)]
public static void FillFromTuple(string[] states, (int Start, int End) run)
{
string state = states[run.End];
for (int i = run.Start; i <= run.End; i++)
{
states[i] = state;
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static void FillFromInts(string[] states, int start, int end)
{
string state = states[end];
for (int i = start; i <= end; i++)
{
states[i] = state;
}
}
}Then:
dotnet publish -c Release -r osx-arm64 -o out
./out/AotTupleIndex tuple # crashes: exit 138 (SIGBUS)
./out/AotTupleIndex ints # prints "ints: bbbbbbbb"
dotnet run -c Release -- tuple # JIT: prints "tuple: bbbbbbbb"
Expected behavior
tuple: bbbbbbbb, the same as the ints path and the JIT.
Actual behavior
The process dies with EXC_BAD_ACCESS (SIGBUS) on the first store inside the loop.
The disassembly of FillFromTuple, from adding <IlcArg Include="--codegenopt:JitDisasm=FillFromTuple" /> to the project:
; Assembly listing for method Repro:FillFromTuple(System.String[],System.ValueTuple`2[int,int]) (FullOpts)
; Emitting BLENDED_CODE for generic ARM64 on Apple
; FullOpts code
; NativeAOT compilation
; optimized code
; fp based frame
; fully interruptible
G_M000_IG01: ;; offset=0x0000
sub sp, sp, #32
stp fp, lr, [sp, #0x10]
add fp, sp, #16
str x1, [sp, #0x08]
G_M000_IG02: ;; offset=0x0010
ldr w2, [sp, #0x0C] ; End
ldr w3, [x0, #0x08]
cmp w2, w3
bhs G_M000_IG07
add x0, x0, #16
ldr x4, [x0, w2, UXTW #3]
cmp w1, w2
bgt G_M000_IG05
G_M000_IG03: ;; offset=0x0030
cmp w3, w2
ccmp w1, #0, nc, gt
blt G_M000_IG06
align [4 bytes for IG04]
align [0 bytes]
align [0 bytes]
align [0 bytes]
G_M000_IG04: ;; offset=0x0040
add x14, x0, x1, LSL #3 ; x1 still holds End in its upper 32 bits on the first iteration
mov x15, x4
bl CORINFO_HELP_ASSIGN_REF
add w1, w1, #1 ; clears the upper half from the second iteration on
cmp w1, w2
ble G_M000_IG04
G_M000_IG05: ;; offset=0x0058
ldp fp, lr, [sp, #0x10]
add sp, sp, #32
ret lr
G_M000_IG06: ;; offset=0x0064
cmp w1, w3
bhs G_M000_IG07
add x14, x0, x1, LSL #3 ; same in the range-checked copy of the loop
mov x15, x4
bl CORINFO_HELP_ASSIGN_REF
add w1, w1, #1 ; clears the upper half from the second iteration on
cmp w1, w2
ble G_M000_IG06
b G_M000_IG05
G_M000_IG07: ;; offset=0x0088
bl CORINFO_HELP_RNGCHKFAIL
brk #0
; Total bytes of code 144
Regression?
Not known. Not tried on .NET 9.
Known Workarounds
Pass the two fields as separate int parameters, or use states.AsSpan(start, end - start + 1).Fill(state).
Configuration
- ILCompiler (NativeAOT): 10.0.0, 10.0.8, 10.0.12 and 10.0.13, all crash; 10.0.13 is the latest stable.
- RID: osx-arm64 on macOS 26.6.2 (25G83), Apple M5. linux-arm64 and win-arm64 not tried.
- Optimisation:
OptimizationPreferenceSize and Speed both crash. - These codegen options didn't help (each tried alone):
JitEnablePhysicalPromotion=0,JitNoStructPromotion=1,JitEnableInductionVariableOpts=0,JitEnableStrengthReduction=0,JitDoRangeAnalysis=0,JitDoAssertionProp=0,JitEnableOptRepeat=0.
Other information
It looks like the loop's address computation uses the incoming struct register directly for the induction variable's first value, without the zero-extension that the w-register increment provides on later iterations.
Activity
dotnet-policy-service commentedon Oct 22, 2026
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.
EgorBo commentedon Oct 22, 2026
It's fixed in main via #134466, I'll backport it to .NET 11.0.
@w2olves is it acceptable for you to update to .NET 11.0 once it's released? (in a few weeks).