diff --git a/docs/design/datacontracts/StackWalk.md b/docs/design/datacontracts/StackWalk.md index c77d5f296736f3..51a55df2b63165 100644 --- a/docs/design/datacontracts/StackWalk.md +++ b/docs/design/datacontracts/StackWalk.md @@ -57,6 +57,9 @@ This contract depends on the following descriptors: | `StubDispatchFrame` | `MethodDescPtr` | Pointer to Frame's method desc | | `StubDispatchFrame` | `RepresentativeMTPtr` | Pointer to Frame's method table pointer | | `StubDispatchFrame` | `RepresentativeSlot` | Frame's method table slot | +| `StubDispatchFrame` | `GCRefMap` | Cached pointer to GC reference map blob for caller stack promotion | +| `ExternalMethodFrame` | `GCRefMap` | Cached pointer to GC reference map blob for caller stack promotion | +| `DynamicHelperFrame` | `DynamicHelperFrameFlags` | Flags indicating which argument registers contain GC references | | `TransitionBlock` | `ReturnAddress` | Return address associated with the TransitionBlock | | `TransitionBlock` | `CalleeSavedRegisters` | Platform specific CalleeSavedRegisters struct associated with the TransitionBlock | | `TransitionBlock` (arm) | `ArgumentRegisters` | ARM specific `ArgumentRegisters` struct | @@ -87,6 +90,9 @@ Global variables used: | Global Name | Type | Purpose | | --- | --- | --- | | For each FrameType ``, `##Identifier` | `FrameIdentifier` enum value | Identifier used to determine concrete type of Frames | +| `TransitionBlockOffsetOfFirstGCRefMapSlot` | `uint32` | Byte offset within TransitionBlock where GCRefMap slot enumeration begins. ARM64: RetBuffArgReg offset; others: ArgumentRegisters offset. | +| `TransitionBlockOffsetOfArgumentRegisters` | `uint32` | Byte offset of the ArgumentRegisters within the TransitionBlock | +| `TransitionBlockOffsetOfArgs` | `uint32` | Byte offset of stack arguments (first arg after registers) = `sizeof(TransitionBlock)` | Constants used: | Source | Name | Value | Purpose | diff --git a/eng/Subsets.props b/eng/Subsets.props index 21db6bbdcb8609..f4ea63b49be08f 100644 --- a/eng/Subsets.props +++ b/eng/Subsets.props @@ -254,6 +254,7 @@ + @@ -528,6 +529,10 @@ + + + + diff --git a/src/coreclr/inc/clrconfigvalues.h b/src/coreclr/inc/clrconfigvalues.h index e5e025d82a18a8..e6419460df6167 100644 --- a/src/coreclr/inc/clrconfigvalues.h +++ b/src/coreclr/inc/clrconfigvalues.h @@ -286,7 +286,7 @@ RETAIL_CONFIG_DWORD_INFO(INTERNAL_JitEnableNoWayAssert, W("JitEnableNoWayAssert" RETAIL_CONFIG_DWORD_INFO(UNSUPPORTED_JitFramed, W("JitFramed"), 0, "Forces EBP frames") CONFIG_DWORD_INFO(INTERNAL_JitThrowOnAssertionFailure, W("JitThrowOnAssertionFailure"), 0, "Throw managed exception on assertion failures during JIT instead of failfast") -CONFIG_DWORD_INFO(INTERNAL_JitGCStress, W("JitGCStress"), 0, "GC stress mode for jit") +CONFIG_DWORD_INFO(INTERNAL_JitGCStress, W("JitGCStress"), 0, "cDAC stress mode for jit") CONFIG_DWORD_INFO(INTERNAL_JitHeartbeat, W("JitHeartbeat"), 0, "") RETAIL_CONFIG_DWORD_INFO(UNSUPPORTED_JITMinOpts, W("JITMinOpts"), 0, "Forces MinOpts") @@ -747,8 +747,10 @@ CONFIG_STRING_INFO(INTERNAL_PerfTypesToLog, W("PerfTypesToLog"), "Log facility L CONFIG_STRING_INFO(INTERNAL_PrestubGC, W("PrestubGC"), "") CONFIG_STRING_INFO(INTERNAL_PrestubHalt, W("PrestubHalt"), "") RETAIL_CONFIG_STRING_INFO(EXTERNAL_RestrictedGCStressExe, W("RestrictedGCStressExe"), "") -RETAIL_CONFIG_DWORD_INFO(INTERNAL_GCStressCdacFailFast, W("GCStressCdacFailFast"), 0, "If nonzero, assert on cDAC/runtime GC ref mismatch during GC stress (GCSTRESS_CDAC mode).") -RETAIL_CONFIG_STRING_INFO(INTERNAL_GCStressCdacLogFile, W("GCStressCdacLogFile"), "Log file path for cDAC GC stress verification results.") +RETAIL_CONFIG_DWORD_INFO(INTERNAL_CdacStressFailFast, W("CdacStressFailFast"), 0, "If nonzero, assert on cDAC/runtime GC ref mismatch during cDAC stress (GCSTRESS_CDAC mode).") +RETAIL_CONFIG_STRING_INFO(INTERNAL_CdacStressLogFile, W("CdacStressLogFile"), "Log file path for cDAC cDAC stress verification results.") +RETAIL_CONFIG_DWORD_INFO(INTERNAL_CdacStressStep, W("CdacStressStep"), 1, "Verify every Nth cDAC stress point (1=every point, 100=every 100th). Reduces overhead while maintaining code path diversity.") +RETAIL_CONFIG_DWORD_INFO(INTERNAL_CdacStress, W("CdacStress"), 0, "Enable cDAC stack reference verification. Bit flags: 0x1=alloc points, 0x2=GC trigger points, 0x4=unique stacks only, 0x8=instruction points.") CONFIG_DWORD_INFO(INTERNAL_ReturnSourceTypeForTesting, W("ReturnSourceTypeForTesting"), 0, "Allows returning the (internal only) source type of an IL to Native mapping for debugging purposes") RETAIL_CONFIG_DWORD_INFO(UNSUPPORTED_RSStressLog, W("RSStressLog"), 0, "Allows turning on logging for RS startup") CONFIG_DWORD_INFO(INTERNAL_SBDumpOnNewIndex, W("SBDumpOnNewIndex"), 0, "Used for Syncblock debugging. It's been a while since any of those have been used.") diff --git a/src/coreclr/vm/CMakeLists.txt b/src/coreclr/vm/CMakeLists.txt index b765e7018f0453..24f26110acd238 100644 --- a/src/coreclr/vm/CMakeLists.txt +++ b/src/coreclr/vm/CMakeLists.txt @@ -329,7 +329,7 @@ set(VM_SOURCES_WKS finalizerthread.cpp floatdouble.cpp floatsingle.cpp - cdacgcstress.cpp + cdacstress.cpp frozenobjectheap.cpp gccover.cpp gcenv.ee.cpp diff --git a/src/coreclr/vm/cdacgcstress.cpp b/src/coreclr/vm/cdacgcstress.cpp deleted file mode 100644 index c1eaf37aacfb02..00000000000000 --- a/src/coreclr/vm/cdacgcstress.cpp +++ /dev/null @@ -1,825 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -// -// cdacgcstress.cpp -// -// Implements in-process cDAC loading and stack reference verification -// for GC stress testing. When GCSTRESS_CDAC (0x20) is enabled, at each -// instruction-level GC stress point we: -// 1. Ask the cDAC to enumerate stack GC references via ISOSDacInterface::GetStackReferences -// 2. Ask the runtime to enumerate stack GC references via StackWalkFrames + GcInfoDecoder -// 3. Compare the two sets and report any mismatches -// - -#include "common.h" - -#ifdef HAVE_GCCOVER - -#include "cdacgcstress.h" -#include "../../native/managed/cdac/inc/cdac_reader.h" -#include "../../debug/datadescriptor-shared/inc/contract-descriptor.h" -#include -#include -#include "threads.h" -#include "eeconfig.h" -#include "gccover.h" -#include "sstring.h" - -#define CDAC_LIB_NAME MAKEDLLNAME_W(W("mscordaccore_universal")) - -// Represents a single GC stack reference for comparison purposes. -struct StackRef -{ - CLRDATA_ADDRESS Address; // Location on stack holding the ref - CLRDATA_ADDRESS Object; // The object pointer value - unsigned int Flags; // SOSRefFlags (interior, pinned) - CLRDATA_ADDRESS Source; // IP or Frame that owns this ref - int SourceType; // SOS_StackSourceIP or SOS_StackSourceFrame - int Register; // Register number (cDAC only) - int Offset; // Register offset (cDAC only) - CLRDATA_ADDRESS StackPointer; // Stack pointer at this ref (cDAC only) -}; - -// Fixed-size buffer for collecting refs during stack walk. -// No heap allocation inside the promote callback — we're under NOTHROW contracts. -static const int MAX_COLLECTED_REFS = 4096; - -// Static state — cDAC -static HMODULE s_cdacModule = NULL; -static intptr_t s_cdacHandle = 0; -static IUnknown* s_cdacSosInterface = nullptr; -static IXCLRDataProcess* s_cdacProcess = nullptr; // Cached QI result for Flush() -static ISOSDacInterface* s_cdacSosDac = nullptr; // Cached QI result for GetStackReferences() - -// Static state — common -static bool s_initialized = false; -static bool s_failFast = true; -static FILE* s_logFile = nullptr; -static CrstStatic s_cdacLock; // Serializes cDAC access from concurrent GC stress threads - -// Verification counters (reported at shutdown) -static volatile LONG s_verifyCount = 0; -static volatile LONG s_verifyPass = 0; -static volatile LONG s_verifyFail = 0; -static volatile LONG s_verifySkip = 0; - -// Thread-local storage for the current thread context at the stress point. -static thread_local PCONTEXT s_currentContext = nullptr; -static thread_local DWORD s_currentThreadId = 0; - -// Extern declaration for the contract descriptor symbol exported from coreclr. -extern "C" struct ContractDescriptor DotNetRuntimeContractDescriptor; - -//----------------------------------------------------------------------------- -// In-process callbacks for the cDAC reader. -// These allow the cDAC to read memory from the current process. -//----------------------------------------------------------------------------- - -// Helper for ReadFromTargetCallback — AVInRuntimeImplOkayHolder cannot be -// directly inside PAL_TRY scope (see controller.cpp:109). -static void ReadFromTargetHelper(void* src, uint8_t* dest, uint32_t count) -{ - AVInRuntimeImplOkayHolder AVOkay; - memcpy(dest, src, count); -} - -static int ReadFromTargetCallback(uint64_t addr, uint8_t* dest, uint32_t count, void* context) -{ - void* src = reinterpret_cast(static_cast(addr)); - struct Param { void* src; uint8_t* dest; uint32_t count; } param; - param.src = src; param.dest = dest; param.count = count; - PAL_TRY(Param *, pParam, ¶m) - { - ReadFromTargetHelper(pParam->src, pParam->dest, pParam->count); - } - PAL_EXCEPT(EXCEPTION_EXECUTE_HANDLER) - { - return E_FAIL; - } - PAL_ENDTRY - return S_OK; -} - -static int WriteToTargetCallback(uint64_t addr, const uint8_t* buff, uint32_t count, void* context) -{ - return E_NOTIMPL; -} - -static int ReadThreadContextCallback(uint32_t threadId, uint32_t contextFlags, uint32_t contextBufferSize, uint8_t* contextBuffer, void* context) -{ - // Return the thread context that was stored by VerifyAtStressPoint. - if (s_currentContext != nullptr && s_currentThreadId == threadId) - { - DWORD copySize = min(contextBufferSize, (uint32_t)sizeof(CONTEXT)); - memcpy(contextBuffer, s_currentContext, copySize); - return S_OK; - } - - LOG((LF_GCROOTS, LL_WARNING, "CDAC GC Stress: ReadThreadContext mismatch: requested=%u stored=%u\n", - threadId, s_currentThreadId)); - return E_FAIL; -} - -//----------------------------------------------------------------------------- -// Initialization / Shutdown -//----------------------------------------------------------------------------- - -bool CdacGcStress::IsEnabled() -{ - return (g_pConfig->GetGCStressLevel() & EEConfig::GCSTRESS_CDAC) != 0; -} - -bool CdacGcStress::IsInitialized() -{ - return s_initialized; -} - -bool CdacGcStress::Initialize() -{ - if (!IsEnabled()) - return false; - - // Load mscordaccore_universal from next to coreclr - PathString path; - if (WszGetModuleFileName(reinterpret_cast(GetCurrentModuleBase()), path) == 0) - { - LOG((LF_GCROOTS, LL_WARNING, "CDAC GC Stress: Failed to get module file name\n")); - return false; - } - - SString::Iterator iter = path.End(); - if (!path.FindBack(iter, DIRECTORY_SEPARATOR_CHAR_W)) - { - LOG((LF_GCROOTS, LL_WARNING, "CDAC GC Stress: Failed to find directory separator\n")); - return false; - } - - iter++; - path.Truncate(iter); - path.Append(CDAC_LIB_NAME); - - s_cdacModule = CLRLoadLibrary(path.GetUnicode()); - if (s_cdacModule == NULL) - { - LOG((LF_GCROOTS, LL_WARNING, "CDAC GC Stress: Failed to load %S\n", path.GetUnicode())); - return false; - } - - // Resolve cdac_reader_init - auto init = reinterpret_cast(::GetProcAddress(s_cdacModule, "cdac_reader_init")); - if (init == nullptr) - { - LOG((LF_GCROOTS, LL_WARNING, "CDAC GC Stress: Failed to resolve cdac_reader_init\n")); - ::FreeLibrary(s_cdacModule); - s_cdacModule = NULL; - return false; - } - - // Get the address of the contract descriptor in our own process - uint64_t descriptorAddr = reinterpret_cast(&DotNetRuntimeContractDescriptor); - - // Initialize the cDAC reader with in-process callbacks - if (init(descriptorAddr, &ReadFromTargetCallback, &WriteToTargetCallback, &ReadThreadContextCallback, nullptr, &s_cdacHandle) != 0) - { - LOG((LF_GCROOTS, LL_WARNING, "CDAC GC Stress: cdac_reader_init failed\n")); - ::FreeLibrary(s_cdacModule); - s_cdacModule = NULL; - return false; - } - - // Create the SOS interface - auto createSos = reinterpret_cast( - ::GetProcAddress(s_cdacModule, "cdac_reader_create_sos_interface")); - if (createSos == nullptr) - { - LOG((LF_GCROOTS, LL_WARNING, "CDAC GC Stress: Failed to resolve cdac_reader_create_sos_interface\n")); - auto freeFn = reinterpret_cast(::GetProcAddress(s_cdacModule, "cdac_reader_free")); - if (freeFn != nullptr) - freeFn(s_cdacHandle); - ::FreeLibrary(s_cdacModule); - s_cdacModule = NULL; - s_cdacHandle = 0; - return false; - } - - if (createSos(s_cdacHandle, nullptr, &s_cdacSosInterface) != 0) - { - LOG((LF_GCROOTS, LL_WARNING, "CDAC GC Stress: cdac_reader_create_sos_interface failed\n")); - auto freeFn = reinterpret_cast(::GetProcAddress(s_cdacModule, "cdac_reader_free")); - if (freeFn != nullptr) - freeFn(s_cdacHandle); - ::FreeLibrary(s_cdacModule); - s_cdacModule = NULL; - s_cdacHandle = 0; - return false; - } - - // Read configuration for fail-fast behavior - s_failFast = CLRConfig::GetConfigValue(CLRConfig::INTERNAL_GCStressCdacFailFast) != 0; - - // Cache QI results so we don't QI on every stress point - { - HRESULT hr = s_cdacSosInterface->QueryInterface(__uuidof(IXCLRDataProcess), reinterpret_cast(&s_cdacProcess)); - if (FAILED(hr) || s_cdacProcess == nullptr) - { - LOG((LF_GCROOTS, LL_WARNING, "CDAC GC Stress: Failed to QI for IXCLRDataProcess (hr=0x%08x)\n", hr)); - } - - hr = s_cdacSosInterface->QueryInterface(__uuidof(ISOSDacInterface), reinterpret_cast(&s_cdacSosDac)); - if (FAILED(hr) || s_cdacSosDac == nullptr) - { - LOG((LF_GCROOTS, LL_WARNING, "CDAC GC Stress: Failed to QI for ISOSDacInterface (hr=0x%08x) - cannot verify\n", hr)); - if (s_cdacProcess != nullptr) - { - s_cdacProcess->Release(); - s_cdacProcess = nullptr; - } - auto freeFn = reinterpret_cast(::GetProcAddress(s_cdacModule, "cdac_reader_free")); - if (freeFn != nullptr) - freeFn(s_cdacHandle); - ::FreeLibrary(s_cdacModule); - s_cdacModule = NULL; - s_cdacHandle = 0; - return false; - } - } - - // Open log file if configured - CLRConfigStringHolder logFilePath(CLRConfig::GetConfigValue(CLRConfig::INTERNAL_GCStressCdacLogFile)); - if (logFilePath != nullptr) - { - SString sLogPath(logFilePath); - fopen_s(&s_logFile, sLogPath.GetUTF8(), "w"); - if (s_logFile != nullptr) - { - fprintf(s_logFile, "=== cDAC GC Stress Verification Log ===\n"); - fprintf(s_logFile, "FailFast: %s\n\n", s_failFast ? "true" : "false"); - } - } - - s_cdacLock.Init(CrstGCCover, CRST_DEFAULT); - s_initialized = true; - LOG((LF_GCROOTS, LL_INFO10, "CDAC GC Stress: Initialized successfully (failFast=%d, logFile=%s)\n", - s_failFast, s_logFile != nullptr ? "yes" : "no")); - return true; -} - -void CdacGcStress::Shutdown() -{ - if (!s_initialized) - return; - - // Print summary to stderr so results are always visible - fprintf(stderr, "CDAC GC Stress: %ld verifications (%ld pass / %ld fail, %ld skipped)\n", - (long)s_verifyCount, (long)s_verifyPass, (long)s_verifyFail, (long)s_verifySkip); - STRESS_LOG3(LF_GCROOTS, LL_ALWAYS, - "CDAC GC Stress shutdown: %d verifications (%d pass / %d fail)\n", - (int)s_verifyCount, (int)s_verifyPass, (int)s_verifyFail); - - if (s_logFile != nullptr) - { - fprintf(s_logFile, "\n=== Summary ===\n"); - fprintf(s_logFile, "Total verifications: %ld\n", (long)s_verifyCount); - fprintf(s_logFile, " Passed: %ld\n", (long)s_verifyPass); - fprintf(s_logFile, " Failed: %ld\n", (long)s_verifyFail); - fprintf(s_logFile, " Skipped: %ld\n", (long)s_verifySkip); - fclose(s_logFile); - s_logFile = nullptr; - } - - if (s_cdacSosDac != nullptr) - { - s_cdacSosDac->Release(); - s_cdacSosDac = nullptr; - } - - if (s_cdacProcess != nullptr) - { - s_cdacProcess->Release(); - s_cdacProcess = nullptr; - } - - if (s_cdacSosInterface != nullptr) - { - s_cdacSosInterface->Release(); - s_cdacSosInterface = nullptr; - } - - if (s_cdacHandle != 0) - { - auto freeFn = reinterpret_cast(::GetProcAddress(s_cdacModule, "cdac_reader_free")); - if (freeFn != nullptr) - freeFn(s_cdacHandle); - s_cdacHandle = 0; - } - - if (s_cdacModule != NULL) - { - ::FreeLibrary(s_cdacModule); - s_cdacModule = NULL; - } - - s_initialized = false; - LOG((LF_GCROOTS, LL_INFO10, "CDAC GC Stress: Shutdown complete\n")); -} - -//----------------------------------------------------------------------------- -// Collect stack refs from the cDAC -//----------------------------------------------------------------------------- - -static bool CollectCdacStackRefs(Thread* pThread, PCONTEXT regs, SArray* pRefs) -{ - _ASSERTE(s_cdacSosDac != nullptr); - - ISOSStackRefEnum* pEnum = nullptr; - HRESULT hr = s_cdacSosDac->GetStackReferences(pThread->GetOSThreadId(), &pEnum); - - if (FAILED(hr) || pEnum == nullptr) - { - LOG((LF_GCROOTS, LL_WARNING, "CDAC GC Stress: GetStackReferences failed (hr=0x%08x)\n", hr)); - return false; - } - - // Enumerate all refs - SOSStackRefData refData; - unsigned int fetched = 0; - while (true) - { - hr = pEnum->Next(1, &refData, &fetched); - if (FAILED(hr) || fetched == 0) - break; - - StackRef ref; - ref.Address = refData.Address; - ref.Object = refData.Object; - ref.Flags = refData.Flags; - ref.Source = refData.Source; - ref.SourceType = refData.SourceType; - ref.Register = refData.Register; - ref.Offset = refData.Offset; - ref.StackPointer = refData.StackPointer; - pRefs->Append(ref); - } - - // Release twice: once for the normal ref, and once for the extra ref-count - // leaked by SOSDacImpl.GetStackReferences for COM compat (see ConvertToUnmanaged call). - pEnum->Release(); - pEnum->Release(); - return true; -} - -//----------------------------------------------------------------------------- -// Collect stack refs from the runtime's own GC scanning -//----------------------------------------------------------------------------- - -struct RuntimeRefCollectionContext -{ - StackRef refs[MAX_COLLECTED_REFS]; - int count; - bool overflow; -}; - -static void CollectRuntimeRefsPromoteFunc(PTR_PTR_Object ppObj, ScanContext* sc, uint32_t flags) -{ - RuntimeRefCollectionContext* ctx = reinterpret_cast(sc->_unused1); - if (ctx == nullptr) - return; - if (ctx->count >= MAX_COLLECTED_REFS) - { - ctx->overflow = true; - return; - } - - StackRef& ref = ctx->refs[ctx->count++]; - - // Always report the real ppObj address. For register-based refs, ppObj points - // into the REGDISPLAY/CONTEXT on the native stack — we can't reliably distinguish - // these from managed stack slots on the runtime side. The comparison logic handles - // this by matching register refs (cDAC Address=0) by (Object, Flags) only. - ref.Address = reinterpret_cast(ppObj); - ref.Object = reinterpret_cast(*ppObj); - - ref.Flags = 0; - if (flags & GC_CALL_INTERIOR) - ref.Flags |= SOSRefInterior; - if (flags & GC_CALL_PINNED) - ref.Flags |= SOSRefPinned; - - ref.Source = 0; - ref.SourceType = 0; - ref.Register = 0; - ref.Offset = 0; - ref.StackPointer = 0; -} - -static bool CollectRuntimeStackRefs(Thread* pThread, PCONTEXT regs, StackRef* outRefs, int* outCount) -{ - RuntimeRefCollectionContext collectCtx; - collectCtx.count = 0; - collectCtx.overflow = false; - - GCCONTEXT gcctx = {}; - - // Set up ScanContext the same way ScanStackRoots does — the stack_limit and - // thread_under_crawl fields are required for PromoteCarefully/IsAddressInStack. - ScanContext sc; - sc.promotion = TRUE; - sc.thread_under_crawl = pThread; - sc._unused1 = &collectCtx; - - Frame* pTopFrame = pThread->GetFrame(); - Object** topStack = (Object**)pTopFrame; - if (InlinedCallFrame::FrameHasActiveCall(pTopFrame)) - { - InlinedCallFrame* pInlinedFrame = dac_cast(pTopFrame); - topStack = (Object**)pInlinedFrame->GetCallSiteSP(); - } - sc.stack_limit = (uintptr_t)topStack; - - gcctx.f = CollectRuntimeRefsPromoteFunc; - gcctx.sc = ≻ - gcctx.cf = NULL; - - // Set FORBIDGC_LOADER_USE_ENABLED so MethodDesc::GetName uses NOTHROW - // instead of THROWS inside EECodeManager::EnumGcRefs. - GCForbidLoaderUseHolder forbidLoaderUse; - - unsigned flagsStackWalk = ALLOW_ASYNC_STACK_WALK | ALLOW_INVALID_OBJECTS; - flagsStackWalk |= GC_FUNCLET_REFERENCE_REPORTING; - - pThread->StackWalkFrames(GcStackCrawlCallBack, &gcctx, flagsStackWalk); - - // NOTE: ScanStackRoots also scans the separate GCFrame linked list - // (Thread::GetGCFrame), but the DAC's GetStackReferences / DacStackReferenceWalker - // does NOT include those. We intentionally omit GCFrame scanning here so our - // runtime-side collection matches what the cDAC is expected to produce. - - // Copy results out - *outCount = collectCtx.count; - memcpy(outRefs, collectCtx.refs, collectCtx.count * sizeof(StackRef)); - return !collectCtx.overflow; -} - -//----------------------------------------------------------------------------- -// Filter cDAC refs to match runtime PromoteCarefully behavior. -// The runtime's PromoteCarefully (siginfo.cpp) skips interior pointers whose -// object value is a stack address. The cDAC reports all GcInfo slots without -// this filter, so we apply it here before comparing against runtime refs. -//----------------------------------------------------------------------------- - -static int FilterInteriorStackRefs(StackRef* refs, int count, Thread* pThread, uintptr_t stackLimit) -{ - int writeIdx = 0; - for (int i = 0; i < count; i++) - { - bool isInterior = (refs[i].Flags & SOSRefInterior) != 0; - if (isInterior && - pThread->IsAddressInStack((void*)(size_t)refs[i].Object) && - (size_t)refs[i].Object >= stackLimit) - { - continue; - } - refs[writeIdx++] = refs[i]; - } - return writeIdx; -} - -//----------------------------------------------------------------------------- -// Deduplicate cDAC refs that have the same (Address, Object, Flags). -// The cDAC may walk the same managed frame at two different offsets due to -// Frames restoring context (e.g. InlinedCallFrame). The same stack slots -// get reported from both offsets. The runtime only walks each frame once, -// so we deduplicate to match. -//----------------------------------------------------------------------------- - -static int CompareStackRefKey(const void* a, const void* b) -{ - const StackRef* refA = static_cast(a); - const StackRef* refB = static_cast(b); - if (refA->Address != refB->Address) - return (refA->Address < refB->Address) ? -1 : 1; - if (refA->Object != refB->Object) - return (refA->Object < refB->Object) ? -1 : 1; - if (refA->Flags != refB->Flags) - return (refA->Flags < refB->Flags) ? -1 : 1; - return 0; -} - -static int DeduplicateRefs(StackRef* refs, int count) -{ - if (count <= 1) - return count; - qsort(refs, count, sizeof(StackRef), CompareStackRefKey); - int writeIdx = 1; - for (int i = 1; i < count; i++) - { - // Only dedup stack-based refs (Address != 0). - // Register refs (Address == 0) are legitimately different entries - // even when Address/Object/Flags match (different registers). - if (refs[i].Address != 0 && - refs[i].Address == refs[i-1].Address && - refs[i].Object == refs[i-1].Object && - refs[i].Flags == refs[i-1].Flags) - { - continue; - } - refs[writeIdx++] = refs[i]; - } - return writeIdx; -} - -//----------------------------------------------------------------------------- -// Report mismatch -//----------------------------------------------------------------------------- - -static void ReportMismatch(const char* message, Thread* pThread, PCONTEXT regs) -{ - LOG((LF_GCROOTS, LL_ERROR, "CDAC GC Stress: %s (Thread=0x%x, IP=0x%p)\n", - message, pThread->GetOSThreadId(), (void*)GetIP(regs))); - - if (s_failFast) - { - _ASSERTE_MSG(false, message); - } -} - -//----------------------------------------------------------------------------- -// Main entry point: verify at a GC stress point -//----------------------------------------------------------------------------- - -void CdacGcStress::VerifyAtStressPoint(Thread* pThread, PCONTEXT regs) -{ - _ASSERTE(s_initialized); - _ASSERTE(pThread != nullptr); - _ASSERTE(regs != nullptr); - - InterlockedIncrement(&s_verifyCount); - - // Serialize cDAC access — the cDAC's ProcessedData cache and COM interfaces - // are not thread-safe, and GC stress can fire on multiple threads. - CrstHolder cdacLock(&s_cdacLock); - - // Set the thread context for the cDAC's ReadThreadContext callback. - s_currentContext = regs; - s_currentThreadId = pThread->GetOSThreadId(); - - // Flush the cDAC's ProcessedData cache so it re-reads from the live process. - if (s_cdacProcess != nullptr) - { - s_cdacProcess->Flush(); - } - - // Collect from cDAC - SArray cdacRefs; - bool haveCdac = CollectCdacStackRefs(pThread, regs, &cdacRefs); - - // Clear the stored context - s_currentContext = nullptr; - s_currentThreadId = 0; - - // Collect runtime refs (doesn't use cDAC, no timing issue) - StackRef runtimeRefsBuf[MAX_COLLECTED_REFS]; - int runtimeCount = 0; - bool runtimeComplete = CollectRuntimeStackRefs(pThread, regs, runtimeRefsBuf, &runtimeCount); - - if (!haveCdac) - { - InterlockedIncrement(&s_verifySkip); - if (s_logFile != nullptr) - fprintf(s_logFile, "[SKIP] Thread=0x%x IP=0x%p - cDAC GetStackReferences failed\n", - pThread->GetOSThreadId(), (void*)GetIP(regs)); - return; - } - - if (!runtimeComplete) - { - InterlockedIncrement(&s_verifySkip); - if (s_logFile != nullptr) - fprintf(s_logFile, "[SKIP] Thread=0x%x IP=0x%p - runtime ref buffer overflow (>%d refs)\n", - pThread->GetOSThreadId(), (void*)GetIP(regs), MAX_COLLECTED_REFS); - return; - } - - // Filter cDAC refs to match runtime PromoteCarefully behavior: - // remove interior pointers whose Object value is a stack address. - // These are register slots (RSP/RBP) that GcInfo marks as live interior - // but don't point to managed heap objects. - Frame* pTopFrame = pThread->GetFrame(); - Object** topStack = (Object**)pTopFrame; - if (InlinedCallFrame::FrameHasActiveCall(pTopFrame)) - { - InlinedCallFrame* pInlinedFrame = dac_cast(pTopFrame); - topStack = (Object**)pInlinedFrame->GetCallSiteSP(); - } - uintptr_t stackLimit = (uintptr_t)topStack; - - int cdacCount = (int)cdacRefs.GetCount(); - if (cdacCount > 0) - { - StackRef* cdacBuf = cdacRefs.OpenRawBuffer(); - cdacCount = FilterInteriorStackRefs(cdacBuf, cdacCount, pThread, stackLimit); - cdacCount = DeduplicateRefs(cdacBuf, cdacCount); - cdacRefs.CloseRawBuffer(); - // Trim the SArray to the filtered count - while ((int)cdacRefs.GetCount() > cdacCount) - cdacRefs.Delete(cdacRefs.End() - 1); - } - - // Sort and deduplicate runtime refs to match cDAC ordering for element-wise comparison. - runtimeCount = DeduplicateRefs(runtimeRefsBuf, runtimeCount); - - // Compare cDAC vs runtime. - // If the stress IP is in a RangeList section (dynamic method / IL Stub), - // the cDAC can't decode GcInfo for it (known gap matching DAC behavior). - // Skip comparison for these — the runtime reports refs from the Frame chain - // that neither DAC nor cDAC can reproduce via GetStackReferences. - PCODE stressIP = GetIP(regs); - bool isDynamicMethod = false; - { - RangeSection* pRS = ExecutionManager::FindCodeRange(stressIP, ExecutionManager::ScanReaderLock); - if (pRS != nullptr) - { - isDynamicMethod = (pRS->_flags & RangeSection::RANGE_SECTION_RANGELIST) != 0; - // Also check if this is a dynamic method by checking the MethodDesc - if (!isDynamicMethod) - { - EECodeInfo ci(stressIP); - if (ci.IsValid() && ci.GetMethodDesc() != nullptr && - (ci.GetMethodDesc()->IsLCGMethod() || ci.GetMethodDesc()->IsILStub())) - isDynamicMethod = true; - } - } - } - - bool pass = (cdacCount == runtimeCount); - if (pass && cdacCount > 0) - { - // Counts match — verify that the same GC refs are reported by both sides. - // - // The cDAC reports register-based refs with Address=0 (the value lives in a - // register, not a stack slot). The runtime always reports the real ppObj address, - // which for register refs points into the REGDISPLAY/CONTEXT on the native stack. - // We can't reliably normalize the runtime side, so we use a two-phase matching: - // Phase 1: Match stack refs (cDAC Address != 0) by exact (Address, Object, Flags) - // Phase 2: Match register refs (cDAC Address == 0) by (Object, Flags) only - StackRef* cdacBuf = cdacRefs.OpenRawBuffer(); - bool matched_rt[MAX_COLLECTED_REFS] = {}; - - // Phase 1: Match cDAC stack refs (Address != 0) to RT refs by exact (Address, Object, Flags) - for (int i = 0; i < cdacCount && pass; i++) - { - if (cdacBuf[i].Address == 0) - continue; // register ref — handled in phase 2 - - bool found = false; - for (int j = 0; j < cdacCount; j++) - { - if (matched_rt[j]) - continue; - if (cdacBuf[i].Address == runtimeRefsBuf[j].Address && - cdacBuf[i].Object == runtimeRefsBuf[j].Object && - cdacBuf[i].Flags == runtimeRefsBuf[j].Flags) - { - matched_rt[j] = true; - found = true; - break; - } - } - if (!found) - pass = false; - } - - // Phase 2: Match cDAC register refs (Address == 0) to remaining RT refs by (Object, Flags) - for (int i = 0; i < cdacCount && pass; i++) - { - if (cdacBuf[i].Address != 0) - continue; // stack ref — already matched in phase 1 - - bool found = false; - for (int j = 0; j < cdacCount; j++) - { - if (matched_rt[j]) - continue; - if (cdacBuf[i].Object == runtimeRefsBuf[j].Object && - cdacBuf[i].Flags == runtimeRefsBuf[j].Flags) - { - matched_rt[j] = true; - found = true; - break; - } - } - if (!found) - pass = false; - } - - cdacRefs.CloseRawBuffer(); - } - if (!pass && isDynamicMethod) - { - // Known gap: dynamic method refs not in cDAC. Treat as pass but log. - pass = true; - } - - if (pass) - InterlockedIncrement(&s_verifyPass); - else - InterlockedIncrement(&s_verifyFail); - - if (s_logFile != nullptr) - { - fprintf(s_logFile, "[%s] Thread=0x%x IP=0x%p cDAC=%d RT=%d\n", - pass ? "PASS" : "FAIL", pThread->GetOSThreadId(), (void*)GetIP(regs), cdacCount, runtimeCount); - - if (!pass) - { - // Log the stress point IP and the first cDAC Source for debugging - fprintf(s_logFile, " stressIP=0x%p firstCdacSource=0x%llx\n", - (void*)stressIP, - cdacCount > 0 ? (unsigned long long)cdacRefs[0].Source : 0ULL); - - // Check if any cDAC ref has the stress IP as its Source - bool leafFound = false; - for (int i = 0; i < cdacCount; i++) - { - if ((PCODE)cdacRefs[i].Source == stressIP) - { - leafFound = true; - break; - } - } - if (!leafFound && cdacCount < runtimeCount) - { - fprintf(s_logFile, " DIAG: Leaf frame at stressIP NOT in cDAC sources (cDAC < RT)\n"); - - // Check if the stress IP is in a managed method - bool isManaged = ExecutionManager::IsManagedCode(stressIP); - fprintf(s_logFile, " DIAG: IsManaged(stressIP)=%d\n", isManaged); - - if (isManaged) - { - // Get the method's code range to see if cDAC walks ANY offset in this method - EECodeInfo codeInfo(stressIP); - if (codeInfo.IsValid()) - { - PCODE methodStart = codeInfo.GetStartAddress(); - MethodDesc* pMD = codeInfo.GetMethodDesc(); - fprintf(s_logFile, " DIAG: Method start=0x%p relOffset=0x%x %s::%s\n", - (void*)methodStart, codeInfo.GetRelOffset(), - pMD ? pMD->m_pszDebugClassName : "?", - pMD ? pMD->m_pszDebugMethodName : "?"); - - // Check if the cDAC can resolve this IP to a MethodDesc - if (s_cdacSosDac != nullptr) - { - CLRDATA_ADDRESS cdacMD = 0; - HRESULT hrMD = s_cdacSosDac->GetMethodDescPtrFromIP((CLRDATA_ADDRESS)stressIP, &cdacMD); - fprintf(s_logFile, " DIAG: cDAC GetMethodDescPtrFromIP hr=0x%x MD=0x%llx\n", - hrMD, (unsigned long long)cdacMD); - } - - // Check if cDAC has ANY ref from this method (Source near methodStart) - bool methodFound = false; - for (int i = 0; i < cdacCount; i++) - { - PCODE src = (PCODE)cdacRefs[i].Source; - if (src >= methodStart && src < methodStart + 0x10000) // rough range - { - methodFound = true; - fprintf(s_logFile, " DIAG: cDAC has ref from same method at Source=0x%llx (offset=0x%llx)\n", - (unsigned long long)src, (unsigned long long)(src - methodStart)); - break; - } - } - if (!methodFound) - fprintf(s_logFile, " DIAG: cDAC has NO refs from this method at all\n"); - } - } - - // Check what the first RT ref looks like - if (runtimeCount > 0) - fprintf(s_logFile, " DIAG: RT[0]: Address=0x%llx Object=0x%llx Flags=0x%x\n", - (unsigned long long)runtimeRefsBuf[0].Address, - (unsigned long long)runtimeRefsBuf[0].Object, - runtimeRefsBuf[0].Flags); - } - - for (int i = 0; i < cdacCount; i++) - fprintf(s_logFile, " cDAC [%d]: Address=0x%llx Object=0x%llx Flags=0x%x Source=0x%llx SourceType=%d Reg=%d Offset=%d SP=0x%llx\n", - i, (unsigned long long)cdacRefs[i].Address, (unsigned long long)cdacRefs[i].Object, - cdacRefs[i].Flags, (unsigned long long)cdacRefs[i].Source, cdacRefs[i].SourceType, - cdacRefs[i].Register, cdacRefs[i].Offset, (unsigned long long)cdacRefs[i].StackPointer); - for (int i = 0; i < runtimeCount; i++) - fprintf(s_logFile, " RT [%d]: Address=0x%llx Object=0x%llx Flags=0x%x\n", - i, (unsigned long long)runtimeRefsBuf[i].Address, (unsigned long long)runtimeRefsBuf[i].Object, runtimeRefsBuf[i].Flags); - fflush(s_logFile); - } - } - - if (!pass) - { - ReportMismatch("cDAC stack reference verification failed - mismatch between cDAC and runtime GC refs", pThread, regs); - } -} - -#endif // HAVE_GCCOVER diff --git a/src/coreclr/vm/cdacgcstress.h b/src/coreclr/vm/cdacgcstress.h deleted file mode 100644 index 5b421becbec050..00000000000000 --- a/src/coreclr/vm/cdacgcstress.h +++ /dev/null @@ -1,46 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -// -// cdacgcstress.h -// -// Infrastructure for verifying cDAC stack reference reporting against the -// runtime's own GC root enumeration at GC stress instruction-level trigger points. -// -// Enabled via GCSTRESS_CDAC (0x20) flag in DOTNET_GCStress. -// - -#ifndef _CDAC_GC_STRESS_H_ -#define _CDAC_GC_STRESS_H_ - -#ifdef HAVE_GCCOVER - -// Forward declarations -class Thread; - -class CdacGcStress -{ -public: - // Initialize the cDAC in-process for GC stress verification. - // Must be called after the contract descriptor is built and GC is initialized. - // Returns true if initialization succeeded. - static bool Initialize(); - - // Shutdown and release cDAC resources. - static void Shutdown(); - - // Returns true if cDAC GC stress verification is initialized and ready. - static bool IsInitialized(); - - // Returns true if GCSTRESS_CDAC flag is set in the GCStress level. - static bool IsEnabled(); - - // Main entry point: verify cDAC stack refs match runtime stack refs at a GC stress point. - // Called from DoGcStress before StressHeap(). - // pThread - the thread being stress-tested - // regs - the register context at the stress point - static void VerifyAtStressPoint(Thread* pThread, PCONTEXT regs); -}; - -#endif // HAVE_GCCOVER -#endif // _CDAC_GC_STRESS_H_ diff --git a/src/coreclr/vm/cdacstress.cpp b/src/coreclr/vm/cdacstress.cpp new file mode 100644 index 00000000000000..3c35e9006f37f6 --- /dev/null +++ b/src/coreclr/vm/cdacstress.cpp @@ -0,0 +1,1192 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// +// CdacStress.cpp +// +// Implements in-process cDAC loading and stack reference verification. +// Enabled via DOTNET_CdacStress (bit flags) or legacy DOTNET_GCStress=0x20. +// At each enabled stress point we: +// 1. Ask the cDAC to enumerate stack GC references via ISOSDacInterface::GetStackReferences +// 2. Ask the runtime to enumerate stack GC references via StackWalkFrames + GcInfoDecoder +// 3. Compare the two sets and report any mismatches +// + +#include "common.h" + +#ifdef HAVE_GCCOVER + +#include "CdacStress.h" +#include "../../native/managed/cdac/inc/cdac_reader.h" +#include "../../debug/datadescriptor-shared/inc/contract-descriptor.h" +#include +#include +#include "threads.h" +#include "eeconfig.h" +#include "gccover.h" +#include "sstring.h" +#include "exinfo.h" + +// Forward-declare the 3-param GcEnumObject used as a GCEnumCallback. +// Defined in gcenv.ee.common.cpp; not exposed in any header. +extern void GcEnumObject(LPVOID pData, OBJECTREF *pObj, uint32_t flags); + +#define CDAC_LIB_NAME MAKEDLLNAME_W(W("mscordaccore_universal")) + +// Represents a single GC stack reference for comparison purposes. +struct StackRef +{ + CLRDATA_ADDRESS Address; // Location on stack holding the ref + CLRDATA_ADDRESS Object; // The object pointer value + unsigned int Flags; // SOSRefFlags (interior, pinned) + CLRDATA_ADDRESS Source; // IP or Frame that owns this ref + int SourceType; // SOS_StackSourceIP or SOS_StackSourceFrame + int Register; // Register number (cDAC only) + int Offset; // Register offset (cDAC only) + CLRDATA_ADDRESS StackPointer; // Stack pointer at this ref (cDAC only) +}; + +// Fixed-size buffer for collecting refs during stack walk. +// No heap allocation inside the promote callback — we're under NOTHROW contracts. +static const int MAX_COLLECTED_REFS = 4096; + +// Static state — cDAC +static HMODULE s_cdacModule = NULL; +static intptr_t s_cdacHandle = 0; +static IUnknown* s_cdacSosInterface = nullptr; +static IXCLRDataProcess* s_cdacProcess = nullptr; // Cached QI result for Flush() +static ISOSDacInterface* s_cdacSosDac = nullptr; // Cached QI result for GetStackReferences() + +// Static state — legacy DAC (for three-way comparison) +static HMODULE s_dacModule = NULL; +static ISOSDacInterface* s_dacSosDac = nullptr; +static IXCLRDataProcess* s_dacProcess = nullptr; + +// Static state — common +static bool s_initialized = false; +static bool s_failFast = true; +static DWORD s_step = 1; // Verify every Nth stress point (1=every point) +static DWORD s_cdacStressLevel = 0; // Resolved CdacStressFlags +static FILE* s_logFile = nullptr; +static CrstStatic s_cdacLock; // Serializes cDAC access from concurrent GC stress threads + +// Unique-stack filtering: hash set of previously seen stack traces. +// Protected by s_cdacLock (already held during VerifyAtStressPoint). +static const int UNIQUE_STACK_DEPTH = 8; // Number of return addresses to hash +static SHash>>* s_seenStacks = nullptr; + +// Thread-local reentrancy guard — prevents infinite recursion when +// allocations inside VerifyAtStressPoint trigger VerifyAtAllocPoint. +thread_local bool t_inVerification = false; + +// Verification counters (reported at shutdown) +static volatile LONG s_verifyCount = 0; +static volatile LONG s_verifyPass = 0; +static volatile LONG s_verifyFail = 0; +static volatile LONG s_verifySkip = 0; + +// Thread-local storage for the current thread context at the stress point. +static thread_local PCONTEXT s_currentContext = nullptr; +static thread_local DWORD s_currentThreadId = 0; + +// Extern declaration for the contract descriptor symbol exported from coreclr. +extern "C" struct ContractDescriptor DotNetRuntimeContractDescriptor; + +//----------------------------------------------------------------------------- +// In-process callbacks for the cDAC reader. +// These allow the cDAC to read memory from the current process. +//----------------------------------------------------------------------------- + +// Helper for ReadFromTargetCallback — AVInRuntimeImplOkayHolder cannot be +// directly inside PAL_TRY scope (see controller.cpp:109). +static void ReadFromTargetHelper(void* src, uint8_t* dest, uint32_t count) +{ + AVInRuntimeImplOkayHolder AVOkay; + memcpy(dest, src, count); +} + +static int ReadFromTargetCallback(uint64_t addr, uint8_t* dest, uint32_t count, void* context) +{ + void* src = reinterpret_cast(static_cast(addr)); + struct Param { void* src; uint8_t* dest; uint32_t count; } param; + param.src = src; param.dest = dest; param.count = count; + PAL_TRY(Param *, pParam, ¶m) + { + ReadFromTargetHelper(pParam->src, pParam->dest, pParam->count); + } + PAL_EXCEPT(EXCEPTION_EXECUTE_HANDLER) + { + return E_FAIL; + } + PAL_ENDTRY + return S_OK; +} + +static int WriteToTargetCallback(uint64_t addr, const uint8_t* buff, uint32_t count, void* context) +{ + return E_NOTIMPL; +} + +static int ReadThreadContextCallback(uint32_t threadId, uint32_t contextFlags, uint32_t contextBufferSize, uint8_t* contextBuffer, void* context) +{ + // Return the thread context that was stored by VerifyAtStressPoint. + if (s_currentContext != nullptr && s_currentThreadId == threadId) + { + DWORD copySize = min(contextBufferSize, (uint32_t)sizeof(CONTEXT)); + memcpy(contextBuffer, s_currentContext, copySize); + return S_OK; + } + + LOG((LF_GCROOTS, LL_WARNING, "CDAC GC Stress: ReadThreadContext mismatch: requested=%u stored=%u\n", + threadId, s_currentThreadId)); + return E_FAIL; +} + +//----------------------------------------------------------------------------- +// Minimal ICLRDataTarget implementation for loading the legacy DAC in-process. +// Routes ReadVirtual/GetThreadContext to the same callbacks as the cDAC. +//----------------------------------------------------------------------------- +class InProcessDataTarget : public ICLRDataTarget +{ + volatile LONG m_refCount; +public: + InProcessDataTarget() : m_refCount(1) {} + + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppObj) override + { + if (riid == IID_IUnknown || riid == __uuidof(ICLRDataTarget)) + { + *ppObj = static_cast(this); + AddRef(); + return S_OK; + } + *ppObj = nullptr; + return E_NOINTERFACE; + } + ULONG STDMETHODCALLTYPE AddRef() override { return InterlockedIncrement(&m_refCount); } + ULONG STDMETHODCALLTYPE Release() override + { + ULONG c = InterlockedDecrement(&m_refCount); + if (c == 0) delete this; + return c; + } + + HRESULT STDMETHODCALLTYPE GetMachineType(ULONG32* machineType) override + { +#ifdef TARGET_AMD64 + *machineType = IMAGE_FILE_MACHINE_AMD64; +#elif defined(TARGET_ARM64) + *machineType = IMAGE_FILE_MACHINE_ARM64; +#elif defined(TARGET_X86) + *machineType = IMAGE_FILE_MACHINE_I386; +#else + return E_NOTIMPL; +#endif + return S_OK; + } + + HRESULT STDMETHODCALLTYPE GetPointerSize(ULONG32* pointerSize) override + { + *pointerSize = sizeof(void*); + return S_OK; + } + + HRESULT STDMETHODCALLTYPE GetImageBase(LPCWSTR imagePath, CLRDATA_ADDRESS* baseAddress) override + { + HMODULE hMod = ::GetModuleHandleW(imagePath); + if (hMod == NULL) return E_FAIL; + *baseAddress = (CLRDATA_ADDRESS)hMod; + return S_OK; + } + + HRESULT STDMETHODCALLTYPE ReadVirtual(CLRDATA_ADDRESS address, BYTE* buffer, ULONG32 bytesRequested, ULONG32* bytesRead) override + { + int hr = ReadFromTargetCallback((uint64_t)address, buffer, bytesRequested, nullptr); + if (hr == S_OK && bytesRead != nullptr) + *bytesRead = bytesRequested; + return hr; + } + + HRESULT STDMETHODCALLTYPE WriteVirtual(CLRDATA_ADDRESS, BYTE*, ULONG32, ULONG32*) override { return E_NOTIMPL; } + + HRESULT STDMETHODCALLTYPE GetTLSValue(ULONG32 threadId, ULONG32 index, CLRDATA_ADDRESS* value) override { return E_NOTIMPL; } + HRESULT STDMETHODCALLTYPE SetTLSValue(ULONG32 threadId, ULONG32 index, CLRDATA_ADDRESS value) override { return E_NOTIMPL; } + HRESULT STDMETHODCALLTYPE GetCurrentThreadID(ULONG32* threadId) override + { + *threadId = ::GetCurrentThreadId(); + return S_OK; + } + + HRESULT STDMETHODCALLTYPE GetThreadContext(ULONG32 threadId, ULONG32 contextFlags, ULONG32 contextSize, BYTE* contextBuffer) override + { + return ReadThreadContextCallback(threadId, contextFlags, contextSize, contextBuffer, nullptr); + } + + HRESULT STDMETHODCALLTYPE SetThreadContext(ULONG32, ULONG32, BYTE*) override { return E_NOTIMPL; } + HRESULT STDMETHODCALLTYPE Request(ULONG32, ULONG32, BYTE*, ULONG32, BYTE*) override { return E_NOTIMPL; } +}; + +//----------------------------------------------------------------------------- +// Initialization / Shutdown +//----------------------------------------------------------------------------- + +bool CdacStress::IsEnabled() +{ + // Check DOTNET_CdacStress first (new config) + DWORD cdacStress = CLRConfig::GetConfigValue(CLRConfig::INTERNAL_CdacStress); + if (cdacStress != 0) + return true; + + // Fall back to legacy DOTNET_GCStress=0x20 + return (g_pConfig->GetGCStressLevel() & EEConfig::GCSTRESS_CDAC) != 0; +} + +bool CdacStress::IsInitialized() +{ + return s_initialized; +} + +DWORD GetCdacStressLevel() +{ + return s_cdacStressLevel; +} + +bool CdacStress::IsUniqueEnabled() +{ + return (s_cdacStressLevel & CDACSTRESS_UNIQUE) != 0; +} + +bool CdacStress::Initialize() +{ + if (!IsEnabled()) + return false; + + // Resolve the stress level from DOTNET_CdacStress or legacy GCSTRESS_CDAC + DWORD cdacStress = CLRConfig::GetConfigValue(CLRConfig::INTERNAL_CdacStress); + if (cdacStress != 0) + { + s_cdacStressLevel = cdacStress; + } + else + { + // Legacy: GCSTRESS_CDAC maps to allocation-point verification + s_cdacStressLevel = CDACSTRESS_ALLOC; + } + + // Load mscordaccore_universal from next to coreclr + PathString path; + if (WszGetModuleFileName(reinterpret_cast(GetCurrentModuleBase()), path) == 0) + { + LOG((LF_GCROOTS, LL_WARNING, "CDAC GC Stress: Failed to get module file name\n")); + return false; + } + + SString::Iterator iter = path.End(); + if (!path.FindBack(iter, DIRECTORY_SEPARATOR_CHAR_W)) + { + LOG((LF_GCROOTS, LL_WARNING, "CDAC GC Stress: Failed to find directory separator\n")); + return false; + } + + iter++; + path.Truncate(iter); + path.Append(CDAC_LIB_NAME); + + s_cdacModule = CLRLoadLibrary(path.GetUnicode()); + if (s_cdacModule == NULL) + { + LOG((LF_GCROOTS, LL_WARNING, "CDAC GC Stress: Failed to load %S\n", path.GetUnicode())); + return false; + } + + // Resolve cdac_reader_init + auto init = reinterpret_cast(::GetProcAddress(s_cdacModule, "cdac_reader_init")); + if (init == nullptr) + { + LOG((LF_GCROOTS, LL_WARNING, "CDAC GC Stress: Failed to resolve cdac_reader_init\n")); + ::FreeLibrary(s_cdacModule); + s_cdacModule = NULL; + return false; + } + + // Get the address of the contract descriptor in our own process + uint64_t descriptorAddr = reinterpret_cast(&DotNetRuntimeContractDescriptor); + + // Initialize the cDAC reader with in-process callbacks + if (init(descriptorAddr, &ReadFromTargetCallback, &WriteToTargetCallback, &ReadThreadContextCallback, nullptr, &s_cdacHandle) != 0) + { + LOG((LF_GCROOTS, LL_WARNING, "CDAC GC Stress: cdac_reader_init failed\n")); + ::FreeLibrary(s_cdacModule); + s_cdacModule = NULL; + return false; + } + + // Create the SOS interface + auto createSos = reinterpret_cast( + ::GetProcAddress(s_cdacModule, "cdac_reader_create_sos_interface")); + if (createSos == nullptr) + { + LOG((LF_GCROOTS, LL_WARNING, "CDAC GC Stress: Failed to resolve cdac_reader_create_sos_interface\n")); + auto freeFn = reinterpret_cast(::GetProcAddress(s_cdacModule, "cdac_reader_free")); + if (freeFn != nullptr) + freeFn(s_cdacHandle); + ::FreeLibrary(s_cdacModule); + s_cdacModule = NULL; + s_cdacHandle = 0; + return false; + } + + if (createSos(s_cdacHandle, nullptr, &s_cdacSosInterface) != 0) + { + LOG((LF_GCROOTS, LL_WARNING, "CDAC GC Stress: cdac_reader_create_sos_interface failed\n")); + auto freeFn = reinterpret_cast(::GetProcAddress(s_cdacModule, "cdac_reader_free")); + if (freeFn != nullptr) + freeFn(s_cdacHandle); + ::FreeLibrary(s_cdacModule); + s_cdacModule = NULL; + s_cdacHandle = 0; + return false; + } + + // Read configuration for fail-fast behavior + s_failFast = CLRConfig::GetConfigValue(CLRConfig::INTERNAL_CdacStressFailFast) != 0; + + // Read step interval for throttling verifications + s_step = CLRConfig::GetConfigValue(CLRConfig::INTERNAL_CdacStressStep); + if (s_step == 0) + s_step = 1; + + // Cache QI results so we don't QI on every stress point + { + HRESULT hr = s_cdacSosInterface->QueryInterface(__uuidof(IXCLRDataProcess), reinterpret_cast(&s_cdacProcess)); + if (FAILED(hr) || s_cdacProcess == nullptr) + { + LOG((LF_GCROOTS, LL_WARNING, "CDAC GC Stress: Failed to QI for IXCLRDataProcess (hr=0x%08x)\n", hr)); + } + + hr = s_cdacSosInterface->QueryInterface(__uuidof(ISOSDacInterface), reinterpret_cast(&s_cdacSosDac)); + if (FAILED(hr) || s_cdacSosDac == nullptr) + { + LOG((LF_GCROOTS, LL_WARNING, "CDAC GC Stress: Failed to QI for ISOSDacInterface (hr=0x%08x) - cannot verify\n", hr)); + if (s_cdacProcess != nullptr) + { + s_cdacProcess->Release(); + s_cdacProcess = nullptr; + } + auto freeFn = reinterpret_cast(::GetProcAddress(s_cdacModule, "cdac_reader_free")); + if (freeFn != nullptr) + freeFn(s_cdacHandle); + ::FreeLibrary(s_cdacModule); + s_cdacModule = NULL; + s_cdacHandle = 0; + return false; + } + } + + // Open log file if configured + CLRConfigStringHolder logFilePath(CLRConfig::GetConfigValue(CLRConfig::INTERNAL_CdacStressLogFile)); + if (logFilePath != nullptr) + { + SString sLogPath(logFilePath); + fopen_s(&s_logFile, sLogPath.GetUTF8(), "w"); + if (s_logFile != nullptr) + { + fprintf(s_logFile, "=== cDAC GC Stress Verification Log ===\n"); + fprintf(s_logFile, "FailFast: %s\n", s_failFast ? "true" : "false"); + fprintf(s_logFile, "Step: %u (verify every %u stress points)\n\n", s_step, s_step); + } + } + + s_cdacLock.Init(CrstGCCover, CRST_DEFAULT); + + if (IsUniqueEnabled()) + { + s_seenStacks = new SHash>>(); + } + + // Load the legacy DAC for three-way comparison (optional — non-fatal if it fails). + { + PathString dacPath; + if (WszGetModuleFileName(reinterpret_cast(GetCurrentModuleBase()), dacPath) != 0) + { + SString::Iterator dacIter = dacPath.End(); + if (dacPath.FindBack(dacIter, DIRECTORY_SEPARATOR_CHAR_W)) + { + dacIter++; + dacPath.Truncate(dacIter); + dacPath.Append(W("mscordaccore.dll")); + + s_dacModule = CLRLoadLibrary(dacPath.GetUnicode()); + if (s_dacModule != NULL) + { + typedef HRESULT (STDAPICALLTYPE *PFN_CLRDataCreateInstance)(REFIID, ICLRDataTarget*, void**); + auto pfnCreate = reinterpret_cast( + ::GetProcAddress(s_dacModule, "CLRDataCreateInstance")); + if (pfnCreate != nullptr) + { + InProcessDataTarget* pTarget = new (nothrow) InProcessDataTarget(); + if (pTarget != nullptr) + { + IUnknown* pDacUnk = nullptr; + HRESULT hr = pfnCreate(__uuidof(IUnknown), pTarget, (void**)&pDacUnk); + pTarget->Release(); + if (SUCCEEDED(hr) && pDacUnk != nullptr) + { + pDacUnk->QueryInterface(__uuidof(ISOSDacInterface), (void**)&s_dacSosDac); + pDacUnk->QueryInterface(__uuidof(IXCLRDataProcess), (void**)&s_dacProcess); + pDacUnk->Release(); + } + } + } + if (s_dacSosDac == nullptr) + { + LOG((LF_GCROOTS, LL_WARNING, "CDAC GC Stress: Legacy DAC loaded but QI for ISOSDacInterface failed\n")); + } + } + else + { + LOG((LF_GCROOTS, LL_INFO10, "CDAC GC Stress: Legacy DAC not found (three-way comparison disabled)\n")); + } + } + } + } + + s_initialized = true; + LOG((LF_GCROOTS, LL_INFO10, "CDAC GC Stress: Initialized successfully (failFast=%d, logFile=%s)\n", + s_failFast, s_logFile != nullptr ? "yes" : "no")); + return true; +} + +void CdacStress::Shutdown() +{ + if (!s_initialized) + return; + + // Print summary to stderr so results are always visible + LONG actualVerifications = s_verifyPass + s_verifyFail + s_verifySkip; + fprintf(stderr, "CDAC GC Stress: %ld stress points, %ld verifications (%ld pass / %ld fail, %ld skipped)\n", + (long)s_verifyCount, (long)actualVerifications, (long)s_verifyPass, (long)s_verifyFail, (long)s_verifySkip); + STRESS_LOG3(LF_GCROOTS, LL_ALWAYS, + "CDAC GC Stress shutdown: %d verifications (%d pass / %d fail)\n", + (int)actualVerifications, (int)s_verifyPass, (int)s_verifyFail); + + if (s_logFile != nullptr) + { + fprintf(s_logFile, "\n=== Summary ===\n"); + fprintf(s_logFile, "Total stress points: %ld\n", (long)s_verifyCount); + fprintf(s_logFile, "Total verifications: %ld\n", (long)actualVerifications); + fprintf(s_logFile, " Passed: %ld\n", (long)s_verifyPass); + fprintf(s_logFile, " Failed: %ld\n", (long)s_verifyFail); + fprintf(s_logFile, " Skipped: %ld\n", (long)s_verifySkip); + fclose(s_logFile); + s_logFile = nullptr; + } + + if (s_cdacSosDac != nullptr) + { + s_cdacSosDac->Release(); + s_cdacSosDac = nullptr; + } + + if (s_cdacProcess != nullptr) + { + s_cdacProcess->Release(); + s_cdacProcess = nullptr; + } + + if (s_cdacSosInterface != nullptr) + { + s_cdacSosInterface->Release(); + s_cdacSosInterface = nullptr; + } + + if (s_cdacHandle != 0) + { + auto freeFn = reinterpret_cast(::GetProcAddress(s_cdacModule, "cdac_reader_free")); + if (freeFn != nullptr) + freeFn(s_cdacHandle); + s_cdacHandle = 0; + } + + // Legacy DAC cleanup + if (s_dacSosDac != nullptr) { s_dacSosDac->Release(); s_dacSosDac = nullptr; } + if (s_dacProcess != nullptr) { s_dacProcess->Release(); s_dacProcess = nullptr; } + + if (s_seenStacks != nullptr) + { + delete s_seenStacks; + s_seenStacks = nullptr; + } + + s_initialized = false; + LOG((LF_GCROOTS, LL_INFO10, "CDAC GC Stress: Shutdown complete\n")); +} + +//----------------------------------------------------------------------------- +// Collect stack refs from the cDAC +//----------------------------------------------------------------------------- + +static bool CollectStackRefs(ISOSDacInterface* pSosDac, DWORD osThreadId, SArray* pRefs) +{ + if (pSosDac == nullptr) + return false; + + ISOSStackRefEnum* pEnum = nullptr; + HRESULT hr = pSosDac->GetStackReferences(osThreadId, &pEnum); + + if (FAILED(hr) || pEnum == nullptr) + return false; + + SOSStackRefData refData; + unsigned int fetched = 0; + while (true) + { + hr = pEnum->Next(1, &refData, &fetched); + if (FAILED(hr) || fetched == 0) + break; + + StackRef ref; + ref.Address = refData.Address; + ref.Object = refData.Object; + ref.Flags = refData.Flags; + ref.Source = refData.Source; + ref.SourceType = refData.SourceType; + ref.Register = refData.Register; + ref.Offset = refData.Offset; + ref.StackPointer = refData.StackPointer; + pRefs->Append(ref); + } + + // Release twice: once for the normal ref, and once for the extra ref-count + // leaked by SOSDacImpl.GetStackReferences for COM compat (see ConvertToUnmanaged call). + pEnum->Release(); + pEnum->Release(); + return true; +} + +//----------------------------------------------------------------------------- +// Collect stack refs from the runtime's own GC scanning +//----------------------------------------------------------------------------- + +struct RuntimeRefCollectionContext +{ + StackRef refs[MAX_COLLECTED_REFS]; + int count; + bool overflow; +}; + +static void CollectRuntimeRefsPromoteFunc(PTR_PTR_Object ppObj, ScanContext* sc, uint32_t flags) +{ + RuntimeRefCollectionContext* ctx = reinterpret_cast(sc->_unused1); + if (ctx == nullptr) + return; + if (ctx->count >= MAX_COLLECTED_REFS) + { + ctx->overflow = true; + return; + } + + StackRef& ref = ctx->refs[ctx->count++]; + + // Always report the real ppObj address. For register-based refs, ppObj points + // into the REGDISPLAY/CONTEXT on the native stack — we can't reliably distinguish + // these from managed stack slots on the runtime side. The comparison logic handles + // this by matching register refs (cDAC Address=0) by (Object, Flags) only. + ref.Address = reinterpret_cast(ppObj); + ref.Object = reinterpret_cast(*ppObj); + + ref.Flags = 0; + if (flags & GC_CALL_INTERIOR) + ref.Flags |= SOSRefInterior; + if (flags & GC_CALL_PINNED) + ref.Flags |= SOSRefPinned; + ref.Source = 0; + ref.SourceType = 0; +} + +static bool CollectRuntimeStackRefs(Thread* pThread, PCONTEXT regs, StackRef* outRefs, int* outCount) +{ + RuntimeRefCollectionContext collectCtx; + collectCtx.count = 0; + collectCtx.overflow = false; + + GCCONTEXT gcctx = {}; + + // Set up ScanContext the same way ScanStackRoots does — the stack_limit and + // thread_under_crawl fields are required for PromoteCarefully/IsAddressInStack. + ScanContext sc; + sc.promotion = TRUE; + sc.thread_under_crawl = pThread; + sc._unused1 = &collectCtx; + + Frame* pTopFrame = pThread->GetFrame(); + Object** topStack = (Object**)pTopFrame; + if (InlinedCallFrame::FrameHasActiveCall(pTopFrame)) + { + InlinedCallFrame* pInlinedFrame = dac_cast(pTopFrame); + topStack = (Object**)pInlinedFrame->GetCallSiteSP(); + } + sc.stack_limit = (uintptr_t)topStack; + + gcctx.f = CollectRuntimeRefsPromoteFunc; + gcctx.sc = ≻ + gcctx.cf = NULL; + + // Set FORBIDGC_LOADER_USE_ENABLED so MethodDesc::GetName uses NOTHROW + // instead of THROWS inside EECodeManager::EnumGcRefs. + GCForbidLoaderUseHolder forbidLoaderUse; + + unsigned flagsStackWalk = ALLOW_ASYNC_STACK_WALK | ALLOW_INVALID_OBJECTS; + flagsStackWalk |= GC_FUNCLET_REFERENCE_REPORTING; + + // Use a callback that matches DAC behavior (DacStackReferenceWalker::Callback): + // Only call EnumGcRefs for frameless frames and GcScanRoots for explicit frames. + // Deliberately skip the post-scan logic (LCG resolver promotion, + // GcReportLoaderAllocator, generic param context) that GcStackCrawlCallBack + // includes — the DAC's callback has that logic disabled (#if 0). + struct DiagContext { GCCONTEXT* gcctx; RuntimeRefCollectionContext* collectCtx; }; + DiagContext diagCtx = { &gcctx, &collectCtx }; + + auto dacLikeCallback = [](CrawlFrame* pCF, VOID* pData) -> StackWalkAction + { + DiagContext* dCtx = (DiagContext*)pData; + GCCONTEXT* gcctx = dCtx->gcctx; + + ResetPointerHolder rph(&gcctx->cf); + gcctx->cf = pCF; + + bool fReportGCReferences = pCF->ShouldCrawlframeReportGCReferences(); + + if (fReportGCReferences) + { + if (pCF->IsFrameless()) + { + ICodeManager* pCM = pCF->GetCodeManager(); + _ASSERTE(pCM != NULL); + unsigned flags = pCF->GetCodeManagerFlags(); + pCM->EnumGcRefs(pCF->GetRegisterSet(), + pCF->GetCodeInfo(), + flags, + GcEnumObject, + gcctx); + } + else + { + Frame* pFrame = pCF->GetFrame(); + pFrame->GcScanRoots(gcctx->f, gcctx->sc); + } + } + + return SWA_CONTINUE; + }; + + pThread->StackWalkFrames(dacLikeCallback, &diagCtx, flagsStackWalk); + + // NOTE: ScanStackRoots also scans the separate GCFrame linked list + // (Thread::GetGCFrame), but the DAC's GetStackReferences / DacStackReferenceWalker + // does NOT include those. We intentionally omit GCFrame scanning here so our + // runtime-side collection matches what the cDAC is expected to produce. + + // Copy results out + *outCount = collectCtx.count; + memcpy(outRefs, collectCtx.refs, collectCtx.count * sizeof(StackRef)); + return !collectCtx.overflow; +} + +//----------------------------------------------------------------------------- +// Filter cDAC refs to match runtime PromoteCarefully behavior. +// The runtime's PromoteCarefully (siginfo.cpp) skips interior pointers whose +// object value is a stack address. The cDAC reports all GcInfo slots without +// this filter, so we apply it here before comparing against runtime refs. +//----------------------------------------------------------------------------- + +static int FilterInteriorStackRefs(StackRef* refs, int count, Thread* pThread, uintptr_t stackLimit) +{ + int writeIdx = 0; + for (int i = 0; i < count; i++) + { + bool isInterior = (refs[i].Flags & SOSRefInterior) != 0; + if (isInterior && + pThread->IsAddressInStack((void*)(size_t)refs[i].Object) && + (size_t)refs[i].Object >= stackLimit) + { + continue; + } + refs[writeIdx++] = refs[i]; + } + return writeIdx; +} + +//----------------------------------------------------------------------------- +// Deduplicate cDAC refs that have the same (Address, Object, Flags). +// The cDAC may walk the same managed frame at two different offsets due to +// Frames restoring context (e.g. InlinedCallFrame). The same stack slots +// get reported from both offsets. The runtime only walks each frame once, +// so we deduplicate to match. +//----------------------------------------------------------------------------- + +static int CompareStackRefKey(const void* a, const void* b) +{ + const StackRef* refA = static_cast(a); + const StackRef* refB = static_cast(b); + if (refA->Address != refB->Address) + return (refA->Address < refB->Address) ? -1 : 1; + if (refA->Object != refB->Object) + return (refA->Object < refB->Object) ? -1 : 1; + if (refA->Flags != refB->Flags) + return (refA->Flags < refB->Flags) ? -1 : 1; + return 0; +} + +static int DeduplicateRefs(StackRef* refs, int count) +{ + if (count <= 1) + return count; + qsort(refs, count, sizeof(StackRef), CompareStackRefKey); + int writeIdx = 1; + for (int i = 1; i < count; i++) + { + // Only dedup stack-based refs (Address != 0). + // Register refs (Address == 0) are legitimately different entries + // even when Address/Object/Flags match (different registers). + if (refs[i].Address != 0 && + refs[i].Address == refs[i-1].Address && + refs[i].Object == refs[i-1].Object && + refs[i].Flags == refs[i-1].Flags) + { + continue; + } + refs[writeIdx++] = refs[i]; + } + return writeIdx; +} + +//----------------------------------------------------------------------------- +// Report mismatch +//----------------------------------------------------------------------------- + +static void ReportMismatch(const char* message, Thread* pThread, PCONTEXT regs) +{ + LOG((LF_GCROOTS, LL_ERROR, "CDAC GC Stress: %s (Thread=0x%x, IP=0x%p)\n", + message, pThread->GetOSThreadId(), (void*)GetIP(regs))); + + if (s_failFast) + { + _ASSERTE_MSG(false, message); + } +} + +//----------------------------------------------------------------------------- +// Compare IXCLRDataStackWalk frame-by-frame between cDAC and legacy DAC. +// Creates a stack walk on each, advances in lockstep, and compares +// GetContext + Request(FRAME_DATA) at each step. +//----------------------------------------------------------------------------- + +static void CompareStackWalks(Thread* pThread, PCONTEXT regs) +{ + if (s_cdacProcess == nullptr || s_dacProcess == nullptr) + return; + + DWORD osThreadId = pThread->GetOSThreadId(); + + // Get IXCLRDataTask for the thread from both processes + IXCLRDataTask* cdacTask = nullptr; + IXCLRDataTask* dacTask = nullptr; + + HRESULT hr1 = s_cdacProcess->GetTaskByOSThreadID(osThreadId, &cdacTask); + HRESULT hr2 = s_dacProcess->GetTaskByOSThreadID(osThreadId, &dacTask); + + if (FAILED(hr1) || cdacTask == nullptr || FAILED(hr2) || dacTask == nullptr) + { + if (cdacTask) cdacTask->Release(); + if (dacTask) dacTask->Release(); + return; + } + + // Create stack walks + IXCLRDataStackWalk* cdacWalk = nullptr; + IXCLRDataStackWalk* dacWalk = nullptr; + + hr1 = cdacTask->CreateStackWalk(0xF /* CLRDATA_SIMPFRAME_MANAGED_METHOD | ... */, &cdacWalk); + hr2 = dacTask->CreateStackWalk(0xF, &dacWalk); + + cdacTask->Release(); + dacTask->Release(); + + if (FAILED(hr1) || cdacWalk == nullptr || FAILED(hr2) || dacWalk == nullptr) + { + if (cdacWalk) cdacWalk->Release(); + if (dacWalk) dacWalk->Release(); + return; + } + + // Walk in lockstep comparing each frame + int frameIdx = 0; + bool mismatch = false; + while (frameIdx < 200) // safety limit + { + // Compare GetContext + BYTE cdacCtx[4096] = {}; + BYTE dacCtx[4096] = {}; + ULONG32 cdacCtxSize = 0, dacCtxSize = 0; + + hr1 = cdacWalk->GetContext(0, sizeof(cdacCtx), &cdacCtxSize, cdacCtx); + hr2 = dacWalk->GetContext(0, sizeof(dacCtx), &dacCtxSize, dacCtx); + + if (hr1 != hr2) + { + if (s_logFile) + fprintf(s_logFile, " [WALK_MISMATCH] Frame %d: GetContext hr mismatch cDAC=0x%x DAC=0x%x\n", + frameIdx, hr1, hr2); + mismatch = true; + break; + } + if (hr1 != S_OK) + break; // both finished + + if (cdacCtxSize != dacCtxSize) + { + if (s_logFile) + fprintf(s_logFile, " [WALK_MISMATCH] Frame %d: Context size differs cDAC=%u DAC=%u\n", + frameIdx, cdacCtxSize, dacCtxSize); + mismatch = true; + } + else if (cdacCtxSize >= sizeof(CONTEXT)) + { + // Compare IP and SP — these are what matter for stack walk parity. + // Other CONTEXT fields (floating-point, debug registers, xstate) may + // differ between cDAC and DAC without affecting the walk. + PCODE cdacIP = GetIP((CONTEXT*)cdacCtx); + PCODE dacIP = GetIP((CONTEXT*)dacCtx); + TADDR cdacSP = GetSP((CONTEXT*)cdacCtx); + TADDR dacSP = GetSP((CONTEXT*)dacCtx); + + if (cdacIP != dacIP || cdacSP != dacSP) + { + fprintf(s_logFile, " [WALK_MISMATCH] Frame %d: Context differs cDAC_IP=0x%llx cDAC_SP=0x%llx DAC_IP=0x%llx DAC_SP=0x%llx\n", + frameIdx, + (unsigned long long)cdacIP, (unsigned long long)cdacSP, + (unsigned long long)dacIP, (unsigned long long)dacSP); + mismatch = true; + } + } + + // Compare Request(FRAME_DATA) + ULONG64 cdacFrameAddr = 0, dacFrameAddr = 0; + hr1 = cdacWalk->Request(0xf0000000, 0, nullptr, sizeof(cdacFrameAddr), (BYTE*)&cdacFrameAddr); + hr2 = dacWalk->Request(0xf0000000, 0, nullptr, sizeof(dacFrameAddr), (BYTE*)&dacFrameAddr); + + if (hr1 == S_OK && hr2 == S_OK && cdacFrameAddr != dacFrameAddr) + { + if (s_logFile) + { + PCODE cdacIP = 0, dacIP = 0; + if (cdacCtxSize >= sizeof(CONTEXT)) + cdacIP = GetIP((CONTEXT*)cdacCtx); + if (dacCtxSize >= sizeof(CONTEXT)) + dacIP = GetIP((CONTEXT*)dacCtx); + fprintf(s_logFile, " [WALK_MISMATCH] Frame %d: FrameAddr cDAC=0x%llx DAC=0x%llx (cDAC_IP=0x%llx DAC_IP=0x%llx)\n", + frameIdx, (unsigned long long)cdacFrameAddr, (unsigned long long)dacFrameAddr, + (unsigned long long)cdacIP, (unsigned long long)dacIP); + } + mismatch = true; + } + + // Advance both + hr1 = cdacWalk->Next(); + hr2 = dacWalk->Next(); + + if (hr1 != hr2) + { + if (s_logFile) + fprintf(s_logFile, " [WALK_MISMATCH] Frame %d: Next hr mismatch cDAC=0x%x DAC=0x%x\n", + frameIdx, hr1, hr2); + mismatch = true; + break; + } + if (hr1 != S_OK) + break; // both finished + + frameIdx++; + } + + if (!mismatch && s_logFile) + fprintf(s_logFile, " [WALK_OK] %d frames matched between cDAC and DAC\n", frameIdx); + + cdacWalk->Release(); + dacWalk->Release(); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +// Compare two ref sets using two-phase matching. +// Phase 1: Match stack refs (Address != 0) by exact (Address, Object, Flags). +// Phase 2: Match register refs (Address == 0) by (Object, Flags) only. +// Returns true if all refs in setA have a match in setB and counts are equal. +//----------------------------------------------------------------------------- + +static bool CompareRefSets(StackRef* refsA, int countA, StackRef* refsB, int countB) +{ + if (countA != countB) + return false; + if (countA == 0) + return true; + + bool matched[MAX_COLLECTED_REFS] = {}; + + for (int i = 0; i < countA; i++) + { + if (refsA[i].Address == 0) + continue; + bool found = false; + for (int j = 0; j < countB; j++) + { + if (matched[j]) continue; + if (refsA[i].Address == refsB[j].Address && + refsA[i].Object == refsB[j].Object && + refsA[i].Flags == refsB[j].Flags) + { + matched[j] = true; + found = true; + break; + } + } + if (!found) return false; + } + + for (int i = 0; i < countA; i++) + { + if (refsA[i].Address != 0) + continue; + bool found = false; + for (int j = 0; j < countB; j++) + { + if (matched[j]) continue; + if (refsA[i].Object == refsB[j].Object && + refsA[i].Flags == refsB[j].Flags) + { + matched[j] = true; + found = true; + break; + } + } + if (!found) return false; + } + + return true; +} + +//----------------------------------------------------------------------------- +// Filter interior stack pointers and deduplicate a ref set in place. +//----------------------------------------------------------------------------- + +static int FilterAndDedup(StackRef* refs, int count, Thread* pThread, uintptr_t stackLimit) +{ + count = FilterInteriorStackRefs(refs, count, pThread, stackLimit); + count = DeduplicateRefs(refs, count); + return count; +} + +//----------------------------------------------------------------------------- +// Main entry point: verify at a GC stress point +//----------------------------------------------------------------------------- + +bool CdacStress::ShouldSkipStressPoint() +{ + LONG count = InterlockedIncrement(&s_verifyCount); + + if (s_step <= 1) + return false; + + return (count % s_step) != 0; +} + +void CdacStress::VerifyAtAllocPoint() +{ + if (!s_initialized) + return; + + // Reentrancy guard: allocations inside VerifyAtStressPoint (e.g., SArray) + // would trigger this function again, causing deadlock on s_cdacLock. + if (t_inVerification) + return; + + if (ShouldSkipStressPoint()) + return; + + Thread* pThread = GetThreadNULLOk(); + if (pThread == nullptr || !pThread->PreemptiveGCDisabled()) + return; + + CONTEXT ctx; + RtlCaptureContext(&ctx); + VerifyAtStressPoint(pThread, &ctx); +} + +void CdacStress::VerifyAtStressPoint(Thread* pThread, PCONTEXT regs) +{ + _ASSERTE(s_initialized); + _ASSERTE(pThread != nullptr); + _ASSERTE(regs != nullptr); + + // RAII guard: set t_inVerification=true on entry, false on exit. + // Prevents infinite recursion when allocations inside this function + // trigger VerifyAtAllocPoint again (which would deadlock on s_cdacLock). + struct ReentrancyGuard { + ReentrancyGuard() { t_inVerification = true; } + ~ReentrancyGuard() { t_inVerification = false; } + } reentrancyGuard; + + // Serialize cDAC access — the cDAC's ProcessedData cache and COM interfaces + // are not thread-safe, and GC stress can fire on multiple threads. + CrstHolder cdacLock(&s_cdacLock); + + // Unique-stack filtering: use IP + SP as a stack identity. + // This skips re-verification at the same code location with the same stack depth. + if (IsUniqueEnabled() && s_seenStacks != nullptr) + { + SIZE_T stackHash = GetIP(regs) ^ (GetSP(regs) * 2654435761u); + if (s_seenStacks->LookupPtr(stackHash) != nullptr) + return; + s_seenStacks->Add(stackHash); + } + + // Set the thread context for the cDAC's ReadThreadContext callback. + s_currentContext = regs; + s_currentThreadId = pThread->GetOSThreadId(); + + // Flush the cDAC's ProcessedData cache so it re-reads from the live process. + if (s_cdacProcess != nullptr) + { + s_cdacProcess->Flush(); + } + + // Flush the legacy DAC cache too. + if (s_dacProcess != nullptr) + { + s_dacProcess->Flush(); + } + + // Compare IXCLRDataStackWalk frame-by-frame between cDAC and legacy DAC. + if (s_cdacStressLevel & CDACSTRESS_WALK) + { + CompareStackWalks(pThread, regs); + } + + // Compare GC stack references. + if (!(s_cdacStressLevel & CDACSTRESS_REFS)) + { + s_currentContext = nullptr; + s_currentThreadId = 0; + return; + } + + // Step 1: Collect raw refs from cDAC (always) and DAC (if USE_DAC). + DWORD osThreadId = pThread->GetOSThreadId(); + + SArray cdacRefs; + bool haveCdac = CollectStackRefs(s_cdacSosDac, osThreadId, &cdacRefs); + + SArray dacRefs; + bool haveDac = false; + if (s_cdacStressLevel & CDACSTRESS_USE_DAC) + { + haveDac = (s_dacSosDac != nullptr) && CollectStackRefs(s_dacSosDac, osThreadId, &dacRefs); + } + + s_currentContext = nullptr; + s_currentThreadId = 0; + + StackRef runtimeRefsBuf[MAX_COLLECTED_REFS]; + int runtimeCount = 0; + CollectRuntimeStackRefs(pThread, regs, runtimeRefsBuf, &runtimeCount); + + if (!haveCdac) + { + InterlockedIncrement(&s_verifySkip); + if (s_logFile != nullptr) + fprintf(s_logFile, "[SKIP] Thread=0x%x IP=0x%p - cDAC GetStackReferences failed\n", + osThreadId, (void*)GetIP(regs)); + return; + } + + // Step 2: Compare cDAC vs DAC raw (before any filtering). + int rawCdacCount = (int)cdacRefs.GetCount(); + int rawDacCount = haveDac ? (int)dacRefs.GetCount() : -1; + bool dacMatch = true; + if (haveDac) + { + StackRef* cdacBuf = cdacRefs.OpenRawBuffer(); + StackRef* dacBuf = dacRefs.OpenRawBuffer(); + dacMatch = CompareRefSets(cdacBuf, rawCdacCount, dacBuf, rawDacCount); + cdacRefs.CloseRawBuffer(); + dacRefs.CloseRawBuffer(); + } + + // Step 3: Filter cDAC refs and compare vs RT (always). + Frame* pTopFrame = pThread->GetFrame(); + Object** topStack = (Object**)pTopFrame; + if (InlinedCallFrame::FrameHasActiveCall(pTopFrame)) + { + InlinedCallFrame* pInlinedFrame = dac_cast(pTopFrame); + topStack = (Object**)pInlinedFrame->GetCallSiteSP(); + } + uintptr_t stackLimit = (uintptr_t)topStack; + + int filteredCdacCount = rawCdacCount; + if (filteredCdacCount > 0) + { + StackRef* cdacBuf = cdacRefs.OpenRawBuffer(); + filteredCdacCount = FilterAndDedup(cdacBuf, filteredCdacCount, pThread, stackLimit); + cdacRefs.CloseRawBuffer(); + } + runtimeCount = DeduplicateRefs(runtimeRefsBuf, runtimeCount); + + StackRef* cdacBuf = cdacRefs.OpenRawBuffer(); + bool rtMatch = CompareRefSets(cdacBuf, filteredCdacCount, runtimeRefsBuf, runtimeCount); + cdacRefs.CloseRawBuffer(); + + // Step 4: Pass requires cDAC vs RT match. + // DAC mismatch is logged separately but doesn't affect pass/fail. + bool pass = rtMatch; + + if (pass) + InterlockedIncrement(&s_verifyPass); + else + InterlockedIncrement(&s_verifyFail); + + // Step 5: Log results. + if (s_logFile != nullptr) + { + const char* label = pass ? "PASS" : "FAIL"; + if (pass && !dacMatch) + label = "DAC_MISMATCH"; + fprintf(s_logFile, "[%s] Thread=0x%x IP=0x%p cDAC=%d DAC=%d RT=%d\n", + label, osThreadId, (void*)GetIP(regs), + rawCdacCount, rawDacCount, runtimeCount); + + if (!pass || !dacMatch) + { + for (int i = 0; i < rawCdacCount; i++) + fprintf(s_logFile, " cDAC [%d]: Address=0x%llx Object=0x%llx Flags=0x%x Source=0x%llx SourceType=%d SP=0x%llx\n", + i, (unsigned long long)cdacRefs[i].Address, (unsigned long long)cdacRefs[i].Object, + cdacRefs[i].Flags, (unsigned long long)cdacRefs[i].Source, cdacRefs[i].SourceType, + (unsigned long long)cdacRefs[i].StackPointer); + if (haveDac) + { + for (int i = 0; i < rawDacCount; i++) + fprintf(s_logFile, " DAC [%d]: Address=0x%llx Object=0x%llx Flags=0x%x Source=0x%llx\n", + i, (unsigned long long)dacRefs[i].Address, (unsigned long long)dacRefs[i].Object, + dacRefs[i].Flags, (unsigned long long)dacRefs[i].Source); + } + for (int i = 0; i < runtimeCount; i++) + fprintf(s_logFile, " RT [%d]: Address=0x%llx Object=0x%llx Flags=0x%x\n", + i, (unsigned long long)runtimeRefsBuf[i].Address, (unsigned long long)runtimeRefsBuf[i].Object, + runtimeRefsBuf[i].Flags); + + fflush(s_logFile); + } + } +} + +#endif // HAVE_GCCOVER diff --git a/src/coreclr/vm/cdacstress.h b/src/coreclr/vm/cdacstress.h new file mode 100644 index 00000000000000..b151155559e9c5 --- /dev/null +++ b/src/coreclr/vm/cdacstress.h @@ -0,0 +1,125 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// +// CdacStress.h +// +// Infrastructure for verifying cDAC stack reference reporting against the +// runtime's own GC root enumeration at stress trigger points. +// +// Enabled via DOTNET_CdacStress (bit flags) or legacy DOTNET_GCStress=0x20. +// + +#ifndef _CDAC_STRESS_H_ +#define _CDAC_STRESS_H_ + +// Trigger points for cDAC stress verification. +enum cdac_trigger_points +{ + cdac_on_alloc, // Verify at allocation points + cdac_on_gc, // Verify at GC trigger points + cdac_on_instr, // Verify at instruction-level stress points (needs GCStress=0x4) +}; + +#ifdef HAVE_GCCOVER + +// Bit flags for DOTNET_CdacStress configuration. +// +// Low nibble: WHERE to trigger verification +// High nibble: WHAT to validate +// Modifier: HOW to filter +enum CdacStressFlags : DWORD +{ + // Trigger points (low nibble — where stress fires) + CDACSTRESS_ALLOC = 0x1, // Verify at allocation points + CDACSTRESS_GC = 0x2, // Verify at GC trigger points (future) + CDACSTRESS_INSTR = 0x4, // Verify at instruction stress points (needs GCStress=0x4) + + // Validation types (high nibble — what to check) + CDACSTRESS_REFS = 0x10, // Compare GC stack references + CDACSTRESS_WALK = 0x20, // Compare IXCLRDataStackWalk frame-by-frame + CDACSTRESS_USE_DAC = 0x40, // Also load legacy DAC and compare cDAC against it + + // Modifiers + CDACSTRESS_UNIQUE = 0x100, // Only verify on unique (IP, SP) pairs +}; + +// Forward declarations +class Thread; + +// Accessor for the resolved stress level — called by template specializations. +DWORD GetCdacStressLevel(); + +class CdacStress +{ +public: + static bool Initialize(); + static void Shutdown(); + static bool IsInitialized(); + + // Returns true if cDAC stress is enabled via DOTNET_CdacStress or legacy GCSTRESS_CDAC. + static bool IsEnabled(); + + // Template-based trigger point check, following the GCStress pattern. + template + static bool IsEnabled(); + + // Returns true if unique-stack filtering is active. + static bool IsUniqueEnabled(); + + // Verify at a stress point if the given trigger is enabled and not skipped. + // Follows the GCStress::MaybeTrigger pattern — call sites are one-liners. + template + FORCEINLINE static void MaybeVerify(Thread* pThread, PCONTEXT regs) + { + if (IsEnabled() && !ShouldSkipStressPoint()) + VerifyAtStressPoint(pThread, regs); + } + + // Allocation-point variant: captures thread context automatically. + template + FORCEINLINE static void MaybeVerify() + { + if (IsEnabled() && !ShouldSkipStressPoint()) + VerifyAtAllocPoint(); + } + + // Main entry point: verify cDAC stack refs match runtime stack refs. + static void VerifyAtStressPoint(Thread* pThread, PCONTEXT regs); + + // Verify at an allocation point. Captures current thread context. + static void VerifyAtAllocPoint(); + + // Returns true if this stress point should be skipped (step throttling). + static bool ShouldSkipStressPoint(); +}; + +template<> FORCEINLINE bool CdacStress::IsEnabled() +{ + return IsInitialized() && (GetCdacStressLevel() & CDACSTRESS_ALLOC) != 0; +} + +template<> FORCEINLINE bool CdacStress::IsEnabled() +{ + return IsInitialized() && (GetCdacStressLevel() & CDACSTRESS_GC) != 0; +} + +template<> FORCEINLINE bool CdacStress::IsEnabled() +{ + return IsInitialized() && (GetCdacStressLevel() & CDACSTRESS_INSTR) != 0; +} + +#else // !HAVE_GCCOVER + +// Stub when HAVE_GCCOVER is not defined — all calls compile to nothing. +class CdacStress +{ +public: + template + FORCEINLINE static void MaybeVerify(Thread* pThread, PCONTEXT regs) { } + template + FORCEINLINE static void MaybeVerify() { } +}; + +#endif // HAVE_GCCOVER +#endif // _CDAC_STRESS_H_ diff --git a/src/coreclr/vm/ceemain.cpp b/src/coreclr/vm/ceemain.cpp index ce5e4d016c9ed0..0d903c3bb52205 100644 --- a/src/coreclr/vm/ceemain.cpp +++ b/src/coreclr/vm/ceemain.cpp @@ -210,7 +210,7 @@ #include "genanalysis.h" #ifdef HAVE_GCCOVER -#include "cdacgcstress.h" +#include "CdacStress.h" #endif HRESULT EEStartup(); @@ -967,9 +967,9 @@ void EEStartupHelper() #ifdef HAVE_GCCOVER MethodDesc::Init(); - if (GCStress::IsEnabled() && (g_pConfig->GetGCStressLevel() & EEConfig::GCSTRESS_CDAC)) + if (CdacStress::IsEnabled()) { - CdacGcStress::Initialize(); + CdacStress::Initialize(); } #endif @@ -1253,7 +1253,7 @@ void STDMETHODCALLTYPE EEShutDownHelper(BOOL fIsDllUnloading) InterlockedOr((LONG*)&g_fEEShutDown, ShutDown_Start); #ifdef HAVE_GCCOVER - CdacGcStress::Shutdown(); + CdacStress::Shutdown(); #endif if (!IsAtProcessExit() && !g_fFastExitProcess) diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index a66b587bf02add..dd2c9d044574db 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -44,6 +44,8 @@ CDAC_TYPE_FIELD(Thread, /*pointer*/, Frame, cdac_data::Frame) CDAC_TYPE_FIELD(Thread, /*pointer*/, CachedStackBase, cdac_data::CachedStackBase) CDAC_TYPE_FIELD(Thread, /*pointer*/, CachedStackLimit, cdac_data::CachedStackLimit) CDAC_TYPE_FIELD(Thread, /*pointer*/, ExceptionTracker, cdac_data::ExceptionTracker) +CDAC_TYPE_FIELD(Thread, /*pointer*/, DebuggerFilterContext, cdac_data::DebuggerFilterContext) +CDAC_TYPE_FIELD(Thread, /*pointer*/, ProfilerFilterContext, cdac_data::ProfilerFilterContext) CDAC_TYPE_FIELD(Thread, GCHandle, GCHandle, cdac_data::ExposedObject) CDAC_TYPE_FIELD(Thread, GCHandle, LastThrownObject, cdac_data::LastThrownObject) CDAC_TYPE_FIELD(Thread, pointer, LinkNext, cdac_data::Link) @@ -144,6 +146,8 @@ CDAC_TYPE_FIELD(ExceptionInfo, /*uint8*/, PassNumber, offsetof(ExInfo, m_passNum CDAC_TYPE_FIELD(ExceptionInfo, /*pointer*/, CSFEHClause, offsetof(ExInfo, m_csfEHClause)) CDAC_TYPE_FIELD(ExceptionInfo, /*pointer*/, CSFEnclosingClause, offsetof(ExInfo, m_csfEnclosingClause)) CDAC_TYPE_FIELD(ExceptionInfo, /*pointer*/, CallerOfActualHandlerFrame, offsetof(ExInfo, m_sfCallerOfActualHandlerFrame)) +CDAC_TYPE_FIELD(ExceptionInfo, /*uint32*/, ClauseForCatchHandlerStartPC, offsetof(ExInfo, m_ClauseForCatch) + offsetof(EE_ILEXCEPTION_CLAUSE, HandlerStartPC)) +CDAC_TYPE_FIELD(ExceptionInfo, /*uint32*/, ClauseForCatchHandlerEndPC, offsetof(ExInfo, m_ClauseForCatch) + offsetof(EE_ILEXCEPTION_CLAUSE, HandlerEndPC)) CDAC_TYPE_END(ExceptionInfo) CDAC_TYPE_BEGIN(GCHandle) @@ -914,8 +918,19 @@ CDAC_TYPE_SIZE(sizeof(StubDispatchFrame)) CDAC_TYPE_FIELD(StubDispatchFrame, /*pointer*/, RepresentativeMTPtr, cdac_data::RepresentativeMTPtr) CDAC_TYPE_FIELD(StubDispatchFrame, /*pointer*/, MethodDescPtr, cdac_data::MethodDescPtr) CDAC_TYPE_FIELD(StubDispatchFrame, /*uint32*/, RepresentativeSlot, cdac_data::RepresentativeSlot) +CDAC_TYPE_FIELD(StubDispatchFrame, /*pointer*/, GCRefMap, cdac_data::GCRefMap) CDAC_TYPE_END(StubDispatchFrame) +CDAC_TYPE_BEGIN(ExternalMethodFrame) +CDAC_TYPE_SIZE(sizeof(ExternalMethodFrame)) +CDAC_TYPE_FIELD(ExternalMethodFrame, /*pointer*/, GCRefMap, cdac_data::GCRefMap) +CDAC_TYPE_END(ExternalMethodFrame) + +CDAC_TYPE_BEGIN(DynamicHelperFrame) +CDAC_TYPE_SIZE(sizeof(DynamicHelperFrame)) +CDAC_TYPE_FIELD(DynamicHelperFrame, /*int32*/, DynamicHelperFrameFlags, cdac_data::DynamicHelperFrameFlags) +CDAC_TYPE_END(DynamicHelperFrame) + #ifdef FEATURE_HIJACK CDAC_TYPE_BEGIN(ResumableFrame) CDAC_TYPE_SIZE(sizeof(ResumableFrame)) @@ -1288,6 +1303,18 @@ CDAC_GLOBAL_POINTER(GCThread, &::g_pSuspensionThread) #undef FRAME_TYPE_NAME CDAC_GLOBAL(MethodDescTokenRemainderBitCount, uint8, METHOD_TOKEN_REMAINDER_BIT_COUNT) + +CDAC_GLOBAL(TransitionBlockOffsetOfArgs, uint32, sizeof(TransitionBlock)) +#if (defined(TARGET_AMD64) && !defined(UNIX_AMD64_ABI)) || defined(TARGET_WASM) +CDAC_GLOBAL(TransitionBlockOffsetOfArgumentRegisters, uint32, sizeof(TransitionBlock)) +CDAC_GLOBAL(TransitionBlockOffsetOfFirstGCRefMapSlot, uint32, sizeof(TransitionBlock)) +#elif defined(TARGET_ARM64) +CDAC_GLOBAL(TransitionBlockOffsetOfArgumentRegisters, uint32, offsetof(TransitionBlock, m_argumentRegisters)) +CDAC_GLOBAL(TransitionBlockOffsetOfFirstGCRefMapSlot, uint32, offsetof(TransitionBlock, m_x8RetBuffReg)) +#else +CDAC_GLOBAL(TransitionBlockOffsetOfArgumentRegisters, uint32, offsetof(TransitionBlock, m_argumentRegisters)) +CDAC_GLOBAL(TransitionBlockOffsetOfFirstGCRefMapSlot, uint32, offsetof(TransitionBlock, m_argumentRegisters)) +#endif #if FEATURE_COMINTEROP CDAC_GLOBAL(FeatureCOMInterop, uint8, 1) #else diff --git a/src/coreclr/vm/frames.h b/src/coreclr/vm/frames.h index 55072025229b0f..eb3fa240ee9148 100644 --- a/src/coreclr/vm/frames.h +++ b/src/coreclr/vm/frames.h @@ -1728,6 +1728,7 @@ struct cdac_data { static constexpr size_t RepresentativeMTPtr = offsetof(StubDispatchFrame, m_pRepresentativeMT); static constexpr uint32_t RepresentativeSlot = offsetof(StubDispatchFrame, m_representativeSlot); + static constexpr size_t GCRefMap = offsetof(StubDispatchFrame, m_pGCRefMap); }; typedef DPTR(class StubDispatchFrame) PTR_StubDispatchFrame; @@ -1763,6 +1764,8 @@ class CallCountingHelperFrame : public FramedMethodFrame class ExternalMethodFrame : public FramedMethodFrame { + friend struct ::cdac_data; + // Indirection and containing module. Used to compute pGCRefMap lazily. PTR_Module m_pZapModule; TADDR m_pIndirection; @@ -1803,8 +1806,16 @@ class ExternalMethodFrame : public FramedMethodFrame typedef DPTR(class ExternalMethodFrame) PTR_ExternalMethodFrame; +template <> +struct cdac_data +{ + static constexpr size_t GCRefMap = offsetof(ExternalMethodFrame, m_pGCRefMap); +}; + class DynamicHelperFrame : public FramedMethodFrame { + friend struct ::cdac_data; + int m_dynamicHelperFrameFlags; public: @@ -1825,6 +1836,12 @@ class DynamicHelperFrame : public FramedMethodFrame typedef DPTR(class DynamicHelperFrame) PTR_DynamicHelperFrame; +template <> +struct cdac_data +{ + static constexpr size_t DynamicHelperFrameFlags = offsetof(DynamicHelperFrame, m_dynamicHelperFrameFlags); +}; + #ifdef FEATURE_COMINTEROP //------------------------------------------------------------------------ diff --git a/src/coreclr/vm/gccover.cpp b/src/coreclr/vm/gccover.cpp index 725e935957cad2..5e516a13ad4246 100644 --- a/src/coreclr/vm/gccover.cpp +++ b/src/coreclr/vm/gccover.cpp @@ -24,7 +24,7 @@ #include "gccover.h" #include "virtualcallstub.h" #include "threadsuspend.h" -#include "cdacgcstress.h" +#include "CdacStress.h" #if defined(TARGET_AMD64) || defined(TARGET_ARM) #include "gcinfodecoder.h" @@ -888,11 +888,7 @@ void DoGcStress (PCONTEXT regs, NativeCodeVersion nativeCodeVersion) // Do the actual stress work // - // Verify cDAC stack references before triggering the GC (while refs haven't moved). - if (CdacGcStress::IsInitialized()) - { - CdacGcStress::VerifyAtStressPoint(pThread, regs); - } + CdacStress::MaybeVerify(pThread, regs); // BUG(github #10318) - when not using allocation contexts, the alloc lock // must be acquired here. Until fixed, this assert prevents random heap corruption. @@ -1202,13 +1198,9 @@ void DoGcStress (PCONTEXT regs, NativeCodeVersion nativeCodeVersion) // Do the actual stress work // - // Verify cDAC stack references before triggering the GC (while refs haven't moved). - if (CdacGcStress::IsInitialized()) - { - CdacGcStress::VerifyAtStressPoint(pThread, regs); - } + CdacStress::MaybeVerify(pThread, regs); - // BUG(github #10318) - when not using allocation contexts, the alloc lock + // BUG(github #10318)- when not using allocation contexts, the alloc lock // must be acquired here. Until fixed, this assert prevents random heap corruption. assert(GCHeapUtilities::UseThreadAllocationContexts()); GCHeapUtilities::GetGCHeap()->StressHeap(&t_runtime_thread_locals.alloc_context.m_GCAllocContext); diff --git a/src/coreclr/vm/gchelpers.cpp b/src/coreclr/vm/gchelpers.cpp index 7eb08201edd85e..21a22e19677ce6 100644 --- a/src/coreclr/vm/gchelpers.cpp +++ b/src/coreclr/vm/gchelpers.cpp @@ -30,6 +30,10 @@ #include "eeprofinterfaces.inl" #include "frozenobjectheap.h" +#ifdef HAVE_GCCOVER +#include "CdacStress.h" +#endif + #ifdef FEATURE_COMINTEROP #include "runtimecallablewrapper.h" #endif // FEATURE_COMINTEROP @@ -411,6 +415,9 @@ inline Object* Alloc(ee_alloc_context* pEEAllocContext, size_t size, GC_ALLOC_FL } } + // Verify cDAC stack references before the allocation-triggered GC (while refs haven't moved). + CdacStress::MaybeVerify(); + GCStress::MaybeTrigger(pAllocContext); // for SOH, if there is enough space in the current allocation context, then @@ -477,6 +484,7 @@ inline Object* Alloc(size_t size, GC_ALLOC_FLAGS flags) if (GCHeapUtilities::UseThreadAllocationContexts()) { ee_alloc_context *threadContext = GetThreadEEAllocContext(); + CdacStress::MaybeVerify(); GCStress::MaybeTrigger(&threadContext->m_GCAllocContext); retVal = Alloc(threadContext, size, flags); } @@ -484,6 +492,7 @@ inline Object* Alloc(size_t size, GC_ALLOC_FLAGS flags) { GlobalAllocLockHolder holder(&g_global_alloc_lock); ee_alloc_context *globalContext = &g_global_alloc_context; + CdacStress::MaybeVerify(); GCStress::MaybeTrigger(&globalContext->m_GCAllocContext); retVal = Alloc(globalContext, size, flags); } diff --git a/src/coreclr/vm/threads.h b/src/coreclr/vm/threads.h index f4eaa99d79e484..7e99ecba7bdb3a 100644 --- a/src/coreclr/vm/threads.h +++ b/src/coreclr/vm/threads.h @@ -3773,6 +3773,8 @@ struct cdac_data static_assert(std::is_same().m_ExceptionState), ThreadExceptionState>::value, "Thread::m_ExceptionState is of type ThreadExceptionState"); static constexpr size_t ExceptionTracker = offsetof(Thread, m_ExceptionState) + offsetof(ThreadExceptionState, m_pCurrentTracker); + static constexpr size_t DebuggerFilterContext = offsetof(Thread, m_debuggerFilterContext); + static constexpr size_t ProfilerFilterContext = offsetof(Thread, m_pProfilerFilterContext); #ifndef TARGET_UNIX static constexpr size_t TEB = offsetof(Thread, m_pTEB); static constexpr size_t UEWatsonBucketTrackerBuckets = offsetof(Thread, m_ExceptionState) + offsetof(ThreadExceptionState, m_UEWatsonBucketTracker) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs index 2a49c5a0d11569..579472df9193ce 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs @@ -151,6 +151,8 @@ public enum DataType HijackFrame, TailCallFrame, StubDispatchFrame, + ExternalMethodFrame, + DynamicHelperFrame, ComCallWrapper, SimpleComCallWrapper, ComMethodTable, diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs index 2585e72902f7ac..185e3ef8486841 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs @@ -75,6 +75,10 @@ public static class Globals public const string MethodDescTokenRemainderBitCount = nameof(MethodDescTokenRemainderBitCount); public const string DirectorySeparator = nameof(DirectorySeparator); + public const string TransitionBlockOffsetOfFirstGCRefMapSlot = nameof(TransitionBlockOffsetOfFirstGCRefMapSlot); + public const string TransitionBlockOffsetOfArgumentRegisters = nameof(TransitionBlockOffsetOfArgumentRegisters); + public const string TransitionBlockOffsetOfArgs = nameof(TransitionBlockOffsetOfArgs); + public const string ExecutionManagerCodeRangeMapAddress = nameof(ExecutionManagerCodeRangeMapAddress); public const string EEJitManagerAddress = nameof(EEJitManagerAddress); public const string StubCodeBlockLast = nameof(StubCodeBlockLast); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.EEJitManager.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.EEJitManager.cs index b275e10ab766fb..eca2b7a047aee7 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.EEJitManager.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.EEJitManager.cs @@ -65,13 +65,17 @@ public override void GetMethodRegionInfo( public override TargetPointer GetUnwindInfo(RangeSection rangeSection, TargetCodePointer jittedCodeAddress) { if (rangeSection.IsRangeList) + { return TargetPointer.Null; + } if (rangeSection.Data == null) throw new ArgumentException(nameof(rangeSection)); TargetPointer codeStart = FindMethodCode(rangeSection, jittedCodeAddress); if (codeStart == TargetPointer.Null) + { return TargetPointer.Null; + } Debug.Assert(codeStart.Value <= jittedCodeAddress.Value); if (!GetRealCodeHeader(rangeSection, codeStart, out Data.RealCodeHeader? realCodeHeader)) @@ -188,7 +192,10 @@ public override void GetExceptionClauses(RangeSection rangeSection, CodeBlockHan throw new ArgumentException(nameof(rangeSection)); Data.RealCodeHeader? realCodeHeader; - if (!GetRealCodeHeader(rangeSection, codeInfoHandle.Address, out realCodeHeader) || realCodeHeader == null) + // codeInfoHandle.Address is the IP, not the code start. We need to find the actual + // method start via the nibble map so GetRealCodeHeader reads at the correct offset. + TargetPointer codeStart = FindMethodCode(rangeSection, new TargetCodePointer(codeInfoHandle.Address.Value)); + if (!GetRealCodeHeader(rangeSection, codeStart, out realCodeHeader) || realCodeHeader == null) return; if (realCodeHeader.JitEHInfo == null) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoDecoder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoDecoder.cs index d6a6a0da8b39f4..219dbaf1fa68c0 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoDecoder.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoDecoder.cs @@ -520,6 +520,26 @@ public IReadOnlyList GetInterruptibleRanges() return _interruptibleRanges; } + /// + public uint? FindFirstInterruptiblePoint(uint startOffset, uint endOffset) + { + EnsureDecodedTo(DecodePoints.InterruptibleRanges); + + foreach (InterruptibleRange range in _interruptibleRanges) + { + if (range.EndOffset <= startOffset) + continue; + + if (startOffset >= range.StartOffset && startOffset < range.EndOffset) + return startOffset; + + if (range.StartOffset < endOffset) + return range.StartOffset; + } + + return null; + } + public uint StackBaseRegister { get diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/IGCInfoDecoder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/IGCInfoDecoder.cs index 86f4210a7cb91d..7c25381f31fb38 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/IGCInfoDecoder.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/IGCInfoDecoder.cs @@ -24,6 +24,12 @@ internal interface IGCInfoDecoder : IGCInfoHandle uint GetCodeLength(); uint StackBaseRegister { get; } + /// + /// Finds the first interruptible point within the given handler range [startOffset, endOffset). + /// Returns null if no interruptible point exists in the range. + /// + uint? FindFirstInterruptiblePoint(uint startOffset, uint endOffset) => null; + /// /// Enumerates all live GC slots at the given instruction offset. /// diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/AMD64/AMD64Unwinder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/AMD64/AMD64Unwinder.cs index 6f4253dbfff624..7c4666a88c1104 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/AMD64/AMD64Unwinder.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/AMD64/AMD64Unwinder.cs @@ -46,12 +46,20 @@ public bool Unwind(ref AMD64Context context) UnwindCode unwindOp; if (_eman.GetCodeBlockHandle(context.InstructionPointer.Value) is not CodeBlockHandle cbh) + { return false; + } TargetPointer controlPC = context.InstructionPointer; TargetPointer imageBase = _eman.GetUnwindInfoBaseAddress(cbh); - Data.RuntimeFunction functionEntry = _target.ProcessedData.GetOrAdd(_eman.GetUnwindInfo(cbh)); + TargetPointer unwindInfoAddr = _eman.GetUnwindInfo(cbh); + + if (unwindInfoAddr == TargetPointer.Null) + { + return false; + } + Data.RuntimeFunction functionEntry = _target.ProcessedData.GetOrAdd(unwindInfoAddr); if (functionEntry.EndAddress is null) return false; if (GetUnwindInfoHeader(imageBase + functionEntry.UnwindData) is not UnwindInfoHeader unwindInfo) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/ExceptionHandling.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/ExceptionHandling.cs index 8f9c79fa6f1cdf..767f8527418e07 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/ExceptionHandling.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/ExceptionHandling.cs @@ -179,4 +179,41 @@ private bool IsInStackRegionUnwoundBySpecifiedException(TargetPointer callerStac return (exceptionInfo.StackLowBound < callerStackPointer) && (callerStackPointer <= exceptionInfo.StackHighBound); } + /// + /// Checks if the current frame is the throw-site frame during exception first-pass. + /// During first pass (UnwindHasStarted=0), the ExInfo's StackLowBound is set to the + /// SP of the frame that threw the exception. The legacy DAC does not report GC refs + /// from this frame during first pass. + /// + private bool IsAtFirstPassExceptionThrowSite(IStackDataFrameHandle stackDataFrameHandle) + { + StackDataFrameHandle handle = AssertCorrectHandle(stackDataFrameHandle); + if (handle.State is not StackWalkState.SW_FRAMELESS) + return false; + + TargetPointer frameSP = handle.Context.StackPointer; + + TargetPointer pExInfo = GetCurrentExceptionTracker(handle); + while (pExInfo != TargetPointer.Null) + { + Data.ExceptionInfo exInfo = _target.ProcessedData.GetOrAdd(pExInfo); + pExInfo = exInfo.PreviousNestedInfo; + + // First pass only (unwind has NOT started) + if ((exInfo.ExceptionFlags & (uint)ExceptionFlagsEnum.UnwindHasStarted) != 0) + continue; + + // Check for empty range (ExInfo just created) + if (exInfo.StackLowBound == TargetPointer.PlatformMaxValue(_target) + && exInfo.StackHighBound == TargetPointer.Null) + continue; + + // The throw-site frame's SP matches the ExInfo's StackLowBound + if (frameSP == exInfo.StackLowBound) + return true; + } + + return false; + } + } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/CorSigParser.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/CorSigParser.cs new file mode 100644 index 00000000000000..b8c6a0a173552b --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/CorSigParser.cs @@ -0,0 +1,212 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; + +/// +/// Minimal signature parser for GC reference classification of method parameters. +/// Parses the ECMA-335 II.23.2.1 MethodDefSig format, classifying each parameter +/// type as a GC reference, interior pointer, value type, or non-GC primitive. +/// +internal ref struct CorSigParser +{ + private ReadOnlySpan _sig; + private int _index; + private readonly int _pointerSize; + + public CorSigParser(ReadOnlySpan signature, int pointerSize) + { + _sig = signature; + _index = 0; + _pointerSize = pointerSize; + } + + public bool AtEnd => _index >= _sig.Length; + + public byte ReadByte() + { + if (_index >= _sig.Length) + throw new InvalidOperationException("Unexpected end of signature."); + return _sig[_index++]; + } + + /// + /// Reads a compressed unsigned integer (ECMA-335 II.23.2). + /// + public uint ReadCompressedUInt() + { + byte b = ReadByte(); + if ((b & 0x80) == 0) + return b; + if ((b & 0xC0) == 0x80) + { + byte b2 = ReadByte(); + return (uint)(((b & 0x3F) << 8) | b2); + } + if ((b & 0xE0) == 0xC0) + { + byte b2 = ReadByte(); + byte b3 = ReadByte(); + byte b4 = ReadByte(); + return (uint)(((b & 0x1F) << 24) | (b2 << 16) | (b3 << 8) | b4); + } + throw new InvalidOperationException("Invalid compressed integer encoding."); + } + + /// + /// Reads the next type from the signature and classifies it for GC scanning. + /// Advances past the full type encoding. + /// + public GcTypeKind ReadTypeAndClassify() + { + CorElementType elemType = (CorElementType)ReadCompressedUInt(); + + switch (elemType) + { + case CorElementType.Void: + case CorElementType.Boolean: + case CorElementType.Char: + case CorElementType.I1: + case CorElementType.U1: + case CorElementType.I2: + case CorElementType.U2: + case CorElementType.I4: + case CorElementType.U4: + case CorElementType.I8: + case CorElementType.U8: + case CorElementType.R4: + case CorElementType.R8: + case CorElementType.I: + case CorElementType.U: + return GcTypeKind.None; + + case CorElementType.String: + case CorElementType.Object: + return GcTypeKind.Ref; + + case CorElementType.Class: + ReadCompressedUInt(); // TypeDefOrRefOrSpecEncoded + return GcTypeKind.Ref; + + case CorElementType.ValueType: + ReadCompressedUInt(); // TypeDefOrRefOrSpecEncoded + return GcTypeKind.Other; + + case CorElementType.SzArray: + SkipType(); // element type + return GcTypeKind.Ref; + + case CorElementType.Array: + SkipType(); // element type + SkipArrayShape(); + return GcTypeKind.Ref; + + case CorElementType.GenericInst: + { + byte baseType = ReadByte(); // CLASS, VALUETYPE, or INTERNAL + if (baseType == (byte)CorElementType.Internal) + { + // ELEMENT_TYPE_INTERNAL embeds a raw pointer to a TypeHandle + _index += _pointerSize; + } + else + { + ReadCompressedUInt(); // TypeDefOrRefOrSpecEncoded + } + uint argCount = ReadCompressedUInt(); + for (uint i = 0; i < argCount; i++) + SkipType(); + // Conservative: treat INTERNAL base as Ref (could be either class or valuetype). + // CLASS-based generics are Ref; VALUETYPE-based and unknown are Other. + return baseType == (byte)CorElementType.Class ? GcTypeKind.Ref : GcTypeKind.Other; + } + + case CorElementType.Byref: + SkipType(); // inner type + return GcTypeKind.Interior; + + case CorElementType.Ptr: + SkipType(); // pointee type + return GcTypeKind.None; + + case CorElementType.FnPtr: + SkipMethodSignature(); + return GcTypeKind.None; + + case CorElementType.TypedByRef: + return GcTypeKind.Other; + + case CorElementType.Var: + case CorElementType.MVar: + ReadCompressedUInt(); // type parameter index + // Conservative: generic type params could be GC refs. + // The runtime resolves these via the generic context. + // For now, treat as potential GC ref to avoid missing references. + return GcTypeKind.Ref; + + case CorElementType.CModReqd: + case CorElementType.CModOpt: + ReadCompressedUInt(); // TypeDefOrRefOrSpecEncoded + return ReadTypeAndClassify(); // recurse past the modifier + + case CorElementType.Sentinel: + return ReadTypeAndClassify(); // skip sentinel, read next type + + case CorElementType.Internal: + // Runtime-internal type: raw pointer to TypeHandle follows. + // Skip the pointer bytes. Conservative: treat as potential GC ref. + _index += _pointerSize; + return GcTypeKind.Ref; + + default: + return GcTypeKind.None; + } + } + + /// + /// Skips over a complete type encoding in the signature. + /// + public void SkipType() + { + ReadTypeAndClassify(); // Same traversal, just discard the result + } + + private void SkipArrayShape() + { + _ = ReadCompressedUInt(); // rank + uint numSizes = ReadCompressedUInt(); + for (uint i = 0; i < numSizes; i++) + ReadCompressedUInt(); + uint numLoBounds = ReadCompressedUInt(); + for (uint i = 0; i < numLoBounds; i++) + ReadCompressedUInt(); // lo bounds are signed but encoded as unsigned + } + + private void SkipMethodSignature() + { + byte callingConv = ReadByte(); + if ((callingConv & 0x10) != 0) // GENERIC + ReadCompressedUInt(); // generic param count + uint paramCount = ReadCompressedUInt(); + SkipType(); // return type + for (uint i = 0; i < paramCount; i++) + SkipType(); + } +} + +/// +/// Classification of a signature type for GC scanning purposes. +/// +internal enum GcTypeKind +{ + /// Not a GC reference (primitives, pointers). + None, + /// Object reference (class, string, array). + Ref, + /// Interior pointer (byref). + Interior, + /// Value type that may contain embedded GC references. + Other, +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GCRefMapDecoder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GCRefMapDecoder.cs new file mode 100644 index 00000000000000..c384a1394431db --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GCRefMapDecoder.cs @@ -0,0 +1,113 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; + +/// +/// Token values from CORCOMPILE_GCREFMAP_TOKENS (corcompile.h). +/// These indicate the type of GC reference at each transition block slot. +/// +internal enum GCRefMapToken +{ + Skip = 0, + Ref = 1, + Interior = 2, + MethodParam = 3, + TypeParam = 4, + VASigCookie = 5, +} + +/// +/// Managed port of the native GCRefMapDecoder (gcrefmap.h:158-246). +/// Decodes a compact bitstream describing which transition block slots +/// contain GC references for a given call site. +/// +internal ref struct GCRefMapDecoder +{ + private readonly Target _target; + private TargetPointer _currentByte; + private int _pendingByte; + private int _pos; + + public GCRefMapDecoder(Target target, TargetPointer blob) + { + _target = target; + _currentByte = blob; + _pendingByte = 0x80; // Forces first byte read + _pos = 0; + } + + public bool AtEnd => _pendingByte == 0; + + public int CurrentPos => _pos; + + private int GetBit() + { + int x = _pendingByte; + if ((x & 0x80) != 0) + { + x = _target.Read(_currentByte); + _currentByte = new TargetPointer(_currentByte.Value + 1); + x |= ((x & 0x80) << 7); + } + _pendingByte = x >> 1; + return x & 1; + } + + private int GetTwoBit() + { + int result = GetBit(); + result |= GetBit() << 1; + return result; + } + + private int GetInt() + { + int result = 0; + int bit = 0; + do + { + result |= GetBit() << (bit++); + result |= GetBit() << (bit++); + result |= GetBit() << (bit++); + } + while (GetBit() != 0); + return result; + } + + /// + /// x86 only: Read the stack pop count from the stream. + /// + public uint ReadStackPop() + { + int x = GetTwoBit(); + if (x == 3) + x = GetInt() + 3; + return (uint)x; + } + + /// + /// Read the next GC reference token from the stream. + /// Advances CurrentPos as appropriate. + /// + public GCRefMapToken ReadToken() + { + int val = GetTwoBit(); + if (val == 3) + { + int ext = GetInt(); + if ((ext & 1) == 0) + { + _pos += (ext >> 1) + 4; + return GCRefMapToken.Skip; + } + else + { + _pos++; + return (GCRefMapToken)((ext >> 1) + 3); + } + } + _pos++; + return (GCRefMapToken)val; + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs index fa72eb606fad75..72063a93fa6c01 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs @@ -23,7 +23,8 @@ public bool EnumGcRefs( IPlatformAgnosticContext context, CodeBlockHandle cbh, CodeManagerFlags flags, - GcScanContext scanContext) + GcScanContext scanContext, + uint? relOffsetOverride = null) { TargetNUInt relativeOffset = _eman.GetRelativeOffset(cbh); _eman.GetGCInfo(cbh, out TargetPointer gcInfoAddr, out uint gcVersion); @@ -41,8 +42,10 @@ public bool EnumGcRefs( // The native code uses GET_CALLER_SP(pRD) which comes from EnsureCallerContextIsValid. TargetPointer? callerSP = null; + uint offsetToUse = relOffsetOverride ?? (uint)relativeOffset.Value; + return decoder.EnumerateLiveSlots( - (uint)relativeOffset.Value, + offsetToUse, flags, (bool isRegister, uint registerNumber, int spOffset, uint spBase, uint gcFlags) => { diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs index a57c598b69b5d0..1e1e271a5d9635 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs @@ -5,6 +5,8 @@ using System.Diagnostics.CodeAnalysis; using System.Diagnostics; using System.Collections.Generic; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; using Microsoft.Diagnostics.DataContractReader.Contracts.GCInfoHelpers; using Microsoft.Diagnostics.DataContractReader.Data; @@ -47,21 +49,23 @@ private record StackDataFrameHandle( bool IsActiveFrame = false) : IStackDataFrameHandle { } - private class StackWalkData(IPlatformAgnosticContext context, StackWalkState state, FrameIterator frameIter, ThreadData threadData, bool skipDuplicateActiveICF = false) + private class StackWalkData(IPlatformAgnosticContext context, StackWalkState state, FrameIterator frameIter, ThreadData threadData, bool skipActiveICFOnce = false) { public IPlatformAgnosticContext Context { get; set; } = context; public StackWalkState State { get; set; } = state; public FrameIterator FrameIter { get; set; } = frameIter; public ThreadData ThreadData { get; set; } = threadData; - // When true, CheckForSkippedFrames will skip past an active InlinedCallFrame - // that was just processed as SW_FRAME without advancing the FrameIterator. - // This prevents a duplicate SW_SKIPPED_FRAME yield for the same managed IP. + // When an active InlinedCallFrame is processed as SW_FRAME without advancing + // the FrameIterator, the same Frame would be re-encountered by + // CheckForSkippedFrames. This one-shot flag tells CheckForSkippedFrames to + // advance past it once, preventing a duplicate SW_SKIPPED_FRAME yield. // // Must be false for ClrDataStackWalk (which needs exact DAC frame parity) // and true for WalkStackReferences (which matches native DacStackReferenceWalker // behavior of not re-enumerating the same InlinedCallFrame). - public bool SkipDuplicateActiveICF { get; } = skipDuplicateActiveICF; + public bool SkipActiveICFOnce { get; } = skipActiveICFOnce; + public bool SkipCurrentFrameInCheck { get; set; } // Track isFirst exactly like native CrawlFrame::isFirst in StackFrameIterator. @@ -137,10 +141,6 @@ private IEnumerable CreateStackWalkCore(ThreadData thread if (skipInitialFrames) { - // Skip Frames below the initial managed frame's caller SP. All Frames - // below this SP belong to the current managed frame or frames pushed more - // recently (e.g., RedirectedThreadFrame from GC stress, active - // InlinedCallFrames from P/Invoke calls within the method). TargetPointer skipBelowSP; if (state == StackWalkState.SW_FRAMELESS) { @@ -158,13 +158,12 @@ private IEnumerable CreateStackWalkCore(ThreadData thread } } - // if the next Frame is not valid and we are not in managed code, there is nothing to return if (state == StackWalkState.SW_FRAME && !frameIterator.IsValid()) { yield break; } - StackWalkData stackWalkData = new(context, state, frameIterator, threadData, skipDuplicateActiveICF: skipInitialFrames); + StackWalkData stackWalkData = new(context, state, frameIterator, threadData, skipActiveICFOnce: skipInitialFrames); yield return stackWalkData.ToDataFrame(); stackWalkData.AdvanceIsFirst(); @@ -194,6 +193,7 @@ IReadOnlyList IStackWalk.WalkStackReferences(ThreadData thre bool reportGcReferences = gcFrame.ShouldCrawlFrameReportGCReferences; + TargetPointer pFrame = ((IStackWalk)this).GetFrameAddress(gcFrame.Frame); scanContext.UpdateScanContext( gcFrame.Frame.Context.StackPointer, @@ -207,50 +207,38 @@ IReadOnlyList IStackWalk.WalkStackReferences(ThreadData thre if (!IsManaged(gcFrame.Frame.Context.InstructionPointer, out CodeBlockHandle? cbh)) throw new InvalidOperationException("Expected managed code"); - // IsActiveFrame was computed during CreateStackWalk, matching native - // CrawlFrame::IsActiveFunc() semantics. Active frames report scratch - // registers; non-active frames skip them. CodeManagerFlags codeManagerFlags = gcFrame.Frame.IsActiveFrame ? CodeManagerFlags.ActiveStackFrame : 0; - // TODO(stackref): Wire up funclet parent frame flags from Filter: - // - ShouldParentToFuncletSkipReportingGCReferences → ParentOfFuncletStackFrame - // (tells GCInfoDecoder to skip reporting since funclet already reported) - // - ShouldParentFrameUseUnwindTargetPCforGCReporting → use exception's - // unwind target IP instead of current IP for GC liveness lookup - // - ShouldParentToFuncletReportSavedFuncletSlots → report funclet's - // callee-saved register slots from the parent frame - // These require careful validation to ensure Filter sets them correctly - // for all stack configurations before wiring them into EnumGcRefs. + if (gcFrame.ShouldParentToFuncletSkipReportingGCReferences) + codeManagerFlags |= CodeManagerFlags.ParentOfFuncletStackFrame; + + uint? relOffsetOverride = null; + if (gcFrame.ShouldParentFrameUseUnwindTargetPCforGCReporting) + { + _eman.GetGCInfo(cbh.Value, out TargetPointer gcInfoAddr, out uint gcVersion); + IGCInfoHandle gcHandle = _target.Contracts.GCInfo.DecodePlatformSpecificGCInfo(gcInfoAddr, gcVersion); + if (gcHandle is IGCInfoDecoder decoder) + { + relOffsetOverride = decoder.FindFirstInterruptiblePoint( + gcFrame.ClauseForCatchHandlerStartPC, + gcFrame.ClauseForCatchHandlerEndPC); + } + } GcScanner gcScanner = new(_target); - gcScanner.EnumGcRefs(gcFrame.Frame.Context, cbh.Value, codeManagerFlags, scanContext); + gcScanner.EnumGcRefs(gcFrame.Frame.Context, cbh.Value, codeManagerFlags, scanContext, relOffsetOverride); } else { - // Non-frameless: capital "F" Frame GcScanRoots dispatch. - // The base Frame::GcScanRoots_Impl is a no-op for most frame types. - // Frame types that override it (StubDispatchFrame, ExternalMethodFrame, - // CallCountingHelperFrame, DynamicHelperFrame, CLRToCOMMethodFrame, - // HijackFrame, ProtectValueClassFrame) call PromoteCallerStack to - // report method arguments from the transition block. - // - // GCFrame is NOT part of the Frame chain — it has its own linked list - // that the GC scans separately. The DAC's DacStackReferenceWalker - // does not scan GCFrame roots. - // - // For now, this is a no-op matching the base Frame behavior. - // TODO(stackref): Implement PromoteCallerStack for stub frames that - // report caller arguments (StubDispatchFrame, ExternalMethodFrame, etc.) ScanFrameRoots(gcFrame.Frame, scanContext); } } } catch (System.Exception ex) { - Debug.WriteLine($"Exception during WalkStackReferences: {ex}"); - // Matching native DAC behavior: capture errors, don't propagate + Debug.WriteLine($"Exception during WalkStackReferences at IP=0x{gcFrame.Frame.Context.InstructionPointer:X}: {ex.GetType().Name}: {ex.Message}"); } } @@ -276,20 +264,11 @@ public GCFrameData(StackDataFrameHandle frame) } public StackDataFrameHandle Frame { get; } - public bool IsFilterFunclet { get; set; } - public bool IsFilterFuncletCached { get; set; } public bool ShouldParentToFuncletSkipReportingGCReferences { get; set; } public bool ShouldCrawlFrameReportGCReferences { get; set; } // required public bool ShouldParentFrameUseUnwindTargetPCforGCReporting { get; set; } - public bool ShouldSaveFuncletInfo { get; set; } - public bool ShouldParentToFuncletReportSavedFuncletSlots { get; set; } - } - - private enum ForceGcReportingStage - { - Off, - LookForManagedFrame, - LookForMarkerFrame, + public uint ClauseForCatchHandlerStartPC { get; set; } + public uint ClauseForCatchHandlerEndPC { get; set; } } private IEnumerable Filter(IEnumerable handles) @@ -301,14 +280,10 @@ private IEnumerable Filter(IEnumerable handle bool processNonFilterFunclet = false; bool processIntermediaryNonFilterFunclet = false; bool didFuncletReportGCReferences = true; - bool funcletNotSeen = false; TargetPointer parentStackFrame = TargetPointer.Null; TargetPointer funcletParentStackFrame = TargetPointer.Null; TargetPointer intermediaryFuncletParentStackFrame; - ForceGcReportingStage forceReportingWhileSkipping = ForceGcReportingStage.Off; - bool foundFirstFunclet = false; - foreach (StackDataFrameHandle handle in handles) { GCFrameData gcFrame = new(handle); @@ -320,33 +295,16 @@ private IEnumerable Filter(IEnumerable handle bool skipFuncletCallback = true; TargetPointer pExInfo = GetCurrentExceptionTracker(handle); + TargetPointer frameSp = handle.State == StackWalkState.SW_FRAME ? handle.FrameAddress : handle.Context.StackPointer; if (pExInfo != TargetPointer.Null && frameSp > pExInfo) { if (!movedPastFirstExInfo) { - Data.ExceptionInfo exInfo = _target.ProcessedData.GetOrAdd(pExInfo); - // TODO: The native StackFrameIterator::Filter checks pExInfo->m_lastReportedFunclet.IP - // to handle the case where a finally funclet was reported in a previous GC run. - // This requires runtime support to persist LastReportedFuncletInfo on ExInfo, - // which is not yet implemented. Until then this block is unreachable. - if (exInfo.PassNumber == 2 && - exInfo.CSFEnclosingClause != TargetPointer.Null && - funcletParentStackFrame == TargetPointer.Null && - false) // TODO: check lastReportedFunclet.IP != 0 when runtime support is added - { - funcletParentStackFrame = exInfo.CSFEnclosingClause; - parentStackFrame = exInfo.CSFEnclosingClause; - processNonFilterFunclet = true; - didFuncletReportGCReferences = false; - funcletNotSeen = true; - } movedPastFirstExInfo = true; } } - gcFrame.ShouldParentToFuncletReportSavedFuncletSlots = false; - // by default, there is no funclet for the current frame // that reported GC references gcFrame.ShouldParentToFuncletSkipReportingGCReferences = false; @@ -354,8 +312,6 @@ private IEnumerable Filter(IEnumerable handle // by default, assume that we are going to report GC references gcFrame.ShouldCrawlFrameReportGCReferences = true; - gcFrame.ShouldSaveFuncletInfo = false; - // by default, assume that parent frame is going to report GC references from // the actual location reported by the stack walk gcFrame.ShouldParentFrameUseUnwindTargetPCforGCReporting = false; @@ -404,15 +360,6 @@ private IEnumerable Filter(IEnumerable handle // Set the parent frame so that the funclet skipping logic (below) can use it. parentStackFrame = intermediaryFuncletParentStackFrame; skippingFunclet = false; - - IPlatformAgnosticContext callerContext = handle.Context.Clone(); - callerContext.Unwind(_target); - if (!IsManaged(callerContext.InstructionPointer, out _)) - { - // Initiate force reporting of references in the new managed exception handling code frames. - // These frames are still alive when we are in a finally funclet. - forceReportingWhileSkipping = ForceGcReportingStage.LookForManagedFrame; - } } } } @@ -446,24 +393,6 @@ private IEnumerable Filter(IEnumerable handle // Set the parent frame so that the funclet skipping logic (below) can use it. parentStackFrame = funcletParentStackFrame; - if (!foundFirstFunclet && - pExInfo > handle.Context.StackPointer && - parentStackFrame > pExInfo) - { - Debug.Assert(pExInfo != TargetPointer.Null); - gcFrame.ShouldSaveFuncletInfo = true; - foundFirstFunclet = true; - } - - IPlatformAgnosticContext callerContext = handle.Context.Clone(); - callerContext.Unwind(_target); - if (!frameWasUnwound && IsManaged(callerContext.InstructionPointer, out _)) - { - // Initiate force reporting of references in the new managed exception handling code frames. - // These frames are still alive when we are in a finally funclet. - forceReportingWhileSkipping = ForceGcReportingStage.LookForManagedFrame; - } - // For non-filter funclets, we will make the callback for the funclet // but skip all the frames until we reach the parent method. When we do, // we will make a callback for it as well and then continue to make callbacks @@ -585,17 +514,11 @@ private IEnumerable Filter(IEnumerable handle gcFrame.ShouldParentFrameUseUnwindTargetPCforGCReporting = true; - // TODO(stackref): Is this required? - // gcFrame.ehClauseForCatch = exInfo.ClauseForCatch; + gcFrame.ClauseForCatchHandlerStartPC = exInfo.ClauseForCatchHandlerStartPC; + gcFrame.ClauseForCatchHandlerEndPC = exInfo.ClauseForCatchHandlerEndPC; } else if (!IsFunclet(handle)) { - if (funcletNotSeen) - { - gcFrame.ShouldParentToFuncletReportSavedFuncletSlots = true; - funcletNotSeen = false; - } - didFuncletReportGCReferences = true; } } @@ -623,26 +546,12 @@ private IEnumerable Filter(IEnumerable handle if (skipFuncletCallback) { - if (parentStackFrame != TargetPointer.Null && - forceReportingWhileSkipping == ForceGcReportingStage.Off) + if (parentStackFrame != TargetPointer.Null) { + // Skip intermediate frames between funclet and parent. + // The native runtime unconditionally skips these frames. break; } - - if (forceReportingWhileSkipping == ForceGcReportingStage.LookForManagedFrame) - { - // State indicating that the next marker frame should turn off the reporting again. That would be the caller of the managed RhThrowEx - forceReportingWhileSkipping = ForceGcReportingStage.LookForMarkerFrame; - // TODO(stackref): Implement marker frame detection. The native code checks - // if the caller IP is within DispatchManagedException / RhThrowEx to - // transition back to Off. Without this, force-reporting stays active - // indefinitely during funclet skipping. - } - - if (forceReportingWhileSkipping != ForceGcReportingStage.Off) - { - // TODO(stackref): add debug assert that we are in the EH code - } } } } @@ -657,6 +566,13 @@ private IEnumerable Filter(IEnumerable handle // Invoke the GC callback for this crawlframe (to keep any dynamic methods alive) but do not report its references. gcFrame.ShouldCrawlFrameReportGCReferences = false; } + else if (IsAtFirstPassExceptionThrowSite(handle)) + { + // During first-pass exception handling, the throw-site frame is + // being dispatched. The legacy DAC does not report GC refs from + // this frame during first pass. Suppress to match DAC behavior. + gcFrame.ShouldCrawlFrameReportGCReferences = false; + } } stop = true; @@ -710,6 +626,10 @@ private bool Next(StackWalkData handle) } break; case StackWalkState.SW_SKIPPED_FRAME: + // Skipped Frames still need UpdateContextFromFrame if they restore + // a context (e.g., SoftwareExceptionFrame, ResumableFrame). The native + // StackFrameIterator always calls UpdateRegDisplay for these frames. + handle.FrameIter.UpdateContextFromFrame(handle.Context); handle.FrameIter.Next(); break; case StackWalkState.SW_FRAME: @@ -718,6 +638,16 @@ private bool Next(StackWalkData handle) { handle.FrameIter.Next(); } + else + { + // Active InlinedCallFrame: FrameIter was NOT advanced. The next + // CheckForSkippedFrames would re-encounter this same Frame and + // create a spurious SW_SKIPPED_FRAME -> SW_FRAMELESS duplicate. + // Only applies to WalkStackReferences path — ClrDataStackWalk + // must yield Frames in the same order as the legacy DAC. + if (handle.SkipActiveICFOnce) + handle.SkipCurrentFrameInCheck = true; + } break; case StackWalkState.SW_ERROR: case StackWalkState.SW_COMPLETE: @@ -770,11 +700,12 @@ private bool CheckForSkippedFrames(StackWalkData handle) return false; } - // If the current Frame was already processed as SW_FRAME (active InlinedCallFrame - // that wasn't advanced), skip past it to avoid a duplicate SW_SKIPPED_FRAME yield. - // Only applies to WalkStackReferences (SkipDuplicateActiveICF=true). - if (handle.SkipDuplicateActiveICF && handle.FrameIter.IsInlineCallFrameWithActiveCall()) + // If the current Frame was already processed as SW_FRAME (e.g., an active + // InlinedCallFrame that wasn't advanced), skip it once to avoid a duplicate + // SW_SKIPPED_FRAME -> SW_FRAMELESS yield for the same managed IP. + if (handle.SkipCurrentFrameInCheck) { + handle.SkipCurrentFrameInCheck = false; handle.FrameIter.Next(); if (!handle.FrameIter.IsValid()) { @@ -877,10 +808,28 @@ private bool IsManaged(TargetPointer ip, [NotNullWhen(true)] out CodeBlockHandle return false; } - private unsafe void FillContextFromThread(IPlatformAgnosticContext context, ThreadData threadData) + private void FillContextFromThread(IPlatformAgnosticContext context, ThreadData threadData) { byte[] bytes = new byte[context.Size]; Span buffer = new Span(bytes); + + // Match the native DacStackReferenceWalker behavior: if the thread has a + // FilterContext or ProfilerFilterContext set, use that instead of calling + // GetThreadContext. During debugger breaks, GC stress redirection, or + // profiler stack walks, these contexts hold the correct managed frame state. + Data.Thread thread = _target.ProcessedData.GetOrAdd(threadData.ThreadAddress); + + TargetPointer filterContext = thread.DebuggerFilterContext; + if (filterContext == TargetPointer.Null) + filterContext = thread.ProfilerFilterContext; + + if (filterContext != TargetPointer.Null) + { + _target.ReadBuffer(filterContext.Value, buffer); + context.FillFromBuffer(buffer); + return; + } + // The underlying ICLRDataTarget.GetThreadContext has some variance depending on the host. // SOS's managed implementation sets the ContextFlags to platform specific values defined in ThreadService.cs (diagnostics repo) // SOS's native implementation keeps the ContextFlags passed into this function. @@ -915,29 +864,63 @@ private static StackDataFrameHandle AssertCorrectHandle(IStackDataFrameHandle st /// private void ScanFrameRoots(StackDataFrameHandle frame, GcScanContext scanContext) { - _ = scanContext; // Will be used when stub frame scanning is implemented - // Read the frame type identifier TargetPointer frameAddress = frame.FrameAddress; if (frameAddress == TargetPointer.Null) return; - // Get the frame name to identify the type - string frameName = ((IStackWalk)this).GetFrameName(frameAddress); + // Read the frame's VTable pointer (Identifier) to determine its type. + // GetFrameName expects a VTable identifier, not a frame address. + Data.Frame frameData = _target.ProcessedData.GetOrAdd(frameAddress); + string frameName = ((IStackWalk)this).GetFrameName(frameData.Identifier); - // Most frame types use the base no-op GcScanRoots_Impl. - // The ones that do work (stub frames) need PromoteCallerStack which - // requires reading the transition block and decoding method signatures. - // This is not yet implemented. switch (frameName) { case "StubDispatchFrame": + { + Data.FramedMethodFrame fmf = _target.ProcessedData.GetOrAdd(frameAddress); + Data.StubDispatchFrame sdf = _target.ProcessedData.GetOrAdd(frameAddress); + if (sdf.GCRefMap != TargetPointer.Null) + { + PromoteCallerStackUsingGCRefMap(fmf.TransitionBlockPtr, sdf.GCRefMap, scanContext); + } + else + { + PromoteCallerStackUsingMetaSig(frameAddress, fmf.TransitionBlockPtr, scanContext); + } + break; + } + case "ExternalMethodFrame": - case "CallCountingHelperFrame": + { + Data.FramedMethodFrame fmf = _target.ProcessedData.GetOrAdd(frameAddress); + Data.ExternalMethodFrame emf = _target.ProcessedData.GetOrAdd(frameAddress); + if (emf.GCRefMap != TargetPointer.Null) + { + PromoteCallerStackUsingGCRefMap(fmf.TransitionBlockPtr, emf.GCRefMap, scanContext); + } + break; + } + case "DynamicHelperFrame": + { + Data.FramedMethodFrame fmf = _target.ProcessedData.GetOrAdd(frameAddress); + Data.DynamicHelperFrame dhf = _target.ProcessedData.GetOrAdd(frameAddress); + ScanDynamicHelperFrame(fmf.TransitionBlockPtr, dhf.DynamicHelperFrameFlags, scanContext); + break; + } + + case "CallCountingHelperFrame": + case "PrestubMethodFrame": + { + Data.FramedMethodFrame fmf = _target.ProcessedData.GetOrAdd(frameAddress); + PromoteCallerStackUsingMetaSig(frameAddress, fmf.TransitionBlockPtr, scanContext); + break; + } + case "CLRToCOMMethodFrame": case "ComPrestubMethodFrame": // These frames call PromoteCallerStack to report method arguments. - // TODO(stackref): Implement PromoteCallerStack / PromoteCallerStackUsingGCRefMap + // TODO(stackref): Implement PromoteCallerStack for COM interop frames break; case "HijackFrame": @@ -952,9 +935,246 @@ private void ScanFrameRoots(StackDataFrameHandle frame, GcScanContext scanContex default: // Base Frame::GcScanRoots_Impl is a no-op — nothing to report. - // This covers: InlinedCallFrame, SoftwareExceptionFrame, FaultingExceptionFrame, - // ResumableFrame, FuncEvalFrame, PrestubMethodFrame, PInvokeCalliFrame, etc. break; } } + + /// + /// Decodes a GCRefMap bitstream and reports GC references in the transition block. + /// Port of native TransitionFrame::PromoteCallerStackUsingGCRefMap (frames.cpp). + /// + private void PromoteCallerStackUsingGCRefMap( + TargetPointer transitionBlock, + TargetPointer gcRefMapBlob, + GcScanContext scanContext) + { + GCRefMapDecoder decoder = new(_target, gcRefMapBlob); + + // x86: skip stack pop count + if (_target.PointerSize == 4) + decoder.ReadStackPop(); + + while (!decoder.AtEnd) + { + int pos = decoder.CurrentPos; + GCRefMapToken token = decoder.ReadToken(); + uint offset = OffsetFromGCRefMapPos(pos); + TargetPointer slotAddress = new(transitionBlock.Value + offset); + + switch (token) + { + case GCRefMapToken.Skip: + break; + + case GCRefMapToken.Ref: + scanContext.GCReportCallback(slotAddress, GcScanFlags.None); + break; + + case GCRefMapToken.Interior: + scanContext.GCReportCallback(slotAddress, GcScanFlags.GC_CALL_INTERIOR); + break; + + case GCRefMapToken.MethodParam: + case GCRefMapToken.TypeParam: + // The DAC skips these (guarded by #ifndef DACCESS_COMPILE in native). + // They represent loader allocator references, not managed GC refs. + break; + + case GCRefMapToken.VASigCookie: + // VASigCookie requires MetaSig parsing — not yet implemented. + // TODO(stackref): Implement VASIG_COOKIE handling + break; + } + } + } + + /// + /// Converts a GCRefMap position to a byte offset within the transition block. + /// Port of native OffsetFromGCRefMapPos (frames.cpp:1624-1633). + /// + private uint OffsetFromGCRefMapPos(int pos) + { + uint firstSlotOffset = _target.ReadGlobal(Constants.Globals.TransitionBlockOffsetOfFirstGCRefMapSlot); + + return firstSlotOffset + (uint)(pos * _target.PointerSize); + } + + /// + /// Scans GC roots for a DynamicHelperFrame based on its flags. + /// Port of native DynamicHelperFrame::GcScanRoots_Impl (frames.cpp:1071-1105). + /// + private void ScanDynamicHelperFrame( + TargetPointer transitionBlock, + int dynamicHelperFrameFlags, + GcScanContext scanContext) + { + const int DynamicHelperFrameFlags_ObjectArg = 1; + const int DynamicHelperFrameFlags_ObjectArg2 = 2; + + uint argRegOffset = _target.ReadGlobal(Constants.Globals.TransitionBlockOffsetOfArgumentRegisters); + + if ((dynamicHelperFrameFlags & DynamicHelperFrameFlags_ObjectArg) != 0) + { + TargetPointer argAddr = new(transitionBlock.Value + argRegOffset); + // On x86, this would need offsetof(ArgumentRegisters, ECX) adjustment. + // For AMD64/ARM64, the first argument register is at the base offset. + scanContext.GCReportCallback(argAddr, GcScanFlags.None); + } + + if ((dynamicHelperFrameFlags & DynamicHelperFrameFlags_ObjectArg2) != 0) + { + TargetPointer argAddr = new(transitionBlock.Value + argRegOffset + (uint)_target.PointerSize); + // On x86, this would need offsetof(ArgumentRegisters, EDX) adjustment. + // For AMD64/ARM64, the second argument is pointer-size after the first. + scanContext.GCReportCallback(argAddr, GcScanFlags.None); + } + } + + /// + /// Promotes caller stack GC references by parsing the method signature via MetaSig. + /// Used when a frame has no precomputed GCRefMap (e.g., dynamic/LCG methods). + /// Port of native TransitionFrame::PromoteCallerStack + PromoteCallerStackHelper (frames.cpp). + /// + private void PromoteCallerStackUsingMetaSig( + TargetPointer frameAddress, + TargetPointer transitionBlock, + GcScanContext scanContext) + { + Data.FramedMethodFrame fmf = _target.ProcessedData.GetOrAdd(frameAddress); + TargetPointer methodDescPtr = fmf.MethodDescPtr; + if (methodDescPtr == TargetPointer.Null) + return; + + ReadOnlySpan signature; + try + { + signature = GetMethodSignatureBytes(methodDescPtr); + } + catch (System.Exception) + { + return; + } + + if (signature.IsEmpty) + return; + + CorSigParser parser = new(signature, _target.PointerSize); + + // Parse calling convention + byte callingConvByte = parser.ReadByte(); + bool hasThis = (callingConvByte & 0x20) != 0; // IMAGE_CEE_CS_CALLCONV_HASTHIS + bool isGeneric = (callingConvByte & 0x10) != 0; + + if (isGeneric) + parser.ReadCompressedUInt(); // skip generic param count + + uint paramCount = parser.ReadCompressedUInt(); + + // Skip return type + parser.SkipType(); + + // Walk through GCRefMap positions. + // The position numbering matches how GCRefMap encodes slots: + // ARM64: pos 0 = RetBuf (x8), pos 1+ = argument registers (x0-x7), then stack + // Others: pos 0 = first argument register/slot, etc. + int pos = 0; + + // On ARM64, position 0 is the return buffer register (x8). + // Methods without a return buffer skip this slot. + // TODO: detect HasRetBuf from the signature's return type when needed. + // For now, we skip the retbuf slot on ARM64 since the common case + // (dynamic invoke stubs) doesn't use return buffers. + bool isArm64 = IsTargetArm64(); + if (isArm64) + pos++; + + // Promote 'this' if present + if (hasThis) + { + uint offset = OffsetFromGCRefMapPos(pos); + TargetPointer slotAddress = new(transitionBlock.Value + offset); + scanContext.GCReportCallback(slotAddress, GcScanFlags.None); + pos++; + } + + // Walk each parameter + for (uint i = 0; i < paramCount; i++) + { + uint offset = OffsetFromGCRefMapPos(pos); + TargetPointer slotAddress = new(transitionBlock.Value + offset); + + GcTypeKind kind = parser.ReadTypeAndClassify(); + + switch (kind) + { + case GcTypeKind.Ref: + scanContext.GCReportCallback(slotAddress, GcScanFlags.None); + break; + + case GcTypeKind.Interior: + scanContext.GCReportCallback(slotAddress, GcScanFlags.GC_CALL_INTERIOR); + break; + + case GcTypeKind.Other: + // Value types may contain embedded GC references. + // Full scanning requires reading the MethodTable's GCDesc. + // TODO(stackref): Implement value type GCDesc scanning for MetaSig path. + break; + + case GcTypeKind.None: + break; + } + + pos++; + } + } + + /// + /// Gets the raw signature bytes for a MethodDesc. + /// For StoredSigMethodDesc (dynamic, array, EEImpl methods), reads the embedded signature. + /// For normal IL methods, reads from module metadata. + /// + private ReadOnlySpan GetMethodSignatureBytes(TargetPointer methodDescPtr) + { + IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem; + MethodDescHandle mdh = rts.GetMethodDescHandle(methodDescPtr); + + // Try StoredSigMethodDesc first (dynamic/LCG/array methods) + if (rts.IsStoredSigMethodDesc(mdh, out ReadOnlySpan storedSig)) + return storedSig; + + // Normal IL methods: get signature from metadata + uint methodToken = rts.GetMethodToken(mdh); + if (methodToken == 0x06000000) // mdtMethodDef with RID 0 = no token + return default; + + TargetPointer methodTablePtr = rts.GetMethodTable(mdh); + TypeHandle typeHandle = rts.GetTypeHandle(methodTablePtr); + TargetPointer modulePtr = rts.GetModule(typeHandle); + + ILoader loader = _target.Contracts.Loader; + ModuleHandle moduleHandle = loader.GetModuleHandleFromModulePtr(modulePtr); + + IEcmaMetadata ecmaMetadata = _target.Contracts.EcmaMetadata; + MetadataReader? mdReader = ecmaMetadata.GetMetadata(moduleHandle); + if (mdReader is null) + return default; + + MethodDefinitionHandle methodDefHandle = MetadataTokens.MethodDefinitionHandle((int)(methodToken & 0x00FFFFFF)); + MethodDefinition methodDef = mdReader.GetMethodDefinition(methodDefHandle); + BlobReader blobReader = mdReader.GetBlobReader(methodDef.Signature); + return blobReader.ReadBytes(blobReader.Length); + } + + /// + /// Detects if the target architecture is ARM64 based on TransitionBlock layout. + /// On ARM64, GetOffsetOfFirstGCRefMapSlot != GetOffsetOfArgumentRegisters + /// (because the first GCRefMap slot is the x8 RetBuf register, not x0). + /// + private bool IsTargetArm64() + { + uint firstGCRefMapSlot = _target.ReadGlobal(Constants.Globals.TransitionBlockOffsetOfFirstGCRefMapSlot); + uint argRegsOffset = _target.ReadGlobal(Constants.Globals.TransitionBlockOffsetOfArgumentRegisters); + return firstGCRefMapSlot != argRegsOffset; + } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ExceptionInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ExceptionInfo.cs index 8f2470d6e71996..c5d5eaffaf43fd 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ExceptionInfo.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ExceptionInfo.cs @@ -23,6 +23,8 @@ public ExceptionInfo(Target target, TargetPointer address) CSFEHClause = target.ReadPointer(address + (ulong)type.Fields[nameof(CSFEHClause)].Offset); CSFEnclosingClause = target.ReadPointer(address + (ulong)type.Fields[nameof(CSFEnclosingClause)].Offset); CallerOfActualHandlerFrame = target.ReadPointer(address + (ulong)type.Fields[nameof(CallerOfActualHandlerFrame)].Offset); + ClauseForCatchHandlerStartPC = target.Read(address + (ulong)type.Fields[nameof(ClauseForCatchHandlerStartPC)].Offset); + ClauseForCatchHandlerEndPC = target.Read(address + (ulong)type.Fields[nameof(ClauseForCatchHandlerEndPC)].Offset); } public TargetPointer PreviousNestedInfo { get; } @@ -35,4 +37,6 @@ public ExceptionInfo(Target target, TargetPointer address) public TargetPointer CSFEHClause { get; } public TargetPointer CSFEnclosingClause { get; } public TargetPointer CallerOfActualHandlerFrame { get; } + public uint ClauseForCatchHandlerStartPC { get; } + public uint ClauseForCatchHandlerEndPC { get; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/DynamicHelperFrame.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/DynamicHelperFrame.cs new file mode 100644 index 00000000000000..652b60fb7bb49d --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/DynamicHelperFrame.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.DataContractReader.Data; + +internal class DynamicHelperFrame : IData +{ + static DynamicHelperFrame IData.Create(Target target, TargetPointer address) + => new DynamicHelperFrame(target, address); + + public DynamicHelperFrame(Target target, TargetPointer address) + { + Target.TypeInfo type = target.GetTypeInfo(DataType.DynamicHelperFrame); + DynamicHelperFrameFlags = target.Read(address + (ulong)type.Fields[nameof(DynamicHelperFrameFlags)].Offset); + } + + public int DynamicHelperFrameFlags { get; } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/ExternalMethodFrame.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/ExternalMethodFrame.cs new file mode 100644 index 00000000000000..1a07c91757f705 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/ExternalMethodFrame.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.DataContractReader.Data; + +internal class ExternalMethodFrame : IData +{ + static ExternalMethodFrame IData.Create(Target target, TargetPointer address) + => new ExternalMethodFrame(target, address); + + public ExternalMethodFrame(Target target, TargetPointer address) + { + Target.TypeInfo type = target.GetTypeInfo(DataType.ExternalMethodFrame); + GCRefMap = target.ReadPointer(address + (ulong)type.Fields[nameof(GCRefMap)].Offset); + } + + public TargetPointer GCRefMap { get; } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/StubDispatchFrame.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/StubDispatchFrame.cs index f4e677dafddaa9..07d9f199523eb5 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/StubDispatchFrame.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Frames/StubDispatchFrame.cs @@ -14,6 +14,7 @@ public StubDispatchFrame(Target target, TargetPointer address) MethodDescPtr = target.ReadPointer(address + (ulong)type.Fields[nameof(MethodDescPtr)].Offset); RepresentativeMTPtr = target.ReadPointer(address + (ulong)type.Fields[nameof(RepresentativeMTPtr)].Offset); RepresentativeSlot = target.Read(address + (ulong)type.Fields[nameof(RepresentativeSlot)].Offset); + GCRefMap = target.ReadPointer(address + (ulong)type.Fields[nameof(GCRefMap)].Offset); Address = address; } @@ -21,4 +22,5 @@ public StubDispatchFrame(Target target, TargetPointer address) public TargetPointer MethodDescPtr { get; } public TargetPointer RepresentativeMTPtr { get; } public uint RepresentativeSlot { get; } + public TargetPointer GCRefMap { get; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Thread.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Thread.cs index 9e78142e7c97af..f66e4a7dc85a9b 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Thread.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Thread.cs @@ -40,6 +40,9 @@ public Thread(Target target, TargetPointer address) ? target.ReadPointer(address + (ulong)watsonFieldInfo.Offset) : TargetPointer.Null; ThreadLocalDataPtr = target.ReadPointer(address + (ulong)type.Fields[nameof(ThreadLocalDataPtr)].Offset); + + DebuggerFilterContext = target.ReadPointer(address + (ulong)type.Fields[nameof(DebuggerFilterContext)].Offset); + ProfilerFilterContext = target.ReadPointer(address + (ulong)type.Fields[nameof(ProfilerFilterContext)].Offset); } public uint Id { get; init; } @@ -56,4 +59,6 @@ public Thread(Target target, TargetPointer address) public TargetPointer ExceptionTracker { get; init; } public TargetPointer UEWatsonBucketTrackerBuckets { get; init; } public TargetPointer ThreadLocalDataPtr { get; init; } + public TargetPointer DebuggerFilterContext { get; init; } + public TargetPointer ProfilerFilterContext { get; init; } } diff --git a/src/native/managed/cdac/cdac.slnx b/src/native/managed/cdac/cdac.slnx index 7449d30624ec2d..4abe615fe50f3b 100644 --- a/src/native/managed/cdac/cdac.slnx +++ b/src/native/managed/cdac/cdac.slnx @@ -14,5 +14,6 @@ + diff --git a/src/native/managed/cdac/tests/GCStressTests/BasicGCStressTests.cs b/src/native/managed/cdac/tests/GCStressTests/BasicGCStressTests.cs new file mode 100644 index 00000000000000..45a83b2694e87a --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/BasicGCStressTests.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Runtime.InteropServices; +using Microsoft.DotNet.XUnitExtensions; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.DataContractReader.Tests.GCStress; + +/// +/// Runs each debuggee app under corerun with DOTNET_GCStress=0x24 and asserts +/// that the cDAC stack reference verification achieves 100% pass rate. +/// +/// +/// Prerequisites: +/// - Build CoreCLR native + cDAC: build.cmd -subset clr.native+tools.cdac -c Debug -rc Checked -lc Release +/// - Generate core_root: src\tests\build.cmd Checked generatelayoutonly /p:LibrariesConfiguration=Release +/// - Build debuggees: dotnet build this test project +/// +/// The tests use CORE_ROOT env var if set, otherwise default to the standard artifacts path. +/// +public class BasicGCStressTests : GCStressTestBase +{ + public BasicGCStressTests(ITestOutputHelper output) : base(output) { } + + public static IEnumerable Debuggees => + [ + ["BasicAlloc"], + ["DeepStack"], + ["Generics"], + ["MultiThread"], + ["Comprehensive"], + ["ExceptionHandling"], + ]; + + public static IEnumerable WindowsOnlyDebuggees => + [ + ["PInvoke"], + ]; + + [Theory] + [MemberData(nameof(Debuggees))] + public void GCStress_AllVerificationsPass(string debuggeeName) + { + GCStressResults results = RunGCStress(debuggeeName); + AssertAllPassed(results, debuggeeName); + } + + [Theory] + [MemberData(nameof(WindowsOnlyDebuggees))] + public void GCStress_WindowsOnly_AllVerificationsPass(string debuggeeName) + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + throw new SkipTestException("P/Invoke debuggee uses kernel32.dll (Windows only)"); + + GCStressResults results = RunGCStress(debuggeeName); + AssertAllPassed(results, debuggeeName); + } +} diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/BasicAlloc/BasicAlloc.csproj b/src/native/managed/cdac/tests/GCStressTests/Debuggees/BasicAlloc/BasicAlloc.csproj new file mode 100644 index 00000000000000..6b512ec9245ec3 --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/BasicAlloc/BasicAlloc.csproj @@ -0,0 +1 @@ + diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/BasicAlloc/Program.cs b/src/native/managed/cdac/tests/GCStressTests/Debuggees/BasicAlloc/Program.cs new file mode 100644 index 00000000000000..f886c0ef72cefe --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/BasicAlloc/Program.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.CompilerServices; + +/// +/// Exercises basic object allocation patterns: objects, strings, arrays. +/// +internal static class Program +{ + [MethodImpl(MethodImplOptions.NoInlining)] + static object AllocAndHold() + { + object o = new object(); + string s = "hello world"; + int[] arr = new int[] { 1, 2, 3 }; + byte[] buf = new byte[256]; + GC.KeepAlive(o); + GC.KeepAlive(s); + GC.KeepAlive(arr); + GC.KeepAlive(buf); + return o; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void ManyLiveRefs() + { + object r0 = new object(); + object r1 = new object(); + object r2 = new object(); + object r3 = new object(); + object r4 = new object(); + object r5 = new object(); + object r6 = new object(); + object r7 = new object(); + string r8 = "live-string"; + int[] r9 = new int[10]; + + GC.KeepAlive(r0); GC.KeepAlive(r1); + GC.KeepAlive(r2); GC.KeepAlive(r3); + GC.KeepAlive(r4); GC.KeepAlive(r5); + GC.KeepAlive(r6); GC.KeepAlive(r7); + GC.KeepAlive(r8); GC.KeepAlive(r9); + } + + static int Main() + { + for (int i = 0; i < 2; i++) + { + AllocAndHold(); + ManyLiveRefs(); + } + return 100; + } +} diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/Comprehensive/Comprehensive.csproj b/src/native/managed/cdac/tests/GCStressTests/Debuggees/Comprehensive/Comprehensive.csproj new file mode 100644 index 00000000000000..6b512ec9245ec3 --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/Comprehensive/Comprehensive.csproj @@ -0,0 +1 @@ + diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/Comprehensive/Program.cs b/src/native/managed/cdac/tests/GCStressTests/Debuggees/Comprehensive/Program.cs new file mode 100644 index 00000000000000..6a2f26f146ef0f --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/Comprehensive/Program.cs @@ -0,0 +1,253 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; + +/// +/// All-in-one comprehensive debuggee that exercises every scenario +/// in a single run: allocations, exceptions, generics, P/Invoke, threading. +/// +internal static class Program +{ + interface IKeepAlive { object GetRef(); } + class BoxHolder : IKeepAlive + { + object _value; + public BoxHolder() { _value = new object(); } + public BoxHolder(object v) { _value = v; } + [MethodImpl(MethodImplOptions.NoInlining)] + public object GetRef() => _value; + } + + struct LargeStruct { public object A, B, C, D; } + + [MethodImpl(MethodImplOptions.NoInlining)] + static object AllocAndHold() + { + object o = new object(); + string s = "hello world"; + int[] arr = new int[] { 1, 2, 3 }; + GC.KeepAlive(o); + GC.KeepAlive(s); + GC.KeepAlive(arr); + return o; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void NestedCall(int depth) + { + object o = new object(); + if (depth > 0) + NestedCall(depth - 1); + GC.KeepAlive(o); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void TryCatchScenario() + { + object before = new object(); + try + { + throw new InvalidOperationException("test"); + } + catch (InvalidOperationException ex) + { + object inCatch = new object(); + GC.KeepAlive(ex); + GC.KeepAlive(inCatch); + } + GC.KeepAlive(before); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void TryFinallyScenario() + { + object outerRef = new object(); + try + { + object innerRef = new object(); + GC.KeepAlive(innerRef); + } + finally + { + object finallyRef = new object(); + GC.KeepAlive(finallyRef); + } + GC.KeepAlive(outerRef); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void NestedExceptionScenario() + { + object a = new object(); + try + { + try + { + throw new ArgumentException("inner"); + } + catch (ArgumentException ex1) + { + GC.KeepAlive(ex1); + throw new InvalidOperationException("outer", ex1); + } + } + catch (InvalidOperationException ex2) + { + GC.KeepAlive(ex2); + } + GC.KeepAlive(a); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void FilterExceptionScenario() + { + object holder = new object(); + try + { + throw new ArgumentException("filter-test"); + } + catch (ArgumentException ex) when (FilterCheck(ex)) + { + GC.KeepAlive(ex); + } + GC.KeepAlive(holder); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static bool FilterCheck(Exception ex) + { + object filterLocal = new object(); + GC.KeepAlive(filterLocal); + return ex.Message.Contains("filter"); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static T GenericAlloc() where T : new() + { + T val = new T(); + object marker = new object(); + GC.KeepAlive(marker); + return val; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void InterfaceDispatchScenario() + { + IKeepAlive holder = new BoxHolder(new int[] { 42, 43 }); + object r = holder.GetRef(); + GC.KeepAlive(holder); + GC.KeepAlive(r); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void DelegateScenario() + { + object captured = new object(); + Func fn = () => { GC.KeepAlive(captured); return new object(); }; + object result = fn(); + GC.KeepAlive(result); + GC.KeepAlive(fn); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void StructWithRefsScenario() + { + LargeStruct ls; + ls.A = new object(); + ls.B = "struct-string"; + ls.C = new int[] { 10, 20 }; + ls.D = new BoxHolder(ls.A); + GC.KeepAlive(ls.A); + GC.KeepAlive(ls.B); + GC.KeepAlive(ls.C); + GC.KeepAlive(ls.D); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void PinnedScenario() + { + byte[] buffer = new byte[64]; + GCHandle pin = GCHandle.Alloc(buffer, GCHandleType.Pinned); + try + { + object other = new object(); + GC.KeepAlive(other); + GC.KeepAlive(buffer); + } + finally + { + pin.Free(); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void MultiThreadScenario() + { + ManualResetEventSlim ready = new ManualResetEventSlim(false); + ManualResetEventSlim go = new ManualResetEventSlim(false); + Thread t = new Thread(() => + { + object threadLocal = new object(); + ready.Set(); + go.Wait(); + NestedCall(5); + GC.KeepAlive(threadLocal); + }); + t.Start(); + ready.Wait(); + go.Set(); + NestedCall(3); + t.Join(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void RethrowScenario() + { + object outerRef = new object(); + try + { + try + { + throw new ApplicationException("rethrow-test"); + } + catch (ApplicationException) + { + object catchRef = new object(); + GC.KeepAlive(catchRef); + throw; + } + } + catch (ApplicationException ex) + { + GC.KeepAlive(ex); + } + GC.KeepAlive(outerRef); + } + + static int Main() + { + for (int i = 0; i < 2; i++) + { + AllocAndHold(); + NestedCall(5); + TryCatchScenario(); + TryFinallyScenario(); + NestedExceptionScenario(); + FilterExceptionScenario(); + GenericAlloc(); + GenericAlloc>(); + InterfaceDispatchScenario(); + DelegateScenario(); + StructWithRefsScenario(); + PinnedScenario(); + MultiThreadScenario(); + RethrowScenario(); + } + return 100; + } +} diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/DeepStack/DeepStack.csproj b/src/native/managed/cdac/tests/GCStressTests/Debuggees/DeepStack/DeepStack.csproj new file mode 100644 index 00000000000000..6b512ec9245ec3 --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/DeepStack/DeepStack.csproj @@ -0,0 +1 @@ + diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/DeepStack/Program.cs b/src/native/managed/cdac/tests/GCStressTests/Debuggees/DeepStack/Program.cs new file mode 100644 index 00000000000000..c98679aea54ac2 --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/DeepStack/Program.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.CompilerServices; + +/// +/// Exercises deep recursion with live GC references at each frame level. +/// +internal static class Program +{ + [MethodImpl(MethodImplOptions.NoInlining)] + static void NestedCall(int depth) + { + object o = new object(); + if (depth > 0) + NestedCall(depth - 1); + GC.KeepAlive(o); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void NestedWithMultipleRefs(int depth) + { + object a = new object(); + string b = $"depth-{depth}"; + int[] c = new int[depth + 1]; + if (depth > 0) + NestedWithMultipleRefs(depth - 1); + GC.KeepAlive(a); + GC.KeepAlive(b); + GC.KeepAlive(c); + } + + static int Main() + { + for (int i = 0; i < 2; i++) + { + NestedCall(10); + NestedWithMultipleRefs(8); + } + return 100; + } +} diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/Directory.Build.props b/src/native/managed/cdac/tests/GCStressTests/Debuggees/Directory.Build.props new file mode 100644 index 00000000000000..eca2240b31f08c --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/Directory.Build.props @@ -0,0 +1,15 @@ + + + + + Exe + $(NetCoreAppToolCurrent) + true + enable + $(ArtifactsBinDir)GCStressTests\$(MSBuildProjectName)\$(Configuration)\ + true + + false + $(NoWarn);SA1400;IDE0059;SYSLIB1054;CA1852;CA1861 + + diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/ExceptionHandling/ExceptionHandling.csproj b/src/native/managed/cdac/tests/GCStressTests/Debuggees/ExceptionHandling/ExceptionHandling.csproj new file mode 100644 index 00000000000000..6b512ec9245ec3 --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/ExceptionHandling/ExceptionHandling.csproj @@ -0,0 +1 @@ + diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/ExceptionHandling/Program.cs b/src/native/managed/cdac/tests/GCStressTests/Debuggees/ExceptionHandling/Program.cs new file mode 100644 index 00000000000000..4bd0a12fe6d145 --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/ExceptionHandling/Program.cs @@ -0,0 +1,143 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.CompilerServices; + +/// +/// Exercises exception handling: try/catch/finally funclets, nested exceptions, +/// filter funclets, and rethrow. +/// +internal static class Program +{ + [MethodImpl(MethodImplOptions.NoInlining)] + static void TryCatchScenario() + { + object before = new object(); + try + { + object inside = new object(); + ThrowHelper(); + GC.KeepAlive(inside); + } + catch (InvalidOperationException ex) + { + object inCatch = new object(); + GC.KeepAlive(ex); + GC.KeepAlive(inCatch); + } + GC.KeepAlive(before); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void ThrowHelper() + { + throw new InvalidOperationException("test exception"); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void TryFinallyScenario() + { + object outerRef = new object(); + try + { + object innerRef = new object(); + GC.KeepAlive(innerRef); + } + finally + { + object finallyRef = new object(); + GC.KeepAlive(finallyRef); + } + GC.KeepAlive(outerRef); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void NestedExceptionScenario() + { + object a = new object(); + try + { + try + { + object c = new object(); + throw new ArgumentException("inner"); + } + catch (ArgumentException ex1) + { + GC.KeepAlive(ex1); + throw new InvalidOperationException("outer", ex1); + } + finally + { + object d = new object(); + GC.KeepAlive(d); + } + } + catch (InvalidOperationException ex2) + { + GC.KeepAlive(ex2); + } + GC.KeepAlive(a); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void FilterExceptionScenario() + { + object holder = new object(); + try + { + throw new ArgumentException("filter-test"); + } + catch (ArgumentException ex) when (FilterCheck(ex)) + { + GC.KeepAlive(ex); + } + GC.KeepAlive(holder); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static bool FilterCheck(Exception ex) + { + object filterLocal = new object(); + GC.KeepAlive(filterLocal); + return ex.Message.Contains("filter"); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void RethrowScenario() + { + object outerRef = new object(); + try + { + try + { + throw new ApplicationException("rethrow-test"); + } + catch (ApplicationException) + { + object catchRef = new object(); + GC.KeepAlive(catchRef); + throw; + } + } + catch (ApplicationException ex) + { + GC.KeepAlive(ex); + } + GC.KeepAlive(outerRef); + } + + static int Main() + { + for (int i = 0; i < 2; i++) + { + TryCatchScenario(); + TryFinallyScenario(); + NestedExceptionScenario(); + FilterExceptionScenario(); + RethrowScenario(); + } + return 100; + } +} diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/Generics/Generics.csproj b/src/native/managed/cdac/tests/GCStressTests/Debuggees/Generics/Generics.csproj new file mode 100644 index 00000000000000..6b512ec9245ec3 --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/Generics/Generics.csproj @@ -0,0 +1 @@ + diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/Generics/Program.cs b/src/native/managed/cdac/tests/GCStressTests/Debuggees/Generics/Program.cs new file mode 100644 index 00000000000000..54b7060c040f5a --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/Generics/Program.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +/// +/// Exercises generic method instantiations and interface dispatch. +/// +internal static class Program +{ + interface IKeepAlive + { + object GetRef(); + } + + class BoxHolder : IKeepAlive + { + object _value; + public BoxHolder() { _value = new object(); } + public BoxHolder(object v) { _value = v; } + + [MethodImpl(MethodImplOptions.NoInlining)] + public object GetRef() => _value; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static T GenericAlloc() where T : new() + { + T val = new T(); + object marker = new object(); + GC.KeepAlive(marker); + return val; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void GenericScenario() + { + var o = GenericAlloc(); + var l = GenericAlloc>(); + var s = GenericAlloc(); + GC.KeepAlive(o); + GC.KeepAlive(l); + GC.KeepAlive(s); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void InterfaceDispatchScenario() + { + IKeepAlive holder = new BoxHolder(new int[] { 42, 43 }); + object r = holder.GetRef(); + GC.KeepAlive(holder); + GC.KeepAlive(r); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void DelegateScenario() + { + object captured = new object(); + Func fn = () => + { + GC.KeepAlive(captured); + return new object(); + }; + object result = fn(); + GC.KeepAlive(result); + GC.KeepAlive(fn); + } + + static int Main() + { + for (int i = 0; i < 2; i++) + { + GenericScenario(); + InterfaceDispatchScenario(); + DelegateScenario(); + } + return 100; + } +} diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/MultiThread/MultiThread.csproj b/src/native/managed/cdac/tests/GCStressTests/Debuggees/MultiThread/MultiThread.csproj new file mode 100644 index 00000000000000..6b512ec9245ec3 --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/MultiThread/MultiThread.csproj @@ -0,0 +1 @@ + diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/MultiThread/Program.cs b/src/native/managed/cdac/tests/GCStressTests/Debuggees/MultiThread/Program.cs new file mode 100644 index 00000000000000..0eea731a6bd313 --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/MultiThread/Program.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.CompilerServices; +using System.Threading; + +/// +/// Exercises concurrent threads with GC references, exercising multi-threaded +/// stack walks and GC ref enumeration. +/// +internal static class Program +{ + [MethodImpl(MethodImplOptions.NoInlining)] + static void NestedCall(int depth) + { + object o = new object(); + if (depth > 0) + NestedCall(depth - 1); + GC.KeepAlive(o); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void ThreadWork(int id) + { + object threadLocal = new object(); + string threadName = $"thread-{id}"; + NestedCall(5); + GC.KeepAlive(threadLocal); + GC.KeepAlive(threadName); + } + + static int Main() + { + for (int iteration = 0; iteration < 2; iteration++) + { + ManualResetEventSlim ready = new ManualResetEventSlim(false); + ManualResetEventSlim go = new ManualResetEventSlim(false); + Thread t = new Thread(() => + { + ready.Set(); + go.Wait(); + ThreadWork(1); + }); + t.Start(); + ready.Wait(); + go.Set(); + ThreadWork(0); + t.Join(); + } + return 100; + } +} diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/PInvoke/PInvoke.csproj b/src/native/managed/cdac/tests/GCStressTests/Debuggees/PInvoke/PInvoke.csproj new file mode 100644 index 00000000000000..6b512ec9245ec3 --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/PInvoke/PInvoke.csproj @@ -0,0 +1 @@ + diff --git a/src/native/managed/cdac/tests/GCStressTests/Debuggees/PInvoke/Program.cs b/src/native/managed/cdac/tests/GCStressTests/Debuggees/PInvoke/Program.cs new file mode 100644 index 00000000000000..83aece921baaea --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Debuggees/PInvoke/Program.cs @@ -0,0 +1,74 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +/// +/// Exercises P/Invoke transitions with GC references before and after native calls, +/// and pinned GC handles. +/// +internal static class Program +{ + [DllImport("kernel32.dll")] + static extern uint GetCurrentThreadId(); + + [MethodImpl(MethodImplOptions.NoInlining)] + static void PInvokeScenario() + { + object before = new object(); + uint tid = GetCurrentThreadId(); + object after = new object(); + GC.KeepAlive(before); + GC.KeepAlive(after); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void PinnedScenario() + { + byte[] buffer = new byte[64]; + GCHandle pin = GCHandle.Alloc(buffer, GCHandleType.Pinned); + try + { + object other = new object(); + GC.KeepAlive(other); + GC.KeepAlive(buffer); + } + finally + { + pin.Free(); + } + } + + struct LargeStruct + { + public object A, B, C, D; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void StructWithRefsScenario() + { + LargeStruct ls; + ls.A = new object(); + ls.B = "struct-string"; + ls.C = new int[] { 10, 20 }; + ls.D = new object(); + GC.KeepAlive(ls.A); + GC.KeepAlive(ls.B); + GC.KeepAlive(ls.C); + GC.KeepAlive(ls.D); + } + + static int Main() + { + for (int i = 0; i < 2; i++) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + PInvokeScenario(); + PinnedScenario(); + StructWithRefsScenario(); + } + return 100; + } +} diff --git a/src/native/managed/cdac/tests/GCStressTests/GCStressResults.cs b/src/native/managed/cdac/tests/GCStressTests/GCStressResults.cs new file mode 100644 index 00000000000000..4004740bbcdcdb --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/GCStressResults.cs @@ -0,0 +1,77 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.RegularExpressions; + +namespace Microsoft.Diagnostics.DataContractReader.Tests.GCStress; + +/// +/// Parses the cdac-gcstress results log file written by the native cdacgcstress.cpp hook. +/// +internal sealed partial class GCStressResults +{ + public int TotalVerifications { get; private set; } + public int Passed { get; private set; } + public int Failed { get; private set; } + public int Skipped { get; private set; } + public string LogFilePath { get; private set; } = ""; + public List FailureDetails { get; } = []; + public List SkipDetails { get; } = []; + + [GeneratedRegex(@"^\[PASS\]")] + private static partial Regex PassPattern(); + + [GeneratedRegex(@"^\[FAIL\]")] + private static partial Regex FailPattern(); + + [GeneratedRegex(@"^\[SKIP\]")] + private static partial Regex SkipPattern(); + + [GeneratedRegex(@"^Total verifications:\s*(\d+)")] + private static partial Regex TotalPattern(); + + public static GCStressResults Parse(string logFilePath) + { + if (!File.Exists(logFilePath)) + throw new FileNotFoundException($"GC stress results log not found: {logFilePath}"); + + var results = new GCStressResults { LogFilePath = logFilePath }; + + foreach (string line in File.ReadLines(logFilePath)) + { + if (PassPattern().IsMatch(line)) + { + results.Passed++; + } + else if (FailPattern().IsMatch(line)) + { + results.Failed++; + results.FailureDetails.Add(line); + } + else if (SkipPattern().IsMatch(line)) + { + results.Skipped++; + results.SkipDetails.Add(line); + } + + Match totalMatch = TotalPattern().Match(line); + if (totalMatch.Success) + { + results.TotalVerifications = int.Parse(totalMatch.Groups[1].Value); + } + } + + if (results.TotalVerifications == 0) + { + results.TotalVerifications = results.Passed + results.Failed + results.Skipped; + } + + return results; + } + + public override string ToString() => + $"Total={TotalVerifications}, Passed={Passed}, Failed={Failed}, Skipped={Skipped}"; +} diff --git a/src/native/managed/cdac/tests/GCStressTests/GCStressTestBase.cs b/src/native/managed/cdac/tests/GCStressTests/GCStressTestBase.cs new file mode 100644 index 00000000000000..75c253ce1eafa0 --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/GCStressTestBase.cs @@ -0,0 +1,209 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.DataContractReader.Tests.GCStress; + +/// +/// Base class for cDAC GC stress tests. Runs a debuggee app under corerun +/// with DOTNET_GCStress=0x24 and parses the verification results. +/// +public abstract class GCStressTestBase +{ + private readonly ITestOutputHelper _output; + + protected GCStressTestBase(ITestOutputHelper output) + { + _output = output; + } + + /// + /// Runs the named debuggee under GC stress and returns the parsed results. + /// + internal GCStressResults RunGCStress(string debuggeeName, int timeoutSeconds = 300) + { + string coreRoot = GetCoreRoot(); + string corerun = GetCoreRunPath(coreRoot); + string debuggeeDll = GetDebuggeePath(debuggeeName); + string logFile = Path.Combine(Path.GetTempPath(), $"cdac-gcstress-{debuggeeName}-{Guid.NewGuid():N}.txt"); + + _output.WriteLine($"Running GC stress: {debuggeeName}"); + _output.WriteLine($" corerun: {corerun}"); + _output.WriteLine($" debuggee: {debuggeeDll}"); + _output.WriteLine($" log: {logFile}"); + + var psi = new ProcessStartInfo + { + FileName = corerun, + Arguments = debuggeeDll, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + }; + psi.Environment["CORE_ROOT"] = coreRoot; + psi.Environment["DOTNET_CdacStress"] = "0x11"; + psi.Environment["DOTNET_CdacStressFailFast"] = "0"; + psi.Environment["DOTNET_CdacStressLogFile"] = logFile; + psi.Environment["DOTNET_CdacStressStep"] = "1"; + psi.Environment["DOTNET_ContinueOnAssert"] = "1"; + + using var process = Process.Start(psi)!; + + // Read stderr asynchronously to avoid deadlock when both pipe buffers fill. + string stderr = ""; + process.ErrorDataReceived += (_, e) => + { + if (e.Data is not null) + stderr += e.Data + Environment.NewLine; + }; + process.BeginErrorReadLine(); + + string stdout = process.StandardOutput.ReadToEnd(); + + bool exited = process.WaitForExit(timeoutSeconds * 1000); + if (!exited) + { + process.Kill(entireProcessTree: true); + Assert.Fail($"GC stress test '{debuggeeName}' timed out after {timeoutSeconds}s"); + } + + _output.WriteLine($" exit code: {process.ExitCode}"); + if (!string.IsNullOrWhiteSpace(stdout)) + _output.WriteLine($" stdout: {stdout.TrimEnd()}"); + if (!string.IsNullOrWhiteSpace(stderr)) + _output.WriteLine($" stderr: {stderr.TrimEnd()}"); + + Assert.True(process.ExitCode == 100, + $"GC stress test '{debuggeeName}' exited with {process.ExitCode} (expected 100).\nstdout: {stdout}\nstderr: {stderr}"); + + Assert.True(File.Exists(logFile), + $"GC stress results log not created: {logFile}"); + + GCStressResults results = GCStressResults.Parse(logFile); + + _output.WriteLine($" results: {results}"); + + return results; + } + + /// + /// Asserts that GC stress verification produced 100% pass rate with no failures or skips. + /// + internal static void AssertAllPassed(GCStressResults results, string debuggeeName) + { + Assert.True(results.TotalVerifications > 0, + $"GC stress test '{debuggeeName}' produced zero verifications — " + + "GCStress may not have triggered or cDAC may not be loaded."); + + if (results.Failed > 0) + { + string details = string.Join("\n", results.FailureDetails); + Assert.Fail( + $"GC stress test '{debuggeeName}' had {results.Failed} failure(s) " + + $"out of {results.TotalVerifications} verifications.\n" + + $"Log: {results.LogFilePath}\n{details}"); + } + + if (results.Skipped > 0) + { + string details = string.Join("\n", results.SkipDetails); + Assert.Fail( + $"GC stress test '{debuggeeName}' had {results.Skipped} skip(s) " + + $"out of {results.TotalVerifications} verifications.\n" + + $"Log: {results.LogFilePath}\n{details}"); + } + } + + /// + /// Asserts that GC stress verification produced a pass rate at or above the given threshold. + /// A small number of failures is expected due to unimplemented frame scanning for + /// dynamic method stubs (InvokeStub / PromoteCallerStack). + /// + internal static void AssertHighPassRate(GCStressResults results, string debuggeeName, double minPassRate) + { + Assert.True(results.TotalVerifications > 0, + $"GC stress test '{debuggeeName}' produced zero verifications — " + + "GCStress may not have triggered or cDAC may not be loaded."); + + double passRate = (double)results.Passed / results.TotalVerifications; + if (passRate < minPassRate) + { + string details = string.Join("\n", results.FailureDetails); + Assert.Fail( + $"GC stress test '{debuggeeName}' pass rate {passRate:P2} is below " + + $"{minPassRate:P1} threshold. {results.Failed} failure(s) out of " + + $"{results.TotalVerifications} verifications.\n{details}"); + } + } + + private static string GetCoreRoot() + { + // Check environment variable first + string? coreRoot = Environment.GetEnvironmentVariable("CORE_ROOT"); + if (!string.IsNullOrEmpty(coreRoot) && Directory.Exists(coreRoot)) + return coreRoot; + + // Default path based on repo layout + string repoRoot = FindRepoRoot(); + string rid = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "windows" : "linux"; + string arch = RuntimeInformation.ProcessArchitecture.ToString().ToLowerInvariant(); + coreRoot = Path.Combine(repoRoot, "artifacts", "tests", "coreclr", $"{rid}.{arch}.Checked", "Tests", "Core_Root"); + + if (!Directory.Exists(coreRoot)) + throw new DirectoryNotFoundException( + $"Core_Root not found at '{coreRoot}'. " + + "Set the CORE_ROOT environment variable or run 'src/tests/build.cmd Checked generatelayoutonly'."); + + return coreRoot; + } + + private static string GetCoreRunPath(string coreRoot) + { + string exe = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "corerun.exe" : "corerun"; + string path = Path.Combine(coreRoot, exe); + Assert.True(File.Exists(path), $"corerun not found at '{path}'"); + + return path; + } + + private static string GetDebuggeePath(string debuggeeName) + { + string repoRoot = FindRepoRoot(); + + // Debuggees are built to artifacts/bin/GCStressTests//Release// + string binDir = Path.Combine(repoRoot, "artifacts", "bin", "GCStressTests", debuggeeName); + + if (!Directory.Exists(binDir)) + throw new DirectoryNotFoundException( + $"Debuggee '{debuggeeName}' not found at '{binDir}'. Build the GCStressTests project first."); + + // Find the dll in any Release/ subdirectory + foreach (string dir in Directory.GetDirectories(binDir, "*", SearchOption.AllDirectories)) + { + string dll = Path.Combine(dir, $"{debuggeeName}.dll"); + if (File.Exists(dll)) + return dll; + } + + throw new FileNotFoundException($"Could not find {debuggeeName}.dll under '{binDir}'"); + } + + private static string FindRepoRoot() + { + string? dir = AppContext.BaseDirectory; + while (dir is not null) + { + if (File.Exists(Path.Combine(dir, "global.json"))) + return dir; + dir = Path.GetDirectoryName(dir); + } + + throw new InvalidOperationException("Could not find repo root (global.json)"); + } +} diff --git a/src/native/managed/cdac/tests/GCStressTests/GCStressTests.targets b/src/native/managed/cdac/tests/GCStressTests/GCStressTests.targets new file mode 100644 index 00000000000000..a06b8ea4263caf --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/GCStressTests.targets @@ -0,0 +1,25 @@ + + + + $(MSBuildThisFileDirectory)Debuggees\ + Release + + + + + + + + + + + + diff --git a/src/native/managed/cdac/tests/GCStressTests/Microsoft.Diagnostics.DataContractReader.GCStressTests.csproj b/src/native/managed/cdac/tests/GCStressTests/Microsoft.Diagnostics.DataContractReader.GCStressTests.csproj new file mode 100644 index 00000000000000..ce6b6c14efadab --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/Microsoft.Diagnostics.DataContractReader.GCStressTests.csproj @@ -0,0 +1,20 @@ + + + true + $(NetCoreAppToolCurrent) + enable + true + + + + + + + + + + + + + + diff --git a/src/native/managed/cdac/tests/GCStressTests/README.md b/src/native/managed/cdac/tests/GCStressTests/README.md new file mode 100644 index 00000000000000..ad1ee681b3944d --- /dev/null +++ b/src/native/managed/cdac/tests/GCStressTests/README.md @@ -0,0 +1,83 @@ +# cDAC GC Stress Tests + +Integration tests that verify the cDAC's stack reference enumeration matches the runtime's +GC root scanning under GC stress conditions. + +## How It Works + +Each test runs a debuggee console app under `corerun` with `DOTNET_GCStress=0x24`, which enables: +- **0x4**: Instruction-level JIT stress (triggers GC at every safe point) +- **0x20**: cDAC verification (compares cDAC stack refs against runtime refs) + +`DOTNET_GCStressCdacStep` throttles verification to every Nth stress point. The default +is 1 (verify every point). Higher values reduce cDAC overhead while maintaining instruction-level +breakpoint coverage for code path diversity. + +The native `cdacgcstress.cpp` hook writes `[PASS]`/`[FAIL]`/`[SKIP]` lines to a log file. +The test framework parses this log and asserts a high pass rate (≥99.9% for most debuggees, +≥99% for ExceptionHandling which has known funclet gaps). + +## Prerequisites + +Build the runtime with the cDAC GC stress hook enabled: + +```powershell +# From repo root +.\build.cmd -subset clr.native+tools.cdac -c Debug -rc Checked -lc Release +.\.dotnet\dotnet.exe msbuild src\libraries\externals.csproj /t:Build /p:Configuration=Release /p:RuntimeConfiguration=Checked /p:TargetOS=windows /p:TargetArchitecture=x64 -v:minimal +.\src\tests\build.cmd Checked generatelayoutonly -SkipRestorePackages /p:LibrariesConfiguration=Release +``` + +## Running Tests + +```powershell +# Build and run all GC stress tests +.\.dotnet\dotnet.exe test src\native\managed\cdac\tests\GCStressTests + +# Run a specific debuggee +.\.dotnet\dotnet.exe test src\native\managed\cdac\tests\GCStressTests --filter "debuggeeName=BasicAlloc" + +# Set CORE_ROOT manually if needed +$env:CORE_ROOT = "path\to\Core_Root" +.\.dotnet\dotnet.exe test src\native\managed\cdac\tests\GCStressTests +``` + +## Adding a New Debuggee + +1. Create a folder under `Debuggees/` with a `.csproj` and `Program.cs` +2. The `.csproj` just needs: `` + (inherits OutputType=Exe and TFM from `Directory.Build.props`) +3. `Main()` must return `100` on success +4. Use `[MethodImpl(MethodImplOptions.NoInlining)]` on methods to prevent inlining +5. Use `GC.KeepAlive()` to ensure objects are live at GC stress points +6. Add the debuggee name to `BasicGCStressTests.Debuggees` + +## Debuggee Catalog + +| Debuggee | Scenarios | +|----------|-----------| +| **BasicAlloc** | Objects, strings, arrays, many live refs | +| **ExceptionHandling** | try/catch/finally funclets, nested exceptions, filter funclets, rethrow | +| **DeepStack** | Deep recursion with live refs at each frame | +| **Generics** | Generic method instantiations, interface dispatch, delegates | +| **PInvoke** | P/Invoke transitions, pinned GC handles, struct with object refs | +| **MultiThread** | Concurrent threads with synchronized GC stress | +| **Comprehensive** | All-in-one: every scenario in a single run | + +## Architecture + +``` +GCStressTestBase.RunGCStress(debuggeeName) + │ + ├── Locate core_root/corerun (CORE_ROOT env or default path) + ├── Locate debuggee DLL (artifacts/bin/GCStressTests//...) + ├── Start Process: corerun + │ Environment: + │ DOTNET_GCStress=0x24 + │ DOTNET_GCStressCdacStep=1 + │ DOTNET_GCStressCdacLogFile= + │ DOTNET_ContinueOnAssert=1 + ├── Wait for exit (timeout: 300s) + ├── Parse results log → GCStressResults + └── Assert: exit=100, pass rate ≥ 99.9% +``` diff --git a/src/native/managed/cdac/tests/Microsoft.Diagnostics.DataContractReader.Tests.csproj b/src/native/managed/cdac/tests/Microsoft.Diagnostics.DataContractReader.Tests.csproj index c9de2a1bac2da7..669f76a1631839 100644 --- a/src/native/managed/cdac/tests/Microsoft.Diagnostics.DataContractReader.Tests.csproj +++ b/src/native/managed/cdac/tests/Microsoft.Diagnostics.DataContractReader.Tests.csproj @@ -6,8 +6,9 @@ - + + diff --git a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.ExecutionManager.cs b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.ExecutionManager.cs index 2cc7a9334daf36..f6457ef2a765df 100644 --- a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.ExecutionManager.cs +++ b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.ExecutionManager.cs @@ -236,6 +236,7 @@ public static RangeSectionMapTestBuilder CreateRangeSection(MockTarget.Architect new(nameof(Data.RealCodeHeader.EHInfo), DataType.pointer), new(nameof(Data.RealCodeHeader.GCInfo), DataType.pointer), new(nameof(Data.RealCodeHeader.NumUnwindInfos), DataType.uint32), + new(nameof(Data.RealCodeHeader.EHInfo), DataType.pointer), new(nameof(Data.RealCodeHeader.UnwindInfos), DataType.pointer), new(nameof(Data.RealCodeHeader.JitEHInfo), DataType.pointer), ] @@ -516,6 +517,7 @@ public TargetCodePointer AddJittedMethod(JittedCodeRange jittedCodeRange, uint c Builder.TargetTestHelpers.Write(chf.Slice(tyInfo.Fields[nameof(Data.RealCodeHeader.NumUnwindInfos)].Offset, sizeof(uint)), 0u); Builder.TargetTestHelpers.WritePointer(chf.Slice(tyInfo.Fields[nameof(Data.RealCodeHeader.UnwindInfos)].Offset, Builder.TargetTestHelpers.PointerSize), TargetPointer.Null); Builder.TargetTestHelpers.WritePointer(chf.Slice(tyInfo.Fields[nameof(Data.RealCodeHeader.JitEHInfo)].Offset, Builder.TargetTestHelpers.PointerSize), TargetPointer.Null); + Builder.TargetTestHelpers.WritePointer(chf.Slice(tyInfo.Fields[nameof(Data.RealCodeHeader.EHInfo)].Offset, Builder.TargetTestHelpers.PointerSize), TargetPointer.Null); return codeStart; } diff --git a/src/native/managed/cdac/tests/gcstress/known-issues.md b/src/native/managed/cdac/tests/gcstress/known-issues.md index 1a9afea91f8852..323ece6d0e0794 100644 --- a/src/native/managed/cdac/tests/gcstress/known-issues.md +++ b/src/native/managed/cdac/tests/gcstress/known-issues.md @@ -1,113 +1,57 @@ # cDAC Stack Reference Walking — Known Issues -This document tracks known gaps and differences between the cDAC's stack reference -enumeration (`ISOSDacInterface::GetStackReferences`) and the runtime's GC root scanning. - -## GC Stress Test Results - -With `DOTNET_GCStress=0x24` (instruction-level JIT stress + cDAC verification): -- ~25,000 PASS / ~125 FAIL out of ~25,100 stress points (99.5% pass rate) - -## Known Issues - -### 1. Dynamic Method / IL Stub GC Refs Not Enumerated - -**Severity**: Low — matches legacy DAC behavior -**Affected methods**: `dynamicclass::InvokeStub_*` (reflection invoke stubs), LCG methods -**Pattern**: `cDAC < RT` (diff=-1), always missing `RT[0]` register ref - -The cDAC (and legacy DAC) cannot resolve code blocks for methods in RangeList-based -code heaps (HostCodeHeap). Both `EEJitManager::JitCodeToMethodInfo` and the cDAC's -`FindMethodCode` return failure for `RANGE_SECTION_RANGELIST` sections. This means -GcInfo cannot be decoded for these methods, and their GC refs are not reported. - -The runtime's `GcStackCrawlCallBack` reports additional refs from these methods -because it processes them through the Frame chain (`ResumableFrame`, `InlinedCallFrame`) -which has access to the register state. - -This is a pre-existing gap in the DAC's diagnostic API, not a cDAC regression. - -**Follow-up**: Implement RangeList-based code lookup in the cDAC's ExecutionManager. -This requires reading the `HostCodeHeap` linked list and matching IPs to code headers -within dynamic code heaps. - -### 2. Frame Context Restoration Causes Duplicate Walks - -**Severity**: Low — mitigated by dedup in stress tool -**Pattern**: `cDAC > RT` (diff=+1 to +3), same Address/Object from two Source IPs - -When a non-leaf Frame's `UpdateContextFromFrame` restores a managed IP that was -already walked from the initial context (or will be walked via normal unwinding), -the same managed frame gets walked twice at different offsets. This produces -duplicate GC slot reports. - -The stress tool's `DeduplicateRefs` filter removes stack-based duplicates -(same Address/Object/Flags), but register-based duplicates (Address=0) with -different Source IPs are not caught. - -**Mitigations in place**: -- `callerSP` Frame skip in `CreateStackWalk` (prevents most leaf-level duplicates) -- `SkipCurrentFrameInCheck` for active `InlinedCallFrame` (prevents ICF re-encounter) -- `DeduplicateRefs` in stress tool (removes stack-based duplicates) - -**Follow-up**: Track walked method address ranges in the cDAC's stack walker and -suppress duplicate `SW_FRAMELESS` yields for methods already visited. - -### 3. PromoteCallerStack Not Implemented for Stub Frames - -**Severity**: Low — not currently manifesting in GC stress tests -**Affected frames**: `StubDispatchFrame`, `ExternalMethodFrame`, `CallCountingHelperFrame`, -`DynamicHelperFrame`, `CLRToCOMMethodFrame` - -These Frame types call `PromoteCallerStack` / `PromoteCallerStackUsingGCRefMap` -to report method arguments from the transition block. The cDAC's `ScanFrameRoots` -is a no-op for these frame types. - -This gap doesn't manifest in GC stress testing because stub frame arguments are -not the source of the current count differences. However, it IS a DAC parity gap — -the legacy DAC reports these refs via `Frame::GcScanRoots`. - -**Follow-up**: Port `GCRefMapDecoder` to managed code and implement -`PromoteCallerStackUsingGCRefMap` in `ScanFrameRoots`. Prototype implementation -exists (stashed as "PromoteCallerStack implementation + GCRefMapDecoder"). - -### 4. Funclet Parent Frame Flags Not Consumed - -**Severity**: Low — only affects exception handling scenarios -**Flags**: `ShouldParentToFuncletSkipReportingGCReferences`, -`ShouldParentFrameUseUnwindTargetPCforGCReporting`, -`ShouldParentToFuncletReportSavedFuncletSlots` - -The `Filter` method computes these flags for funclet parent frames, but -`WalkStackReferences` does not act on them. This could cause: -- Double-reporting of slots already reported by a funclet -- Using the wrong IP for GC liveness lookup on catch/finally parent frames -- Missing callee-saved register slots from unwound funclets - -**Follow-up**: Wire up `ParentOfFuncletStackFrame` flag to `EnumGcRefs`. -Requires careful validation — an initial attempt caused 253 regressions -because `Filter` sets the flag too aggressively. - -### 5. Interior Stack Pointers - -**Severity**: Informational — handled in stress tool -**Pattern**: cDAC reports interior pointers whose Object is a stack address - -The runtime's `PromoteCarefully` (siginfo.cpp) filters out interior pointers -whose object value is a stack address. These are callee-saved register values -(RSP/RBP) that GcInfo marks as live interior slots but don't point to managed -heap objects. The cDAC reports all GcInfo slots faithfully. - -**Mitigation**: The stress tool's `FilterInteriorStackRefs` removes these -before comparison, matching the runtime's behavior. - -### 6. forceReportingWhileSkipping State Machine Incomplete - -**Severity**: Low — theoretical gap -**Location**: `StackWalk_1.cs` Filter method - -The `ForceGcReportingStage` state machine transitions `Off → LookForManagedFrame -→ LookForMarkerFrame` but never transitions back to `Off`. The native code checks -if the caller IP is within `DispatchManagedException` / `RhThrowEx` to deactivate. - -**Follow-up**: Implement marker frame detection. +This document tracks known gaps between the cDAC's stack reference enumeration +and the legacy DAC's `GetStackReferences`. + +## Current Test Results + +Using `DOTNET_CdacStress` with cDAC-vs-DAC comparison: + +| Mode | Non-EH debuggees (6) | ExceptionHandling | +|------|-----------------------|-------------------| +| INSTR (0x8 + GCStress=0x4, step=10) | 0 failures | 0-2 failures | +| ALLOC+UNIQUE (0x5) | 0 failures | 4 failures | +| Walk comparison (0x20, IP+SP) | 0 mismatches | N/A | + +## Known Issue: cDAC Cannot Unwind Through Native Frames + +**Severity**: Low — only affects live-process stress testing during active +exception first-pass dispatch. Does not affect dump analysis where the thread +is suspended with a consistent Frame chain. + +**Pattern**: `cDAC < DAC` (cDAC reports 4 refs, DAC reports 10-13). +ExceptionHandling debuggee only, 4 deterministic occurrences per run. + +**Root cause**: The cDAC's `AMD64Unwinder.Unwind` (and equivalents for other +architectures) can only unwind **managed** frames — it checks +`ExecutionManager.GetCodeBlockHandle(IP)` first and returns false if the IP +is not in a managed code range. This means it cannot unwind through native +runtime frames (allocation helpers, EH dispatch code, etc.). + +When the allocation stress point fires during exception first-pass dispatch: + +1. The thread's `m_pFrame` is `FRAME_TOP` (no explicit Frames in the chain + because the InlinedCallFrame/SoftwareExceptionFrame have been popped or + not yet pushed at that point in the EH dispatch sequence) +2. The initial IP is in native code (allocation helper) +3. The cDAC attempts to unwind through native frames but + `GetCodeBlockHandle` returns null for native IPs → unwind fails +4. With no Frames and no ability to unwind, the walk stops early + +The legacy DAC's `DacStackReferenceWalker::WalkStack` succeeds because +`StackWalkFrames` calls `VirtualUnwindToFirstManagedCallFrame` which uses +OS-level unwind (`RtlVirtualUnwind` on Windows, `PAL_VirtualUnwind` on Unix) +that can unwind ANY native frame using PE `.pdata`/`.xdata` sections. + +**Possible fixes**: +1. **Ensure Frames are always available** — change the runtime to keep + an explicit Frame pushed during allocation points within EH dispatch. + The cDAC cannot do OS-level native unwind (it operates on dumps where + `RtlVirtualUnwind` is not available). The Frame chain is the only + mechanism the cDAC has for transitioning through native code to reach + managed frames. If `m_pFrame = FRAME_TOP` when the IP is native, the + cDAC cannot proceed. +2. **Accept as known limitation** — these failures only occur during + live-process stress testing at a narrow window during EH first-pass + dispatch. In dumps, the exception state is frozen and the Frame chain + is consistent. diff --git a/src/native/managed/cdac/tests/gcstress/test-cdac-gcstress.ps1 b/src/native/managed/cdac/tests/gcstress/test-cdac-gcstress.ps1 index cfd78c303e61d4..ea16f2d9cfca42 100644 --- a/src/native/managed/cdac/tests/gcstress/test-cdac-gcstress.ps1 +++ b/src/native/managed/cdac/tests/gcstress/test-cdac-gcstress.ps1 @@ -123,10 +123,38 @@ New-Item -ItemType Directory -Force $testDir | Out-Null $testSource = @" using System; +using System.Collections.Generic; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; + +// ------------------------------------------------------------------- +// Comprehensive cDAC GC stress test exercising many frame types +// ------------------------------------------------------------------- + +interface IKeepAlive +{ + object GetRef(); +} + +class BoxHolder : IKeepAlive +{ + object _value; + public BoxHolder() { _value = new object(); } + public BoxHolder(object v) { _value = v; } + + [MethodImpl(MethodImplOptions.NoInlining)] + public object GetRef() => _value; +} + +struct LargeStruct +{ + public object A, B, C, D; +} class CdacGcStressTest { + // 1. Basic allocation — the original test [MethodImpl(MethodImplOptions.NoInlining)] static object AllocAndHold() { @@ -139,6 +167,7 @@ class CdacGcStressTest return o; } + // 2. Deep recursion — many managed frames [MethodImpl(MethodImplOptions.NoInlining)] static void NestedCall(int depth) { @@ -148,14 +177,296 @@ class CdacGcStressTest GC.KeepAlive(o); } + // 3. Try/catch — funclet frames (catch handler is a funclet on AMD64) + [MethodImpl(MethodImplOptions.NoInlining)] + static void TryCatchScenario() + { + object before = new object(); + try + { + object inside = new object(); + ThrowHelper(); + GC.KeepAlive(inside); + } + catch (InvalidOperationException ex) + { + object inCatch = new object(); + GC.KeepAlive(ex); + GC.KeepAlive(inCatch); + } + GC.KeepAlive(before); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void ThrowHelper() + { + throw new InvalidOperationException("test exception"); + } + + // 4. Try/finally — finally funclet + [MethodImpl(MethodImplOptions.NoInlining)] + static void TryFinallyScenario() + { + object outerRef = new object(); + try + { + object innerRef = new object(); + GC.KeepAlive(innerRef); + } + finally + { + object finallyRef = new object(); + GC.KeepAlive(finallyRef); + } + GC.KeepAlive(outerRef); + } + + // 5. Nested exception handling — funclet within funclet parent + [MethodImpl(MethodImplOptions.NoInlining)] + static void NestedExceptionScenario() + { + object a = new object(); + try + { + object b = new object(); + try + { + object c = new object(); + throw new ArgumentException("inner"); + } + catch (ArgumentException ex1) + { + GC.KeepAlive(ex1); + throw new InvalidOperationException("outer", ex1); + } + finally + { + object d = new object(); + GC.KeepAlive(d); + } + } + catch (InvalidOperationException ex2) + { + GC.KeepAlive(ex2); + } + GC.KeepAlive(a); + } + + // 6. Filter funclet (when clause via helper) + [MethodImpl(MethodImplOptions.NoInlining)] + static void FilterExceptionScenario() + { + object holder = new object(); + try + { + throw new ArgumentException("filter-test"); + } + catch (ArgumentException ex) when (FilterCheck(ex)) + { + GC.KeepAlive(ex); + } + GC.KeepAlive(holder); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static bool FilterCheck(Exception ex) + { + object filterLocal = new object(); + GC.KeepAlive(filterLocal); + return ex.Message.Contains("filter"); + } + + // 7. Generic methods — different instantiations + [MethodImpl(MethodImplOptions.NoInlining)] + static T GenericAlloc() where T : new() + { + T val = new T(); + object marker = new object(); + GC.KeepAlive(marker); + return val; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void GenericScenario() + { + var o = GenericAlloc(); + var l = GenericAlloc>(); + var s = GenericAlloc(); + GC.KeepAlive(o); + GC.KeepAlive(l); + GC.KeepAlive(s); + } + + // 8. Interface dispatch — virtual calls through interface + [MethodImpl(MethodImplOptions.NoInlining)] + static void InterfaceDispatchScenario() + { + IKeepAlive holder = new BoxHolder(new int[] { 42, 43 }); + object r = holder.GetRef(); + GC.KeepAlive(holder); + GC.KeepAlive(r); + } + + // 9. Delegate invocation + [MethodImpl(MethodImplOptions.NoInlining)] + static void DelegateScenario() + { + object captured = new object(); + Func fn = () => + { + GC.KeepAlive(captured); + return new object(); + }; + object result = fn(); + GC.KeepAlive(result); + GC.KeepAlive(fn); + } + + // 10. Struct with object references on stack + [MethodImpl(MethodImplOptions.NoInlining)] + static void StructWithRefsScenario() + { + LargeStruct ls; + ls.A = new object(); + ls.B = "struct-string"; + ls.C = new int[] { 10, 20 }; + ls.D = new BoxHolder(ls.A); + GC.KeepAlive(ls.A); + GC.KeepAlive(ls.B); + GC.KeepAlive(ls.C); + GC.KeepAlive(ls.D); + } + + // 11. Pinned references via GCHandle + [MethodImpl(MethodImplOptions.NoInlining)] + static void PinnedScenario() + { + byte[] buffer = new byte[64]; + GCHandle pin = GCHandle.Alloc(buffer, GCHandleType.Pinned); + try + { + object other = new object(); + GC.KeepAlive(other); + GC.KeepAlive(buffer); + } + finally + { + pin.Free(); + } + } + + // 12. Multiple threads — concurrent stack walks + [MethodImpl(MethodImplOptions.NoInlining)] + static void MultiThreadScenario() + { + ManualResetEventSlim ready = new ManualResetEventSlim(false); + ManualResetEventSlim go = new ManualResetEventSlim(false); + Thread t = new Thread(() => + { + object threadLocal = new object(); + ready.Set(); + go.Wait(); + NestedCall(5); + GC.KeepAlive(threadLocal); + }); + t.Start(); + ready.Wait(); + go.Set(); + + // Main thread also does work concurrently + NestedCall(3); + t.Join(); + } + + // 13. Many live references — stress GC slot reporting + [MethodImpl(MethodImplOptions.NoInlining)] + static void ManyLiveRefsScenario() + { + object r0 = new object(); + object r1 = new object(); + object r2 = new object(); + object r3 = new object(); + object r4 = new object(); + object r5 = new object(); + object r6 = new object(); + object r7 = new object(); + string r8 = "live-string"; + int[] r9 = new int[10]; + List r10 = new List { r0, r1, r2 }; + object[] r11 = new object[] { r3, r4, r5, r6, r7 }; + + GC.KeepAlive(r0); GC.KeepAlive(r1); + GC.KeepAlive(r2); GC.KeepAlive(r3); + GC.KeepAlive(r4); GC.KeepAlive(r5); + GC.KeepAlive(r6); GC.KeepAlive(r7); + GC.KeepAlive(r8); GC.KeepAlive(r9); + GC.KeepAlive(r10); GC.KeepAlive(r11); + } + + // 14. P/Invoke transition — native frame on stack + [DllImport("kernel32.dll")] + static extern uint GetCurrentThreadId(); + + [MethodImpl(MethodImplOptions.NoInlining)] + static void PInvokeScenario() + { + object before = new object(); + uint tid = GetCurrentThreadId(); + object after = new object(); + GC.KeepAlive(before); + GC.KeepAlive(after); + } + + // 15. Exception rethrow — stack trace preservation + [MethodImpl(MethodImplOptions.NoInlining)] + static void RethrowScenario() + { + object outerRef = new object(); + try + { + try + { + throw new ApplicationException("rethrow-test"); + } + catch (ApplicationException) + { + object catchRef = new object(); + GC.KeepAlive(catchRef); + throw; // rethrow preserves original stack + } + } + catch (ApplicationException ex) + { + GC.KeepAlive(ex); + } + GC.KeepAlive(outerRef); + } + static int Main() { - Console.WriteLine("Starting cDAC GC Stress test..."); - for (int i = 0; i < 5; i++) + Console.WriteLine("Starting comprehensive cDAC GC Stress test..."); + + for (int i = 0; i < 3; i++) { + Console.WriteLine($" Iteration {i + 1}/3"); + AllocAndHold(); - NestedCall(3); + NestedCall(5); + TryCatchScenario(); + TryFinallyScenario(); + NestedExceptionScenario(); + FilterExceptionScenario(); + GenericScenario(); + InterfaceDispatchScenario(); + DelegateScenario(); + StructWithRefsScenario(); + PinnedScenario(); + MultiThreadScenario(); + ManyLiveRefsScenario(); + PInvokeScenario(); + RethrowScenario(); } + Console.WriteLine("cDAC GC Stress test completed successfully."); return 100; } @@ -172,12 +483,16 @@ if (-not $cscPath) { Write-Error "Could not find csc.dll in .dotnet SDK"; exit 1 $sysRuntime = Join-Path $coreRoot "System.Runtime.dll" $sysConsole = Join-Path $coreRoot "System.Console.dll" $sysCoreLib = Join-Path $coreRoot "System.Private.CoreLib.dll" +$sysThread = Join-Path $coreRoot "System.Threading.dll" +$sysInterop = Join-Path $coreRoot "System.Runtime.InteropServices.dll" & $dotnetExe exec $cscPath.FullName ` - "/out:$testDll" /target:exe /nologo ` + "/out:$testDll" /target:exe /nologo /unsafe ` "/r:$sysRuntime" ` "/r:$sysConsole" ` "/r:$sysCoreLib" ` + "/r:$sysThread" ` + "/r:$sysInterop" ` $testCs if ($LASTEXITCODE -ne 0) { Write-Error "Test compilation failed"; exit 1 }