Uh oh!
There was an error while loading. Please reload this page.
Fix alignment of 8-byte thread statics on direct TLS path - #129749
Conversation
GetTLSIndexForThreadStatic computes the alignment for a thread static placed on the direct-on-thread-local-data bump allocator. The chain of size checks was missing an `else` before the `>= 4` case, so an 8-byte field (long/double) first set alignment = 8 and then immediately overwrote it with alignment = 4. The resulting offset could land on a 4-mod-8 boundary, producing a misaligned long/double. On arm64 this causes Interlocked.Increment(ref threadStaticLong) to throw DataMisalignedException, since the atomic lowers to ldaxr/stlxr which fault on a misaligned address. Fixesdotnet#129733 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Tagging subscribers to this area: @JulieLeeMSFT, @VSadov |
There was a problem hiding this comment.
Pull request overview
Fixes a correctness bug in CoreCLR’s thread statics TLS-index allocation logic where 8-byte non-GC thread static blocks on the direct-on-thread-local-data path could be aligned only to 4 bytes, leading to misaligned long/double storage on some platforms.
Changes:
- Correct the alignment selection logic so
bytesNeeded >= 8preserves 8-byte alignment (viaelse ifchaining). - Prevent potential misalignment of 8-byte thread statics allocated from
ThreadLocalData::ExtendedDirectThreadLocalTLSData.
Uh oh!
There was an error while loading. Please reload this page.
jkotas
commented
Jun 23, 2026
Customer reported regression. It makes sense to backport to me. This bug may lead to performance issues as well. |
EgorBo
commented
Jun 24, 2026
/backport to release/10.0 |
Started backporting to |
Fixes#129733 ## Problem `Interlocked.Increment(ref threadStaticLong)` on a `[ThreadStatic] static long` throws `System.DataMisalignedException` on osx-arm64. ## Root cause In `GetTLSIndexForThreadStatic` (`src/coreclr/vm/threadstatics.cpp`), small non-collectible thread statics of already-initialized classes are placed via a top-down bump allocator on the direct-on-thread-local-data path. The per-field alignment was computed with a missing `else`: ```cpp uint32_t alignment; if (bytesNeeded >= 8) alignment = 8; if (bytesNeeded >= 4) // <-- missing 'else' alignment = 4; else if (bytesNeeded >= 2) alignment = 2; else alignment = 1; ``` For an 8-byte field (`long`/`double`), the first `if` sets `alignment = 8`, but the second `if (bytesNeeded >= 4)` is also true and overwrites it with `alignment = 4`. The offset is then chosen via `AlignDown(indexOffsetWithoutAlignment, alignment)`, so with `alignment == 4` it can land on a 4-mod-8 boundary. Since `ThreadLocalData` (and thus `ExtendedDirectThreadLocalTLSData`) is only 8-byte aligned, the resulting absolute address of the `long` can be 4-byte but not 8-byte aligned. ## Why it only reproduces on arm64 (and intermittently) - x64 tolerates misaligned atomic operations; arm64 `Interlocked.Increment` lowers to `ldaxr`/`stlxr`, which fault on a misaligned address → `DataMisalignedException`. - It is layout-dependent: whether the 4-aligned offset also happens to be 8-aligned depends on how many bytes earlier thread statics already consumed in the bump allocator. This is why a real app (e.g. ASP.NET/Kestrel, which registers other thread statics first) trips it while a trivial repro may not. ## Fix Add the missing `else` so an 8-byte field keeps `alignment = 8`. ## Testing I was unable to build/run locally for this change (development box is linux-x64 while the failure manifests on osx-arm64, where misaligned atomics fault). The fix is a one-line correction to the alignment selection logic and was verified by source inspection. CI will exercise the relevant platforms. > [!NOTE] > This pull request was authored with the assistance of GitHub Copilot. Co-authored-by: EgorBo <egorbo@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fixes#129733
Problem
Interlocked.Increment(ref threadStaticLong)on a[ThreadStatic] static longthrowsSystem.DataMisalignedExceptionon osx-arm64.Root cause
In
GetTLSIndexForThreadStatic(src/coreclr/vm/threadstatics.cpp), small non-collectible thread statics of already-initialized classes are placed via a top-down bump allocator on the direct-on-thread-local-data path. The per-field alignment was computed with a missingelse:For an 8-byte field (
long/double), the firstifsetsalignment = 8, but the secondif (bytesNeeded >= 4)is also true and overwrites it withalignment = 4. The offset is then chosen viaAlignDown(indexOffsetWithoutAlignment, alignment), so withalignment == 4it can land on a 4-mod-8 boundary. SinceThreadLocalData(and thusExtendedDirectThreadLocalTLSData) is only 8-byte aligned, the resulting absolute address of thelongcan be 4-byte but not 8-byte aligned.Why it only reproduces on arm64 (and intermittently)
Interlocked.Incrementlowers toldaxr/stlxr, which fault on a misaligned address →DataMisalignedException.Fix
Add the missing
elseso an 8-byte field keepsalignment = 8.Testing
I was unable to build/run locally for this change (development box is linux-x64 while the failure manifests on osx-arm64, where misaligned atomics fault). The fix is a one-line correction to the alignment selection logic and was verified by source inspection. CI will exercise the relevant platforms.
Note
This pull request was authored with the assistance of GitHub Copilot.