dotnet / dotnet/runtime

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

Open
#133,236 5 comments 0 reactions 1 assignee Claimed by @steveisok View on GitHub
area-System.Reflection
Dominant language
C#
Stars
18.3k
Forks
5.6k
PR merge metrics
PR metrics pending

Description

### Description

`FieldDesc::GetInstanceField` and `FieldDesc::SetInstanceField` in `src/coreclr/vm/field.cpp` access 1/2/4/8-byte instance fields through `VolatileLoad` / `VolatileStore`:

https://github.com/dotnet/runtime/blob/e1d442a67c5f2202bd2c98e3efe0e2cdc9474d58/src/coreclr/vm/field.cpp#L335-L337

https://github.com/dotnet/runtime/blob/e1d442a67c5f2202bd2c98e3efe0e2cdc9474d58/src/coreclr/vm/field.cpp#L411-L413

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](https://github.com/user-attachments/files/31836160/Repro.cs).

Run with

```bash
dotnet run --file Repro.cs
```

It reproduces 5/5 here. The struct under test:

```csharp
[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:

```csharp
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_

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.