`FieldInfo.GetValue`/`SetValue` on an under-aligned field of a packed struct raises SIGBUS on arm64
- 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
Assessment
This issue has not been assessed yet.