Skip to content

FieldInfo.GetValue/SetValue on an under-aligned field of a packed struct raises SIGBUS on arm64 #133236

Description

@tgjones-unity3d

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

  • Status
    No status

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions