Repository navigation
Intermittent SIGSEGV in GetThreadLocalStaticBase on macOS arm64 #127776
Description
Summary
Intermittent SIGSEGV crash in GetThreadLocalStaticBase on macOS arm64 during thread-local static access. This affects dotnet/sdk CI on the osx.15.arm64.open Helix queue, crashing Microsoft.CodeAnalysis.NetAnalyzers.UnitTests with high frequency.
Crash Signature
All crashes share the same native call stack:
libcoreclr.dylib!GetThreadLocalStaticBase(TLSIndex) ← SIGSEGV here
libcoreclr.dylib!GetThreadStaticsByMethodTable
<JIT frame>
System.Collections.Immutable!AllocFreeConcurrentStack`1.TryTake()
System.Collections.Immutable!SecureObjectPool`2.TryTake()
System.Collections.Immutable!SortedInt32KeyNode`1+Enumerator..ctor()
System.Collections.Immutable!ImmutableHashSet`1.GetEnumerator() (or ImmutableDictionary`2)
The crash occurs on thread pool worker threads during immutable collection enumeration. The callers vary (Roslyn CSharpCommandLineParser, VS MEF AttributedPartDiscovery.CreatePart) — the common factor is GetThreadLocalStaticBase faulting when accessing TLS.
Runtime Version (at time of capture)
11.0.26.20910 @Commit: 0cf6b19ed68d2d52e097e6af6d6046b4eeefefe2
Architecture: arm64
OS: macOS 15.7.5
Note: This crash has been occurring for an extended period prior to these captures — it is not a recent regression introduced by this specific runtime version. The builds above are simply the most recent occurrences with available crash artifacts.
Occurrences
| Build | Date | Work Item | Helix Job |
|---|---|---|---|
| 1405265 | 2026-05-01 | Microsoft.CodeAnalysis.NetAnalyzers.UnitTests.dll.27 |
b9aa540b-7485-469a-bd56-82d74ab61289 |
| 1404596 | 2026-04-30 | Microsoft.CodeAnalysis.NetAnalyzers.UnitTests.dll.133 |
e1016c9a-afe5-4bd5-9bac-114f1e948a0f |
| 1403714 | 2026-04-30 | Microsoft.CodeAnalysis.NetAnalyzers.UnitTests.dll.133 |
81cb7ab2-afed-4240-83da-6148f73f78bf |
This crash continues to occur frequently in SDK CI.
Crash Dumps
Core dumps (~2.4GB each) are available from the Helix work item file listings linked above. The .crashreport.json files (attached below) contain the full native + managed stacks for all threads.
Download links (may expire after ~20 days):
- Build 1405265: coredump.4125
- Build 1404596: coredump.9012
- Build 1403714: coredump.925
Representative Crashing Thread (Build 1404596)
libcoreclr.dylib!GetThreadLocalStaticBase(TLSIndex)
libcoreclr.dylib!GetThreadStaticsByMethodTable
<JIT code>
System.Collections.Immutable!AllocFreeConcurrentStack`1[[System.__Canon]].TryTake(System.__Canon ByRef)
System.Collections.Immutable!SecureObjectPool`2[...].TryTake(...)
System.Collections.Immutable!SortedInt32KeyNode`1+Enumerator[[ImmutableHashSet`1+HashBucket[...]]..ctor(...)
System.Collections.Immutable!ImmutableHashSet`1[[System.__Canon]].GetEnumerator()
Microsoft.CodeAnalysis.CSharp!CSharpCommandLineParser.ParseWarnings(...)
Microsoft.CodeAnalysis.CSharp!CSharpCommandLineParser.AddWarnings(...)
Microsoft.CodeAnalysis.CSharp!CSharpCommandLineParser.Parse(...)
Microsoft.CodeAnalysis!CommonCompiler.RunCore(...)
Microsoft.CodeAnalysis.CSharp!CSharpCompiler.Run(...)
Microsoft.CodeAnalysis.CSharp!Csc.Run(...)
...
System.Private.CoreLib!System.Threading.ThreadPoolWorkQueue.Dispatch()
System.Private.CoreLib!System.Threading.PortableThreadPool+WorkerThread.WorkerDoWork(...)
Analysis
- The crash is in native CoreCLR code (
GetThreadLocalStaticBase) — not in managed code or JIT output. - It only affects macOS arm64 (no reports on Linux or Windows).
- It is intermittent (not 100% repro) but very frequent under parallel workloads.
- The managed callers vary — the common thread is accessing
[ThreadStatic]fields via the TLS infrastructure during concurrent operations.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsNo status
Activity
MichaelSimons commentedon May 4, 2026
Attached crashreport.json files (contain full native + managed stacks for all threads):
These files are too large to paste inline but can be downloaded from the Helix artifact storage:
These links may expire after ~20 days.
dotnet-policy-service commentedon May 4, 2026
Tagging subscribers to this area: @dotnet/area-system-collections
See info in area-owners.md if you want to be subscribed.
jkotas commentedon May 5, 2026
cc @mangod9
dotnet-policy-service commentedon May 5, 2026
Tagging subscribers to this area: @agocke
See info in area-owners.md if you want to be subscribed.
mangod9 commentedon May 5, 2026
Symptoms
GetThreadLocalStaticBase(TLSIndex)on macOS ARM64 (Apple Silicon)ldr x8, [x20, #0x28]with x20 = NULLCrash Stack Trace (all 3 dumps identical)
Dump Evidence
Consistent Pattern Across All 3 Dumps
Register State at Crash (dump 9012, thread 0x261ae)
pMT(MethodTable pointer) is NULLldr x8, [x20, #0x28]→ loadingpMT->m_pEEClassfrom NULL+0x28Map State (dump 9012)
g_pThreadStaticNonCollectibleTypeIndices→ map struct at 0x600000e80280pMap= 0x600002098800,m_maxIndex= 16 (minimum/initial size — no grow occurred)g_NextNonCollectibleTlsSlot= 15 → index 14 was the last allocated NonCollectible indexThreadLocalData.cNonCollectibleTlsData= 24 (TLS array exists, slot 14 was null → first access on this thread)Key Observation
Index 14's entry became visible between crash time and dump time — the store buffer drained during signal handler execution, proving the write existed but was not yet observable to the crashing thread.
Root Cause
ARM64 store-visibility race in
TLSIndexToMethodTableMapbetween lockless reader and lock-protected writer.Writer Path (under
g_TLSCrstlock)Called from
EnsureTlsIndexAllocated()→GetTLSIndexForThreadStatic()during type loading (MethodTableBuilder::SetupMethodTable2).Reader Path (NO lock)
Called from
LookupMethodTableAndFlagForThreadStatic()→GetThreadLocalStaticBase()on the hot path.Why This Crashes on ARM64
g_TLSCrst. The reader is lockless (hot path optimization).VolatileLoadWithoutBarrier= plainldron ARM64 — no acquire semantics, can return stale cache-line data.IsTlsIndexAllocatedflag usesInterlockedOr(store) /VolatileLoad(acquire read), but the subsequent map lookup usesVolatileLoadWithoutBarrierwhich bypasses the ordering that the acquire established.Race Sequence
Origin
eb8f54d92b(2024-06-13) — PR Make normal statics simpler #99183 "Make normal statics simpler" by David WrightonTLSIndexToMethodTableMapwithVolatileLoadWithoutBarrierinLookup()26612c84cad(2024-10-30) — PR Move static helpers to managed #108167 "Move static helpers to managed"GetThreadStaticsByMethodTableQCall enabling parallel thread-local access during class initDisassembly Confirmation
Set()— all stores are plainstr(nostlr/store-release)Lookup()(inlined inGetThreadLocalStaticBase) — entry read is plainldrThe reader acquires the pointer with
ldapr, but reads the entry data with plainldr. Without a matchingstlron the writer side, there is no happens-before relationship for the entry data on ARM64.Proposed Fix
Option 1 (Minimal — fix writer): Change plain store to store-release in
Set():Option 2 (Belt-and-suspenders — fix reader): Change to acquire-load in
Lookup():Option 3 (Defense-in-depth): Add NULL check after
LookupMethodTableAndFlagForThreadStatic():Recommended: Option 1 + Option 3. The store-release fixes the root cause with minimal perf impact (store-release is cheap on ARM64). The NULL check provides a safety net against any remaining edge cases.
Note on the grow path:
Set()also haspMap = newMapandm_maxIndex = newSizeas plain stores aftermemcpy. These should also useVolatileStorefor correctness, even though this particular crash was NOT triggered by a grow (m_maxIndex=16 was the initial/only allocation).Fix a series of possible race conditions in thread static variable in…
Fix a series of possible race conditions in thread static variable in…