From 3edd422f9cc8e3a107ca9ab1654e9ec44fad49d4 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Fri, 11 Sep 2026 11:53:14 -0500 Subject: [PATCH] [wasm] Fix caller identification across R2R prestub transitions Exclude completed non-FCall prestub activations from function-only stack walks while retaining their GC roots and physical unwind state. Replace the StackTrace-specific duplicate filter and extend caller, recursion, and GC regression coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/debugdebugger.cpp | 13 ----- src/coreclr/vm/frames.h | 26 +++++++++ src/coreclr/vm/prestub.cpp | 9 +++ src/coreclr/vm/stackwalk.cpp | 7 +++ .../regressions/github60486.cs | 57 +++++++++++++++++++ 5 files changed, 99 insertions(+), 13 deletions(-) diff --git a/src/coreclr/vm/debugdebugger.cpp b/src/coreclr/vm/debugdebugger.cpp index dc2f24e55a4a3d..2f73a86a64b5e4 100644 --- a/src/coreclr/vm/debugdebugger.cpp +++ b/src/coreclr/vm/debugdebugger.cpp @@ -196,19 +196,6 @@ static StackWalkAction GetStackFramesCallback(CrawlFrame* pCf, VOID* data) MethodDesc* pFunc = pCf->GetFunction(); DebugStackTrace::GetStackFramesData* pData = (DebugStackTrace::GetStackFramesData*)data; -#ifdef TARGET_WASM - // The portable-entrypoint slow path keeps its prestub frame active while it invokes a newly - // discovered R2R body. The body is already reported as a frameless method. - if (!pCf->IsFrameless() && - pCf->GetFrame()->GetFrameIdentifier() == FrameIdentifier::PrestubMethodFrame && - pData->cElements > 0 && - pData->pElements[pData->cElements - 1].pFunc == pFunc && - pData->pElements[pData->cElements - 1].ip != (PCODE)NULL) - { - return SWA_CONTINUE; - } -#endif // TARGET_WASM - if (pFunc != nullptr && pFunc == g_pEnvironmentCallEntryPointMethodDesc) { return SWA_CONTINUE; diff --git a/src/coreclr/vm/frames.h b/src/coreclr/vm/frames.h index c06706e722ab85..e4c7f5463aeb84 100644 --- a/src/coreclr/vm/frames.h +++ b/src/coreclr/vm/frames.h @@ -300,6 +300,7 @@ class Frame FRAME_ATTR_EXCEPTION = 1, // This frame caused an exception FRAME_ATTR_FAULTED = 4, // Exception caused by Win32 fault FRAME_ATTR_RESUMABLE = 8, // We may resume from this frame + FRAME_ATTR_NO_MANAGED_ACTIVATION = 16, // Retained for rooting and unwinding, not an additional managed call }; unsigned GetFrameAttribs_Impl() { @@ -1397,9 +1398,34 @@ typedef DPTR(class PrestubMethodFrame) PTR_PrestubMethodFrame; class PrestubMethodFrame : public FramedMethodFrame { +#ifdef TARGET_WASM + bool m_isPrestubComplete = false; +#endif // TARGET_WASM + public: PrestubMethodFrame(TransitionBlock * pTransitionBlock, MethodDesc * pMD); +#ifdef TARGET_WASM + void MarkPrestubComplete() + { + CONTRACTL + { + NOTHROW; + GC_NOTRIGGER; + MODE_COOPERATIVE; + } + CONTRACTL_END; + + m_isPrestubComplete = true; + } + + unsigned GetFrameAttribs_Impl() + { + LIMITED_METHOD_DAC_CONTRACT; + return m_isPrestubComplete ? FRAME_ATTR_NO_MANAGED_ACTIVATION : FRAME_ATTR_NONE; + } +#endif // TARGET_WASM + void GcScanRoots_Impl(promote_func *fn, ScanContext* sc) { WRAPPER_NO_CONTRACT; diff --git a/src/coreclr/vm/prestub.cpp b/src/coreclr/vm/prestub.cpp index 88e96a747c21a4..93565560097f8e 100644 --- a/src/coreclr/vm/prestub.cpp +++ b/src/coreclr/vm/prestub.cpp @@ -2207,6 +2207,15 @@ void ExecuteInterpretedMethodWithArgs_PortableEntryPoint_Complex(PCODE portableE if (targetIp == NULL) { _ASSERTE(!PortableEntryPoint::PrefersInterpreterEntryPoint(portableEntrypoint)); +#ifdef TARGET_WASM + // Keep the transition and argument roots while invoking R2R code, but report + // the managed activation through its R2R body rather than this prestub. + // An FCall can still need the prestub to represent its native implementation. + if (!pMethod->IsFCall()) + { + pPFrame->MarkPrestubComplete(); + } +#endif // TARGET_WASM Object* continuationRet = nullptr; Object** pContinuationRet = nullptr; #ifdef TARGET_WASM diff --git a/src/coreclr/vm/stackwalk.cpp b/src/coreclr/vm/stackwalk.cpp index 9c5a224175e1e7..33e545a260db49 100644 --- a/src/coreclr/vm/stackwalk.cpp +++ b/src/coreclr/vm/stackwalk.cpp @@ -2039,6 +2039,13 @@ StackWalkAction StackFrameIterator::Filter(void) case SFITER_SKIPPED_FRAME_FUNCTION: if (!fSkippingFunclet) { +#ifdef TARGET_WASM + if ((m_flags & FUNCTIONSONLY) && + (m_crawl.pFrame->GetFrameAttribs() & Frame::FRAME_ATTR_NO_MANAGED_ACTIVATION)) + { + break; + } +#endif // TARGET_WASM if (m_flags & GC_FUNCLET_REFERENCE_REPORTING) { // If we are enumerating frames for GC reporting and we determined that diff --git a/src/tests/Loader/classloader/DefaultInterfaceMethods/regressions/github60486.cs b/src/tests/Loader/classloader/DefaultInterfaceMethods/regressions/github60486.cs index a62a25fde5aade..4f27f5a2882df3 100644 --- a/src/tests/Loader/classloader/DefaultInterfaceMethods/regressions/github60486.cs +++ b/src/tests/Loader/classloader/DefaultInterfaceMethods/regressions/github60486.cs @@ -1,4 +1,5 @@ using System; +using System.Reflection; using System.Runtime.CompilerServices; using Xunit; using TestLibrary; @@ -117,10 +118,66 @@ public class Program : ProgramBase, TestItf2 [Fact] public static void TestEntryPoint() { + ValidateCurrentMethod(); new Program().Start(); ValidateExceptionStackTrace(); } + private static void ValidateCurrentMethod() + { + for (int i = 0; i < 2; i++) + { + Assert.Equal(nameof(ValidateCurrentMethod), MethodBase.GetCurrentMethod().Name); + Assert.Equal(nameof(GetCurrentMethodInlineable), GetCurrentMethodInlineable().Name); + MethodBase genericMethod = GetCurrentMethodGeneric(); + Assert.Equal(nameof(GetCurrentMethodGeneric), genericMethod.Name); + Assert.True(genericMethod.IsGenericMethodDefinition); + Assert.Equal(typeof(Program).Assembly, Assembly.GetExecutingAssembly()); + Assert.Equal(typeof(Program).Assembly, Assembly.GetCallingAssembly()); + ValidateRecursiveCurrentMethod(3); + InputData byref = new InputData { i = 2 }; + ValidateTransitionRoots(new InputData { i = 1 }, ref byref, + (new InputData { i = 3 }, new InputData { i = 4 })); + } + } + + private static MethodBase GetCurrentMethodInlineable() => MethodBase.GetCurrentMethod(); + + private static MethodBase GetCurrentMethodGeneric() => MethodBase.GetCurrentMethod(); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ValidateTransitionRoots(InputData value, ref InputData byref, (InputData, InputData) pair) + { + GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, blocking: true, compacting: true); + Assert.Equal(1, value.i); + Assert.Equal(2, byref.i); + Assert.Equal(3, pair.Item1.i); + Assert.Equal(4, pair.Item2.i); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ValidateRecursiveCurrentMethod(int depth) + { + Assert.Equal(nameof(ValidateRecursiveCurrentMethod), MethodBase.GetCurrentMethod().Name); + if (depth > 0) + { + ValidateRecursiveCurrentMethod(depth - 1); + } + else + { + int frameCount = 0; + foreach (System.Diagnostics.StackFrame frame in new System.Diagnostics.StackTrace().GetFrames()) + { + if (frame.GetMethod().Name == nameof(ValidateRecursiveCurrentMethod)) + { + frameCount++; + } + } + + Assert.Equal(4, frameCount); + } + } + [MethodImpl(MethodImplOptions.NoInlining)] private static void ThrowForStackTrace() => throw new Exception();