Skip to content

Fix NativeAOT thread-static debug info - #133109

Merged
max-charlamb merged 1 commit into
dotnet:mainfrom
max-charlamb:max-charlamb/nativeaot-thread-static-debug-info
Sep 3, 2026
Merged

Fix NativeAOT thread-static debug info#133109
max-charlamb merged 1 commit into
dotnet:mainfrom
max-charlamb:max-charlamb/nativeaot-thread-static-debug-info

Conversation

@max-charlamb

Copy link
Copy Markdown
Member

Summary

  • Treat thread-static, GC-static, and non-GC-static fields as mutually exclusive storage categories when emitting NativeAOT debug types.
  • Preserve thread-static fields when the owning type has a generated thread-static index but no separately generated ordinary GC-static base.

Problem

FieldDesc.HasGCStaticBase returns true for NativeAOT thread-static fields. The debug type writer first accepted a thread-static field based on HasThreadStaticBase, but then independently tested the same field as an ordinary GC-static field:

if (fieldDesc.IsThreadStatic && !hasThreadStatics)
    continue;

if (fieldDesc.HasGCStaticBase)
{
    if (!hasGcStatics)
        continue;
}

This dropped all thread-static fields from types that had no separately generated GC-static base. An example is the AsyncDispatcherInfo ref struct:

internal unsafe ref struct AsyncDispatcherInfo
{
    // Instance fields omitted.

    [ThreadStatic]
    internal static AsyncDispatcherInfo* t_current;
}

The index symbol was emitted because the type had thread-static storage, but the PDB omitted the typed parent member, thread-static storage UDT, helper UDT, and t_current field offset.

The fix makes the three storage categories mutually exclusive by changing the second test to else if.

CDB validation

I built a small runtime-async NativeAOT application that roots AsyncDispatcherInfo and inspected its Windows PDB with CDB.

Before the fix, the index existed only as an untyped symbol. The parent type had no __THREADSTATICINDEX, and the thread-static storage type did not exist:

0:000> x AsyncDispatcherPdb!S_P_CoreLib_System_Runtime_CompilerServices_AsyncDispatcherInfo::__THREADSTATICINDEX
00007ff7`cd8a7ed8 AsyncDispatcherPdb!S_P_CoreLib_System_Runtime_CompilerServices_AsyncDispatcherInfo::__THREADSTATICINDEX = <no type information>

0:000> dt AsyncDispatcherPdb!S_P_CoreLib_System_Runtime_CompilerServices_AsyncDispatcherInfo
   +0x000 Next             : Ptr64 S_P_CoreLib_System_Runtime_CompilerServices_AsyncDispatcherInfo
   +0x008 NextContinuation : Ptr64 S_P_CoreLib_System_Runtime_CompilerServices_Continuation
   +0x010 CurrentTask      : Ptr64 S_P_CoreLib_System_Threading_Tasks_Task
   +0x018 AsyncProfilerInfo : S_P_CoreLib_System_Runtime_CompilerServices_AsyncProfiler_Info

0:000> dt AsyncDispatcherPdb!__type__THREADSTATICSS_P_CoreLib_System_Runtime_CompilerServices_AsyncDispatcherInfo
Symbol AsyncDispatcherPdb!__type__THREADSTATICSS_P_CoreLib_System_Runtime_CompilerServices_AsyncDispatcherInfo not found.

After the fix, the parent UDT has a typed __THREADSTATICINDEX, the storage UDT contains t_current at offset +0x008, and the helper UDT is present:

0:000> dt AsyncDispatcherPdb!S_P_CoreLib_System_Runtime_CompilerServices_AsyncDispatcherInfo
   +0x000 Next             : Ptr64 S_P_CoreLib_System_Runtime_CompilerServices_AsyncDispatcherInfo
   +0x008 NextContinuation : Ptr64 S_P_CoreLib_System_Runtime_CompilerServices_Continuation
   +0x010 CurrentTask      : Ptr64 S_P_CoreLib_System_Threading_Tasks_Task
   +0x018 AsyncProfilerInfo : S_P_CoreLib_System_Runtime_CompilerServices_AsyncProfiler_Info
   =00007ff6`a5957ed8 __THREADSTATICINDEX : __ThreadStaticHelper<__type__THREADSTATICSS_P_CoreLib_System_Runtime_CompilerServices_AsyncDispatcherInfo>

0:000> dt AsyncDispatcherPdb!__type__THREADSTATICSS_P_CoreLib_System_Runtime_CompilerServices_AsyncDispatcherInfo
   +0x000 __VFN_table      : Ptr64
   +0x000 m_pEEType        : Ptr64 S_P_CoreLib_Internal_Runtime_MethodTable
   +0x008 t_current        : Ptr64 S_P_CoreLib_System_Runtime_CompilerServices_AsyncDispatcherInfo

0:000> dt AsyncDispatcherPdb!__ThreadStaticHelper*AsyncDispatcherInfo*
          AsyncDispatcherPdb!__ThreadStaticHelper<__type__THREADSTATICSS_P_CoreLib_System_Runtime_CompilerServices_AsyncDispatcherInfo>

Testing

ILCompiler.Compiler.Tests: Passed 22, Failed 0, Skipped 0

Note

This PR description was generated with GitHub Copilot.

Treat thread-static, GC-static, and non-GC-static fields as mutually exclusive storage categories when emitting debug types.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 32bd0ba5-1c4a-4c99-bdd4-969279117e18
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

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

@max-charlamb
max-charlamb marked this pull request as ready for review September 2, 2026 17:00
@max-charlamb
max-charlamb requested review from VSadov and jkotas and a lite review from Copilot September 2, 2026 17:00
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The change is narrowly scoped, matches the existing static storage categorization logic in the same method, and resolves the confirmed conditional misclassification without introducing new API surface.

Pull request overview

Adjusts NativeAOT debug type emission for static fields so thread-static fields aren’t incorrectly filtered out when HasGCStaticBase also reports true, improving PDB/UDA fidelity for types with thread statics.

Changes:

  • Makes the thread-static vs GC-static storage checks mutually exclusive by converting the GC-static check into an else if.
  • Ensures thread-static fields are preserved even when a separate ordinary GC-static base isn’t generated for the owning type.
File summaries
File Description
src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/UserDefinedTypeDescriptor.cs Fixes the static-field filtering logic to avoid dropping thread-static fields during debug type emission.
Review details
  • Files reviewed: 1/1 changed files
  • Comments generated: 0
  • Review effort level: Lite

@max-charlamb
max-charlamb enabled auto-merge (squash) September 2, 2026 20:11
@max-charlamb

Copy link
Copy Markdown
Member Author

/ba-g Helix Monitor failure

@max-charlamb
max-charlamb merged commit 1389900 into dotnet:main Sep 3, 2026
108 of 111 checks passed
@max-charlamb

Copy link
Copy Markdown
Member Author

/backport to release/11.0

@max-charlamb
max-charlamb deleted the max-charlamb/nativeaot-thread-static-debug-info branch September 3, 2026 02:14
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Started backporting to release/11.0 (link to workflow run)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants