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

  1. added
    area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI
    on Sep 4, 2026
  2. dotnet-policy-service commented on Sep 4, 2026

    @dotnet-policy-service
    Contributor

    Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
    See info in area-owners.md if you want to be subscribed.

  3. added and removed
    area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI
    on Sep 4, 2026
  4. dotnet-policy-service commented on Sep 4, 2026

    @dotnet-policy-service
    Contributor

    Tagging subscribers to this area: @agocke
    See info in area-owners.md if you want to be subscribed.

  5. tannergooding commented on Sep 4, 2026

    @tannergooding
    Member

    Nothing in the reflection API contract makes a field's alignment the caller's concern, and the same code works on x64.

    Notably .NET requires and expects data to be naturally aligned and the runtime spec (ECMA-335) documents this explicitly. From I.12.6.2 Alignment:

    There is a special prefix instruction, unaligned., that can immediately precede an ldind, stind,
    initblk, or cpblk instruction. This prefix indicates that the data can have arbitrary alignment; the
    JIT compiler is required to generate code that correctly performs the effect of the instructions
    regardless of the actual alignment. Otherwise, if the data is not properly aligned, and no
    unaligned. prefix has been specified, executing the instruction can generate unaligned memory
    faults or incorrect data.

    This extends to all operations throughout .NET, whether they explicitly document it or not. It's rather only those that explicitly document themselves as working with unaligned data that you can expect will actually function (and even then it may not have all the guarantees that aligned data would otherwise have; like there may not be atomicity).

    Types that have explicit layout are fundamentally unsafe and with the unsafe evolution feature will even start requiring you to annotate them as such. I don't recall off the top of my head if we've made a similar decision for explicit packing, but that should really be the same consideration.

    .NET in general just leaves it to the underlying operating system and/or architecture. Some platforms tend to "just work" (albeit possibly slower) while others may not have any such handling. For some of the platforms without, the operating system may step in and have its own first chance fault handler to account for misalignment, but that doesn't light up in all contexts and isn't itself a guarantee either.

    There's potentially some things we can here, but this is a behavior going back to .NET Framework 1.0, is unsafe, and is explicitly documented as something that can lead to such faults.

    CC. @jkotas as well for input and possible direction

  6. jkotas commented on Sep 5, 2026

    @jkotas
    Member

    I don't recall off the top of my head if we've made a similar decision for explicit packing, but that should really be the same consideration.

    Opened dotnet/roslyn#85173 to handle this case in unsafe evolution.

    Reflection behavior with misaligned fields is a separate problem. In general, reflection is expected to work for methods and fields regardless of whether their access is considered safe or unsafe. The problem is that reflection APIs like FieldDesc.GetValue/SetValue do not provide a way to specify volatile or unaligned access. As you can see above, FieldDesc.GetValue/SetValue addressed this by using atomic volatile reads and writes unconditionally for selected sizes. This ensures correct behavior in case the field requires atomic volatile access. The extra overhead of the memory barrier is negligible compared to the total reflection overhead. We can consider similar approach for misaligned fields: check the field's alignment before performing an atomic volatile read or write, and fall back to memcpy when the field is misaligned.

  7. dotnet-policy-service commented on Sep 5, 2026

    @dotnet-policy-service
    Contributor

    Tagging subscribers to this area: @steveisok, @dotnet/area-system-reflection
    See info in area-owners.md if you want to be subscribed.

  8. removed
    untriagedNew issue has not been triaged by the area owner
    on Sep 14, 2026
  9. added this to the 12.0.0 milestone on Sep 14, 2026
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