From 331a3a2eff210dd13c5236d73463c2c99e8abe34 Mon Sep 17 00:00:00 2001 From: Juan Sebastian Hoyos Ayala Date: Fri, 24 Jul 2026 03:04:03 -0700 Subject: [PATCH 1/5] dbgshim: route live debugging through cDAC-capable DBI first - Add a SetCDacLoadPolicy export and process-wide setting lets the live-debugging connection APIs select the cDAC/legacy DAC policy. - Call into the bundled DBI to check whether using cDAC with it suceeded. Each path invokes the DBI with the cDAC first and falls back to the legacy provider/side-by-side path. - Use simple unified helper to ensure cdac errors prevail over non-cdac ones in fallback scenario. - Add SetCDacLoadPolicy test wrapper plus live policy tests for the version3 and RegisterForRuntimeStartup3 paths asserting the cDAC/legacy gating and provider fallback paths. --- src/dbgshim/dbgshim.cpp | 274 ++++++++++++++------ src/dbgshim/dbgshim.h | 20 ++ src/dbgshim/dbgshim.ntdef | 1 + src/dbgshim/dbgshim_unixexports.src | 1 + src/dbgshim/debugshim.cpp | 67 +++-- src/dbgshim/debugshim.h | 25 +- src/tests/DbgShim.UnitTests/DbgShimAPI.cs | 18 ++ src/tests/DbgShim.UnitTests/DbgShimTests.cs | 228 ++++++++++++++++ 8 files changed, 508 insertions(+), 126 deletions(-) diff --git a/src/dbgshim/dbgshim.cpp b/src/dbgshim/dbgshim.cpp index c0b2f9f021..c5c45f073c 100644 --- a/src/dbgshim/dbgshim.cpp +++ b/src/dbgshim/dbgshim.cpp @@ -229,6 +229,51 @@ HRESULT CreateCoreDbg( return hr; } +// Policy for the live-debugging paths (RegisterForRuntimeStartup* and +// CreateDebuggingInterfaceFromVersion*), which don't flow through ICLRDebuggingPolicy. Set via the +// SetCDacLoadPolicy export. +static CDacLoadPolicy g_cdacLoadPolicy = CDacLoadPolicy_PreferCDac; + +// Only the bundled DBI can report whether it can consume the cDAC for this target, so this invokes it +// rather than pre-judging from file presence. +static HRESULT TryCreateCoreDbgWithCDac( + HMODULE hModule, + DWORD processId, + LPCWSTR lpApplicationGroupId, + int iDebuggerVersion, + IUnknown** ppCordb) +{ + SString cdacPath; + SString dbiPath; + if (!GetCDacAndDbiPaths(cdacPath, dbiPath)) + { + return E_FAIL; + } + + return CreateCoreDbg(hModule, processId, dbiPath, cdacPath, lpApplicationGroupId, iDebuggerVersion, ppCordb); +} + +// Legacy fallback that resolves the DBI/DAC through the library provider and activates them. +static HRESULT TryCreateCoreDbgWithProvider( + ClrInfo& clrInfo, + ICLRDebuggingLibraryProvider3* pLibraryProvider, + HMODULE hModule, + DWORD processId, + LPCWSTR lpApplicationGroupId, + int iDebuggerVersion, + IUnknown** ppCordb) +{ + SString dbiModulePath; + SString dacModulePath; + HRESULT hr = CLRDebuggingImpl::ResolveLibraryPaths(clrInfo, pLibraryProvider, dbiModulePath, dacModulePath); + if (FAILED(hr)) + { + return hr; + } + + return CreateCoreDbg(hModule, processId, dbiModulePath, dacModulePath, lpApplicationGroupId, iDebuggerVersion, ppCordb); +} + // // Helper class for RegisterForRuntimeStartup // @@ -250,7 +295,7 @@ class RuntimeStartupHelper #endif // TARGET_UNIX public: - + RuntimeStartupHelper(DWORD dwProcessId, ICLRDebuggingLibraryProvider3* pLibraryProvider, PSTARTUP_CALLBACK pfnCallback, PVOID parameter) : m_ref(1), m_processId(dwProcessId), @@ -361,36 +406,52 @@ class RuntimeStartupHelper // Get the DBI/DAC index info for regular and single-file apps hr = GetTargetCLRMetrics(clrInfo.RuntimeModulePath, NULL, &clrInfo, NULL); if (FAILED(hr)) - { + { // Runtime module not found (return false). This isn't an error that needs to be reported via the callback. return false; } SString dbiModulePath; SString dacModulePath; - if (m_pLibraryProvider != NULL) + + // Snapshot so a concurrent SetCDacLoadPolicy can't change decisions mid-call. + CDacLoadPolicy policy = g_cdacLoadPolicy; + + HRESULT cdacHr = E_FAIL; + bool cdacEvaluated = policy != CDacLoadPolicy_LegacyDacOnly; + if (cdacEvaluated) { - hr = CLRDebuggingImpl::ResolveLibraryPaths(clrInfo, m_pLibraryProvider, dbiModulePath, dacModulePath); - if (FAILED(hr)) - { - goto exit; - } + cdacHr = TryCreateCoreDbgWithCDac(hModule, m_processId, m_applicationGroupId, CorDebugVersion_2_0, &pCordb); } - else + + HRESULT fallbackHr = E_FAIL; + if (FAILED(cdacHr) && policy != CDacLoadPolicy_CDacOnly) { - // Fallback to loading DBI side-by-side the runtime module - const char *pszLast = strrchr(pszModulePath, DIRECTORY_SEPARATOR_CHAR_A); - if (pszLast == NULL) + if (m_pLibraryProvider != NULL) { - _ASSERT(!"InvokeStartupCallback: can find separator in coreclr path\n"); - hr = E_INVALIDARG; - goto exit; + fallbackHr = TryCreateCoreDbgWithProvider(clrInfo, m_pLibraryProvider, hModule, m_processId, m_applicationGroupId, CorDebugVersion_2_0, &pCordb); + } + else + { + // Load DBI side-by-side the runtime module. Unlike the other paths this skips the + // version check because the runtime path here is the ASCII module path reported by + // the startup callback rather than a resolved index. + const char *pszLast = strrchr(pszModulePath, DIRECTORY_SEPARATOR_CHAR_A); + if (pszLast == NULL) + { + _ASSERT(!"InvokeStartupCallback: can find separator in coreclr path\n"); + fallbackHr = E_INVALIDARG; + } + else + { + dbiModulePath.SetASCII(pszModulePath, pszLast - pszModulePath); + AppendDbiDllName(dbiModulePath); + fallbackHr = CreateCoreDbg(hModule, m_processId, dbiModulePath, dacModulePath, m_applicationGroupId, CorDebugVersion_2_0, &pCordb); + } } - dbiModulePath.SetASCII(pszModulePath, pszLast - pszModulePath); - AppendDbiDllName(dbiModulePath); } - hr = CreateCoreDbg(hModule, m_processId, dbiModulePath, dacModulePath, m_applicationGroupId, CorDebugVersion_2_0, &pCordb); + hr = SelectActivationResult(cdacHr, fallbackHr, cdacEvaluated); _ASSERTE((pCordb == NULL) == FAILED(hr)); if (FAILED(hr)) { @@ -471,7 +532,7 @@ class RuntimeStartupHelper { return hr; } - // If GetRuntime succeeded but the handle is INVALID_HANDLE_VALUE, then sleep and retry also. This fixes a + // If GetRuntime succeeded but the handle is INVALID_HANDLE_VALUE, then sleep and retry also. This fixes a // race condition where dbgshim catches the coreclr module just being loaded but before g_hContinueStartupEvent // has been initialized. if (clrRuntimeInfo.ContinueStartupEvent != INVALID_HANDLE_VALUE) @@ -546,38 +607,46 @@ class RuntimeStartupHelper SString dbiModulePath; SString dacModulePath; - if (m_pLibraryProvider != NULL) + + // Snapshot so a concurrent SetCDacLoadPolicy can't change decisions mid-call. + CDacLoadPolicy policy = g_cdacLoadPolicy; + + HRESULT cdacHr = E_FAIL; + bool cdacEvaluated = policy != CDacLoadPolicy_LegacyDacOnly; + if (cdacEvaluated) { - hr = CLRDebuggingImpl::ResolveLibraryPaths(clrRuntimeInfo.ClrInfo, m_pLibraryProvider, dbiModulePath, dacModulePath); - if (FAILED(hr)) - { - goto exit; - } + cdacHr = TryCreateCoreDbgWithCDac(clrRuntimeInfo.ModuleHandle, m_processId, NULL, clrRuntimeInfo.EngineMetrics.dwDbiVersion, &pCordb); } - else + + HRESULT fallbackHr = E_FAIL; + if (FAILED(cdacHr) && policy != CDacLoadPolicy_CDacOnly) { - dbiModulePath.Set(clrRuntimeInfo.ClrInfo.RuntimeModulePath); - SString::Iterator iter = dbiModulePath.End(); - if (dbiModulePath.FindBack(iter, DIRECTORY_SEPARATOR_CHAR_W)) + if (m_pLibraryProvider != NULL) { - iter++; - dbiModulePath.Truncate(iter); + fallbackHr = TryCreateCoreDbgWithProvider(clrRuntimeInfo.ClrInfo, m_pLibraryProvider, clrRuntimeInfo.ModuleHandle, m_processId, NULL, clrRuntimeInfo.EngineMetrics.dwDbiVersion, &pCordb); } - else + else { - hr = E_FAIL; - goto exit; - } - AppendDbiDllName(dbiModulePath); + dbiModulePath.Set(clrRuntimeInfo.ClrInfo.RuntimeModulePath); + SString::Iterator iter = dbiModulePath.End(); + if (dbiModulePath.FindBack(iter, DIRECTORY_SEPARATOR_CHAR_W)) + { + iter++; + dbiModulePath.Truncate(iter); + AppendDbiDllName(dbiModulePath); - if (!CheckDbiAndRuntimeVersion(dbiModulePath, clrRuntimeInfo.ClrInfo.RuntimeModulePath)) - { - hr = CORDBG_E_INCOMPATIBLE_PROTOCOL; - goto exit; + fallbackHr = CheckDbiAndRuntimeVersion(dbiModulePath, clrRuntimeInfo.ClrInfo.RuntimeModulePath) + ? CreateCoreDbg(clrRuntimeInfo.ModuleHandle, m_processId, dbiModulePath, dacModulePath, NULL, clrRuntimeInfo.EngineMetrics.dwDbiVersion, &pCordb) + : CORDBG_E_INCOMPATIBLE_PROTOCOL; + } + else + { + fallbackHr = E_FAIL; + } } } - hr = CreateCoreDbg(clrRuntimeInfo.ModuleHandle, m_processId, dbiModulePath, dacModulePath, NULL, clrRuntimeInfo.EngineMetrics.dwDbiVersion, &pCordb); + hr = SelectActivationResult(cdacHr, fallbackHr, cdacEvaluated); _ASSERTE((pCordb == NULL) == FAILED(hr)); if (FAILED(hr)) { @@ -1156,20 +1225,20 @@ GetTargetCLRMetrics( } // If we are looking for the DotNetRuntimeInfo export for a single-file app, do this before looking for - // engine metrics export ordinal for a faster out of the module search loop. There are plenty of other + // engine metrics export ordinal for a faster out of the module search loop. There are plenty of other // native modules with the metrics ordinal #2. if (pClrInfoOut != NULL) { if (IsCoreClr(wszModulePath)) { - PEDecoder_ResourceCallbackFunction callback = ([](LPCWSTR lpName, LPCWSTR lpType, DWORD langid, BYTE* data, COUNT_T cbData, void* context) { + PEDecoder_ResourceCallbackFunction callback = ([](LPCWSTR lpName, LPCWSTR lpType, DWORD langid, BYTE* data, COUNT_T cbData, void* context) { CLR_DEBUG_RESOURCE* pDebugResource = (CLR_DEBUG_RESOURCE*)data; ClrInfo* pClrInfo = (ClrInfo*)context; if (cbData != sizeof(CLR_DEBUG_RESOURCE) || pDebugResource->dwVersion != 0 || pDebugResource->signature != CLR_ID_ONECORE_CLR) { return false; } - pClrInfo->IndexType = LIBRARY_PROVIDER_INDEX_TYPE::Identity; + pClrInfo->IndexType = LIBRARY_PROVIDER_INDEX_TYPE::Identity; pClrInfo->DbiTimeStamp = pDebugResource->dwDbiTimeStamp; pClrInfo->DbiSizeOfImage = pDebugResource->dwDbiSizeOfImage; pClrInfo->DacTimeStamp = pDebugResource->dwDacTimeStamp; @@ -1185,7 +1254,7 @@ GetTargetCLRMetrics( } } else - { + { PTR_VOID runtimeInfoExport = pedecoder.GetExport(RUNTIME_INFO_SIGNATURE); if (runtimeInfoExport == NULL) { @@ -1209,7 +1278,7 @@ GetTargetCLRMetrics( { return E_FAIL; } - pClrInfoOut->IndexType = LIBRARY_PROVIDER_INDEX_TYPE::Identity; + pClrInfoOut->IndexType = LIBRARY_PROVIDER_INDEX_TYPE::Identity; pClrInfoOut->DbiTimeStamp = *((DWORD*)&pRuntimeInfo->DbiModuleIndex[1]); pClrInfoOut->DbiSizeOfImage = *((DWORD*)&pRuntimeInfo->DbiModuleIndex[5]); pClrInfoOut->DacTimeStamp = *((DWORD*)&pRuntimeInfo->DacModuleIndex[1]); @@ -1232,10 +1301,10 @@ GetTargetCLRMetrics( reinterpret_cast(pedecoder.GetDirectoryData(pExportDirectoryEntry)); // At this point we have checked that everything in the export directory is readable. - + // Check to make sure the ordinal we have fits in the table in the export directory. // The "base" here is like the starting index of the arrays in the export directory. - if ((pExportDir->Base > kOrdinalForMetrics) || + if ((pExportDir->Base > kOrdinalForMetrics) || (pExportDir->NumberOfFunctions < (kOrdinalForMetrics - pExportDir->Base))) { return E_FAIL; @@ -1304,13 +1373,13 @@ GetTargetCLRMetrics( // Get the runtime index info (build id) for Linux/MacOS. If getting the build id fails for any reason, return success // but with an invalid ClrInfo (unknown index type, no build id) so ResolveLibraryPaths fails in InvokeStartupCallback and // invokes the callback with an error. - if (TryGetBuildIdFromFile(wszModulePath, pClrInfoOut->RuntimeBuildId, MAX_BUILDID_SIZE, &pClrInfoOut->RuntimeBuildIdSize)) + if (TryGetBuildIdFromFile(wszModulePath, pClrInfoOut->RuntimeBuildId, MAX_BUILDID_SIZE, &pClrInfoOut->RuntimeBuildIdSize)) { pClrInfoOut->IndexType = LIBRARY_PROVIDER_INDEX_TYPE::Runtime; } } else - { + { RuntimeInfo runtimeInfo; if (!TryReadSymbolFromFile(wszModulePath, RUNTIME_INFO_SIGNATURE, (BYTE*)&runtimeInfo, sizeof(RuntimeInfo))) { @@ -1320,7 +1389,7 @@ GetTargetCLRMetrics( { return E_FAIL; } - pClrInfoOut->IndexType = LIBRARY_PROVIDER_INDEX_TYPE::Identity; + pClrInfoOut->IndexType = LIBRARY_PROVIDER_INDEX_TYPE::Identity; // The first byte is the number of bytes in the index pClrInfoOut->DbiBuildIdSize = runtimeInfo.DbiModuleIndex[0]; @@ -1425,7 +1494,7 @@ GetRuntime( return hr; } - // This assumes we are only going to find one .NET runtime in the process. We do the module + // This assumes we are only going to find one .NET runtime in the process. We do the module // enumeration only once because looking for single-file runtime info symbol is expensive. WCHAR modulePath[MAX_LONGPATH]; @@ -1441,7 +1510,7 @@ GetRuntime( modulePath[MAX_LONGPATH - 1] = 0; // on older OS'es this doesn't get null terminated automatically on truncation } - // Get the DBI/DAC index info for the regular coreclr module or check if single-file app by looking for the + // Get the DBI/DAC index info for the regular coreclr module or check if single-file app by looking for the // DotNetRuntimeInfo export. We need to get the metrics too because that is required to get the startup event. DWORD rvaContinueStartupEvent = 0; hr = GetTargetCLRMetrics(modulePath, &clrRuntimeInfo.EngineMetrics, &clrRuntimeInfo.ClrInfo, &rvaContinueStartupEvent); @@ -1467,7 +1536,7 @@ GetRuntime( clrRuntimeInfo.ContinueStartupEvent = continueEvent; } } - else + else { clrRuntimeInfo.ContinueStartupEvent = continueEvent; } @@ -2040,6 +2109,12 @@ CreateDebuggingInterfaceFromVersion3( SString szFullDacPath; HRESULT hr = S_OK; + // Declared before any goto so the error labels don't bypass their initialization. + CDacLoadPolicy policy = g_cdacLoadPolicy; + bool cdacEvaluated = policy != CDacLoadPolicy_LegacyDacOnly; + HRESULT cdacHr = E_FAIL; + HRESULT fallbackHr = E_FAIL; + LOG((LF_CORDB, LL_EVERYTHING, "Calling CreateDebuggerInterfaceFromVersion3, ver=%S\n", szDebuggeeVersion)); if ((szDebuggeeVersion == NULL) || (ppCordb == NULL)) @@ -2068,48 +2143,55 @@ CreateDebuggingInterfaceFromVersion3( EX_TRY { - SString szFullCoreClrPath; - GetDbiFilenameNextToRuntime(pidDebuggee, hmodTargetCLR, szFullDbiPath, szFullCoreClrPath); + if (cdacEvaluated) + { + cdacHr = TryCreateCoreDbgWithCDac(hmodTargetCLR, pidDebuggee, szApplicationGroupId, iDebuggerVersion, &pCordb); + } - if (pLibraryProvider != NULL) - { - // Get the DBI/DAC index info for regular and single-file apps - ClrInfo clrInfo; - hr = GetTargetCLRMetrics(szFullCoreClrPath, NULL, &clrInfo, NULL); - if (SUCCEEDED(hr)) + if (FAILED(cdacHr) && policy != CDacLoadPolicy_CDacOnly) + { + SString szFullCoreClrPath; + GetDbiFilenameNextToRuntime(pidDebuggee, hmodTargetCLR, szFullDbiPath, szFullCoreClrPath); + + if (pLibraryProvider != NULL) { - clrInfo.RuntimeModulePath.Set(szFullCoreClrPath); - hr = CLRDebuggingImpl::ResolveLibraryPaths(clrInfo, pLibraryProvider, szFullDbiPath, szFullDacPath); + // Get the DBI/DAC index info for regular and single-file apps + ClrInfo clrInfo; + fallbackHr = GetTargetCLRMetrics(szFullCoreClrPath, NULL, &clrInfo, NULL); + if (SUCCEEDED(fallbackHr)) + { + clrInfo.RuntimeModulePath.Set(szFullCoreClrPath); + fallbackHr = TryCreateCoreDbgWithProvider(clrInfo, pLibraryProvider, hmodTargetCLR, pidDebuggee, szApplicationGroupId, iDebuggerVersion, &pCordb); + } + + // ERROR_PARTIAL_COPY/ERROR_BAD_LENGTH from CreateToolhelp32Snapshot() are transient and + // preserved so the debugger can retry; anything else maps to a missing-component error. + if (FAILED(fallbackHr) && + (fallbackHr != HRESULT_FROM_WIN32(ERROR_PARTIAL_COPY)) && + (fallbackHr != HRESULT_FROM_WIN32(ERROR_BAD_LENGTH))) + { + fallbackHr = CORDBG_E_DEBUG_COMPONENT_MISSING; + } } - } - else - { - // Check for dbi next to target CLR. - // This will be very common for internal developer setups, but not common in end-user setups. - if (!CheckDbiAndRuntimeVersion(szFullDbiPath, szFullCoreClrPath)) + else { - hr = CORDBG_E_INCOMPATIBLE_PROTOCOL; - goto exit; + fallbackHr = CheckDbiAndRuntimeVersion(szFullDbiPath, szFullCoreClrPath) + ? CreateCoreDbg(hmodTargetCLR, pidDebuggee, szFullDbiPath, szFullDacPath, szApplicationGroupId, iDebuggerVersion, &pCordb) + : CORDBG_E_INCOMPATIBLE_PROTOCOL; } } } EX_CATCH_HRESULT(hr); - if (FAILED(hr)) + if (SUCCEEDED(hr)) { - // Check for the following two HRESULTs and return them specifically. These are returned by - // CreateToolhelp32Snapshot() and could be transient errors. The debugger may choose to retry. - if ((hr != HRESULT_FROM_WIN32(ERROR_PARTIAL_COPY)) && (hr != HRESULT_FROM_WIN32(ERROR_BAD_LENGTH))) - { - hr = CORDBG_E_DEBUG_COMPONENT_MISSING; - } - goto exit; + hr = SelectActivationResult(cdacHr, fallbackHr, cdacEvaluated); + } + else if ((hr != HRESULT_FROM_WIN32(ERROR_PARTIAL_COPY)) && (hr != HRESULT_FROM_WIN32(ERROR_BAD_LENGTH))) + { + hr = CORDBG_E_DEBUG_COMPONENT_MISSING; } - // - // Step 3: Load DBI and instantiate an ICorDebug instance. - // - hr = CreateCoreDbg(hmodTargetCLR, pidDebuggee, szFullDbiPath, szFullDacPath, szApplicationGroupId, iDebuggerVersion, &pCordb); _ASSERTE((pCordb == NULL) == FAILED(hr)); exit: @@ -2165,6 +2247,30 @@ CLRCreateInstance( return pDebuggingImpl->QueryInterface(riid, ppInterface); } +//----------------------------------------------------------------------------- +// Public API. +// +// Sets the process-wide cDAC load policy for the live-debugging creation exports +// (RegisterForRuntimeStartup* and CreateDebuggingInterfaceFromVersion*), which create an ICorDebug +// directly and so can't use the per-instance ICLRDebuggingPolicy that OpenVirtualProcess honors. +// Returns E_INVALIDARG for an unrecognized policy. +//----------------------------------------------------------------------------- +DLLEXPORT +HRESULT +SetCDacLoadPolicy( + _In_ CDacLoadPolicy policy) +{ + PUBLIC_CONTRACT; + + if (policy > CDacLoadPolicy_LegacyDacOnly) + { + return E_INVALIDARG; + } + + g_cdacLoadPolicy = policy; + return S_OK; +} + HRESULT CreateCoreDbgRemotePort(HMODULE hDBIModule, LPCWSTR szIp, DWORD dwPort, LPCWSTR szPlatform, BOOL bIsServer, LPCWSTR assemblyBasePath, IUnknown **ppCordb) { PUBLIC_CONTRACT; diff --git a/src/dbgshim/dbgshim.h b/src/dbgshim/dbgshim.h index b85d8cf513..d12302ac5e 100644 --- a/src/dbgshim/dbgshim.h +++ b/src/dbgshim/dbgshim.h @@ -5,11 +5,25 @@ // //***************************************************************************** +#ifndef _DBG_SHIM_H_ +#define _DBG_SHIM_H_ + #include #include "metahost.h" typedef VOID (*PSTARTUP_CALLBACK)(IUnknown *pCordb, PVOID parameter, HRESULT hr); +// Selects how dbgshim locates the data-access layer (DAC/cDAC) for a target. +enum CDacLoadPolicy +{ + // Prefer the debugging tool bundled cDAC, falling back to the legacy DAC. This is the default. + CDacLoadPolicy_PreferCDac = 0, + // Use only the cDAC. + CDacLoadPolicy_CDacOnly = 1, + // Use only the legacy DAC. + CDacLoadPolicy_LegacyDacOnly = 2, +}; + EXTERN_C HRESULT CreateProcessForLaunch( _In_ LPWSTR lpCommandLine, @@ -115,3 +129,9 @@ RegisterForRuntimeStartupRemotePort( _In_ LPCWSTR szMscordbiPath, _In_ LPCWSTR szAssemblyBasePath, _Out_ IUnknown ** ppCordb); + +EXTERN_C HRESULT +SetCDacLoadPolicy( + _In_ CDacLoadPolicy policy); + +#endif // _DBG_SHIM_H_ diff --git a/src/dbgshim/dbgshim.ntdef b/src/dbgshim/dbgshim.ntdef index c06fc5e399..04af568399 100644 --- a/src/dbgshim/dbgshim.ntdef +++ b/src/dbgshim/dbgshim.ntdef @@ -19,3 +19,4 @@ EXPORTS CreateDebuggingInterfaceFromVersion3 CLRCreateInstance RegisterForRuntimeStartupRemotePort + SetCDacLoadPolicy diff --git a/src/dbgshim/dbgshim_unixexports.src b/src/dbgshim/dbgshim_unixexports.src index 709b33705f..7344d32982 100644 --- a/src/dbgshim/dbgshim_unixexports.src +++ b/src/dbgshim/dbgshim_unixexports.src @@ -18,3 +18,4 @@ CreateDebuggingInterfaceFromVersion2 CreateDebuggingInterfaceFromVersion3 CLRCreateInstance RegisterForRuntimeStartupRemotePort +SetCDacLoadPolicy diff --git a/src/dbgshim/debugshim.cpp b/src/dbgshim/debugshim.cpp index ab1bbeb936..172e46ed99 100644 --- a/src/dbgshim/debugshim.cpp +++ b/src/dbgshim/debugshim.cpp @@ -64,10 +64,6 @@ static HRESULT OpenCorDebugProcessWithProvider( IUnknown** ppProcess, CLR_DEBUGGING_PROCESS_FLAGS* pFlags); static VOID RetargetDacIfNeeded(DWORD* pdwTimeStamp, DWORD* pdwSizeOfImage); -static HRESULT SelectActivationResult( - HRESULT cdacHr, - HRESULT fallbackHr, - bool cdacEvaluated); typedef HRESULT (STDAPICALLTYPE *OpenVirtualProcessImpl2FnPtr)(ULONG64 clrInstanceId, IUnknown * pDataTarget, @@ -283,7 +279,8 @@ static HRESULT OpenCorDebugProcessWithCDac( CLR_DEBUGGING_PROCESS_FLAGS* pFlags) { SString dbiModulePath; - if (!GetDbiPath(dbiModulePath)) + SString cdacModulePath; + if (!GetCDacAndDbiPaths(cdacModulePath, dbiModulePath)) { return E_FAIL; } @@ -301,19 +298,14 @@ static HRESULT OpenCorDebugProcessWithCDac( return CORDBG_E_MISSING_DEBUGGER_EXPORTS; } - SString cdacModulePath; - HRESULT hr = E_FAIL; - if (GetCDacPath(cdacModulePath)) - { - hr = ovpFn( - moduleBaseAddress, - pDataTarget, - cdacModulePath, - pMaxDebuggerSupportedVersion, - riidProcess, - ppProcess, - pFlags); - } + HRESULT hr = ovpFn( + moduleBaseAddress, + pDataTarget, + cdacModulePath, + pMaxDebuggerSupportedVersion, + riidProcess, + ppProcess, + pFlags); if (SUCCEEDED(hr) && ppProcess != NULL && *ppProcess == NULL) { @@ -693,13 +685,10 @@ static HRESULT LoadResolvedLibraries( return hr; } -// Selects the final HRESULT after the cDAC and/or fallback activation attempts have run. -// - A cDAC success wins, then a fallback success. -// - On total failure, if the cDAC was evaluated (any policy other than LegacyDacOnly) its error -// is surfaced so tools can log why the cDAC rejected the target. -// - Otherwise (LegacyDacOnly) the fallback error is surfaced. Callers seed fallbackHr with -// E_POINTER so the LegacyDacOnly no-provider case preserves its historical HRESULT. -static HRESULT SelectActivationResult( +// Picks the final HRESULT after a cDAC attempt (cdacHr) and a legacy fallback attempt (fallbackHr). +// On total failure the cDAC error is surfaced when it was attempted so tools can log why the cDAC +// rejected the target; otherwise the fallback error (seeded by callers) is surfaced. +HRESULT SelectActivationResult( HRESULT cdacHr, HRESULT fallbackHr, bool cdacEvaluated) @@ -1196,6 +1185,30 @@ static bool GetCDacPath(SString& cdacPath) cdacPath); } +bool GetCDacAndDbiPaths(SString& cdacPath, SString& dbiPath) +{ + SString resolvedDbiPath; + SString resolvedCDacPath; + if (!GetDbiPath(resolvedDbiPath) || !GetCDacPath(resolvedCDacPath)) + { + return false; + } + + DWORD dbiAttributes = GetFileAttributesW(resolvedDbiPath); + DWORD cdacAttributes = GetFileAttributesW(resolvedCDacPath); + if (dbiAttributes == INVALID_FILE_ATTRIBUTES || + (dbiAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0 || + cdacAttributes == INVALID_FILE_ATTRIBUTES || + (cdacAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) + { + return false; + } + + cdacPath.Set(resolvedCDacPath); + dbiPath.Set(resolvedDbiPath); + return true; +} + // Loads the given DAC/cDAC module and creates the requested data-access interface from it by // calling its CLRDataCreateInstance export. The module is intentionally left resident. static HRESULT DacCreateInstance( @@ -1349,13 +1362,13 @@ static bool IsDataAccessInterface(REFIID riid) return riid == IID_IXCLRDataProcess_Local || riid == IID_ISOSDacInterface_Local; } -STDMETHODIMP CLRDebuggingImpl::SetCDacLoadPolicy(DWORD policy) +STDMETHODIMP CLRDebuggingImpl::SetCDacLoadPolicy(CDacLoadPolicy policy) { if (policy > CDacLoadPolicy_LegacyDacOnly) { return E_INVALIDARG; } - m_cdacLoadPolicy = (CDacLoadPolicy)policy; + m_cdacLoadPolicy = policy; return S_OK; } diff --git a/src/dbgshim/debugshim.h b/src/dbgshim/debugshim.h index 2728dc2e7d..44a0af2aba 100644 --- a/src/dbgshim/debugshim.h +++ b/src/dbgshim/debugshim.h @@ -17,6 +17,7 @@ #include #include #include "runtimeinfo.h" +#include "dbgshim.h" #if defined (HOST_WINDOWS) && defined(HOST_X86) #define CLRDEBUGINFO_RESOURCE_NAME W("CLRDEBUGINFOWINDOWSX86") @@ -50,18 +51,6 @@ // The cDAC (contract-based data access) is bundled next to dbgshim and is never downloaded. #define CORECLR_CDAC_MODULE_NAME_W W("mscordaccore_universal") -// Controls how OpenVirtualProcess locates the data-access layer when a data-access interface -// (for example IXCLRDataProcess) is requested. Mirrors the diagnostics CDacLoadPolicy. -enum CDacLoadPolicy -{ - // Prefer the co-located cDAC; fall back to the legacy DAC via the library provider. (default) - CDacLoadPolicy_PreferCDac = 0, - // Use only the cDAC; do not fall back to the legacy DAC. - CDacLoadPolicy_CDacOnly = 1, - // Use only the legacy DAC; do not try the cDAC. - CDacLoadPolicy_LegacyDacOnly = 2, -}; - // A small dbgshim-owned control interface, implemented by the same object as ICLRDebugging, that // lets a consumer attach a cDAC load policy to the debugging object before requesting a data-access // interface from OpenVirtualProcess. ICLRDebugging itself is a frozen published interface, so the @@ -71,8 +60,7 @@ MIDL_INTERFACE("2D3B4F6A-1C7E-4B2A-9E5D-7F1A6C0B8D34") ICLRDebuggingPolicy : public IUnknown { public: - // policy is a CDacLoadPolicy value. - virtual HRESULT STDMETHODCALLTYPE SetCDacLoadPolicy(DWORD policy) = 0; + virtual HRESULT STDMETHODCALLTYPE SetCDacLoadPolicy(CDacLoadPolicy policy) = 0; virtual HRESULT STDMETHODCALLTYPE GetCDacLoadPolicy(DWORD* pPolicy) = 0; }; @@ -222,7 +210,7 @@ class CLRDebuggingImpl : public ICLRDebugging, public ICLRDebuggingPolicy STDMETHOD(CanUnloadNow(HMODULE hModule)); // ICLRDebuggingPolicy methods: - STDMETHOD(SetCDacLoadPolicy(DWORD policy)); + STDMETHOD(SetCDacLoadPolicy(CDacLoadPolicy policy)); STDMETHOD(GetCDacLoadPolicy(DWORD* pPolicy)); // IUnknown methods: @@ -281,4 +269,11 @@ class CLRDebuggingImpl : public ICLRDebugging, public ICLRDebuggingPolicy }; // class CLRDebuggingImpl +// Resolves the paths to the cDAC and DBI bundled with dbgshim (or the DOTNET_CDAC_PATH/DOTNET_DBI_PATH +// overrides), returning false if either is missing. +bool GetCDacAndDbiPaths(SString& cdacPath, SString& dbiPath); + +// Picks the final HRESULT after tthe different activation attempts (cDAC vs. fallback) have completed. +HRESULT SelectActivationResult(HRESULT cdacHr, HRESULT fallbackHr, bool cdacEvaluated); + #endif diff --git a/src/tests/DbgShim.UnitTests/DbgShimAPI.cs b/src/tests/DbgShim.UnitTests/DbgShimAPI.cs index 4d06bd4b4c..32d017633f 100644 --- a/src/tests/DbgShim.UnitTests/DbgShimAPI.cs +++ b/src/tests/DbgShim.UnitTests/DbgShimAPI.cs @@ -34,6 +34,8 @@ public class DbgShimAPI private static CLRCreateInstanceDelegate _clrCreateInstance; + private static SetCDacLoadPolicyDelegate _setCDacLoadPolicy; + private static IntPtr _dbgshimModuleHandle = IntPtr.Zero; public const int CorDebugVersion_2_0 = 3; @@ -66,6 +68,7 @@ public static void Initialize(string dbgshimPath) _createDebuggingInterfaceFromVersion2 = GetDelegateFunction("CreateDebuggingInterfaceFromVersion2"); _createDebuggingInterfaceFromVersion3 = GetDelegateFunction("CreateDebuggingInterfaceFromVersion3", optional: true); _clrCreateInstance = GetDelegateFunction("CLRCreateInstance"); + _setCDacLoadPolicy = GetDelegateFunction("SetCDacLoadPolicy", optional: true); _initialized = true; } @@ -73,6 +76,8 @@ public static void Initialize(string dbgshimPath) public static bool IsCreateDebuggingInterfaceFromVersion3Supported => _createDebuggingInterfaceFromVersion3 != default; + public static bool IsSetCDacLoadPolicySupported => _setCDacLoadPolicy != default; + public static HResult CreateProcessForLaunch(string commandLine, bool suspendProcess, string currentDirectory, out int processId, out IntPtr resumeHandle) { return _createProcessForLaunch(commandLine, suspendProcess, lpEnvironment: IntPtr.Zero, currentDirectory, out processId, out resumeHandle); @@ -242,6 +247,15 @@ public static HResult CLRCreateInstance(out ICLRDebugging clrDebugging) return hr; } + public static HResult SetCDacLoadPolicy(DbgShimCDacLoadPolicy policy) + { + if (_setCDacLoadPolicy == default) + { + throw new NotSupportedException("SetCDacLoadPolicy not supported"); + } + return _setCDacLoadPolicy(policy); + } + private static T GetDelegateFunction(string functionName, bool optional = false) where T : Delegate { @@ -357,6 +371,10 @@ private unsafe delegate int CLRCreateInstanceDelegate( in Guid riid, out IntPtr pInterface); + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + private delegate int SetCDacLoadPolicyDelegate( + DbgShimCDacLoadPolicy policy); + #endregion } } diff --git a/src/tests/DbgShim.UnitTests/DbgShimTests.cs b/src/tests/DbgShim.UnitTests/DbgShimTests.cs index ab000e0096..870ae4deaf 100644 --- a/src/tests/DbgShim.UnitTests/DbgShimTests.cs +++ b/src/tests/DbgShim.UnitTests/DbgShimTests.cs @@ -64,6 +64,23 @@ public static IEnumerable OpenVirtualProcessLoadPolicyConfigurations } } + public static IEnumerable LiveLoadPolicyConfigurations + { + get + { + foreach (object[] configuration in GetConfigurations("TestName", null)) + { + TestConfiguration config = (TestConfiguration)configuration[0]; + // A fake cDAC path forces the cDAC attempt to fail so provider fallback is observable. + // CDacOnly must not fall back to the provider and must fail; the permissive policies + // fall back to the provider and succeed. + yield return new object[] { config, DbgShimCDacLoadPolicy.CDacOnly, false, false }; + yield return new object[] { config, DbgShimCDacLoadPolicy.LegacyDacOnly, true, true }; + yield return new object[] { config, DbgShimCDacLoadPolicy.PreferCDac, true, true }; + } + } + } + private ITestOutputHelper Output { get; } public DbgShimTests(ITestOutputHelper output) @@ -264,6 +281,94 @@ await RemoteInvoke(config, nameof(CreateDebuggingInterfaceFromVersion3), static }); } + /// + /// Test that the SetCDacLoadPolicy export gates the cDAC/legacy fallback for the + /// CreateDebuggingInterfaceFromVersion3 live path. + /// + [SkippableTheory, MemberData(nameof(LiveLoadPolicyConfigurations))] + public async Task CreateDebuggingInterfaceFromVersion3LoadPolicy( + TestConfiguration config, + DbgShimCDacLoadPolicy policy, + bool successExpected, + bool providerCallExpected) + { + if (OS.Kind == OSKind.OSX && config.PublishSingleFile) + { + throw new SkipTestException("CreateDebuggingInterfaceFromVersion3 single-file on MacOS"); + } + DbgShimAPI.Initialize(config.DbgShimPath()); + if (!DbgShimAPI.IsCreateDebuggingInterfaceFromVersion3Supported) + { + throw new SkipTestException("CreateDebuggingInterfaceFromVersion3 not supported"); + } + if (!DbgShimAPI.IsSetCDacLoadPolicySupported) + { + throw new SkipTestException("SetCDacLoadPolicy not supported"); + } + TestConfiguration policyConfig = new(new Dictionary(config.AllSettings) + { + [LoadPolicyKey] = policy.ToString(), + [LoadPolicySuccessExpectedKey] = successExpected.ToString(), + [LoadPolicyProviderCallsExpectedKey] = providerCallExpected.ToString(), + }); + await RemoteInvoke( + policyConfig, + $"{nameof(CreateDebuggingInterfaceFromVersion3LoadPolicy)}.{policy}", + static async (string configXml) => { + using DebuggeeInfo debuggeeInfo = await StartDebuggee(configXml, launch: false); + TestConfiguration cfg = debuggeeInfo.TestConfiguration; + DbgShimCDacLoadPolicy policy = Enum.Parse(cfg.AllSettings[LoadPolicyKey]); + bool successExpected = bool.Parse(cfg.AllSettings[LoadPolicySuccessExpectedKey]); + bool providerCallExpected = bool.Parse(cfg.AllSettings[LoadPolicyProviderCallsExpectedKey]); + TestCreateDebuggingInterfaceLoadPolicy(debuggeeInfo, policy, successExpected, providerCallExpected); + return 0; + }); + } + + /// + /// Test that the SetCDacLoadPolicy export gates the cDAC/legacy fallback for the + /// RegisterForRuntimeStartup3 live path. + /// + [SkippableTheory, MemberData(nameof(LiveLoadPolicyConfigurations))] + public async Task RegisterForRuntimeStartup3LoadPolicy( + TestConfiguration config, + DbgShimCDacLoadPolicy policy, + bool successExpected, + bool providerCallExpected) + { + if (OS.Kind == OSKind.OSX && config.PublishSingleFile) + { + throw new SkipTestException("RegisterForRuntimeStartup3 single-file on MacOS"); + } + DbgShimAPI.Initialize(config.DbgShimPath()); + if (!DbgShimAPI.IsRegisterForRuntimeStartup3Supported) + { + throw new SkipTestException("RegisterForRuntimeStartup3 not supported"); + } + if (!DbgShimAPI.IsSetCDacLoadPolicySupported) + { + throw new SkipTestException("SetCDacLoadPolicy not supported"); + } + TestConfiguration policyConfig = new(new Dictionary(config.AllSettings) + { + [LoadPolicyKey] = policy.ToString(), + [LoadPolicySuccessExpectedKey] = successExpected.ToString(), + [LoadPolicyProviderCallsExpectedKey] = providerCallExpected.ToString(), + }); + await RemoteInvoke( + policyConfig, + $"{nameof(RegisterForRuntimeStartup3LoadPolicy)}.{policy}", + static async (string configXml) => { + using DebuggeeInfo debuggeeInfo = await StartDebuggee(configXml, launch: false); + TestConfiguration cfg = debuggeeInfo.TestConfiguration; + DbgShimCDacLoadPolicy policy = Enum.Parse(cfg.AllSettings[LoadPolicyKey]); + bool successExpected = bool.Parse(cfg.AllSettings[LoadPolicySuccessExpectedKey]); + bool providerCallExpected = bool.Parse(cfg.AllSettings[LoadPolicyProviderCallsExpectedKey]); + TestRegisterForRuntimeStartup3LoadPolicy(debuggeeInfo, policy, successExpected, providerCallExpected); + return 0; + }); + } + [SkippableTheory, MemberData(nameof(GetConfigurations), "TestName", "OpenVirtualProcess")] public async Task OpenVirtualProcess(TestConfiguration config) { @@ -630,6 +735,76 @@ private static void TestRegisterForRuntimeStartup(DebuggeeInfo debuggeeInfo, int Trace.TraceInformation("RegisterForRuntimeStartup pid {0} DONE", debuggeeInfo.ProcessId); } + private static void TestRegisterForRuntimeStartup3LoadPolicy( + DebuggeeInfo debuggeeInfo, + DbgShimCDacLoadPolicy policy, + bool successExpected, + bool providerCallExpected) + { + TestConfiguration config = debuggeeInfo.TestConfiguration; + AutoResetEvent wait = new(false); + (IntPtr, GCHandle) unregister = (IntPtr.Zero, default); + HResult callbackResult = HResult.S_OK; + Exception callbackException = null; + ICorDebug corDebug = null; + + Trace.TraceInformation("TestRegisterForRuntimeStartup3LoadPolicy pid {0} policy {1} START", debuggeeInfo.ProcessId, policy); + + string originalCDacPath = Environment.GetEnvironmentVariable("DOTNET_CDAC_PATH"); + Environment.SetEnvironmentVariable( + "DOTNET_CDAC_PATH", + Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.dll")); + try + { + AssertResult(DbgShimAPI.SetCDacLoadPolicy(policy)); + + DbgShimAPI.RuntimeStartupCallbackDelegate callback = (ICorDebug cordbg, object parameter, HResult hr) => { + corDebug = cordbg; + callbackResult = hr; + try + { + if (hr) + { + TestICorDebug(debuggeeInfo, cordbg); + } + } + catch (Exception ex) + { + Trace.TraceError(ex.ToString()); + callbackException = ex; + } + wait.Set(); + }; + + LibraryProviderWrapper libraryProvider = new(config.RuntimeModulePath(), config.DbiModulePath(), config.DacModulePath()); + AssertResult(DbgShimAPI.RegisterForRuntimeStartup3(debuggeeInfo.ProcessId, applicationGroupId: null, parameter: IntPtr.Zero, libraryProvider.ILibraryProvider, out unregister, callback)); + + Assert.True(wait.WaitOne()); + AssertResult(DbgShimAPI.UnregisterForRuntimeStartup(unregister)); + Assert.Null(callbackException); + + Assert.Equal(successExpected, callbackResult == HResult.S_OK); + Assert.Equal(providerCallExpected, libraryProvider.CallCount > 0); + + if (successExpected) + { + AssertResult(debuggeeInfo.WaitForCreateProcess()); + Assert.Equal(0, corDebug.Release()); + } + else + { + Assert.Null(corDebug); + debuggeeInfo.Kill(); + } + } + finally + { + Environment.SetEnvironmentVariable("DOTNET_CDAC_PATH", originalCDacPath); + } + + Trace.TraceInformation("TestRegisterForRuntimeStartup3LoadPolicy pid {0} DONE", debuggeeInfo.ProcessId); + } + private static void TestCreateDebuggingInterface(DebuggeeInfo debuggeeInfo, int api) { Trace.TraceInformation("TestCreateDebuggingInterface pid {0} api {1} START", debuggeeInfo.ProcessId, api); @@ -700,6 +875,59 @@ private static void TestCreateDebuggingInterface(DebuggeeInfo debuggeeInfo, int Trace.TraceInformation("TestCreateDebuggingInterface pid {0} DONE", debuggeeInfo.ProcessId); } + private static void TestCreateDebuggingInterfaceLoadPolicy( + DebuggeeInfo debuggeeInfo, + DbgShimCDacLoadPolicy policy, + bool successExpected, + bool providerCallExpected) + { + Trace.TraceInformation("TestCreateDebuggingInterfaceLoadPolicy pid {0} policy {1} START", debuggeeInfo.ProcessId, policy); + + string originalCDacPath = Environment.GetEnvironmentVariable("DOTNET_CDAC_PATH"); + Environment.SetEnvironmentVariable( + "DOTNET_CDAC_PATH", + Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.dll")); + try + { + AssertResult(DbgShimAPI.SetCDacLoadPolicy(policy)); + + HResult hr = DbgShimAPI.EnumerateCLRs(debuggeeInfo.ProcessId, (IntPtr[] continueEventHandles, string[] moduleNames) => { + TestConfiguration config = debuggeeInfo.TestConfiguration; + Assert.Single(continueEventHandles); + Assert.Single(moduleNames); + for (int i = 0; i < continueEventHandles.Length; i++) + { + AssertResult(DbgShimAPI.CreateVersionStringFromModule(debuggeeInfo.ProcessId, moduleNames[i], out string versionString)); + Assert.False(string.IsNullOrWhiteSpace(versionString)); + + LibraryProviderWrapper libraryProvider = new(config.RuntimeModulePath(), config.DbiModulePath(), config.DacModulePath()); + HResult result = DbgShimAPI.CreateDebuggingInterfaceFromVersion3(DbgShimAPI.CorDebugVersion_4_0, versionString, applicationGroupId: null, libraryProvider.ILibraryProvider, out ICorDebug corDebug); + + Assert.Equal(successExpected, result == HResult.S_OK); + Assert.Equal(providerCallExpected, libraryProvider.CallCount > 0); + + if (successExpected) + { + TestICorDebug(debuggeeInfo, corDebug); + AssertResult(debuggeeInfo.WaitForCreateProcess()); + Assert.Equal(0, corDebug.Release()); + } + else + { + Assert.Null(corDebug); + } + } + }); + AssertResult(hr); + } + finally + { + Environment.SetEnvironmentVariable("DOTNET_CDAC_PATH", originalCDacPath); + } + + Trace.TraceInformation("TestCreateDebuggingInterfaceLoadPolicy pid {0} DONE", debuggeeInfo.ProcessId); + } + private static readonly Guid IID_ICorDebugProcess = new("3D6F5F64-7538-11D3-8D5B-00104B35E7EF"); private static void TestICorDebug(DebuggeeInfo debuggeeInfo, ICorDebug corDebug) From 43786638b249d6009b9336b61d880cca625bd842 Mon Sep 17 00:00:00 2001 From: Juan Sebastian Hoyos Ayala Date: Fri, 24 Jul 2026 03:49:26 -0700 Subject: [PATCH 2/5] dbgshim: add side-by-side live debugging configurations and tests --- src/tests/DbgShim.UnitTests/DbgShimTests.cs | 120 ++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/src/tests/DbgShim.UnitTests/DbgShimTests.cs b/src/tests/DbgShim.UnitTests/DbgShimTests.cs index 870ae4deaf..93928f8a6c 100644 --- a/src/tests/DbgShim.UnitTests/DbgShimTests.cs +++ b/src/tests/DbgShim.UnitTests/DbgShimTests.cs @@ -34,6 +34,7 @@ public class DbgShimTests : IDisposable private const string LoadPolicyKey = "LoadPolicy"; private const string LoadPolicySuccessExpectedKey = "LoadPolicySuccessExpected"; private const string LoadPolicyProviderCallsExpectedKey = "LoadPolicyProviderCallsExpected"; + private const string LoadPolicySideBySideAllowedKey = "LoadPolicySideBySideAllowed"; public static IEnumerable GetConfigurations(string key, string value) { @@ -81,6 +82,24 @@ public static IEnumerable LiveLoadPolicyConfigurations } } + public static IEnumerable LiveSideBySideConfigurations + { + get + { + foreach (object[] configuration in GetConfigurations("TestName", null)) + { + TestConfiguration config = (TestConfiguration)configuration[0]; + // No library provider is supplied, so activation must fall back to the DBI + // collocated with the runtime module. A fake cDAC path forces the cDAC attempt + // to fail so the side-by-side path is what is exercised. CDacOnly must not fall + // back to side-by-side; the permissive policies must. + yield return new object[] { config, DbgShimCDacLoadPolicy.CDacOnly, false }; + yield return new object[] { config, DbgShimCDacLoadPolicy.LegacyDacOnly, true }; + yield return new object[] { config, DbgShimCDacLoadPolicy.PreferCDac, true }; + } + } + } + private ITestOutputHelper Output { get; } public DbgShimTests(ITestOutputHelper output) @@ -369,6 +388,48 @@ static async (string configXml) => { }); } + /// + /// Test that the no-provider (side-by-side) live path activates using the DBI collocated + /// with the runtime module reported by EnumerateCLRs, and that SetCDacLoadPolicy still gates + /// the cDAC/legacy fallback when no library provider is supplied. + /// + [SkippableTheory, MemberData(nameof(LiveSideBySideConfigurations))] + public async Task CreateDebuggingInterfaceFromVersion3SideBySide( + TestConfiguration config, + DbgShimCDacLoadPolicy policy, + bool sideBySideAllowed) + { + if (OS.Kind == OSKind.OSX && config.PublishSingleFile) + { + throw new SkipTestException("CreateDebuggingInterfaceFromVersion3 single-file on MacOS"); + } + DbgShimAPI.Initialize(config.DbgShimPath()); + if (!DbgShimAPI.IsCreateDebuggingInterfaceFromVersion3Supported) + { + throw new SkipTestException("CreateDebuggingInterfaceFromVersion3 not supported"); + } + if (!DbgShimAPI.IsSetCDacLoadPolicySupported) + { + throw new SkipTestException("SetCDacLoadPolicy not supported"); + } + TestConfiguration policyConfig = new(new Dictionary(config.AllSettings) + { + [LoadPolicyKey] = policy.ToString(), + [LoadPolicySideBySideAllowedKey] = sideBySideAllowed.ToString(), + }); + await RemoteInvoke( + policyConfig, + $"{nameof(CreateDebuggingInterfaceFromVersion3SideBySide)}.{policy}", + static async (string configXml) => { + using DebuggeeInfo debuggeeInfo = await StartDebuggee(configXml, launch: false); + TestConfiguration cfg = debuggeeInfo.TestConfiguration; + DbgShimCDacLoadPolicy policy = Enum.Parse(cfg.AllSettings[LoadPolicyKey]); + bool sideBySideAllowed = bool.Parse(cfg.AllSettings[LoadPolicySideBySideAllowedKey]); + TestCreateDebuggingInterfaceSideBySide(debuggeeInfo, policy, sideBySideAllowed); + return 0; + }); + } + [SkippableTheory, MemberData(nameof(GetConfigurations), "TestName", "OpenVirtualProcess")] public async Task OpenVirtualProcess(TestConfiguration config) { @@ -928,6 +989,65 @@ private static void TestCreateDebuggingInterfaceLoadPolicy( Trace.TraceInformation("TestCreateDebuggingInterfaceLoadPolicy pid {0} DONE", debuggeeInfo.ProcessId); } + private static void TestCreateDebuggingInterfaceSideBySide( + DebuggeeInfo debuggeeInfo, + DbgShimCDacLoadPolicy policy, + bool sideBySideAllowed) + { + TestConfiguration config = debuggeeInfo.TestConfiguration; + Trace.TraceInformation("TestCreateDebuggingInterfaceSideBySide pid {0} policy {1} START", debuggeeInfo.ProcessId, policy); + + string originalCDacPath = Environment.GetEnvironmentVariable("DOTNET_CDAC_PATH"); + Environment.SetEnvironmentVariable( + "DOTNET_CDAC_PATH", + Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.dll")); + try + { + AssertResult(DbgShimAPI.SetCDacLoadPolicy(policy)); + + HResult hr = DbgShimAPI.EnumerateCLRs(debuggeeInfo.ProcessId, (IntPtr[] continueEventHandles, string[] moduleNames) => { + Assert.Single(continueEventHandles); + Assert.Single(moduleNames); + for (int i = 0; i < continueEventHandles.Length; i++) + { + AssertResult(DbgShimAPI.CreateVersionStringFromModule(debuggeeInfo.ProcessId, moduleNames[i], out string versionString)); + Assert.False(string.IsNullOrWhiteSpace(versionString)); + + // With no library provider the shim derives the DBI from the runtime module + // EnumerateCLRs reported, so the DBI it loads is the one next to that module. + // Confirm that collocated DBI is present exactly when the runtime is not a + // single-file app (whose runtime module has no adjacent DBI). + string collocatedDbi = Path.Combine(Path.GetDirectoryName(moduleNames[i]), Path.GetFileName(config.DbiModulePath())); + bool collocatedDbiExists = !config.PublishSingleFile; + Assert.Equal(collocatedDbiExists, File.Exists(collocatedDbi)); + + bool successExpected = sideBySideAllowed && collocatedDbiExists; + HResult result = DbgShimAPI.CreateDebuggingInterfaceFromVersion3(DbgShimAPI.CorDebugVersion_4_0, versionString, applicationGroupId: null, libraryProvider: IntPtr.Zero, out ICorDebug corDebug); + + Assert.Equal(successExpected, result == HResult.S_OK); + + if (successExpected) + { + TestICorDebug(debuggeeInfo, corDebug); + AssertResult(debuggeeInfo.WaitForCreateProcess()); + Assert.Equal(0, corDebug.Release()); + } + else + { + Assert.Null(corDebug); + } + } + }); + AssertResult(hr); + } + finally + { + Environment.SetEnvironmentVariable("DOTNET_CDAC_PATH", originalCDacPath); + } + + Trace.TraceInformation("TestCreateDebuggingInterfaceSideBySide pid {0} DONE", debuggeeInfo.ProcessId); + } + private static readonly Guid IID_ICorDebugProcess = new("3D6F5F64-7538-11D3-8D5B-00104B35E7EF"); private static void TestICorDebug(DebuggeeInfo debuggeeInfo, ICorDebug corDebug) From 0aeb8bec6910130f62968565e2e4a8d94c39f165 Mon Sep 17 00:00:00 2001 From: Juan Hoyos <19413848+hoyosjs@users.noreply.github.com> Date: Fri, 24 Jul 2026 21:16:26 -0700 Subject: [PATCH 3/5] Update src/dbgshim/debugshim.h Co-authored-by: Noah Falk --- src/dbgshim/debugshim.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dbgshim/debugshim.h b/src/dbgshim/debugshim.h index 44a0af2aba..e70fc564b1 100644 --- a/src/dbgshim/debugshim.h +++ b/src/dbgshim/debugshim.h @@ -61,7 +61,7 @@ ICLRDebuggingPolicy : public IUnknown { public: virtual HRESULT STDMETHODCALLTYPE SetCDacLoadPolicy(CDacLoadPolicy policy) = 0; - virtual HRESULT STDMETHODCALLTYPE GetCDacLoadPolicy(DWORD* pPolicy) = 0; + virtual HRESULT STDMETHODCALLTYPE GetCDacLoadPolicy(CDacLoadPolicy * pPolicy) = 0; }; #define MAX_BUILDID_SIZE 24 From 958cb04b985f0f80f127a4a0d53a4d68175a76de Mon Sep 17 00:00:00 2001 From: Juan Hoyos <19413848+hoyosjs@users.noreply.github.com> Date: Sat, 25 Jul 2026 02:56:35 -0700 Subject: [PATCH 4/5] Apply suggestions from code review Co-authored-by: Juan Hoyos <19413848+hoyosjs@users.noreply.github.com> --- src/dbgshim/debugshim.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dbgshim/debugshim.h b/src/dbgshim/debugshim.h index e70fc564b1..4211c64043 100644 --- a/src/dbgshim/debugshim.h +++ b/src/dbgshim/debugshim.h @@ -61,7 +61,7 @@ ICLRDebuggingPolicy : public IUnknown { public: virtual HRESULT STDMETHODCALLTYPE SetCDacLoadPolicy(CDacLoadPolicy policy) = 0; - virtual HRESULT STDMETHODCALLTYPE GetCDacLoadPolicy(CDacLoadPolicy * pPolicy) = 0; + virtual HRESULT STDMETHODCALLTYPE GetCDacLoadPolicy(CDacLoadPolicy* pPolicy) = 0; }; #define MAX_BUILDID_SIZE 24 @@ -211,7 +211,7 @@ class CLRDebuggingImpl : public ICLRDebugging, public ICLRDebuggingPolicy // ICLRDebuggingPolicy methods: STDMETHOD(SetCDacLoadPolicy(CDacLoadPolicy policy)); - STDMETHOD(GetCDacLoadPolicy(DWORD* pPolicy)); + STDMETHOD(GetCDacLoadPolicy(CDacLoadPolicy* pPolicy)); // IUnknown methods: STDMETHOD(QueryInterface(REFIID riid, void **ppvObject)); From e866f530fc8294320f58bdb5ee1d9299f3496a5b Mon Sep 17 00:00:00 2001 From: Juan Sebastian Hoyos Ayala Date: Sat, 25 Jul 2026 04:05:04 -0700 Subject: [PATCH 5/5] Fix remaining issues --- src/dbgshim/dbgshim.cpp | 4 ++-- src/dbgshim/debugshim.cpp | 6 +++--- src/dbgshim/debugshim.h | 2 +- src/tests/DbgShim.UnitTests/DbgShimTests.cs | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/dbgshim/dbgshim.cpp b/src/dbgshim/dbgshim.cpp index c5c45f073c..3adb420420 100644 --- a/src/dbgshim/dbgshim.cpp +++ b/src/dbgshim/dbgshim.cpp @@ -232,7 +232,7 @@ HRESULT CreateCoreDbg( // Policy for the live-debugging paths (RegisterForRuntimeStartup* and // CreateDebuggingInterfaceFromVersion*), which don't flow through ICLRDebuggingPolicy. Set via the // SetCDacLoadPolicy export. -static CDacLoadPolicy g_cdacLoadPolicy = CDacLoadPolicy_PreferCDac; +static Volatile g_cdacLoadPolicy = CDacLoadPolicy_PreferCDac; // Only the bundled DBI can report whether it can consume the cDAC for this target, so this invokes it // rather than pre-judging from file presence. @@ -247,7 +247,7 @@ static HRESULT TryCreateCoreDbgWithCDac( SString dbiPath; if (!GetCDacAndDbiPaths(cdacPath, dbiPath)) { - return E_FAIL; + return CORDBG_E_DEBUG_COMPONENT_MISSING; } return CreateCoreDbg(hModule, processId, dbiPath, cdacPath, lpApplicationGroupId, iDebuggerVersion, ppCordb); diff --git a/src/dbgshim/debugshim.cpp b/src/dbgshim/debugshim.cpp index 172e46ed99..aafca0f352 100644 --- a/src/dbgshim/debugshim.cpp +++ b/src/dbgshim/debugshim.cpp @@ -282,7 +282,7 @@ static HRESULT OpenCorDebugProcessWithCDac( SString cdacModulePath; if (!GetCDacAndDbiPaths(cdacModulePath, dbiModulePath)) { - return E_FAIL; + return CORDBG_E_DEBUG_COMPONENT_MISSING; } HMODULE hDbi = LoadLibraryW(dbiModulePath); @@ -1372,13 +1372,13 @@ STDMETHODIMP CLRDebuggingImpl::SetCDacLoadPolicy(CDacLoadPolicy policy) return S_OK; } -STDMETHODIMP CLRDebuggingImpl::GetCDacLoadPolicy(DWORD* pPolicy) +STDMETHODIMP CLRDebuggingImpl::GetCDacLoadPolicy(CDacLoadPolicy* pPolicy) { if (pPolicy == NULL) { return E_POINTER; } - *pPolicy = (DWORD)m_cdacLoadPolicy; + *pPolicy = m_cdacLoadPolicy; return S_OK; } diff --git a/src/dbgshim/debugshim.h b/src/dbgshim/debugshim.h index 4211c64043..b6873d06c3 100644 --- a/src/dbgshim/debugshim.h +++ b/src/dbgshim/debugshim.h @@ -273,7 +273,7 @@ class CLRDebuggingImpl : public ICLRDebugging, public ICLRDebuggingPolicy // overrides), returning false if either is missing. bool GetCDacAndDbiPaths(SString& cdacPath, SString& dbiPath); -// Picks the final HRESULT after tthe different activation attempts (cDAC vs. fallback) have completed. +// Picks the final HRESULT after the different activation attempts (cDAC vs. fallback) have completed. HRESULT SelectActivationResult(HRESULT cdacHr, HRESULT fallbackHr, bool cdacEvaluated); #endif diff --git a/src/tests/DbgShim.UnitTests/DbgShimTests.cs b/src/tests/DbgShim.UnitTests/DbgShimTests.cs index 93928f8a6c..dc79704b1d 100644 --- a/src/tests/DbgShim.UnitTests/DbgShimTests.cs +++ b/src/tests/DbgShim.UnitTests/DbgShimTests.cs @@ -764,7 +764,7 @@ private static void TestRegisterForRuntimeStartup(DebuggeeInfo debuggeeInfo, int AssertResult(result); Trace.TraceInformation("RegisterForRuntimeStartup pid {0} waiting for callback", debuggeeInfo.ProcessId); - Assert.True(wait.WaitOne()); + Assert.True(wait.WaitOne(TimeSpan.FromMinutes(5)), "Timed out waiting for the RegisterForRuntimeStartup callback."); Trace.TraceInformation("RegisterForRuntimeStartup pid {0} after callback wait", debuggeeInfo.ProcessId); AssertResult(DbgShimAPI.UnregisterForRuntimeStartup(unregister)); @@ -840,7 +840,7 @@ private static void TestRegisterForRuntimeStartup3LoadPolicy( LibraryProviderWrapper libraryProvider = new(config.RuntimeModulePath(), config.DbiModulePath(), config.DacModulePath()); AssertResult(DbgShimAPI.RegisterForRuntimeStartup3(debuggeeInfo.ProcessId, applicationGroupId: null, parameter: IntPtr.Zero, libraryProvider.ILibraryProvider, out unregister, callback)); - Assert.True(wait.WaitOne()); + Assert.True(wait.WaitOne(TimeSpan.FromMinutes(5)), "Timed out waiting for the RegisterForRuntimeStartup3 callback."); AssertResult(DbgShimAPI.UnregisterForRuntimeStartup(unregister)); Assert.Null(callbackException);