Description
FieldDesc::GetInstanceField and FieldDesc::SetInstanceField in src/coreclr/vm/field.cpp access 1/2/4/8-byte instance fields through VolatileLoad<T> / VolatileStore<T>:
|
case 8: |
|
*(INT64*)pOutVal = VolatileLoad<INT64>(PTR_INT64(pFieldAddress)); |
|
break; |
|
case 8: |
|
VolatileStore<INT64>((INT64*)pFieldAddress, *(INT64*)pInVal); |
|
break; |
On arm64 those lower to ldapr / stlr, which require natural alignment. [StructLayout(Pack = N)] can place a 4- or 8-byte field at an under-aligned offset, so the field address on a boxed instance is not naturally aligned and the access faults.
The condition is narrower than plain misalignment, which is why this presents as intermittent. On Apple Silicon an unaligned acquire/release access faults only when it crosses a 16-byte granule; within a single granule it succeeds. Boxed objects are 8-byte, not 16-byte, aligned, so a given field lands on a crossing address roughly half the time depending on allocation placement.
Measured on an M3 Max:
| Field |
Address (mod 16) |
Crosses granule |
Result |
double at offset 12, Pack = 4 |
4 |
no |
ok |
double at offset 12, Pack = 4 |
12 |
yes |
SIGBUS |
int at offset 5, Pack = 1 |
5 |
no |
ok |
int at offset 5, Pack = 1 |
13 |
yes |
SIGBUS |
This is reachable with no reflection in user code. ValueType.Equals and ValueType.GetHashCode fall back to a FieldInfo.GetValue field walk whenever the type is ineligible for the bitwise fast path, which a floating-point or reference field causes. That is how we hit it: an NUnit Is.EquivalentTo assertion over a packed struct with a double field crashed the Unity editor process, at a rate of about 5 in 10 CI runs on macOS arm64.
Reproduction Steps
See attached Repro.cs.
Run with
dotnet run --file Repro.cs
It reproduces 5/5 here. The struct under test:
[StructLayout(LayoutKind.Sequential, Pack = 4)]
struct Packed
{
public int A;
public int B;
public int C;
public double V; // Pack = 4 places this at offset 12
}
The program allocates boxed instances until one lands across a 16-byte granule, keeps it pinned so it cannot move, then calls FieldInfo.GetValue on V. The search is what makes it deterministic. Looping over plain allocations is not enough, because boxes allocated back to back are equally spaced and one process therefore tends to see a single residue throughout.
Three one-line edits to the same file cover the rest of the evidence:
- Confirm the granule condition. Change the residue check from
address % 16 != 12 to address % 16 != 4. The access is still 4-byte aligned rather than 8, but it no longer crosses a granule, and the read succeeds.
- Confirm 4-byte fields are affected too. Change the struct to
[StructLayout(LayoutKind.Sequential, Pack = 1)] struct Packed { public byte Tag; public int A; public int B; }, read "B" (offset 5) instead of "V", and use offset 5 with residue 13. Faults. Residue 5 succeeds.
- Confirm
SetValue is affected. Replace the GetValue call with typeof(Packed).GetField("V").SetValue(boxed, 1.25). Faults identically.
For the implicit path, replacing the GetValue call with a ValueType.Equals against a second instance faults in the same place, with no reflection in user code:
object other = new Packed { A = 1, B = 2, C = 3, V = 1.23 };
boxed.Equals(other); // SIGBUS in FieldDesc::GetInstanceField
A/B/C must match in both values so the field walk gets as far as V. Note that ValueType.Equals does not short-circuit on a bitwise compare first: bitwise-identical values fault just as readily, verified over 200-iteration loops.
Expected behavior
FieldInfo.GetValue returns the field value and FieldInfo.SetValue writes it, for any field of any loadable struct, regardless of packing. ValueType.Equals returns a bool. Nothing in the reflection API contract makes a field's alignment the caller's concern, and the same code works on x64.
Actual behavior
The process dies with SIGBUS. Exit code 138 (128 + 10). From the macOS crash report:
exception: EXC_BAD_ACCESS, signal SIGBUS
subtype: EXC_ARM_DA_ALIGN at 0x143e13d7c (4 mod 8, 12 mod 16)
codes: 0x0000000000000101, 0x0000000143e13d7c
frames:
libcoreclr.dylib FieldDesc::GetInstanceField(Object*, void*)
libcoreclr.dylib InvokeUtil::GetFieldValue(FieldDesc*, TypeHandle, Object**, TypeHandle, int*)
libcoreclr.dylib RuntimeFieldHandle_GetValue
code=257 is 0x101, EXC_ARM_DA_ALIGN. Disassembly of the faulting site, offset +172 in FieldDesc::GetInstanceField:
+172: ldapr x8, [x20] <-- faults
+176: str x8, [x19]
The 1/2/4-byte arms of the same switch are at +148, +196 and +88, using ldaprb / ldaprh / ldapr w8 respectively.
Regression?
No response
Known Workarounds
No response
Configuration
- .NET SDK 10.0.301, runtime 10.0.9,
osx-arm64
- macOS 26.6, Apple M3 Max
- Release and Debug both affected; not specific to a build configuration
- Does not reproduce on x64, where unaligned acquire/release accesses are legal
- Not specific to macOS in principle:
ldapr/stlr alignment requirements apply to arm64 generally, so linux-arm64 and win-arm64 should be checked. I have only tested osx-arm64.
- Originally found on a Unity fork of the runtime, then reproduced on stock Microsoft builds with no Unity code involved.
Other information
No response
Description
FieldDesc::GetInstanceFieldandFieldDesc::SetInstanceFieldinsrc/coreclr/vm/field.cppaccess 1/2/4/8-byte instance fields throughVolatileLoad<T>/VolatileStore<T>:runtime/src/coreclr/vm/field.cpp
Lines 335 to 337 in e1d442a
runtime/src/coreclr/vm/field.cpp
Lines 411 to 413 in e1d442a
On arm64 those lower to
ldapr/stlr, which require natural alignment.[StructLayout(Pack = N)]can place a 4- or 8-byte field at an under-aligned offset, so the field address on a boxed instance is not naturally aligned and the access faults.The condition is narrower than plain misalignment, which is why this presents as intermittent. On Apple Silicon an unaligned acquire/release access faults only when it crosses a 16-byte granule; within a single granule it succeeds. Boxed objects are 8-byte, not 16-byte, aligned, so a given field lands on a crossing address roughly half the time depending on allocation placement.
Measured on an M3 Max:
doubleat offset 12,Pack = 4doubleat offset 12,Pack = 4intat offset 5,Pack = 1intat offset 5,Pack = 1This is reachable with no reflection in user code.
ValueType.EqualsandValueType.GetHashCodefall back to aFieldInfo.GetValuefield walk whenever the type is ineligible for the bitwise fast path, which a floating-point or reference field causes. That is how we hit it: an NUnitIs.EquivalentToassertion over a packed struct with adoublefield crashed the Unity editor process, at a rate of about 5 in 10 CI runs on macOS arm64.Reproduction Steps
See attached Repro.cs.
Run with
It reproduces 5/5 here. The struct under test:
The program allocates boxed instances until one lands across a 16-byte granule, keeps it pinned so it cannot move, then calls
FieldInfo.GetValueonV. The search is what makes it deterministic. Looping over plain allocations is not enough, because boxes allocated back to back are equally spaced and one process therefore tends to see a single residue throughout.Three one-line edits to the same file cover the rest of the evidence:
address % 16 != 12toaddress % 16 != 4. The access is still 4-byte aligned rather than 8, but it no longer crosses a granule, and the read succeeds.[StructLayout(LayoutKind.Sequential, Pack = 1)] struct Packed { public byte Tag; public int A; public int B; }, read"B"(offset 5) instead of"V", and use offset5with residue13. Faults. Residue5succeeds.SetValueis affected. Replace theGetValuecall withtypeof(Packed).GetField("V").SetValue(boxed, 1.25). Faults identically.For the implicit path, replacing the
GetValuecall with aValueType.Equalsagainst a second instance faults in the same place, with no reflection in user code:A/B/Cmust match in both values so the field walk gets as far asV. Note thatValueType.Equalsdoes not short-circuit on a bitwise compare first: bitwise-identical values fault just as readily, verified over 200-iteration loops.Expected behavior
FieldInfo.GetValuereturns the field value andFieldInfo.SetValuewrites it, for any field of any loadable struct, regardless of packing.ValueType.Equalsreturns abool. Nothing in the reflection API contract makes a field's alignment the caller's concern, and the same code works on x64.Actual behavior
The process dies with SIGBUS. Exit code 138 (128 + 10). From the macOS crash report:
code=257is0x101,EXC_ARM_DA_ALIGN. Disassembly of the faulting site, offset+172inFieldDesc::GetInstanceField:The 1/2/4-byte arms of the same
switchare at+148,+196and+88, usingldaprb/ldaprh/ldapr w8respectively.Regression?
No response
Known Workarounds
No response
Configuration
osx-arm64ldapr/stlralignment requirements apply to arm64 generally, so linux-arm64 and win-arm64 should be checked. I have only tested osx-arm64.Other information
No response