From 69581d4e40c14dbb35c211fbef275bb4df93c5f7 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Fri, 24 Apr 2026 14:18:09 -0400 Subject: [PATCH 1/3] [cDAC] Add native GC stress verification framework cdacstress.cpp: three-way comparison of cDAC/DAC/runtime stack GC references at allocation and instruction stress points. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/Subsets.props | 5 + src/coreclr/vm/cdacstress.cpp | 614 ++++++++++++++++++++++++++-------- src/coreclr/vm/gccover.cpp | 30 ++ 3 files changed, 514 insertions(+), 135 deletions(-) diff --git a/eng/Subsets.props b/eng/Subsets.props index c66fbd778f175a..0b1746c90f8315 100644 --- a/eng/Subsets.props +++ b/eng/Subsets.props @@ -254,6 +254,7 @@ + @@ -535,6 +536,10 @@ + + + + diff --git a/src/coreclr/vm/cdacstress.cpp b/src/coreclr/vm/cdacstress.cpp index b750c069891af0..b12bb5e2911b08 100644 --- a/src/coreclr/vm/cdacstress.cpp +++ b/src/coreclr/vm/cdacstress.cpp @@ -72,7 +72,7 @@ static CrstStatic s_cdacLock; // Serializes cDAC access from concurr // 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 @@ -146,12 +146,11 @@ static int ReadThreadContextCallback(uint32_t threadId, uint32_t contextFlags, u // Minimal ICLRDataTarget implementation for loading the legacy DAC in-process. // Routes ReadVirtual/GetThreadContext to the same callbacks as the cDAC. //----------------------------------------------------------------------------- -class InProcessDataTarget : public ICLRDataTarget, public ICLRRuntimeLocator +class InProcessDataTarget : public ICLRDataTarget { volatile LONG m_refCount; public: InProcessDataTarget() : m_refCount(1) {} - virtual ~InProcessDataTarget() = default; HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppObj) override { @@ -161,12 +160,6 @@ class InProcessDataTarget : public ICLRDataTarget, public ICLRRuntimeLocator AddRef(); return S_OK; } - if (riid == __uuidof(ICLRRuntimeLocator)) - { - *ppObj = static_cast(this); - AddRef(); - return S_OK; - } *ppObj = nullptr; return E_NOINTERFACE; } @@ -178,14 +171,6 @@ class InProcessDataTarget : public ICLRDataTarget, public ICLRRuntimeLocator return c; } - // ICLRRuntimeLocator — provides the CLR base address directly so the DAC - // does not fall back to GetImageBase (which needs GetModuleHandleW, unavailable on Linux). - HRESULT STDMETHODCALLTYPE GetRuntimeBase(CLRDATA_ADDRESS* baseAddress) override - { - *baseAddress = (CLRDATA_ADDRESS)GetCurrentModuleBase(); - return S_OK; - } - HRESULT STDMETHODCALLTYPE GetMachineType(ULONG32* machineType) override { #ifdef TARGET_AMD64 @@ -208,8 +193,10 @@ class InProcessDataTarget : public ICLRDataTarget, public ICLRRuntimeLocator HRESULT STDMETHODCALLTYPE GetImageBase(LPCWSTR imagePath, CLRDATA_ADDRESS* baseAddress) override { - // Not needed — the DAC uses ICLRRuntimeLocator::GetRuntimeBase() instead. - return E_NOTIMPL; + 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 @@ -282,8 +269,8 @@ bool CdacStress::Initialize() } else { - // Legacy: GCSTRESS_CDAC maps to allocation-point + reference verification - s_cdacStressLevel = CDACSTRESS_ALLOC | CDACSTRESS_REFS; + // Legacy: GCSTRESS_CDAC maps to allocation-point verification + s_cdacStressLevel = CDACSTRESS_ALLOC; } // Load mscordaccore_universal from next to coreclr @@ -449,15 +436,27 @@ bool CdacStress::Initialize() pDacUnk->QueryInterface(__uuidof(IXCLRDataProcess), (void**)&s_dacProcess); pDacUnk->Release(); } + else if (s_logFile != nullptr) + { + fprintf(s_logFile, "DAC: CLRDataCreateInstance failed hr=0x%08x pDacUnk=%p\n", hr, pDacUnk); + } } } if (s_dacSosDac == nullptr) { + if (s_logFile != nullptr) + fprintf(s_logFile, "DAC: Loaded mscordaccore.dll but QI for ISOSDacInterface failed\n"); LOG((LF_GCROOTS, LL_WARNING, "CDAC GC Stress: Legacy DAC loaded but QI for ISOSDacInterface failed\n")); } + else if (s_logFile != nullptr) + { + fprintf(s_logFile, "DAC: Legacy DAC loaded successfully (s_dacSosDac=%p)\n", s_dacSosDac); + } } else { + if (s_logFile != nullptr) + fprintf(s_logFile, "DAC: mscordaccore.dll not found at path\n"); LOG((LF_GCROOTS, LL_INFO10, "CDAC GC Stress: Legacy DAC not found (three-way comparison disabled)\n")); } } @@ -535,11 +534,53 @@ void CdacStress::Shutdown() LOG((LF_GCROOTS, LL_INFO10, "CDAC GC Stress: Shutdown complete\n")); } +//----------------------------------------------------------------------------- +// Resolve a managed IP to a method name using ISOSDacInterface. +// Falls back to "" or "" if resolution fails. +// Uses the cDAC's ISOSDacInterface by default. +//----------------------------------------------------------------------------- + +static void ResolveMethodName(CLRDATA_ADDRESS source, int sourceType, char* buf, int bufLen) +{ + if (bufLen <= 0) + return; + + // Frame addresses (not managed IPs) — show as frame type + if (sourceType != 0) // SOS_StackSourceFrame + { + snprintf(buf, bufLen, "", (unsigned long long)source); + return; + } + + // Try to resolve the IP using the cDAC's ISOSDacInterface + ISOSDacInterface* pSos = s_cdacSosDac; + if (pSos == nullptr) + pSos = s_dacSosDac; // Fallback to legacy DAC + + if (pSos != nullptr) + { + CLRDATA_ADDRESS mdAddr = 0; + if (SUCCEEDED(pSos->GetMethodDescPtrFromIP(source, &mdAddr)) && mdAddr != 0) + { + WCHAR wname[256] = {}; + unsigned int nameLen = 0; + if (SUCCEEDED(pSos->GetMethodDescName(mdAddr, ARRAY_SIZE(wname), wname, &nameLen)) && nameLen > 0) + { + WideCharToMultiByte(CP_UTF8, 0, wname, -1, buf, bufLen, NULL, NULL); + return; + } + } + } + + snprintf(buf, bufLen, "", (unsigned long long)source); +} + //----------------------------------------------------------------------------- // Collect stack refs from the cDAC //----------------------------------------------------------------------------- -static bool CollectStackRefs(ISOSDacInterface* pSosDac, DWORD osThreadId, SArray* pRefs) +static bool CollectStackRefs(ISOSDacInterface* pSosDac, DWORD osThreadId, SArray* pRefs, + const char* label = nullptr) { if (pSosDac == nullptr) return false; @@ -548,7 +589,20 @@ static bool CollectStackRefs(ISOSDacInterface* pSosDac, DWORD osThreadId, SArray HRESULT hr = pSosDac->GetStackReferences(osThreadId, &pEnum); if (FAILED(hr) || pEnum == nullptr) + { + // Log the first failure per source for diagnostics + static bool s_loggedCdacFailure = false; + static bool s_loggedDacFailure = false; + bool* pLogged = (label != nullptr && strcmp(label, "DAC") == 0) ? &s_loggedDacFailure : &s_loggedCdacFailure; + if (s_logFile != nullptr && !*pLogged) + { + fprintf(s_logFile, "%s: GetStackReferences failed hr=0x%08x pEnum=%p thread=0x%x\n", + label ? label : "???", hr, pEnum, osThreadId); + fflush(s_logFile); + *pLogged = true; + } return false; + } SOSStackRefData refData; unsigned int fetched = 0; @@ -835,7 +889,6 @@ static void CompareStackWalks(Thread* pThread, PCONTEXT regs) bool mismatch = false; while (frameIdx < 200) // safety limit { - // Compare GetContext BYTE cdacCtx[4096] = {}; BYTE dacCtx[4096] = {}; ULONG32 cdacCtxSize = 0, dacCtxSize = 0; @@ -854,18 +907,8 @@ static void CompareStackWalks(Thread* pThread, PCONTEXT regs) 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)) + if (cdacCtxSize >= sizeof(CONTEXT) && dacCtxSize >= 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); @@ -882,28 +925,6 @@ static void CompareStackWalks(Thread* pThread, PCONTEXT regs) } } - // 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(); @@ -929,29 +950,29 @@ static void CompareStackWalks(Thread* pThread, PCONTEXT regs) } //----------------------------------------------------------------------------- -//----------------------------------------------------------------------------- -// 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. +// Per-frame ref comparison. +// +// Groups refs by Source (IP or Frame address), aligns the two sets by Source, +// and reports per-frame differences with resolved method names. +// Returns true if all refs match. //----------------------------------------------------------------------------- -static bool CompareRefSets(StackRef* refsA, int countA, StackRef* refsB, int countB) +// Compare two ref sets using two-phase matching (for RT comparison where we +// don't have Source info). Returns true if all refs match. +static bool CompareRefSetsFlat(StackRef* refsA, int countA, StackRef* refsB, int countB) { if (countA != countB) return false; if (countA == 0) return true; - if (countA > MAX_COLLECTED_REFS) - return false; bool matched[MAX_COLLECTED_REFS] = {}; + // Phase 1: Match stack refs (Address != 0) by exact (Address, Object, Flags). 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; @@ -960,18 +981,16 @@ static bool CompareRefSets(StackRef* refsA, int countA, StackRef* refsB, int cou refsA[i].Flags == refsB[j].Flags) { matched[j] = true; - found = true; break; } } - if (!found) return false; } + // Phase 2: Match register refs (Address == 0) by (Object, Flags) only. 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; @@ -979,16 +998,223 @@ static bool CompareRefSets(StackRef* refsA, int countA, StackRef* refsB, int cou refsA[i].Flags == refsB[j].Flags) { matched[j] = true; - found = true; break; } } - if (!found) return false; } + // Check that every ref in B was matched + for (int j = 0; j < countB; j++) + { + if (!matched[j]) + return false; + } return true; } +// Represents a group of refs from the same Source (managed frame or explicit Frame). +struct FrameRefGroup +{ + CLRDATA_ADDRESS Source; + int SourceType; // 0 = IP, 1 = Frame + int StartIdx; // Index into the original ref array + int Count; // Number of refs in this group +}; + +// Build a sorted list of unique Sources with their ref index ranges. +// The refs array is sorted by Source as a side effect. +static int __cdecl CompareBySource(const void* a, const void* b) +{ + const StackRef* ra = static_cast(a); + const StackRef* rb = static_cast(b); + if (ra->Source != rb->Source) + return (ra->Source < rb->Source) ? -1 : 1; + return 0; +} + +static int GroupRefsByFrame(StackRef* refs, int count, FrameRefGroup* groups, int maxGroups) +{ + if (count == 0) + return 0; + + qsort(refs, count, sizeof(StackRef), CompareBySource); + + int groupCount = 0; + CLRDATA_ADDRESS currentSource = refs[0].Source; + int startIdx = 0; + + for (int i = 1; i <= count; i++) + { + if (i == count || refs[i].Source != currentSource) + { + if (groupCount < maxGroups) + { + groups[groupCount].Source = currentSource; + groups[groupCount].SourceType = refs[startIdx].SourceType; + groups[groupCount].StartIdx = startIdx; + groups[groupCount].Count = i - startIdx; + groupCount++; + } + if (i < count) + { + currentSource = refs[i].Source; + startIdx = i; + } + } + } + return groupCount; +} + +// Compare refs within a single frame. Returns the number of unmatched refs in each set. +static void CompareFrameRefs(StackRef* refsA, int countA, StackRef* refsB, int countB, + int* unmatchedA, int* unmatchedB, + bool* aUsed, bool* bUsed) +{ + // Phase 1: exact (Address, Object, Flags) for Address != 0 + for (int i = 0; i < countA; i++) + { + if (refsA[i].Address == 0) + continue; + for (int j = 0; j < countB; j++) + { + if (bUsed[j]) continue; + if (refsA[i].Address == refsB[j].Address && + refsA[i].Object == refsB[j].Object && + refsA[i].Flags == refsB[j].Flags) + { + aUsed[i] = bUsed[j] = true; + break; + } + } + } + + // Phase 2: (Object, Flags) for Address=0 or unmatched refs + for (int i = 0; i < countA; i++) + { + if (aUsed[i]) continue; + for (int j = 0; j < countB; j++) + { + if (bUsed[j]) continue; + if (refsA[i].Object == refsB[j].Object && + refsA[i].Flags == refsB[j].Flags) + { + aUsed[i] = bUsed[j] = true; + break; + } + } + } + + *unmatchedA = 0; + *unmatchedB = 0; + for (int i = 0; i < countA; i++) + if (!aUsed[i]) (*unmatchedA)++; + for (int j = 0; j < countB; j++) + if (!bUsed[j]) (*unmatchedB)++; +} + +// Per-frame comparison: groups refs by Source, compares per-frame, logs structured diff. +// Returns true if all refs match between the two sets. +static bool ComparePerFrame(StackRef* refsA, int countA, const char* labelA, + StackRef* refsB, int countB, const char* labelB) +{ + static const int MAX_GROUPS = 256; + FrameRefGroup groupsA[MAX_GROUPS], groupsB[MAX_GROUPS]; + int numGroupsA = GroupRefsByFrame(refsA, countA, groupsA, MAX_GROUPS); + int numGroupsB = GroupRefsByFrame(refsB, countB, groupsB, MAX_GROUPS); + + bool allMatch = true; + int idxA = 0, idxB = 0; + + if (s_logFile) + fprintf(s_logFile, " [COMPARE %s-vs-%s]\n", labelA, labelB); + + while (idxA < numGroupsA || idxB < numGroupsB) + { + if (idxA < numGroupsA && idxB < numGroupsB && groupsA[idxA].Source == groupsB[idxB].Source) + { + // Both have this frame — compare refs within + int cA = groupsA[idxA].Count; + int cB = groupsB[idxB].Count; + bool aUsed[MAX_COLLECTED_REFS] = {}; + bool bUsed[MAX_COLLECTED_REFS] = {}; + int unmatchedA = 0, unmatchedB = 0; + + CompareFrameRefs(&refsA[groupsA[idxA].StartIdx], cA, + &refsB[groupsB[idxB].StartIdx], cB, + &unmatchedA, &unmatchedB, aUsed, bUsed); + + if (unmatchedA > 0 || unmatchedB > 0) + { + allMatch = false; + if (s_logFile) + { + char methodName[256]; + ResolveMethodName(groupsA[idxA].Source, groupsA[idxA].SourceType, methodName, sizeof(methodName)); + + // Log SP from first ref in each group for unwinder comparison + auto spA = refsA[groupsA[idxA].StartIdx].StackPointer; + auto spB = refsB[groupsB[idxB].StartIdx].StackPointer; + fprintf(s_logFile, " [FRAME_DIFF] Source=0x%llx (%s): %s=%d %s=%d SP_%s=0x%llx SP_%s=0x%llx%s\n", + (unsigned long long)groupsA[idxA].Source, methodName, labelA, cA, labelB, cB, + labelA, (unsigned long long)spA, labelB, (unsigned long long)spB, + spA != spB ? " <-- SP MISMATCH" : ""); + + // Dump ALL refs from both sides for detailed comparison + for (int i = 0; i < cA; i++) + { + auto& r = refsA[groupsA[idxA].StartIdx + i]; + fprintf(s_logFile, " [%s_%s] Addr=0x%llx Obj=0x%llx Flags=0x%x Reg=%d Off=%d\n", + labelA, aUsed[i] ? "MATCHED" : "ONLY", + (unsigned long long)r.Address, (unsigned long long)r.Object, r.Flags, + r.Register, r.Offset); + } + for (int j = 0; j < cB; j++) + { + auto& r = refsB[groupsB[idxB].StartIdx + j]; + fprintf(s_logFile, " [%s_%s] Addr=0x%llx Obj=0x%llx Flags=0x%x Reg=%d Off=%d\n", + labelB, bUsed[j] ? "MATCHED" : "ONLY", + (unsigned long long)r.Address, (unsigned long long)r.Object, r.Flags, + r.Register, r.Offset); + } + } + } + idxA++; + idxB++; + } + else if (idxB >= numGroupsB || (idxA < numGroupsA && groupsA[idxA].Source < groupsB[idxB].Source)) + { + // Frame only in A + allMatch = false; + if (s_logFile) + { + char methodName[256]; + ResolveMethodName(groupsA[idxA].Source, groupsA[idxA].SourceType, methodName, sizeof(methodName)); + fprintf(s_logFile, " [FRAME_%s_ONLY] Source=0x%llx (%s): %s=%d\n", + labelA, (unsigned long long)groupsA[idxA].Source, methodName, labelA, groupsA[idxA].Count); + } + idxA++; + } + else + { + // Frame only in B + allMatch = false; + if (s_logFile) + { + char methodName[256]; + ResolveMethodName(groupsB[idxB].Source, groupsB[idxB].SourceType, methodName, sizeof(methodName)); + fprintf(s_logFile, " [FRAME_%s_ONLY] Source=0x%llx (%s): %s=%d\n", + labelB, (unsigned long long)groupsB[idxB].Source, methodName, labelB, groupsB[idxB].Count); + } + idxB++; + } + } + + if (allMatch && s_logFile) + fprintf(s_logFile, " [MATCH] All %d refs matched\n", countA); + + return allMatch; +} + //----------------------------------------------------------------------------- // Filter interior stack pointers and deduplicate a ref set in place. //----------------------------------------------------------------------------- @@ -1024,12 +1250,33 @@ void CdacStress::VerifyAtAllocPoint() if (t_inVerification) return; + if (ShouldSkipStressPoint()) + return; + Thread* pThread = GetThreadNULLOk(); if (pThread == nullptr || !pThread->PreemptiveGCDisabled()) return; + // Capture the current context and unwind past VerifyAtAllocPoint + // so the walk starts from the caller, not from inside this function. CONTEXT ctx; RtlCaptureContext(&ctx); +#ifndef TARGET_UNIX + { + ULONG64 imageBase = 0; + PRUNTIME_FUNCTION pFunctionEntry = RtlLookupFunctionEntry(GetIP(&ctx), &imageBase, nullptr); + if (pFunctionEntry != nullptr) + { + void* handlerData = nullptr; + ULONG64 establisherFrame = 0; + RtlVirtualUnwind(UNW_FLAG_NHANDLER, imageBase, GetIP(&ctx), + pFunctionEntry, &ctx, &handlerData, &establisherFrame, nullptr); + } + } +#else + PAL_VirtualUnwind(&ctx); +#endif + VerifyAtStressPoint(pThread, &ctx); } @@ -1067,21 +1314,15 @@ void CdacStress::VerifyAtStressPoint(Thread* pThread, PCONTEXT regs) // 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)) @@ -1092,54 +1333,45 @@ void CdacStress::VerifyAtStressPoint(Thread* pThread, PCONTEXT regs) } // Step 1: Collect raw refs from cDAC (always) and DAC (if USE_DAC). + // Save and restore the thread's Frame chain around the cDAC call because + // the cDAC runs as NativeAOT managed code on the same thread and may + // push its own Frames during execution. DWORD osThreadId = pThread->GetOSThreadId(); + Frame* pSavedFrame = pThread->GetFrame(); SArray cdacRefs; - bool haveCdac = CollectStackRefs(s_cdacSosDac, osThreadId, &cdacRefs); + bool haveCdac = CollectStackRefs(s_cdacSosDac, osThreadId, &cdacRefs, "cDAC"); + + // Restore the Frame chain to what it was before the cDAC walk + pThread->SetFrame(pSavedFrame); SArray dacRefs; bool haveDac = false; if (s_cdacStressLevel & CDACSTRESS_USE_DAC) - { - haveDac = (s_dacSosDac != nullptr) && CollectStackRefs(s_dacSosDac, osThreadId, &dacRefs); - } + haveDac = (s_dacSosDac != nullptr) && CollectStackRefs(s_dacSosDac, osThreadId, &dacRefs, "DAC"); s_currentContext = nullptr; s_currentThreadId = 0; StackRef runtimeRefsBuf[MAX_COLLECTED_REFS]; int runtimeCount = 0; - bool haveRuntime = CollectRuntimeStackRefs(pThread, regs, runtimeRefsBuf, &runtimeCount); + bool rtOverflow = !CollectRuntimeStackRefs(pThread, regs, runtimeRefsBuf, &runtimeCount); + if (rtOverflow && s_logFile != nullptr) + { + fprintf(s_logFile, "[SKIP] Thread=0x%x IP=0x%p - RT overflow (>%d refs)\n", + osThreadId, (void*)GetIP(regs), MAX_COLLECTED_REFS); + } - if (!haveCdac || !haveRuntime) + if (!haveCdac) { InterlockedIncrement(&s_verifySkip); if (s_logFile != nullptr) - { - if (!haveCdac) - fprintf(s_logFile, "[SKIP] Thread=0x%x IP=0x%p - cDAC GetStackReferences failed\n", - osThreadId, (void*)GetIP(regs)); - else - fprintf(s_logFile, "[SKIP] Thread=0x%x IP=0x%p - runtime CollectRuntimeStackRefs overflowed\n", - osThreadId, (void*)GetIP(regs)); - } + 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). + // Step 2: Compute stack limit for filtering. Frame* pTopFrame = pThread->GetFrame(); Object** topStack = (Object**)pTopFrame; if (InlinedCallFrame::FrameHasActiveCall(pTopFrame)) @@ -1149,56 +1381,168 @@ void CdacStress::VerifyAtStressPoint(Thread* pThread, PCONTEXT regs) } uintptr_t stackLimit = (uintptr_t)topStack; + // Step 3: Apply unified filtering to both cDAC and DAC refs. + int rawCdacCount = (int)cdacRefs.GetCount(); + int rawDacCount = haveDac ? (int)dacRefs.GetCount() : -1; + int filteredCdacCount = rawCdacCount; if (filteredCdacCount > 0) { - StackRef* cdacBuf = cdacRefs.OpenRawBuffer(); - filteredCdacCount = FilterAndDedup(cdacBuf, filteredCdacCount, pThread, stackLimit); + StackRef* buf = cdacRefs.OpenRawBuffer(); + filteredCdacCount = FilterAndDedup(buf, filteredCdacCount, pThread, stackLimit); cdacRefs.CloseRawBuffer(); } + + int filteredDacCount = rawDacCount; + if (haveDac && filteredDacCount > 0) + { + StackRef* buf = dacRefs.OpenRawBuffer(); + filteredDacCount = FilterAndDedup(buf, filteredDacCount, pThread, stackLimit); + dacRefs.CloseRawBuffer(); + } + runtimeCount = DeduplicateRefs(runtimeRefsBuf, runtimeCount); - StackRef* cdacBuf = cdacRefs.OpenRawBuffer(); - bool rtMatch = CompareRefSets(cdacBuf, filteredCdacCount, runtimeRefsBuf, runtimeCount); - cdacRefs.CloseRawBuffer(); + StackRef* cdacBuf = nullptr; - // Step 4: Pass requires cDAC vs RT match. - // DAC mismatch is logged separately but doesn't affect pass/fail. - bool pass = rtMatch; + // Step 4: Compare cDAC vs DAC (determines pass/fail when USE_DAC is set). + bool dacMatch = true; + if (haveDac) + { + cdacBuf = cdacRefs.OpenRawBuffer(); + StackRef* dacBuf = dacRefs.OpenRawBuffer(); + dacMatch = CompareRefSetsFlat(cdacBuf, filteredCdacCount, dacBuf, filteredDacCount); + cdacRefs.CloseRawBuffer(); + dacRefs.CloseRawBuffer(); + } + + // Step 5: Compare cDAC vs RT (informational). + cdacBuf = cdacRefs.OpenRawBuffer(); + bool rtMatch = CompareRefSetsFlat(cdacBuf, filteredCdacCount, runtimeRefsBuf, runtimeCount); + cdacRefs.CloseRawBuffer(); + bool pass = haveDac ? dacMatch : rtMatch; if (pass) InterlockedIncrement(&s_verifyPass); else InterlockedIncrement(&s_verifyFail); - // Step 5: Log results. + // Step 6: Log structured 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) + if (pass && rtMatch) { - 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) + // Clean pass — one-liner + fprintf(s_logFile, "[PASS] Thread=0x%x IP=0x%p cDAC=%d DAC=%d RT=%d\n", + osThreadId, (void*)GetIP(regs), filteredCdacCount, filteredDacCount, runtimeCount); + } + else + { + // Failure or RT mismatch — structured per-frame output + const char* label = pass ? "PASS" : "FAIL"; + fprintf(s_logFile, "[%s] Thread=0x%x IP=0x%p cDAC=%d DAC=%d RT=%d\n", + label, osThreadId, (void*)GetIP(regs), filteredCdacCount, filteredDacCount, runtimeCount); + + if (!dacMatch && 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); + cdacBuf = cdacRefs.OpenRawBuffer(); + StackRef* dacBuf = dacRefs.OpenRawBuffer(); + ComparePerFrame(cdacBuf, filteredCdacCount, "cDAC", + dacBuf, filteredDacCount, "DAC"); + cdacRefs.CloseRawBuffer(); + dacRefs.CloseRawBuffer(); + } + + if (!rtMatch) + { + fprintf(s_logFile, " [RT_DIFF] cDAC=%d RT=%d (cDAC matches DAC but differs from RT)\n", + filteredCdacCount, runtimeCount); + } + + // Log a stack trace derived from unique Source IPs in the refs + if (!pass) + { + fprintf(s_logFile, " [STACK_TRACE] (cDAC=%d DAC=%d RT=%d)\n", + filteredCdacCount, filteredDacCount, runtimeCount); + + // Collect unique Sources from cDAC refs (sorted by SP descending = top of stack first) + struct FrameEntry { CLRDATA_ADDRESS Source; int SourceType; CLRDATA_ADDRESS SP; }; + FrameEntry frames[256]; + int frameCount = 0; + + cdacBuf = cdacRefs.OpenRawBuffer(); + for (int i = 0; i < filteredCdacCount && frameCount < 256; i++) + { + bool dup = false; + for (int j = 0; j < frameCount; j++) + { + if (frames[j].Source == cdacBuf[i].Source) + { + dup = true; + break; + } + } + if (!dup) + { + frames[frameCount].Source = cdacBuf[i].Source; + frames[frameCount].SourceType = cdacBuf[i].SourceType; + frames[frameCount].SP = cdacBuf[i].StackPointer; + frameCount++; + } + } + cdacRefs.CloseRawBuffer(); + + // Also include DAC-only sources that the cDAC missed + if (haveDac) + { + StackRef* dacBuf = dacRefs.OpenRawBuffer(); + for (int i = 0; i < filteredDacCount && frameCount < 256; i++) + { + bool dup = false; + for (int j = 0; j < frameCount; j++) + { + if (frames[j].Source == dacBuf[i].Source) + { + dup = true; + break; + } + } + if (!dup) + { + frames[frameCount].Source = dacBuf[i].Source; + frames[frameCount].SourceType = dacBuf[i].SourceType; + frames[frameCount].SP = 0; // DAC doesn't provide SP + frameCount++; + } + } + dacRefs.CloseRawBuffer(); + } + + for (int i = 0; i < frameCount; i++) + { + char methodName[256]; + ResolveMethodName(frames[i].Source, frames[i].SourceType, methodName, sizeof(methodName)); + + // Count refs from this source in cDAC and DAC + int cdacCount = 0, dacCount = 0; + cdacBuf = cdacRefs.OpenRawBuffer(); + for (int j = 0; j < filteredCdacCount; j++) + if (cdacBuf[j].Source == frames[i].Source) cdacCount++; + cdacRefs.CloseRawBuffer(); + + if (haveDac) + { + StackRef* dacBuf2 = dacRefs.OpenRawBuffer(); + for (int j = 0; j < filteredDacCount; j++) + if (dacBuf2[j].Source == frames[i].Source) dacCount++; + dacRefs.CloseRawBuffer(); + } + + const char* marker = (cdacCount != dacCount) ? " <-- MISMATCH" : ""; + fprintf(s_logFile, " #%d %s (cDAC=%d DAC=%d)%s\n", + i, methodName, cdacCount, dacCount, marker); + } } - 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); } diff --git a/src/coreclr/vm/gccover.cpp b/src/coreclr/vm/gccover.cpp index 64f22359891a57..7069ddb818f7d6 100644 --- a/src/coreclr/vm/gccover.cpp +++ b/src/coreclr/vm/gccover.cpp @@ -853,6 +853,24 @@ void DoGcStress (PCONTEXT regs, NativeCodeVersion nativeCodeVersion) enableWhenDone = true; } + // When DOTNET_CdacStressStep > 1, skip most stress points (both cDAC verification + // and StressHeap) to reduce overhead. + if (CdacStress::IsInitialized() && CdacStress::ShouldSkipStressPoint()) + { + if (pThread->HasPendingGCStressInstructionUpdate()) + UpdateGCStressInstructionWithoutGC(); + + FlushInstructionCache(GetCurrentProcess(), (LPCVOID)instrPtr, 4); + + if (enableWhenDone) + { + BOOL b = GC_ON_TRANSITIONS(FALSE); + pThread->EnablePreemptiveGC(); + GC_ON_TRANSITIONS(b); + } + return; + } + // // If we redirect for gc stress, we don't need this frame on the stack, // the redirection will push a resumable frame. @@ -1177,6 +1195,18 @@ void DoGcStress (PCONTEXT regs, NativeCodeVersion nativeCodeVersion) // code and it will just raise a STATUS_ACCESS_VIOLATION. pThread->PostGCStressInstructionUpdate((BYTE*)instrPtr, &gcCover->savedCode[offset]); + // When DOTNET_CdacStressStep > 1, skip most stress points (both cDAC verification + // and StressHeap) to reduce overhead. We still restore the instruction since the + // breakpoint must be removed regardless. + if (CdacStress::IsInitialized() && CdacStress::ShouldSkipStressPoint()) + { + if (pThread->HasPendingGCStressInstructionUpdate()) + UpdateGCStressInstructionWithoutGC(); + + FlushInstructionCache(GetCurrentProcess(), (LPCVOID)instrPtr, 4); + return; + } + // we should be in coop mode. _ASSERTE(pThread->PreemptiveGCDisabled()); From f7eacbeaf8d401c88af7051dfe6da4e0c12ae953 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Fri, 24 Apr 2026 14:18:32 -0400 Subject: [PATCH 2/3] [cDAC] Add managed stress tests and CI pipeline 9 debuggee apps, xunit test harness, Helix pipeline for running cDAC GC stress verification in CI. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../cdac/prepare-cdac-stress-helix-steps.yml | 45 +++ eng/pipelines/runtime-diagnostics.yml | 12 +- .../tests/StressTests/BasicCdacStressTests.cs | 63 ++++ .../tests/StressTests/CdacStressResults.cs | 275 ++++++++++++++++++ .../tests/StressTests/CdacStressTestBase.cs | 237 +++++++++++++++ .../Debuggees/DeepStack/DeepStack.csproj | 1 + .../Debuggees/DeepStack/Program.cs | 43 +++ .../DynamicMethods/DynamicMethods.csproj | 1 + .../Debuggees/DynamicMethods/Program.cs | 149 ++++++++++ .../ExceptionHandling.csproj | 1 + .../Debuggees/ExceptionHandling/Program.cs | 143 +++++++++ .../Debuggees/Generics/Generics.csproj | 1 + .../StressTests/Debuggees/Generics/Program.cs | 81 ++++++ .../Debuggees/MultiThread/MultiThread.csproj | 1 + .../Debuggees/MultiThread/Program.cs | 53 ++++ .../Debuggees/PInvoke/PInvoke.csproj | 1 + .../StressTests/Debuggees/PInvoke/Program.cs | 74 +++++ .../Debuggees/StructScenarios/Program.cs | 157 ++++++++++ .../StructScenarios/StructScenarios.csproj | 1 + ...tics.DataContractReader.StressTests.csproj | 21 ++ .../managed/cdac/tests/StressTests/README.md | 157 +++++----- .../tests/StressTests/StressTests.targets | 70 +++++ .../tests/StressTests/cdac-stress-helix.proj | 76 +++++ .../cdac/tests/StressTests/known-issues.md | 173 +++++++---- 24 files changed, 1702 insertions(+), 134 deletions(-) create mode 100644 eng/pipelines/cdac/prepare-cdac-stress-helix-steps.yml create mode 100644 src/native/managed/cdac/tests/StressTests/BasicCdacStressTests.cs create mode 100644 src/native/managed/cdac/tests/StressTests/CdacStressResults.cs create mode 100644 src/native/managed/cdac/tests/StressTests/CdacStressTestBase.cs create mode 100644 src/native/managed/cdac/tests/StressTests/Debuggees/DeepStack/DeepStack.csproj create mode 100644 src/native/managed/cdac/tests/StressTests/Debuggees/DeepStack/Program.cs create mode 100644 src/native/managed/cdac/tests/StressTests/Debuggees/DynamicMethods/DynamicMethods.csproj create mode 100644 src/native/managed/cdac/tests/StressTests/Debuggees/DynamicMethods/Program.cs create mode 100644 src/native/managed/cdac/tests/StressTests/Debuggees/ExceptionHandling/ExceptionHandling.csproj create mode 100644 src/native/managed/cdac/tests/StressTests/Debuggees/ExceptionHandling/Program.cs create mode 100644 src/native/managed/cdac/tests/StressTests/Debuggees/Generics/Generics.csproj create mode 100644 src/native/managed/cdac/tests/StressTests/Debuggees/Generics/Program.cs create mode 100644 src/native/managed/cdac/tests/StressTests/Debuggees/MultiThread/MultiThread.csproj create mode 100644 src/native/managed/cdac/tests/StressTests/Debuggees/MultiThread/Program.cs create mode 100644 src/native/managed/cdac/tests/StressTests/Debuggees/PInvoke/PInvoke.csproj create mode 100644 src/native/managed/cdac/tests/StressTests/Debuggees/PInvoke/Program.cs create mode 100644 src/native/managed/cdac/tests/StressTests/Debuggees/StructScenarios/Program.cs create mode 100644 src/native/managed/cdac/tests/StressTests/Debuggees/StructScenarios/StructScenarios.csproj create mode 100644 src/native/managed/cdac/tests/StressTests/Microsoft.Diagnostics.DataContractReader.StressTests.csproj create mode 100644 src/native/managed/cdac/tests/StressTests/StressTests.targets create mode 100644 src/native/managed/cdac/tests/StressTests/cdac-stress-helix.proj diff --git a/eng/pipelines/cdac/prepare-cdac-stress-helix-steps.yml b/eng/pipelines/cdac/prepare-cdac-stress-helix-steps.yml new file mode 100644 index 00000000000000..436153bc7cc8ef --- /dev/null +++ b/eng/pipelines/cdac/prepare-cdac-stress-helix-steps.yml @@ -0,0 +1,45 @@ +# prepare-cdac-stress-helix-steps.yml - Steps for preparing cDAC stress test Helix payloads. +# +# Used by CdacDumpTests stage in runtime-diagnostics.yml. +# Handles: building stress test debuggees, preparing Helix payload, finding testhost. + +steps: +- script: $(Build.SourcesDirectory)$(dir).dotnet$(dir)dotnet$(exeExt) msbuild + $(Build.SourcesDirectory)/src/native/managed/cdac/tests/StressTests/Microsoft.Diagnostics.DataContractReader.StressTests.csproj + /t:BuildDebuggeesOnly + /p:Configuration=$(_BuildConfig) + /p:TargetArchitecture=$(archType) + -bl:$(Build.SourcesDirectory)/artifacts/log/BuildStressDebuggees.binlog + displayName: 'Build Stress Debuggees' + +- script: $(Build.SourcesDirectory)$(dir).dotnet$(dir)dotnet$(exeExt) build + $(Build.SourcesDirectory)/src/native/managed/cdac/tests/StressTests/Microsoft.Diagnostics.DataContractReader.StressTests.csproj + /p:PrepareHelixPayload=true + /p:Configuration=$(_BuildConfig) + /p:HelixPayloadDir=$(Build.SourcesDirectory)/artifacts/helixPayload/cdac-stress + -bl:$(Build.SourcesDirectory)/artifacts/log/StressTestPayload.binlog + displayName: 'Prepare Stress Test Helix Payload' + +- pwsh: | + $testhostDir = Get-ChildItem -Directory -Path "$(Build.SourcesDirectory)/artifacts/bin/testhost/net*-$(osGroup)-*-$(archType)" | Select-Object -First 1 -ExpandProperty FullName + if (-not $testhostDir) { + Write-Error "No testhost directory found" + exit 1 + } + Write-Host "TestHost root: $testhostDir" + Write-Host "##vso[task.setvariable variable=StressTestHostRootDir]$testhostDir" + + $queue = switch ("$(osGroup)_$(archType)") { + "windows_x64" { "$(helix_windows_x64)" } + "windows_x86" { "$(helix_windows_x64)" } + "windows_arm64" { "$(helix_windows_arm64)" } + "linux_x64" { "$(helix_linux_x64_oldest)" } + "linux_arm64" { "$(helix_linux_arm64_oldest)" } + "linux_arm" { "$(helix_linux_arm32_oldest)" } + "osx_x64" { "$(helix_macos_x64)" } + "osx_arm64" { "$(helix_macos_arm64)" } + default { Write-Error "Unsupported platform: $(osGroup)_$(archType)"; exit 1 } + } + Write-Host "Helix queue: $queue" + Write-Host "##vso[task.setvariable variable=CdacStressHelixQueue]$queue" + displayName: 'Find Stress TestHost and Helix Queue' diff --git a/eng/pipelines/runtime-diagnostics.yml b/eng/pipelines/runtime-diagnostics.yml index ca38617222003e..9065835250c724 100644 --- a/eng/pipelines/runtime-diagnostics.yml +++ b/eng/pipelines/runtime-diagnostics.yml @@ -285,7 +285,7 @@ extends: shouldContinueOnError: true jobParameters: nameSuffix: CdacDumpTest - buildArgs: -s clr+libs+tools.cdac+tools.cdacdumptests -c $(_BuildConfig) -rc checked -lc $(_BuildConfig) /p:SkipDumpVersions=net10.0 + buildArgs: -s clr+libs+tools.cdac+tools.cdacdumptests+tools.cdacstresstests -c $(_BuildConfig) -rc checked -lc $(_BuildConfig) /p:SkipDumpVersions=net10.0 timeoutInMinutes: 180 postBuildSteps: - template: /eng/pipelines/cdac/prepare-cdac-helix-steps.yml @@ -304,6 +304,16 @@ extends: displayName: 'Publish Dump Artifacts' condition: and(always(), ne(variables['Agent.JobStatus'], 'Succeeded')) continueOnError: true + # cDAC Stress Tests — run GC stress verification on the same Checked build + - template: /eng/pipelines/cdac/prepare-cdac-stress-helix-steps.yml + - template: /eng/pipelines/common/templates/runtimes/send-to-helix-inner-step.yml + parameters: + displayName: 'Send cDAC Stress Tests to Helix' + sendParams: $(Build.SourcesDirectory)/src/native/managed/cdac/tests/StressTests/cdac-stress-helix.proj /t:Test /p:TargetOS=$(osGroup) /p:TargetArchitecture=$(archType) /p:HelixTargetQueues="$(CdacStressHelixQueue)" /p:TestHostPayload=$(StressTestHostRootDir) /p:StressTestsPayload=$(Build.SourcesDirectory)/artifacts/helixPayload/cdac-stress /bl:$(Build.SourcesDirectory)/artifacts/log/SendStressToHelix.binlog + environment: + _Creator: dotnet-bot + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + NUGET_PACKAGES: $(Build.SourcesDirectory)$(dir).packages - pwsh: | if ("$(Agent.JobStatus)" -ne "Succeeded") { Write-Error "One or more cDAC dump test failures were detected. Failing the job." diff --git a/src/native/managed/cdac/tests/StressTests/BasicCdacStressTests.cs b/src/native/managed/cdac/tests/StressTests/BasicCdacStressTests.cs new file mode 100644 index 00000000000000..5da773f7828f7b --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/BasicCdacStressTests.cs @@ -0,0 +1,63 @@ +// 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_CdacStress=0x51 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 BasicStressTests : CdacStressTestBase +{ + public BasicStressTests(ITestOutputHelper output) : base(output) { } + + public static IEnumerable Debuggees => + [ + ["BasicAlloc"], + ["DeepStack"], + ["Generics"], + ["MultiThread"], + ["Comprehensive"], + ["ExceptionHandling"], + ["StructScenarios"], + ["DynamicMethods"], + ]; + + public static IEnumerable WindowsOnlyDebuggees => + [ + ["PInvoke"], + ]; + + [Theory] + [MemberData(nameof(Debuggees))] + public void GCStress_AllVerificationsPass(string debuggeeName) + { + CdacStressResults 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)"); + + CdacStressResults results = RunGCStress(debuggeeName); + AssertAllPassed(results, debuggeeName); + } +} diff --git a/src/native/managed/cdac/tests/StressTests/CdacStressResults.cs b/src/native/managed/cdac/tests/StressTests/CdacStressResults.cs new file mode 100644 index 00000000000000..dd2222741ebed3 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/CdacStressResults.cs @@ -0,0 +1,275 @@ +// 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.Linq; +using System.Text.RegularExpressions; + +namespace Microsoft.Diagnostics.DataContractReader.Tests.GCStress; + +/// +/// Parses the cdac stress results log file written by the native cdacstress.cpp hook. +/// +internal sealed partial class CdacStressResults +{ + 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 int RtDiffs { get; private set; } + public string LogFilePath { get; private set; } = string.Empty; + public List FailureDetails { get; } = []; + public List SkipDetails { get; } = []; + public List FailedVerifications { 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(); + + [GeneratedRegex(@"\[RT_DIFF\]")] + private static partial Regex RtDiffPattern(); + + [GeneratedRegex(@"\[FRAME_DIFF\]\s+Source=0x(\w+)\s+\(([^)]+)\):\s+(\w+)=(\d+)\s+(\w+)=(\d+)")] + private static partial Regex FrameDiffPattern(); + + [GeneratedRegex(@"\[FRAME_(\w+)_ONLY\]\s+Source=0x(\w+)\s+\(([^)]+)\):\s+\w+=(\d+)")] + private static partial Regex FrameOnlyPattern(); + + [GeneratedRegex(@"\[(cDAC|DAC|RT)_ONLY\]\s+Addr=0x(\w+)\s+Obj=0x(\w+)\s+Flags=0x(\w+)")] + private static partial Regex RefOnlyPattern(); + + [GeneratedRegex(@"cDAC \[\d+\]:\s+Addr=0x(\w+)\s+Obj=0x(\w+)\s+Flags=0x(\w+)\s+Src=(.+)")] + private static partial Regex CdacRefPattern(); + + [GeneratedRegex(@"RT\s+\[\d+\]:\s+Addr=0x(\w+)\s+Obj=0x(\w+)\s+Flags=0x(\w+)")] + private static partial Regex RtRefPattern(); + + public static CdacStressResults Parse(string logFilePath) + { + if (!File.Exists(logFilePath)) + throw new FileNotFoundException($"GC stress results log not found: {logFilePath}"); + + var results = new CdacStressResults { LogFilePath = logFilePath }; + FailedVerification? currentFailure = null; + FrameDiff? currentFrame = null; + + foreach (string line in File.ReadLines(logFilePath)) + { + string trimmed = line.Trim(); + + if (PassPattern().IsMatch(trimmed)) + { + currentFailure = null; + currentFrame = null; + results.Passed++; + } + else if (FailPattern().IsMatch(trimmed)) + { + results.Failed++; + results.FailureDetails.Add(trimmed); + currentFailure = new FailedVerification { Header = trimmed }; + results.FailedVerifications.Add(currentFailure); + currentFrame = null; + } + else if (SkipPattern().IsMatch(trimmed)) + { + currentFailure = null; + currentFrame = null; + results.Skipped++; + results.SkipDetails.Add(trimmed); + } + else if (RtDiffPattern().IsMatch(trimmed)) + { + results.RtDiffs++; + } + else if (currentFailure is not null) + { + // Parse structured per-frame output + Match frameDiff = FrameDiffPattern().Match(trimmed); + if (frameDiff.Success) + { + currentFrame = new FrameDiff + { + Source = ulong.Parse(frameDiff.Groups[1].Value, System.Globalization.NumberStyles.HexNumber), + MethodName = frameDiff.Groups[2].Value, + CdacCount = int.Parse(frameDiff.Groups[4].Value), + DacCount = int.Parse(frameDiff.Groups[6].Value), + Kind = FrameDiffKind.Different, + }; + currentFailure.FrameDiffs.Add(currentFrame); + continue; + } + + Match frameOnly = FrameOnlyPattern().Match(trimmed); + if (frameOnly.Success) + { + string ownerLabel = frameOnly.Groups[1].Value; + currentFrame = new FrameDiff + { + Source = ulong.Parse(frameOnly.Groups[2].Value, System.Globalization.NumberStyles.HexNumber), + MethodName = frameOnly.Groups[3].Value, + Kind = ownerLabel == "cDAC" ? FrameDiffKind.CdacOnly : FrameDiffKind.DacOnly, + }; + int count = int.Parse(frameOnly.Groups[4].Value); + if (currentFrame.Kind == FrameDiffKind.CdacOnly) + currentFrame.CdacCount = count; + else + currentFrame.DacCount = count; + currentFailure.FrameDiffs.Add(currentFrame); + continue; + } + + Match refOnly = RefOnlyPattern().Match(trimmed); + if (refOnly.Success && currentFrame is not null) + { + var r = new StackRef + { + Address = ulong.Parse(refOnly.Groups[2].Value, System.Globalization.NumberStyles.HexNumber), + Object = ulong.Parse(refOnly.Groups[3].Value, System.Globalization.NumberStyles.HexNumber), + Flags = uint.Parse(refOnly.Groups[4].Value, System.Globalization.NumberStyles.HexNumber), + }; + currentFrame.UnmatchedRefs.Add(($"{refOnly.Groups[1].Value}_ONLY", r)); + continue; + } + + // Parse flat cDAC/RT ref lines (for cDAC-vs-RT comparison) + Match cdacRef = CdacRefPattern().Match(trimmed); + if (cdacRef.Success) + { + currentFailure.CdacRefs.Add(new StackRef + { + Address = ulong.Parse(cdacRef.Groups[1].Value, System.Globalization.NumberStyles.HexNumber), + Object = ulong.Parse(cdacRef.Groups[2].Value, System.Globalization.NumberStyles.HexNumber), + Flags = uint.Parse(cdacRef.Groups[3].Value, System.Globalization.NumberStyles.HexNumber), + }); + continue; + } + + Match rtRef = RtRefPattern().Match(trimmed); + if (rtRef.Success) + { + currentFailure.RtRefs.Add(new StackRef + { + Address = ulong.Parse(rtRef.Groups[1].Value, System.Globalization.NumberStyles.HexNumber), + Object = ulong.Parse(rtRef.Groups[2].Value, System.Globalization.NumberStyles.HexNumber), + Flags = uint.Parse(rtRef.Groups[3].Value, System.Globalization.NumberStyles.HexNumber), + }); + continue; + } + + // Parse [STACK_TRACE] frame lines: #N MethodName (cDAC=X DAC=Y) + if (trimmed.StartsWith("#") && trimmed.Contains("(cDAC=")) + { + currentFailure.StackTrace.Add(trimmed); + } + } + + Match totalMatch = TotalPattern().Match(trimmed); + 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}, RtDiffs={RtDiffs}"; + + /// + /// Formats the first N failed verifications using the structured per-frame data + /// logged by the native code. No re-analysis needed — just presents what was logged. + /// + public string AnalyzeFailures(int maxFailures = 3) + { + var sb = new System.Text.StringBuilder(); + + foreach (var failure in FailedVerifications.Take(maxFailures)) + { + sb.AppendLine(failure.Header); + + if (failure.FrameDiffs.Count > 0) + { + sb.AppendLine(" Per-frame diff (cDAC vs DAC):"); + foreach (var frame in failure.FrameDiffs) + { + string kindLabel = frame.Kind switch + { + FrameDiffKind.Different => $"cDAC={frame.CdacCount} DAC={frame.DacCount}", + FrameDiffKind.CdacOnly => $"cDAC={frame.CdacCount} (cDAC-only frame)", + FrameDiffKind.DacOnly => $"DAC={frame.DacCount} (DAC-only frame)", + _ => "unknown", + }; + sb.AppendLine($" {frame.MethodName}: {kindLabel}"); + foreach (var (label, r) in frame.UnmatchedRefs) + sb.AppendLine($" [{label}] Addr=0x{r.Address:X} Obj=0x{r.Object:X} Flags=0x{r.Flags:X}"); + } + } + + if (failure.CdacRefs.Count > 0 || failure.RtRefs.Count > 0) + { + sb.AppendLine($" cDAC vs RT: cDAC={failure.CdacRefs.Count} RT={failure.RtRefs.Count}"); + } + + if (failure.StackTrace.Count > 0) + { + sb.AppendLine(" Stack trace:"); + foreach (string frame in failure.StackTrace) + sb.AppendLine($" {frame}"); + } + + sb.AppendLine(); + } + + return sb.ToString(); + } +} + +internal struct StackRef +{ + public ulong Address; + public ulong Object; + public uint Flags; +} + +internal enum FrameDiffKind +{ + Different, + CdacOnly, + DacOnly, +} + +internal sealed class FrameDiff +{ + public ulong Source { get; set; } + public string MethodName { get; set; } = ""; + public int CdacCount { get; set; } + public int DacCount { get; set; } + public FrameDiffKind Kind { get; set; } + public List<(string Label, StackRef Ref)> UnmatchedRefs { get; } = []; +} + +internal sealed class FailedVerification +{ + public string Header { get; set; } = ""; + public List FrameDiffs { get; } = []; + public List CdacRefs { get; } = []; + public List RtRefs { get; } = []; + public List StackTrace { get; } = []; +} diff --git a/src/native/managed/cdac/tests/StressTests/CdacStressTestBase.cs b/src/native/managed/cdac/tests/StressTests/CdacStressTestBase.cs new file mode 100644 index 00000000000000..8a3eb52219ba38 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/CdacStressTestBase.cs @@ -0,0 +1,237 @@ +// 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 stress tests. Runs a debuggee app under corerun +/// with DOTNET_CdacStress=0x51 and parses the verification results. +/// +public abstract class CdacStressTestBase +{ + private readonly ITestOutputHelper _output; + + protected CdacStressTestBase(ITestOutputHelper output) + { + _output = output; + } + + /// + /// Runs the named debuggee under GC stress and returns the parsed results. + /// + internal CdacStressResults 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; + // Default to 0x51 (ALLOC + REFS + USE_DAC) for three-way comparison. + // Override via outer DOTNET_CdacStress env var if needed. + psi.Environment["DOTNET_CdacStress"] = + Environment.GetEnvironmentVariable("DOTNET_CdacStress") ?? "0x51"; + 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 both stdout and stderr asynchronously to avoid deadlock + // when pipe buffers fill, and to allow WaitForExit timeout to work. + string stderr = ""; + string stdout = ""; + process.ErrorDataReceived += (_, e) => + { + if (e.Data is not null) + stderr += e.Data + Environment.NewLine; + }; + process.OutputDataReceived += (_, e) => + { + if (e.Data is not null) + stdout += e.Data + Environment.NewLine; + }; + process.BeginErrorReadLine(); + process.BeginOutputReadLine(); + + 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}"); + + CdacStressResults results = CdacStressResults.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(CdacStressResults 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 analysis = results.AnalyzeFailures(maxFailures: 3); + Assert.Fail( + $"GC stress test '{debuggeeName}' had {results.Failed} failure(s) " + + $"out of {results.TotalVerifications} verifications.\n" + + $"Log: {results.LogFilePath}\n\n{analysis}"); + } + + 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. + /// Useful for instruction-level stress where a small number of failures may occur + /// due to known limitations. + /// + internal static void AssertHighPassRate(CdacStressResults 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" + : RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "osx" + : "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) + { + // On Helix, debuggees are in the work item payload's debuggees/ directory. + // The test assembly is in /tests/, so AppContext.BaseDirectory is there. + // The debuggees are siblings at /debuggees//. + string? helixPayload = Environment.GetEnvironmentVariable("HELIX_WORKITEM_PAYLOAD"); + if (!string.IsNullOrEmpty(helixPayload)) + { + string helixDebuggeesDir = Path.Combine(helixPayload, "debuggees", debuggeeName); + if (Directory.Exists(helixDebuggeesDir)) + { + foreach (string dir in Directory.GetDirectories(helixDebuggeesDir, "*", SearchOption.AllDirectories)) + { + string dll = Path.Combine(dir, $"{debuggeeName}.dll"); + if (File.Exists(dll)) + return dll; + } + } + } + + // Local development: debuggees are built to artifacts/bin/StressTests// + string repoRoot = FindRepoRoot(); + string binDir = Path.Combine(repoRoot, "artifacts", "bin", "StressTests", debuggeeName); + + if (!Directory.Exists(binDir)) + throw new DirectoryNotFoundException( + $"Debuggee '{debuggeeName}' not found at '{binDir}'. Build the StressTests 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/StressTests/Debuggees/DeepStack/DeepStack.csproj b/src/native/managed/cdac/tests/StressTests/Debuggees/DeepStack/DeepStack.csproj new file mode 100644 index 00000000000000..6b512ec9245ec3 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/DeepStack/DeepStack.csproj @@ -0,0 +1 @@ + diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/DeepStack/Program.cs b/src/native/managed/cdac/tests/StressTests/Debuggees/DeepStack/Program.cs new file mode 100644 index 00000000000000..c98679aea54ac2 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/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/StressTests/Debuggees/DynamicMethods/DynamicMethods.csproj b/src/native/managed/cdac/tests/StressTests/Debuggees/DynamicMethods/DynamicMethods.csproj new file mode 100644 index 00000000000000..6b512ec9245ec3 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/DynamicMethods/DynamicMethods.csproj @@ -0,0 +1 @@ + diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/DynamicMethods/Program.cs b/src/native/managed/cdac/tests/StressTests/Debuggees/DynamicMethods/Program.cs new file mode 100644 index 00000000000000..865d338e1e935f --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/DynamicMethods/Program.cs @@ -0,0 +1,149 @@ +// 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.Reflection; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; + +/// +/// Exercises the MetaSig (non-GCRefMap) path by creating and invoking +/// DynamicMethod (LCG) methods. These methods use StoredSigMethodDesc +/// and don't have pre-computed GCRefMaps, forcing PromoteCallerStack +/// to walk the signature via MetaSig. +/// +/// Scenarios: +/// - Simple object parameter (GcTypeKind.Ref) +/// - Multiple object parameters +/// - Byref parameter (GcTypeKind.Interior) +/// - Mixed ref and primitive parameters +/// - Method with 'this' (instance delegate) +/// - Method returning object (tests return type parsing) +/// +internal static class Program +{ + static int Main() + { + for (int i = 0; i < 50; i++) + { + SimpleObjectParam(); + MultipleObjectParams(); + MixedParams(); + ObjectReturn(); + KeepAliveInDynamic(); + } + return 100; + } + + // ===== Scenario 1: Single object parameter ===== + [MethodImpl(MethodImplOptions.NoInlining)] + static void SimpleObjectParam() + { + // Create: void DynMethod(object o) + DynamicMethod dm = new("DynSimple", typeof(void), new[] { typeof(object) }); + ILGenerator il = dm.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, typeof(GC).GetMethod(nameof(GC.KeepAlive))!); + il.Emit(OpCodes.Ret); + + Action del = dm.CreateDelegate>(); + object live = new object(); + del(live); + GC.KeepAlive(live); + } + + // ===== Scenario 2: Multiple object parameters ===== + [MethodImpl(MethodImplOptions.NoInlining)] + static void MultipleObjectParams() + { + // Create: void DynMulti(object a, string b, int[] c) + DynamicMethod dm = new("DynMulti", typeof(void), + new[] { typeof(object), typeof(string), typeof(int[]) }); + ILGenerator il = dm.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, typeof(GC).GetMethod(nameof(GC.KeepAlive))!); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, typeof(GC).GetMethod(nameof(GC.KeepAlive))!); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Call, typeof(GC).GetMethod(nameof(GC.KeepAlive))!); + il.Emit(OpCodes.Ret); + + var del = dm.CreateDelegate>(); + object a = new object(); + string b = "hello"; + int[] c = new[] { 1, 2, 3 }; + del(a, b, c); + GC.KeepAlive(a); + GC.KeepAlive(b); + GC.KeepAlive(c); + } + + // ===== Scenario 3: Mixed ref and primitive parameters ===== + [MethodImpl(MethodImplOptions.NoInlining)] + static void MixedParams() + { + // Create: void DynMixed(object o, int x, string s, long y) + DynamicMethod dm = new("DynMixed", typeof(void), + new[] { typeof(object), typeof(int), typeof(string), typeof(long) }); + ILGenerator il = dm.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, typeof(GC).GetMethod(nameof(GC.KeepAlive))!); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Call, typeof(GC).GetMethod(nameof(GC.KeepAlive))!); + il.Emit(OpCodes.Ret); + + var del = dm.CreateDelegate>(); + object o = new object(); + string s = "world"; + del(o, 42, s, 999L); + GC.KeepAlive(o); + GC.KeepAlive(s); + } + + // ===== Scenario 4: Object return type ===== + [MethodImpl(MethodImplOptions.NoInlining)] + static void ObjectReturn() + { + // Create: object DynReturn(object o) + DynamicMethod dm = new("DynReturn", typeof(object), new[] { typeof(object) }); + ILGenerator il = dm.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ret); + + var del = dm.CreateDelegate>(); + object input = new object(); + object result = del(input); + GC.KeepAlive(result); + GC.KeepAlive(input); + } + + // ===== Scenario 5: Multiple allocations inside dynamic method ===== + [MethodImpl(MethodImplOptions.NoInlining)] + static void KeepAliveInDynamic() + { + // Create: void DynAlloc(object a, object b, object c, object d) + DynamicMethod dm = new("DynAlloc", typeof(void), + new[] { typeof(object), typeof(object), typeof(object), typeof(object) }); + ILGenerator il = dm.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, typeof(GC).GetMethod(nameof(GC.KeepAlive))!); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, typeof(GC).GetMethod(nameof(GC.KeepAlive))!); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Call, typeof(GC).GetMethod(nameof(GC.KeepAlive))!); + il.Emit(OpCodes.Ldarg_3); + il.Emit(OpCodes.Call, typeof(GC).GetMethod(nameof(GC.KeepAlive))!); + il.Emit(OpCodes.Ret); + + var del = dm.CreateDelegate>(); + object a = new object(); + object b = "str"; + object c = new int[] { 1 }; + object d = new byte[16]; + del(a, b, c, d); + GC.KeepAlive(a); + GC.KeepAlive(b); + GC.KeepAlive(c); + GC.KeepAlive(d); + } +} diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/ExceptionHandling/ExceptionHandling.csproj b/src/native/managed/cdac/tests/StressTests/Debuggees/ExceptionHandling/ExceptionHandling.csproj new file mode 100644 index 00000000000000..6b512ec9245ec3 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/ExceptionHandling/ExceptionHandling.csproj @@ -0,0 +1 @@ + diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/ExceptionHandling/Program.cs b/src/native/managed/cdac/tests/StressTests/Debuggees/ExceptionHandling/Program.cs new file mode 100644 index 00000000000000..4bd0a12fe6d145 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/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/StressTests/Debuggees/Generics/Generics.csproj b/src/native/managed/cdac/tests/StressTests/Debuggees/Generics/Generics.csproj new file mode 100644 index 00000000000000..6b512ec9245ec3 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/Generics/Generics.csproj @@ -0,0 +1 @@ + diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/Generics/Program.cs b/src/native/managed/cdac/tests/StressTests/Debuggees/Generics/Program.cs new file mode 100644 index 00000000000000..54b7060c040f5a --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/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/StressTests/Debuggees/MultiThread/MultiThread.csproj b/src/native/managed/cdac/tests/StressTests/Debuggees/MultiThread/MultiThread.csproj new file mode 100644 index 00000000000000..6b512ec9245ec3 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/MultiThread/MultiThread.csproj @@ -0,0 +1 @@ + diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/MultiThread/Program.cs b/src/native/managed/cdac/tests/StressTests/Debuggees/MultiThread/Program.cs new file mode 100644 index 00000000000000..0eea731a6bd313 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/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/StressTests/Debuggees/PInvoke/PInvoke.csproj b/src/native/managed/cdac/tests/StressTests/Debuggees/PInvoke/PInvoke.csproj new file mode 100644 index 00000000000000..6b512ec9245ec3 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/PInvoke/PInvoke.csproj @@ -0,0 +1 @@ + diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/PInvoke/Program.cs b/src/native/managed/cdac/tests/StressTests/Debuggees/PInvoke/Program.cs new file mode 100644 index 00000000000000..83aece921baaea --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/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/StressTests/Debuggees/StructScenarios/Program.cs b/src/native/managed/cdac/tests/StressTests/Debuggees/StructScenarios/Program.cs new file mode 100644 index 00000000000000..9067337495def2 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/StructScenarios/Program.cs @@ -0,0 +1,157 @@ +// 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 struct-related GC scanning scenarios that stress the MetaSig path: +/// - Value type 'this' (interior pointer for struct instance methods) +/// - Small struct returns (retbuf detection precision) +/// - Struct parameters containing embedded GC references +/// +internal static class Program +{ + static int Main() + { + for (int i = 0; i < 100; i++) + { + ValueTypeThisScenario(); + SmallStructReturnScenario(); + StructWithRefsScenario(); + InterfaceDispatchScenario(); + } + return 100; + } + + // ===== Scenario 1: Value type 'this' ===== + // When a struct instance method is called through interface dispatch, + // 'this' is an interior pointer (pointing into the boxed struct, past + // the MethodTable pointer). The GC needs GC_CALL_INTERIOR to handle it. + + interface IKeepAlive + { + object GetRef(); + } + + struct StructWithRef : IKeepAlive + { + public object Field; + + [MethodImpl(MethodImplOptions.NoInlining)] + public object GetRef() => Field; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void ValueTypeThisScenario() + { + IKeepAlive s = new StructWithRef { Field = new object() }; + object r = s.GetRef(); + GC.KeepAlive(r); + GC.KeepAlive(s); + } + + // ===== Scenario 2: Small struct returns ===== + // Methods returning small structs (1/2/4/8 bytes, power-of-2) do NOT need + // a return buffer on AMD64 Windows — the value is returned in RAX. + // Conservative HasRetBuffArg=true shifts all parameter offsets by 1 slot. + + struct SmallResult + { + public int Value; + } + + struct TinyResult + { + public byte Value; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static SmallResult MakeSmallResult(object keepAlive) + { + GC.KeepAlive(keepAlive); + return new SmallResult { Value = 42 }; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static TinyResult MakeTinyResult(object keepAlive) + { + GC.KeepAlive(keepAlive); + return new TinyResult { Value = 1 }; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void SmallStructReturnScenario() + { + object live = new object(); + SmallResult sr = MakeSmallResult(live); + TinyResult tr = MakeTinyResult(live); + GC.KeepAlive(sr); + GC.KeepAlive(tr); + GC.KeepAlive(live); + } + + // ===== Scenario 3: Struct parameters with embedded GC refs ===== + // Value type parameters containing object references require GCDesc + // scanning to find the embedded refs. Without this, the refs inside + // the struct are invisible to the GC. + + struct Holder + { + public object Ref1; + public string Ref2; + public int[] Array; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void ProcessHolder(Holder h) + { + GC.KeepAlive(h.Ref1); + GC.KeepAlive(h.Ref2); + GC.KeepAlive(h.Array); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void StructWithRefsScenario() + { + Holder h = new Holder + { + Ref1 = new object(), + Ref2 = "hello", + Array = new int[] { 1, 2, 3 }, + }; + ProcessHolder(h); + GC.KeepAlive(h.Ref1); + } + + // ===== Scenario 4: Interface dispatch with generics ===== + // Shared generic methods going through stub dispatch combine + // RequiresInstArg with value type 'this'. + + interface IGenericOp + { + T Get(); + } + + struct GenericStruct : IGenericOp + { + public T Value; + + [MethodImpl(MethodImplOptions.NoInlining)] + public T Get() => Value; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void InterfaceDispatchScenario() + { + IGenericOp g = new GenericStruct { Value = new object() }; + object r = g.Get(); + GC.KeepAlive(r); + GC.KeepAlive(g); + + IGenericOp gs = new GenericStruct { Value = "test" }; + string s = gs.Get(); + GC.KeepAlive(s); + GC.KeepAlive(gs); + } +} diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/StructScenarios/StructScenarios.csproj b/src/native/managed/cdac/tests/StressTests/Debuggees/StructScenarios/StructScenarios.csproj new file mode 100644 index 00000000000000..6b512ec9245ec3 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/StructScenarios/StructScenarios.csproj @@ -0,0 +1 @@ + diff --git a/src/native/managed/cdac/tests/StressTests/Microsoft.Diagnostics.DataContractReader.StressTests.csproj b/src/native/managed/cdac/tests/StressTests/Microsoft.Diagnostics.DataContractReader.StressTests.csproj new file mode 100644 index 00000000000000..d6bd3aa5a13459 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Microsoft.Diagnostics.DataContractReader.StressTests.csproj @@ -0,0 +1,21 @@ + + + true + $(NetCoreAppToolCurrent) + enable + true + + + + + + + + + + + + + + + diff --git a/src/native/managed/cdac/tests/StressTests/README.md b/src/native/managed/cdac/tests/StressTests/README.md index c5bcde5675b3f0..97a676edfa2892 100644 --- a/src/native/managed/cdac/tests/StressTests/README.md +++ b/src/native/managed/cdac/tests/StressTests/README.md @@ -1,18 +1,39 @@ # cDAC Stress Tests -This folder contains stress tests that verify the cDAC's stack reference -enumeration against the runtime's GC root scanning. The tests run managed -debuggee applications under `corerun` with cDAC stress flags enabled, -triggering verification at allocation points, GC points, or instruction-level -GC stress points. +Integration tests that verify the cDAC's stack reference enumeration matches the runtime's +GC root scanning under GC stress conditions. -## Quick Start +## How It Works + +Each test runs a debuggee console app under `corerun` with `DOTNET_CdacStress=0x51`, which enables: +- **0x01**: Allocation-point verification (triggers at every managed allocation) +- **0x10**: GC reference comparison (compares cDAC stack refs against runtime refs) +- **0x40**: Legacy DAC comparison (three-way: cDAC vs DAC vs runtime) + +The native `cdacstress.cpp` hook writes structured per-frame comparison results to a log file. +On failure, it shows per-frame diffs with resolved method names, making it easy to identify +which frame and method has mismatched GC references. + +Pass/fail semantics: +- **[PASS]**: cDAC matches DAC (may include `[RT_DIFF]` annotation if RT differs) +- **[FAIL]**: cDAC does NOT match DAC +- **[SKIP]**: cDAC GetStackReferences failed (e.g., during EH) + +## Prerequisites + +Build the runtime with the cDAC stress hook enabled: ```powershell -# Prerequisites: build CoreCLR Checked and generate core_root -# build.cmd clr+libs -rc Checked -lc Release -# src\tests\build.cmd Checked generatelayoutonly /p:LibrariesConfiguration=Release +# From repo root +.\build.cmd -subset clr.native+tools.cdac -c Debug -rc Checked -lc Release +.\src\tests\build.cmd Checked generatelayoutonly -SkipRestorePackages /p:LibrariesConfiguration=Release +``` +## Running Tests + +### Using RunStressTests.ps1 + +```powershell # Run all debuggees (allocation-point verification, no GCStress) ./RunStressTests.ps1 -SkipBuild @@ -21,88 +42,60 @@ GC stress points. # Run with instruction-level GCStress (slower, more thorough) ./RunStressTests.ps1 -SkipBuild -CdacStress 0x14 -GCStress 0x4 - -# Full comparison including walk parity and DAC cross-check -./RunStressTests.ps1 -SkipBuild -CdacStress 0x74 -GCStress 0x4 ``` -## How It Works - -### DOTNET_CdacStress Flags - -The `DOTNET_CdacStress` environment variable is a bitmask that controls -**where** and **what** the runtime verifies: +### Using dotnet test (xUnit) -| Bit | Flag | Description | -|-----|------|-------------| -| 0x1 | ALLOC | Verify at managed allocation points | -| 0x2 | GC | Verify at GC collection points | -| 0x4 | INSTR | Verify at instruction-level GC stress points (requires `DOTNET_GCStress`) | -| 0x10 | REFS | Compare GC stack references (cDAC vs runtime) | -| 0x20 | WALK | Compare stack walk frame ordering (cDAC vs DAC) | -| 0x40 | USE_DAC | Also compare GC refs against the legacy DAC | -| 0x100 | UNIQUE | Only verify each instruction pointer once | - -Common combinations: -- `0x11` — ALLOC + REFS (fast, default) -- `0x14` — INSTR + REFS (thorough, requires `DOTNET_GCStress=0x4`) -- `0x31` — ALLOC + REFS + WALK (fast with walk parity check) -- `0x74` — INSTR + REFS + WALK + USE_DAC (full comparison) - -### Verification Flow +```powershell +# Build and run all stress tests +.\.dotnet\dotnet.exe test src\native\managed\cdac\tests\StressTests -At each stress point, the native hook (`cdacstress.cpp`) in the runtime: +# Run a specific debuggee +.\.dotnet\dotnet.exe test src\native\managed\cdac\tests\StressTests --filter "FullyQualifiedName~BasicAlloc" -1. Suspends the current thread's context -2. Calls the cDAC's `GetStackReferences` to enumerate GC roots -3. Compares against the runtime's own GC root enumeration -4. Optionally compares against the legacy DAC's enumeration -5. Optionally compares stack walk frame ordering -6. Logs `[PASS]` or `[FAIL]` per verification point +# Set CORE_ROOT manually if needed +$env:CORE_ROOT = "path\to\Core_Root" +.\.dotnet\dotnet.exe test src\native\managed\cdac\tests\StressTests +``` -The script collects these results and reports aggregate pass/fail counts. +## Adding a New Debuggee -## Debuggees +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 `BasicStressTests.Debuggees` -Each debuggee is a standalone console application under `Debuggees/`: +## Debuggee Catalog | Debuggee | Scenarios | |----------|-----------| -| **BasicAlloc** | Object allocation, strings, arrays, many live refs | -| **Comprehensive** | All-in-one: allocations, deep stacks, exceptions, generics, P/Invoke, threading | - -All debuggees return exit code 100 on success. - -### Adding a New Debuggee - -1. Create a new folder under `Debuggees/` (e.g., `Debuggees/MyScenario/`) -2. Add a minimal `.csproj`: - ```xml - - ``` - The `Directory.Build.props` provides all common settings. -3. Add a `Program.cs` with a `Main()` that returns 100 -4. Use `[MethodImpl(MethodImplOptions.NoInlining)]` and `GC.KeepAlive()` - to prevent the JIT from optimizing away allocations and references +| **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 | +| **StructScenarios** | Struct returns, by-ref params | +| **DynamicMethods** | DynamicMethod / IL emit | + +## Architecture -The script auto-discovers all debuggees by scanning for `.csproj` files. - -## Script Parameters - -| Parameter | Default | Description | -|-----------|---------|-------------| -| `-Configuration` | `Checked` | Runtime build configuration | -| `-CdacStress` | `0x11` | Hex bitmask for `DOTNET_CdacStress` | -| `-GCStress` | _(empty)_ | Hex value for `DOTNET_GCStress` (e.g., `0x4`) | -| `-Debuggee` | _(all)_ | Which debuggee(s) to run | -| `-SkipBuild` | off | Skip CoreCLR/cDAC build step | -| `-SkipBaseline` | off | Skip baseline (no-stress) verification | - -## Expected Results - -Most runs achieve >99.5% pass rate. A small number of failures (~0.2%) -are expected due to the ScanFrameRoots gap — the cDAC does not yet enumerate -GC roots from explicit frame stub data (e.g., `StubDispatchFrame`, -`PInvokeCalliFrame`). These are tracked in [known-issues.md](known-issues.md). - -Walk parity (`WALK` flag) should show 0 mismatches. +``` +CdacStressTestBase.RunGCStress(debuggeeName) + │ + ├── Locate core_root/corerun (CORE_ROOT env or default path) + ├── Locate debuggee DLL (artifacts/bin/StressTests//...) + ├── Start Process: corerun + │ Environment: + │ DOTNET_CdacStress=0x51 + │ DOTNET_CdacStressStep=1 + │ DOTNET_CdacStressLogFile= + │ DOTNET_ContinueOnAssert=1 + ├── Wait for exit (timeout: 300s) + ├── Parse results log → CdacStressResults + └── Assert: exit=100, zero failures +``` diff --git a/src/native/managed/cdac/tests/StressTests/StressTests.targets b/src/native/managed/cdac/tests/StressTests/StressTests.targets new file mode 100644 index 00000000000000..b88b4132e0d4a3 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/StressTests.targets @@ -0,0 +1,70 @@ + + + + $(MSBuildThisFileDirectory)Debuggees\ + Release + + + + + + + + + + + + + + + + + + + + + <_HelixTestsDir>$([MSBuild]::NormalizeDirectory('$(HelixPayloadDir)', 'tests')) + <_HelixDebuggeesDir>$([MSBuild]::NormalizeDirectory('$(HelixPayloadDir)', 'debuggees')) + + + + + <_TestOutput Include="$(OutputPath)**\*" /> + + + + + + <_XunitConsoleFiles Include="$([System.IO.Path]::GetDirectoryName('$(XunitConsoleNetCoreAppPath)'))\*" /> + + + + + + + + + + + <_DebuggeeOutputDir>$([MSBuild]::NormalizeDirectory('$(RepoRoot)', 'artifacts', 'bin', 'StressTests', '$(DebuggeeName)', '$(DebuggeeConfiguration)')) + + + <_DebuggeeFiles Include="$(_DebuggeeOutputDir)**\*" /> + + + + diff --git a/src/native/managed/cdac/tests/StressTests/cdac-stress-helix.proj b/src/native/managed/cdac/tests/StressTests/cdac-stress-helix.proj new file mode 100644 index 00000000000000..9deecf71c49ca4 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/cdac-stress-helix.proj @@ -0,0 +1,76 @@ + + + + + + msbuild + true + true + true + $(_Creator) + true + $(BUILD_BUILDNUMBER) + test/cdac/stresstests/ + pr/dotnet/runtime/cdac-stress-tests + 00:30:00 + + + + + + %(Identity) + + + + + + + + + + + + + + + @(HelixPreCommand) + + + + + + <_StressTestCommand>%25HELIX_CORRELATION_PAYLOAD%25\dotnet.exe exec --runtimeconfig %25HELIX_WORKITEM_PAYLOAD%25\tests\Microsoft.Diagnostics.DataContractReader.StressTests.runtimeconfig.json --depsfile %25HELIX_WORKITEM_PAYLOAD%25\tests\Microsoft.Diagnostics.DataContractReader.StressTests.deps.json %25HELIX_WORKITEM_PAYLOAD%25\tests\xunit.console.dll %25HELIX_WORKITEM_PAYLOAD%25\tests\Microsoft.Diagnostics.DataContractReader.StressTests.dll -xml testResults.xml -nologo + + + <_StressTestCommand>$HELIX_CORRELATION_PAYLOAD/dotnet exec --runtimeconfig $HELIX_WORKITEM_PAYLOAD/tests/Microsoft.Diagnostics.DataContractReader.StressTests.runtimeconfig.json --depsfile $HELIX_WORKITEM_PAYLOAD/tests/Microsoft.Diagnostics.DataContractReader.StressTests.deps.json $HELIX_WORKITEM_PAYLOAD/tests/xunit.console.dll $HELIX_WORKITEM_PAYLOAD/tests/Microsoft.Diagnostics.DataContractReader.StressTests.dll -xml testResults.xml -nologo + + + + + $(StressTestsPayload) + $(_StressTestCommand) + $(WorkItemTimeout) + + + + + diff --git a/src/native/managed/cdac/tests/StressTests/known-issues.md b/src/native/managed/cdac/tests/StressTests/known-issues.md index 6445d255b67362..51815fca6a5100 100644 --- a/src/native/managed/cdac/tests/StressTests/known-issues.md +++ b/src/native/managed/cdac/tests/StressTests/known-issues.md @@ -1,57 +1,128 @@ # cDAC Stack Reference Walking — Known Issues This document tracks known gaps between the cDAC's stack reference enumeration -and the legacy DAC's `GetStackReferences`. +and the legacy DAC / runtime's GC stack scanning. ## Current Test Results -Using `DOTNET_CdacStress` with cDAC-vs-DAC comparison: - -| Mode | Non-EH debuggees (6) | ExceptionHandling | -|------|-----------------------|-------------------| -| INSTR (0x4 + GCStress=0x4, step=10) | 0 failures | 0-2 failures | -| ALLOC+UNIQUE (0x101) | 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. +### Unit tests: 1374/1374 pass + +### ALLOC+WALK+USE_DAC (0x61) — Stack walk frame comparison +**7/7 debuggees: 100% clean (zero WALK_FAIL) when tested** + +### ALLOC+REFS+USE_DAC (0x51) — Three-way GC ref comparison + +| Debuggee | Result | Notes | +|----------|--------|-------| +| BasicAlloc | 0 failures | | +| Comprehensive | 0 failures | | +| DeepStack | 0 failures | | +| Generics | 0 failures | | +| MultiThread | 0 failures | | +| PInvoke | 0 failures | Windows only | +| DynamicMethods | 0 failures | | +| StructScenarios | 0 failures | | +| ExceptionHandling | 0 failures | Fixed via ExecutionAborted | + +## Issue 1: ELEMENT_TYPE_INTERNAL in PromoteCallerStack (instruction-level stress only) + +**Affected**: Explicit Frames whose method signature contains `ELEMENT_TYPE_INTERNAL` (0x21) +**Frequency**: ~3 per 25K verifications (0.01%) +**Root cause**: IDENTIFIED — follow-up fix needed + +**Where it happens**: `FrameIterator.PromoteCallerStack()` in +`src/native/managed/cdac/.../Contracts/StackWalk/FrameHandling/FrameIterator.cs` +(around line 604). This is the fallback path used when a Frame's GCRefMap is +unavailable and we must decode the method signature to determine which caller +arguments are GC references. + +**Pattern**: The DAC reports 1 ref from an explicit Frame that the cDAC fails to scan. +The `PromoteCallerStack` fallback decodes the method signature using +`System.Reflection.Metadata.SignatureDecoder`, which only handles standard ECMA-335 +type codes. Runtime-internal signatures (generated for IL stubs, marshalling stubs, +unsafe accessors, etc.) may contain `ELEMENT_TYPE_INTERNAL` (0x21), which encodes a +raw pointer-sized `TypeHandle` directly in the signature blob. The SRM decoder doesn't +recognize this type code and throws `BadImageFormatException`. + +``` +System.BadImageFormatException: Unexpected SignatureTypeCode: (0x21). + at SignatureDecoder`2.DecodeType(BlobReader&, Boolean, Int32) + at SignatureDecoder`2.DecodeGenericTypeInstance(BlobReader&) + at FrameIterator.PromoteCallerStack(...) + at FrameIterator.GcScanRoots(...) +``` + +The exception is caught by the per-frame exception handler in `WalkStackReferences()` +(`StackWalk_1.cs`, around line 245), which silently swallows it and continues the +walk — causing the Frame's GC refs to be unreported. + +**How the DAC handles it**: The native DAC uses `MetaSig` + `ArgIterator` +(`frames.cpp:1520-1596`) instead of the SRM decoder. `MetaSig` natively understands +`ELEMENT_TYPE_INTERNAL` — it reads the embedded TypeHandle pointer and follows it to +determine the actual type for GC classification. + +**How the Legacy cDAC handles it**: `SigFormat.cs` (line 157-175) already handles +`ELEMENT_TYPE_INTERNAL` by reading the pointer-sized TypeHandle, resolving it via +`RuntimeTypeSystem.GetTypeHandle()`, and checking `GetSignatureCorElementType()`. + +**Current workaround**: A `catch (BadImageFormatException)` in `PromoteCallerStack` +returns without reporting refs for the frame. + +**Follow-up fix**: Replace the SRM `SignatureDecoder` usage with a custom signature +walker that: +1. Pre-processes the signature bytes, handling `ELEMENT_TYPE_INTERNAL` (0x21) by + reading the pointer-sized TypeHandle and resolving through `RuntimeTypeSystem` + (following the pattern in `SigFormat.cs:157-175`) +2. Delegates standard ECMA-335 type codes to the existing `GcSignatureTypeProvider` +3. Handles `ELEMENT_TYPE_CMOD_INTERNAL` (0x22) similarly if encountered + +## IsFirst not preserved for skipped frames (FIXED) + +Previously ~4 per 25K failures at instruction-level stress. The cDAC's +`AdvanceIsFirst` was updating `IsFirst` for `SW_SKIPPED_FRAME` based on the +Frame's resumable attribute, but the native walker does NOT modify `isFirst` +in the `SFITER_SKIPPED_FRAME_FUNCTION` path (stackwalk.cpp:2086-2128). Fixed +by making `AdvanceIsFirst` skip the `IsFirst` update for `SW_SKIPPED_FRAME`. + +## EH ThrowHelper (FIXED) + +Previously 8-9 failures per run. Fixed by detecting `SoftwareExceptionFrame` +and `FaultingExceptionFrame` as interrupted frames and setting `ExecutionAborted` +flag, matching native `CrawlFrame::GetCodeManagerFlags`. + +## Allocation-level stress results + +At allocation-level stress (`DOTNET_CdacStress=0x51`, the default): +- All 9 debuggees pass 100% (0 failures across ~45K total verifications) + +## Instruction-level stress results + +At instruction-level stress (`DOTNET_GCStress=0x4 + DOTNET_CdacStress=0x54`): +- Comprehensive: 25,512 pass / 3 fail (99.988%) + - 0 FRAME_DIFF (fixed via IsFirst skipped-frame preservation) + - 3 FRAME_DAC_ONLY (ELEMENT_TYPE_INTERNAL in PromoteCallerStack — follow-up) + +## Future work + +- Investigate the GcInfo safe-point bitmap decoding difference for QCall frames +- Replace `fprintf`-based stress logging in `cdacstress.cpp` with a more + structured mechanism (e.g., ETW events or StressLog) for better tooling + integration and reduced I/O overhead during stress runs. + +## Log Format + +The stress log uses structured per-frame output with method name resolution: + +``` +[PASS] Thread=0x... IP=0x... cDAC=N DAC=N RT=M +[FAIL] Thread=0x... IP=0x... cDAC=N DAC=M RT=M + [COMPARE cDAC-vs-DAC] + [FRAME_DIFF] Source=0x... (MethodName): cDAC=X DAC=Y + [cDAC_ONLY] Addr=0x... Obj=0x... Flags=0x... + [DAC_ONLY] Addr=0x... Obj=0x... Flags=0x... + [FRAME_cDAC_ONLY] Source=0x... (MethodName): cDAC=X + [FRAME_DAC_ONLY] Source=0x... (): DAC=Y + [RT_DIFF] cDAC=N RT=M (cDAC matches DAC but differs from RT) + [STACK_TRACE] (cDAC=N DAC=M RT=M) + #i MethodName (cDAC=X DAC=Y) [<-- MISMATCH] +``` From 1c39f70d3a6ff9c56a07df1f777dd2262ecb858e Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Thu, 28 May 2026 13:55:52 -0400 Subject: [PATCH 3/3] [cDAC] Fix cdacstress.cpp Linux build errors - Add virtual destructor to InProcessDataTarget to satisfy -Werror=delete-non-virtual-dtor (clang and gcc). - Guard GetModuleHandleW call with HOST_WINDOWS; return E_NOTIMPL on non-Windows where the API isn't available. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/cdacstress.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/coreclr/vm/cdacstress.cpp b/src/coreclr/vm/cdacstress.cpp index b12bb5e2911b08..0e846c34a46145 100644 --- a/src/coreclr/vm/cdacstress.cpp +++ b/src/coreclr/vm/cdacstress.cpp @@ -151,6 +151,7 @@ class InProcessDataTarget : public ICLRDataTarget volatile LONG m_refCount; public: InProcessDataTarget() : m_refCount(1) {} + virtual ~InProcessDataTarget() = default; HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppObj) override { @@ -193,10 +194,14 @@ class InProcessDataTarget : public ICLRDataTarget HRESULT STDMETHODCALLTYPE GetImageBase(LPCWSTR imagePath, CLRDATA_ADDRESS* baseAddress) override { +#ifdef HOST_WINDOWS HMODULE hMod = ::GetModuleHandleW(imagePath); if (hMod == NULL) return E_FAIL; *baseAddress = (CLRDATA_ADDRESS)hMod; return S_OK; +#else + return E_NOTIMPL; +#endif } HRESULT STDMETHODCALLTYPE ReadVirtual(CLRDATA_ADDRESS address, BYTE* buffer, ULONG32 bytesRequested, ULONG32* bytesRead) override