From f796fd9fafebaf144682efd641e1075c5cb9fae5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Strehovsk=C3=BD?= Date: Fri, 1 May 2026 18:13:32 +0900 Subject: [PATCH 1/3] Fix NativeAOT GC roots after universal transition When a GC stack walk starts from a hijacked universal-transition frame, the iterator unwinds through the thunk and then yields the managed caller at the post-call IP. That caller is not actually the active frame yet, so reporting scratch registers from its post-call GC info can expose stale thunk state. In the failing System.Linq.Tests NativeAOT case, the precise GC root came from REGDISPLAY.pRax while CoffNativeCodeManager::EnumGcRefs was called with isActiveStackFrame=true. RAX contained the resolved interface dispatch target, System.Linq.Enumerable.Iterator.System.Collections.IEnumerator.get_Current, so object validation treated a code pointer as a GC object and fail-fast asserted. Clear ActiveStackFrame after unwinding the non-EH universal-transition thunk sequence so the yielded managed caller still reports its non-scratch roots and the conservative thunk range, but does not report scratch registers until the thunk has completed. Validation: before the fix, the parallel System.Linq.Tests NativeAOT stress loop completed 69 runs with 63 successes and 6 fail-fast crashes; sampled dumps all showed the same pRax code-pointer root. After rebuilding with this fix, the same loop ran for 612.3 seconds at parallelism 4 and completed 132 runs with 132 successes, 0 crashes, and 0 test failures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/nativeaot/Runtime/StackFrameIterator.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/coreclr/nativeaot/Runtime/StackFrameIterator.cpp b/src/coreclr/nativeaot/Runtime/StackFrameIterator.cpp index 928e16439e5bad..f90c2dd32dddd1 100644 --- a/src/coreclr/nativeaot/Runtime/StackFrameIterator.cpp +++ b/src/coreclr/nativeaot/Runtime/StackFrameIterator.cpp @@ -2072,6 +2072,10 @@ void StackFrameIterator::UnwindNonEHThunkSequence() // The iterator has reached the next managed frame. Publish the computed lower bound value. ASSERT(m_pConservativeStackRangeLowerBound == NULL); m_pConservativeStackRangeLowerBound = pLowestLowerBound; + + // The active frame was the thunk we just unwound through, not the managed caller. Do not + // report scratch registers from the caller's post-call GC state until the thunk has completed. + m_dwFlags &= ~ActiveStackFrame; } // This function is called immediately before a given frame is yielded from the iterator From 16d235dfde7ac8391130eef03e164ae66ea1d2c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Strehovsk=C3=BD?= Date: Fri, 1 May 2026 03:53:15 -0700 Subject: [PATCH 2/3] Update Program.cs --- src/coreclr/tools/aot/ILCompiler/Program.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler/Program.cs b/src/coreclr/tools/aot/ILCompiler/Program.cs index 2ba003f6a177c5..469a09b95224a7 100644 --- a/src/coreclr/tools/aot/ILCompiler/Program.cs +++ b/src/coreclr/tools/aot/ILCompiler/Program.cs @@ -257,8 +257,7 @@ public int Run() } } - if (Get(_command.EnableDebugInfo)) - compilationRoots.Add(new ManagedDataDescriptorProvider()); + compilationRoots.Add(new ManagedDataDescriptorProvider()); string win32resourcesModule = Get(_command.Win32ResourceModuleName); if (typeSystemContext.Target.IsWindows && !string.IsNullOrEmpty(win32resourcesModule)) From 59ee045a7c54fe89518e6ea05f17179c81321cfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Strehovsk=C3=BD?= Date: Fri, 1 May 2026 14:16:56 -0700 Subject: [PATCH 3/3] Conditional addition of ManagedDataDescriptorProvider --- src/coreclr/tools/aot/ILCompiler/Program.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/coreclr/tools/aot/ILCompiler/Program.cs b/src/coreclr/tools/aot/ILCompiler/Program.cs index 469a09b95224a7..2ba003f6a177c5 100644 --- a/src/coreclr/tools/aot/ILCompiler/Program.cs +++ b/src/coreclr/tools/aot/ILCompiler/Program.cs @@ -257,7 +257,8 @@ public int Run() } } - compilationRoots.Add(new ManagedDataDescriptorProvider()); + if (Get(_command.EnableDebugInfo)) + compilationRoots.Add(new ManagedDataDescriptorProvider()); string win32resourcesModule = Get(_command.Win32ResourceModuleName); if (typeSystemContext.Target.IsWindows && !string.IsNullOrEmpty(win32resourcesModule))