From b9a5ed65fa553d7296c4c47658abe3ac57fc23c4 Mon Sep 17 00:00:00 2001 From: Juan Sebastian Hoyos Ayala Date: Sat, 11 Jul 2026 23:31:14 -0700 Subject: [PATCH 1/5] Change DAC/DBI download policy and verify DBI on load - SOS: only download the DAC/DBI from a symbol server on a Windows host when DAC signature verification is enabled. Otherwise the matching DAC/DBI must be provided locally (collocated runtime or 'setclrpath'). GetLibraryPath now takes an allowDownload argument; the cDAC continues to never download. - SOS: verify the DBI signature on load like the DAC. DAC, DBI and cDAC now load through a shared VerifyAndLoadLibrary helper; DAC/DBI verification is driven by the single DacSignatureVerificationEnabled setting, and the cDAC loads without verification. - IRuntime.GetDbiFilePath now returns whether the DBI requires signature verification, matching GetDacFilePath. - dotnet-symbol: add KeyTypeFlags.WindowsDebuggingLibrariesOnly and apply it whenever DAC/DBI keys are requested, so only the Windows and Windows-hosted cross-OS (PE) debugging libraries are staged and native ELF/Mach-O DAC/DBI are not. Warn on non-Windows --debugging. --- .../Runtime.cs | 70 +++++++++++++++---- .../IRuntime.cs | 3 +- .../KeyGenerators/ELFFileKeyGenerator.cs | 9 ++- .../KeyGenerators/KeyGenerator.cs | 10 ++- .../KeyGenerators/MachOKeyGenerator.cs | 7 +- src/SOS/SOS.Hosting/RuntimeWrapper.cs | 46 +++++++----- src/Tools/dotnet-symbol/Program.cs | 18 ++++- src/tests/DbgShim.UnitTests/DbgShimTests.cs | 2 +- .../KeyGeneratorTests.cs | 14 ++++ 9 files changed, 144 insertions(+), 35 deletions(-) diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/Runtime.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/Runtime.cs index 28c444bb62..469a5ba8be 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/Runtime.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/Runtime.cs @@ -23,10 +23,10 @@ public class Runtime : IRuntime, IDisposable private readonly IHostAssetResolver _hostAssetResolver; private readonly ISettingsService _settingsService; private readonly ISymbolService _symbolService; + private readonly IConsoleService _consoleService; private Version _runtimeVersion; private ClrRuntime _clrRuntime; private string _dacFilePath; - private bool _verifySignature; // This only applies to the regular DAC, not the CDAC private string _cdacFilePath; private string _dbiFilePath; @@ -43,6 +43,9 @@ public Runtime(IServiceProvider services, int id, ClrInfo clrInfo) _hostAssetResolver = services.GetService(); _settingsService = services.GetService() ?? throw new ArgumentException("ISettingsService required"); _symbolService = services.GetService() ?? throw new ArgumentException("ISymbolService required"); + // IConsoleService is optional: when present it is used to surface actionable guidance + // (for example, to run 'setclrpath') when the DAC/DBI cannot be found or downloaded. + _consoleService = services.GetService(); RuntimeType = GetRuntimeType(clrInfo.Flavor); RuntimeModule = services.GetService().GetModuleFromBaseAddress(clrInfo.ModuleInfo.ImageBase); @@ -101,13 +104,13 @@ public string GetDacFilePath(out bool verifySignature) { if (_dacFilePath is null) { - _dacFilePath = GetLibraryPath(DebugLibraryKind.Dac); - if (_dacFilePath is not null) + _dacFilePath = GetLibraryPath(DebugLibraryKind.Dac, allowDownload: DownloadAllowed); + if (_dacFilePath is null) { - _verifySignature = _settingsService.DacSignatureVerificationEnabled; + WriteDebugLibraryNotFoundWarning(DebugLibraryKind.Dac); } } - verifySignature = _verifySignature; + verifySignature = VerifyDebugLibrarySignature(_dacFilePath); return _dacFilePath; } @@ -122,7 +125,7 @@ public string GetCDacFilePath() // The cDAC is bundled with the diagnostics tool and is never downloaded, so a missing // path means it isn't available for this host. - _cdacFilePath ??= GetLibraryPath(DebugLibraryKind.CDac); + _cdacFilePath ??= GetLibraryPath(DebugLibraryKind.CDac, allowDownload: false); if (_cdacFilePath is null && _settingsService.CDacLoadPolicy == CDacLoadPolicy.UseCDac) { // The cDAC was explicitly forced but isn't bundled with this tool. @@ -131,12 +134,28 @@ public string GetCDacFilePath() return _cdacFilePath; } - public string GetDbiFilePath() + public string GetDbiFilePath(out bool verifySignature) { - _dbiFilePath ??= GetLibraryPath(DebugLibraryKind.Dbi); + if (_dbiFilePath is null) + { + _dbiFilePath = GetLibraryPath(DebugLibraryKind.Dbi, allowDownload: DownloadAllowed); + if (_dbiFilePath is null) + { + WriteDebugLibraryNotFoundWarning(DebugLibraryKind.Dbi); + } + } + verifySignature = VerifyDebugLibrarySignature(_dbiFilePath); return _dbiFilePath; } + /// + /// The DAC and DBI are both verified according to the single DacSignatureVerificationEnabled + /// setting; there is no path where one is verified and the other is not. The cDAC is the only + /// debugging library that is never verified, and it is loaded through a separate path. + /// + private bool VerifyDebugLibrarySignature(string libraryPath) => + libraryPath is not null && _settingsService.DacSignatureVerificationEnabled; + #endregion /// @@ -218,7 +237,7 @@ InvalidDataException or return null; } - private string GetLibraryPath(DebugLibraryKind kind) + private string GetLibraryPath(DebugLibraryKind kind, bool allowDownload) { Architecture currentArch = RuntimeInformation.ProcessArchitecture; string libraryPath = null; @@ -239,7 +258,7 @@ private string GetLibraryPath(DebugLibraryKind kind) { continue; } - if (libraryInfo.ArchivedUnder != SymbolProperties.None) + if (libraryInfo.ArchivedUnder != SymbolProperties.None && allowDownload) { libraryPath = DownloadFile(libraryInfo); if (libraryPath is not null) @@ -253,6 +272,32 @@ private string GetLibraryPath(DebugLibraryKind kind) return libraryPath; } + /// + /// Symbol-server download of a DAC/DBI is only permitted when the downloaded binary will be + /// authenticode-verified before it is loaded and run. That requires a Windows host (authenticode + /// verification is Windows-only, and a non-Windows host cannot load a foreign-format PE DAC/DBI + /// anyway) AND that verification has not been disabled. The DacSignatureVerification override only + /// relaxes verification for locally-provided DAC/DBI (see 'setclrpath'); it must never allow a + /// remotely-acquired, unauthenticated binary to be loaded. When download is not permitted the + /// matching DAC/DBI must be provided locally. + /// + private bool DownloadAllowed => + RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && _settingsService.DacSignatureVerificationEnabled; + + private void WriteDebugLibraryNotFoundWarning(DebugLibraryKind kind) + { + if (DownloadAllowed) + { + return; + } + string library = kind == DebugLibraryKind.Dbi ? "DBI" : "DAC"; + _consoleService?.WriteWarning( + $"Could not find matching {library} for runtime: {RuntimeModule.FileName}{Environment.NewLine}" + + $"Downloading debugging libraries from the symbol server is only supported on Windows with DAC signature verification enabled.{Environment.NewLine}" + + $"Use 'setclrpath ' to point at the directory that contains the matching DAC/DBI files{Environment.NewLine}" + + $"(for example the runtime's shared framework directory). See 'soshelp setclrpath' for more information.{Environment.NewLine}"); + } + private string GetLocalPath(DebugLibraryInfo libraryInfo) { string localFilePath; @@ -408,7 +453,7 @@ public override string ToString() if (_dacFilePath is not null) { sb.AppendLine(); - string verify = _verifySignature ? "(verify)" : "(don't verify)"; + string verify = VerifyDebugLibrarySignature(_dacFilePath) ? "(verify)" : "(don't verify)"; sb.Append($" DAC: {_dacFilePath} {verify}"); } if (_cdacFilePath is not null) @@ -419,7 +464,8 @@ public override string ToString() if (_dbiFilePath is not null) { sb.AppendLine(); - sb.Append($" DBI: {_dbiFilePath}"); + string verify = VerifyDebugLibrarySignature(_dbiFilePath) ? "(verify)" : "(don't verify)"; + sb.Append($" DBI: {_dbiFilePath} {verify}"); } return sb.ToString(); } diff --git a/src/Microsoft.Diagnostics.DebugServices/IRuntime.cs b/src/Microsoft.Diagnostics.DebugServices/IRuntime.cs index 9400736d17..6be7fc3d4e 100644 --- a/src/Microsoft.Diagnostics.DebugServices/IRuntime.cs +++ b/src/Microsoft.Diagnostics.DebugServices/IRuntime.cs @@ -75,6 +75,7 @@ public interface IRuntime /// /// Returns the DBI file path /// - string GetDbiFilePath(); + /// returns whether the returned DBI requires signature verification. + string GetDbiFilePath(out bool verifySignature); } } diff --git a/src/Microsoft.SymbolStore/KeyGenerators/ELFFileKeyGenerator.cs b/src/Microsoft.SymbolStore/KeyGenerators/ELFFileKeyGenerator.cs index d5f918cd79..9c424b1dff 100644 --- a/src/Microsoft.SymbolStore/KeyGenerators/ELFFileKeyGenerator.cs +++ b/src/Microsoft.SymbolStore/KeyGenerators/ELFFileKeyGenerator.cs @@ -166,7 +166,14 @@ public static IEnumerable GetKeys(KeyTypeFlags flags, string pat // Creates all the special CLR keys if the path is the coreclr module for this platform if (fileName == CoreClrFileName) { - foreach (string specialFileName in (flags & KeyTypeFlags.ClrKeys) != 0 ? s_coreClrSpecialFiles : s_dacdbiSpecialFiles) + IEnumerable specialFiles = (flags & KeyTypeFlags.ClrKeys) != 0 ? s_coreClrSpecialFiles : s_dacdbiSpecialFiles; + if ((flags & KeyTypeFlags.WindowsDebuggingLibrariesOnly) != 0) + { + // Only the Windows-hosted cross-OS DAC/DBI (mscordaccore.dll / mscordbi.dll) + // are PE images that can be authenticode-verified. Skip the native ELF images. + specialFiles = specialFiles.Where(static specialFile => specialFile.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)); + } + foreach (string specialFileName in specialFiles) { yield return BuildKey(specialFileName, CoreClrPrefix, buildId); } diff --git a/src/Microsoft.SymbolStore/KeyGenerators/KeyGenerator.cs b/src/Microsoft.SymbolStore/KeyGenerators/KeyGenerator.cs index 03307b909a..35938b0b7b 100644 --- a/src/Microsoft.SymbolStore/KeyGenerators/KeyGenerator.cs +++ b/src/Microsoft.SymbolStore/KeyGenerators/KeyGenerator.cs @@ -65,7 +65,15 @@ public enum KeyTypeFlags /// /// Generate the r2r perfmap key of the binary (if one exists). /// - PerfMapKeys = 0x80 + PerfMapKeys = 0x80, + + /// + /// When combined with or , restricts the + /// generated DAC/DBI/SOS special-file keys to Windows (PE) images only: the native Windows + /// DAC/DBI and the Windows-hosted cross-OS DAC/DBI. This is a defense in depth measure + /// to ensure we only enumerate files we can authenticate when that are intended to run. + /// + WindowsDebuggingLibrariesOnly = 0x100 } /// diff --git a/src/Microsoft.SymbolStore/KeyGenerators/MachOKeyGenerator.cs b/src/Microsoft.SymbolStore/KeyGenerators/MachOKeyGenerator.cs index b4369ad6ef..d52929ad32 100644 --- a/src/Microsoft.SymbolStore/KeyGenerators/MachOKeyGenerator.cs +++ b/src/Microsoft.SymbolStore/KeyGenerators/MachOKeyGenerator.cs @@ -141,7 +141,12 @@ public static IEnumerable GetKeys(KeyTypeFlags flags, string pat /// Creates all the special CLR keys if the path is the coreclr module for this platform if (fileName == CoreClrFileName) { - foreach (string specialFileName in (flags & KeyTypeFlags.ClrKeys) != 0 ? s_coreClrSpecialFiles : s_dacdbiSpecialFiles) + IEnumerable specialFiles = (flags & KeyTypeFlags.ClrKeys) != 0 ? s_coreClrSpecialFiles : s_dacdbiSpecialFiles; + if ((flags & KeyTypeFlags.WindowsDebuggingLibrariesOnly) != 0) + { + specialFiles = specialFiles.Where(static specialFile => specialFile.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)); + } + foreach (string specialFileName in specialFiles) { yield return BuildKey(specialFileName, CoreClrPrefix, uuid); } diff --git a/src/SOS/SOS.Hosting/RuntimeWrapper.cs b/src/SOS/SOS.Hosting/RuntimeWrapper.cs index a704534190..45597caeb1 100644 --- a/src/SOS/SOS.Hosting/RuntimeWrapper.cs +++ b/src/SOS/SOS.Hosting/RuntimeWrapper.cs @@ -383,7 +383,7 @@ private IntPtr CreateClrDataProcess(IntPtr dacHandle) private IntPtr CreateCorDebugProcess() { - string dbiFilePath = _runtime.GetDbiFilePath(); + string dbiFilePath = _runtime.GetDbiFilePath(out bool verifyDbiSignature); if (dbiFilePath == null) { Trace.TraceError($"Could not find matching DBI {dbiFilePath ?? ""} for this runtime: {_runtime.RuntimeModule.FileName}"); @@ -405,16 +405,13 @@ private IntPtr CreateCorDebugProcess() if (_dbiHandle == IntPtr.Zero) { - try - { - _dbiHandle = DataTarget.PlatformFunctions.LoadLibrary(dbiFilePath); - } - catch (Exception ex) when (ex is DllNotFoundException or BadImageFormatException) + // Verify the DBI signature (when required) and load it, holding the verification + // file lock through LoadLibrary to prevent a TOCTOU swap between verify and load. + _dbiHandle = VerifyAndLoadLibrary(dbiFilePath, verifyDbiSignature, "DBI"); + if (_dbiHandle == IntPtr.Zero) { - Trace.TraceError($"LoadLibrary({dbiFilePath}) FAILED {ex}"); return IntPtr.Zero; } - Debug.Assert(_dbiHandle != IntPtr.Zero); } ClrDebuggingVersion maxDebuggerSupportedVersion = new() { @@ -549,38 +546,53 @@ private IntPtr GetCDacHandle() return _cdacHandle; } - private static IntPtr LoadDacLibrary(string dacFilePath, bool verifySignature) + /// + /// Verifies the signature (when required) and loads the native library, holding the + /// verification file lock through LoadLibrary to prevent a TOCTOU race where the file could + /// be swapped after verification but before loading. Returns IntPtr.Zero on failure. + /// + private static IntPtr VerifyAndLoadLibrary(string filePath, bool verifySignature, string description) { - IntPtr dacHandle = IntPtr.Zero; + IntPtr handle = IntPtr.Zero; IDisposable fileLock = null; try { if (verifySignature) { - Trace.TraceInformation($"Verifying DAC signing and cert {dacFilePath}"); + Trace.TraceInformation($"Verifying {description} signing and cert {filePath}"); - // Check if the DAC cert is valid before loading - if (!AuthenticodeUtil.VerifyDacDll(dacFilePath, out fileLock)) + // Check if the cert is valid before loading + if (!AuthenticodeUtil.VerifyDacDll(filePath, out fileLock)) { return IntPtr.Zero; } } try { - dacHandle = DataTarget.PlatformFunctions.LoadLibrary(dacFilePath); + handle = DataTarget.PlatformFunctions.LoadLibrary(filePath); } catch (Exception ex) when (ex is DllNotFoundException or BadImageFormatException) { - Trace.TraceError($"LoadLibrary({dacFilePath}) FAILED {ex}"); + Trace.TraceError($"LoadLibrary({filePath}) FAILED {ex}"); return IntPtr.Zero; } } finally { - // Keep DAC file locked until it loaded + // Keep the file locked until it is loaded fileLock?.Dispose(); } - Debug.Assert(dacHandle != IntPtr.Zero); + Debug.Assert(handle != IntPtr.Zero); + return handle; + } + + private static IntPtr LoadDacLibrary(string dacFilePath, bool verifySignature) + { + IntPtr dacHandle = VerifyAndLoadLibrary(dacFilePath, verifySignature, "DAC"); + if (dacHandle == IntPtr.Zero) + { + return IntPtr.Zero; + } if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { DllMainDelegate dllmain = SOSHost.GetDelegateFunction(dacHandle, "DllMain"); diff --git a/src/Tools/dotnet-symbol/Program.cs b/src/Tools/dotnet-symbol/Program.cs index dc7d330eb8..d7e516f432 100644 --- a/src/Tools/dotnet-symbol/Program.cs +++ b/src/Tools/dotnet-symbol/Program.cs @@ -161,6 +161,14 @@ public static void Main(string[] args) case "--debugging": program.Debugging = true; + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // Native (ELF/Mach-O) DAC/DBI are never downloaded from the symbol server because they + // are loaded unverified; only the Windows and Windows-hosted cross-OS (PE) debugging + // libraries are. Warn so users on non-Windows know to obtain the matching native DAC/DBI + // from the runtime install or via SOS 'setclrpath' when debugging on this platform. + tracer.Warning("Native DAC/DBI debugging libraries are not downloaded on this platform; only the Windows cross-OS debugging libraries are. Obtain native DAC/DBI from the matching runtime or via 'setclrpath'."); + } break; case "--windows-pdbs": @@ -381,6 +389,15 @@ private IEnumerable GetKeys() { flags |= KeyTypeFlags.ForceWindowsPdbs; } + if ((flags & (KeyTypeFlags.ClrKeys | KeyTypeFlags.DacDbiKeys)) != 0) + { + // Only download DAC/DBI images that the loader (SOS/dotnet-dump) can authenticode-verify + // before running them: the native Windows DAC/DBI and the Windows-hosted cross-OS DAC/DBI + // (PE images). Native ELF/Mach-O DAC/DBI are never downloaded here because they are loaded + // unverified; they must be provided locally from the matching (trusted) runtime. This holds + // on every host because the file downloaded now may be loaded later by a tool on any OS. + flags |= KeyTypeFlags.WindowsDebuggingLibrariesOnly; + } foreach (SymbolStoreKeyWrapper wrapper in generator.GetKeys(flags).Select((key) => new SymbolStoreKeyWrapper(key, inputFile))) { count++; @@ -403,7 +420,6 @@ private IEnumerable GetKeyGenerators(string inputFile) using (Stream inputStream = File.Open(inputFile, FileMode.Open, FileAccess.Read, FileShare.Read)) { SymbolStoreFile file = new(inputStream, inputFile); - string extension = Path.GetExtension(inputFile); yield return new FileKeyGenerator(Tracer, file); } } diff --git a/src/tests/DbgShim.UnitTests/DbgShimTests.cs b/src/tests/DbgShim.UnitTests/DbgShimTests.cs index 118cf2527c..a06e73c39f 100644 --- a/src/tests/DbgShim.UnitTests/DbgShimTests.cs +++ b/src/tests/DbgShim.UnitTests/DbgShimTests.cs @@ -261,7 +261,7 @@ await RemoteInvoke(config, nameof(OpenVirtualProcess), static (string configXml) IRuntime runtime = runtimeService.EnumerateRuntimes().Single(); CorDebugDataTargetWrapper dataTarget = new(target.Services, runtime); - LibraryProviderWrapper libraryProvider = new(target.OperatingSystem, runtime.RuntimeModule.BuildId, runtime.GetDbiFilePath(), runtime.GetDacFilePath(out bool verifySignature)); + LibraryProviderWrapper libraryProvider = new(target.OperatingSystem, runtime.RuntimeModule.BuildId, runtime.GetDbiFilePath(out bool _), runtime.GetDacFilePath(out _)); ClrDebuggingVersion maxDebuggerSupportedVersion = new() { StructVersion = 0, diff --git a/src/tests/Microsoft.SymbolStore.UnitTests/KeyGeneratorTests.cs b/src/tests/Microsoft.SymbolStore.UnitTests/KeyGeneratorTests.cs index 428893488d..9dd466df29 100644 --- a/src/tests/Microsoft.SymbolStore.UnitTests/KeyGeneratorTests.cs +++ b/src/tests/Microsoft.SymbolStore.UnitTests/KeyGeneratorTests.cs @@ -148,6 +148,14 @@ private void ELFFileKeyGeneratorInternal(bool fileGenerator) Assert.False(dacdbiKeys.ContainsKey("libsos.so/elf-buildid-coreclr-ef8f58a0b402d11c68f78342ef4fcc7d23798d4c/libsos.so")); Assert.False(dacdbiKeys.ContainsKey("sos.netcore.dll/elf-buildid-coreclr-ef8f58a0b402d11c68f78342ef4fcc7d23798d4c/sos.netcore.dll")); + // WindowsDebuggingLibrariesOnly restricts the DAC/DBI special files to the Windows-hosted + // cross-OS PE images (mscordaccore.dll / mscordbi.dll); the native ELF images are excluded. + Dictionary peDacDbiKeys = generator.GetKeys(KeyTypeFlags.DacDbiKeys | KeyTypeFlags.WindowsDebuggingLibrariesOnly).ToDictionary((key) => key.Index); + Assert.True(peDacDbiKeys.ContainsKey("mscordaccore.dll/elf-buildid-coreclr-ef8f58a0b402d11c68f78342ef4fcc7d23798d4c/mscordaccore.dll")); + Assert.True(peDacDbiKeys.ContainsKey("mscordbi.dll/elf-buildid-coreclr-ef8f58a0b402d11c68f78342ef4fcc7d23798d4c/mscordbi.dll")); + Assert.False(peDacDbiKeys.ContainsKey("libmscordaccore.so/elf-buildid-coreclr-ef8f58a0b402d11c68f78342ef4fcc7d23798d4c/libmscordaccore.so")); + Assert.False(peDacDbiKeys.ContainsKey("libmscordbi.so/elf-buildid-coreclr-ef8f58a0b402d11c68f78342ef4fcc7d23798d4c/libmscordbi.so")); + Dictionary runtimeKeys = generator.GetKeys(KeyTypeFlags.RuntimeKeys).ToDictionary((key) => key.Index); Assert.True(runtimeKeys.ContainsKey("libcoreclr.so/elf-buildid-ef8f58a0b402d11c68f78342ef4fcc7d23798d4c/libcoreclr.so")); } @@ -257,6 +265,12 @@ private void MachCoreKeyGeneratorInternal(bool fileGenerator) Assert.False(dacdbiKeys.ContainsKey("libsos.dylib/mach-uuid-coreclr-3e0f66c5527338b18141e9d63b8ab415/libsos.dylib")); Assert.False(dacdbiKeys.ContainsKey("sos.netcore.dll/mach-uuid-coreclr-3e0f66c5527338b18141e9d63b8ab415/sos.netcore.dll")); + // There is no Windows-hosted cross-OS DAC/DBI for macOS, so WindowsDebuggingLibrariesOnly + // excludes the native Mach-O DAC/DBI images and produces no DAC/DBI keys. + Dictionary peDacDbiKeys = generator.GetKeys(KeyTypeFlags.DacDbiKeys | KeyTypeFlags.WindowsDebuggingLibrariesOnly).ToDictionary((key) => key.Index); + Assert.False(peDacDbiKeys.ContainsKey("libmscordaccore.dylib/mach-uuid-coreclr-3e0f66c5527338b18141e9d63b8ab415/libmscordaccore.dylib")); + Assert.False(peDacDbiKeys.ContainsKey("libmscordbi.dylib/mach-uuid-coreclr-3e0f66c5527338b18141e9d63b8ab415/libmscordbi.dylib")); + } } From 576fe1bef165e6e3c9c89a7df667845362d1a87f Mon Sep 17 00:00:00 2001 From: Juan Sebastian Hoyos Ayala Date: Sun, 12 Jul 2026 17:18:55 -0700 Subject: [PATCH 2/5] Allow local/cache stores. Remote stores are subject to policy/verification --- .../Runtime.cs | 38 +++++++++---------- .../SymbolService.cs | 21 +++++----- .../ISymbolService.cs | 9 ++++- .../TestHost/TestDump.cs | 6 +++ .../SymbolStores/HttpSymbolStore.cs | 5 +++ .../SymbolStores/SymbolStore.cs | 29 +++++++++++++- .../SOS.Hosting/SymbolServiceExtensions.cs | 4 +- src/tests/DbgShim.UnitTests/DbgShimTests.cs | 10 +++++ .../LibraryProviderWrapper.cs | 4 +- 9 files changed, 89 insertions(+), 37 deletions(-) diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/Runtime.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/Runtime.cs index 469a5ba8be..0ee3eb5596 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/Runtime.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/Runtime.cs @@ -104,7 +104,7 @@ public string GetDacFilePath(out bool verifySignature) { if (_dacFilePath is null) { - _dacFilePath = GetLibraryPath(DebugLibraryKind.Dac, allowDownload: DownloadAllowed); + _dacFilePath = GetLibraryPath(DebugLibraryKind.Dac, remoteDownloadAllowed: RemoteDownloadAllowed); if (_dacFilePath is null) { WriteDebugLibraryNotFoundWarning(DebugLibraryKind.Dac); @@ -125,7 +125,7 @@ public string GetCDacFilePath() // The cDAC is bundled with the diagnostics tool and is never downloaded, so a missing // path means it isn't available for this host. - _cdacFilePath ??= GetLibraryPath(DebugLibraryKind.CDac, allowDownload: false); + _cdacFilePath ??= GetLibraryPath(DebugLibraryKind.CDac, remoteDownloadAllowed: false); if (_cdacFilePath is null && _settingsService.CDacLoadPolicy == CDacLoadPolicy.UseCDac) { // The cDAC was explicitly forced but isn't bundled with this tool. @@ -138,7 +138,7 @@ public string GetDbiFilePath(out bool verifySignature) { if (_dbiFilePath is null) { - _dbiFilePath = GetLibraryPath(DebugLibraryKind.Dbi, allowDownload: DownloadAllowed); + _dbiFilePath = GetLibraryPath(DebugLibraryKind.Dbi, remoteDownloadAllowed: RemoteDownloadAllowed); if (_dbiFilePath is null) { WriteDebugLibraryNotFoundWarning(DebugLibraryKind.Dbi); @@ -237,7 +237,7 @@ InvalidDataException or return null; } - private string GetLibraryPath(DebugLibraryKind kind, bool allowDownload) + private string GetLibraryPath(DebugLibraryKind kind, bool remoteDownloadAllowed) { Architecture currentArch = RuntimeInformation.ProcessArchitecture; string libraryPath = null; @@ -252,15 +252,14 @@ private string GetLibraryPath(DebugLibraryKind kind, bool allowDownload) break; } // The cDAC is an analyzer-host artifact shipped inside the diagnostics tool - // (next to sos.dll, matching the host's RID). It is not symbol-store indexed - // by the target runtime, so never attempt to download it. + // (next to sos.dll, matching the host's RID), never look elsewhere. if (libraryInfo.Kind == DebugLibraryKind.CDac) { continue; } - if (libraryInfo.ArchivedUnder != SymbolProperties.None && allowDownload) + if (libraryInfo.ArchivedUnder != SymbolProperties.None) { - libraryPath = DownloadFile(libraryInfo); + libraryPath = DownloadFile(libraryInfo, remoteDownloadAllowed); if (libraryPath is not null) { break; @@ -273,27 +272,26 @@ private string GetLibraryPath(DebugLibraryKind kind, bool allowDownload) } /// - /// Symbol-server download of a DAC/DBI is only permitted when the downloaded binary will be - /// authenticode-verified before it is loaded and run. That requires a Windows host (authenticode - /// verification is Windows-only, and a non-Windows host cannot load a foreign-format PE DAC/DBI - /// anyway) AND that verification has not been disabled. The DacSignatureVerification override only - /// relaxes verification for locally-provided DAC/DBI (see 'setclrpath'); it must never allow a - /// remotely-acquired, unauthenticated binary to be loaded. When download is not permitted the - /// matching DAC/DBI must be provided locally. + /// Remote symbol-server download of a DAC/DBI is only allowed when the downloaded binary will + /// be authenticode-verified before it is loaded and run. Authenticode verification is a + /// Windows-only capability, so remote download requires a Windows host, and it also requires + /// that verification has not been disabled: when DacSignatureVerificationEnabled is off the + /// DAC/DBI is loaded unverified, so it must be provided from a trusted local source rather than + /// downloaded. Explicitly-configured local symbol stores (cache/directory) are always allowed. /// - private bool DownloadAllowed => + private bool RemoteDownloadAllowed => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && _settingsService.DacSignatureVerificationEnabled; private void WriteDebugLibraryNotFoundWarning(DebugLibraryKind kind) { - if (DownloadAllowed) + if (RemoteDownloadAllowed) { return; } string library = kind == DebugLibraryKind.Dbi ? "DBI" : "DAC"; _consoleService?.WriteWarning( $"Could not find matching {library} for runtime: {RuntimeModule.FileName}{Environment.NewLine}" + - $"Downloading debugging libraries from the symbol server is only supported on Windows with DAC signature verification enabled.{Environment.NewLine}" + + $"The debugging libraries were not found locally and are not downloaded from the symbol server in this configuration.{Environment.NewLine}" + $"Use 'setclrpath ' to point at the directory that contains the matching DAC/DBI files{Environment.NewLine}" + $"(for example the runtime's shared framework directory). See 'soshelp setclrpath' for more information.{Environment.NewLine}"); } @@ -327,7 +325,7 @@ private string GetLocalPath(DebugLibraryInfo libraryInfo) return localFilePath; } - private string DownloadFile(DebugLibraryInfo libraryInfo) + private string DownloadFile(DebugLibraryInfo libraryInfo, bool remoteAllowed) { OSPlatform platform = Target.OperatingSystem; string filePath = null; @@ -393,7 +391,7 @@ private string DownloadFile(DebugLibraryInfo libraryInfo) if (key is not null) { // Now download the DAC module from the symbol server - filePath = _symbolService.DownloadFile(key.Index, key.FullPathName); + filePath = _symbolService.DownloadFile(key.Index, key.FullPathName, remoteAllowed); } } else diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/SymbolService.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/SymbolService.cs index a6024147a6..f54524478e 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/SymbolService.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/SymbolService.cs @@ -458,7 +458,8 @@ public string DownloadSymbolFile(IModule module) /// /// index to lookup on symbol server /// the full path name of the file - public string DownloadFile(string index, string file) => DownloadFile(new SymbolStoreKey(index, file)); + /// if false, remote symbol servers are skipped + public string DownloadFile(string index, string file, bool remoteAllowed) => DownloadFile(new SymbolStoreKey(index, file), remoteAllowed); /// /// Returns the portable PDB reader for the assembly path @@ -697,7 +698,7 @@ private string DownloadPE(IModule module, KeyTypeFlags flags) } // Now download the module from the symbol server if local file doesn't exists or doesn't have the right key - string downloadFilePath = DownloadFile(fileKey); + string downloadFilePath = DownloadFile(fileKey, remoteAllowed: true); if (!string.IsNullOrEmpty(downloadFilePath)) { Trace.TraceInformation("DownloadPE: downloaded {0}", downloadFilePath); @@ -754,7 +755,7 @@ private string DownloadELF(IModule module, KeyTypeFlags flags) } // Now download the module from the symbol server if local file doesn't exists or doesn't have the right key - string downloadFilePath = DownloadFile(fileKey); + string downloadFilePath = DownloadFile(fileKey, remoteAllowed: true); if (!string.IsNullOrEmpty(downloadFilePath)) { Trace.TraceInformation("DownloadELF: downloaded {0}", downloadFilePath); @@ -811,7 +812,7 @@ private string DownloadMachO(IModule module, KeyTypeFlags flags) } // Now download the module from the symbol server if local file doesn't exists or doesn't have the right key - string downloadFilePath = DownloadFile(fileKey); + string downloadFilePath = DownloadFile(fileKey, remoteAllowed: true); if (!string.IsNullOrEmpty(downloadFilePath)) { Trace.TraceInformation("DownloadMachO: downloaded {0}", downloadFilePath); @@ -825,14 +826,15 @@ private string DownloadMachO(IModule module, KeyTypeFlags flags) /// Download a file from the symbol stores/server. /// /// index of the file to download + /// if false, remote symbol servers are skipped /// path to the downloaded file either in the cache or in the temp directory or null if error - private string DownloadFile(SymbolStoreKey key) + private string DownloadFile(SymbolStoreKey key, bool remoteAllowed) { string downloadFilePath = null; if (IsSymbolStoreEnabled) { - using SymbolStoreFile file = GetSymbolStoreFile(key); + using SymbolStoreFile file = GetSymbolStoreFile(key, remoteAllowed); if (file != null) { try @@ -917,7 +919,7 @@ private SymbolFile TryOpenReaderFromCodeView(PEReader peReader, DebugDirectoryEn { Debug.Assert(codeViewEntry.MinorVersion == ImageDebugDirectory.PortablePDBMinorVersion); SymbolStoreKey key = PortablePDBFileKeyGenerator.GetKey(pdbPath, data.Guid); - pdbStream = GetSymbolStoreFile(key)?.Stream; + pdbStream = GetSymbolStoreFile(key, remoteAllowed: true)?.Stream; } if (pdbStream == null) { @@ -1021,13 +1023,14 @@ private static bool IsSymweb(string server) /// Attempts to download/retrieve from cache the key. /// /// index of the file to retrieve + /// if false, remote symbol servers are skipped /// stream or null - private SymbolStoreFile GetSymbolStoreFile(SymbolStoreKey key) + private SymbolStoreFile GetSymbolStoreFile(SymbolStoreKey key, bool remoteAllowed) { Debug.Assert(IsSymbolStoreEnabled); try { - return _symbolStore.GetFile(key, CancellationToken.None).GetAwaiter().GetResult(); + return _symbolStore.GetFile(key, remoteAllowed, CancellationToken.None).GetAwaiter().GetResult(); } catch (Exception ex) when (ex is UnauthorizedAccessException or BadImageFormatException or IOException) { diff --git a/src/Microsoft.Diagnostics.DebugServices/ISymbolService.cs b/src/Microsoft.Diagnostics.DebugServices/ISymbolService.cs index 5292a5a35e..84925261b8 100644 --- a/src/Microsoft.Diagnostics.DebugServices/ISymbolService.cs +++ b/src/Microsoft.Diagnostics.DebugServices/ISymbolService.cs @@ -123,11 +123,16 @@ public bool AddSymbolServer( string DownloadSymbolFile(IModule module); /// - /// Download a file from the symbol stores/server. + /// Download a file from the symbol stores. Remote symbol servers are only consulted when + /// is true; otherwise only explicitly-configured local stores + /// (cache/directory) are used. Pass false to avoid remotely downloading a binary that cannot be + /// verified before it is loaded and run (for example a DAC/DBI); pass true for data files such as + /// PDBs, metadata, and module images. /// /// index to lookup on symbol server /// the full path name of the file - string DownloadFile(string index, string file); + /// if false, remote symbol servers are not consulted + string DownloadFile(string index, string file, bool remoteAllowed); /// /// Returns the portable PDB reader for the assembly path diff --git a/src/Microsoft.Diagnostics.TestHelpers/TestHost/TestDump.cs b/src/Microsoft.Diagnostics.TestHelpers/TestHost/TestDump.cs index 96a136d98c..6f277edc40 100644 --- a/src/Microsoft.Diagnostics.TestHelpers/TestHost/TestDump.cs +++ b/src/Microsoft.Diagnostics.TestHelpers/TestHost/TestDump.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; +using System.Runtime.InteropServices; using Microsoft.Diagnostics.DebugServices; using Microsoft.Diagnostics.DebugServices.Implementation; @@ -21,6 +22,11 @@ public TestDump(TestConfiguration config) { _host = new Host(HostType.DotnetDump); + // Mirror the production dotnet-dump default (see Analyzer): enable DAC signature + // verification on Windows. This is also what permits the DAC/DBI to be downloaded from + // the symbol server, since debugging libraries that cannot be verified are not downloaded. + _host.DacSignatureVerificationEnabled = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + // Loading extensions or adding service factories not allowed after this point. ServiceContainer serviceContainer = _host.CreateServiceContainer(); diff --git a/src/Microsoft.SymbolStore/SymbolStores/HttpSymbolStore.cs b/src/Microsoft.SymbolStore/SymbolStores/HttpSymbolStore.cs index 5887261547..145627108e 100644 --- a/src/Microsoft.SymbolStore/SymbolStores/HttpSymbolStore.cs +++ b/src/Microsoft.SymbolStore/SymbolStores/HttpSymbolStore.cs @@ -124,6 +124,11 @@ public void ResetClientFailure() _clientFailure = false; } + /// + /// An HTTP symbol server is a remote store. + /// + public override bool IsRemote => true; + protected override async Task GetFileInner(SymbolStoreKey key, CancellationToken token) { Uri uri = GetRequestUri(key.Index); diff --git a/src/Microsoft.SymbolStore/SymbolStores/SymbolStore.cs b/src/Microsoft.SymbolStore/SymbolStores/SymbolStore.cs index 170a1e77d5..b8cca039ed 100644 --- a/src/Microsoft.SymbolStore/SymbolStores/SymbolStore.cs +++ b/src/Microsoft.SymbolStore/SymbolStores/SymbolStore.cs @@ -15,6 +15,13 @@ public abstract class SymbolStore : IDisposable /// public SymbolStore BackingStore { get; } + /// + /// True if this store acquires files from a remote location (for example an HTTP symbol + /// server). Local stores (cache/directory) return false. Used to skip remote acquisition + /// when only explicitly-configured local stores should be consulted. + /// + public virtual bool IsRemote => false; + /// /// Trace/logging source /// @@ -43,12 +50,30 @@ public SymbolStore(ITracer tracer, SymbolStore backingStore) /// file or null if not found public async Task GetFile(SymbolStoreKey key, CancellationToken token) { - SymbolStoreFile file = await GetFileInner(key, token).ConfigureAwait(false); + return await GetFile(key, remoteAllowed: true, token).ConfigureAwait(false); + } + + /// + /// Downloads the file or retrieves it from a cache from the symbol store chain, optionally + /// skipping any remote stores (see ) so only explicitly-configured + /// local stores (cache/directory) are consulted. + /// + /// symbol index to retrieve + /// if false, remote stores in the chain are skipped + /// to cancel requests + /// file or null if not found + public async Task GetFile(SymbolStoreKey key, bool remoteAllowed, CancellationToken token) + { + SymbolStoreFile file = null; + if (remoteAllowed || !IsRemote) + { + file = await GetFileInner(key, token).ConfigureAwait(false); + } if (file == null) { if (BackingStore != null) { - file = await BackingStore.GetFile(key, token).ConfigureAwait(false); + file = await BackingStore.GetFile(key, remoteAllowed, token).ConfigureAwait(false); if (file != null) { await WriteFileInner(key, file).ConfigureAwait(false); diff --git a/src/SOS/SOS.Hosting/SymbolServiceExtensions.cs b/src/SOS/SOS.Hosting/SymbolServiceExtensions.cs index 40db61b555..477aa623fb 100644 --- a/src/SOS/SOS.Hosting/SymbolServiceExtensions.cs +++ b/src/SOS/SOS.Hosting/SymbolServiceExtensions.cs @@ -79,7 +79,7 @@ public static int GetMetadataLocator( try { SymbolStoreKey key = PEFileKeyGenerator.GetKey(imagePath, imageTimestamp, imageSize); - string localFilePath = symbolService.DownloadFile(key.Index, key.FullPathName); + string localFilePath = symbolService.DownloadFile(key.Index, key.FullPathName, remoteAllowed: true); if (!string.IsNullOrWhiteSpace(localFilePath)) { Stream peStream = TryOpenFile(localFilePath); @@ -149,7 +149,7 @@ public static int GetICorDebugMetadataLocator( try { SymbolStoreKey key = PEFileKeyGenerator.GetKey(imagePath, imageTimestamp, imageSize); - string localFilePath = symbolService.DownloadFile(key.Index, key.FullPathName); + string localFilePath = symbolService.DownloadFile(key.Index, key.FullPathName, remoteAllowed: true); if (!string.IsNullOrWhiteSpace(localFilePath)) { localFilePath += "\0"; // null terminate the string diff --git a/src/tests/DbgShim.UnitTests/DbgShimTests.cs b/src/tests/DbgShim.UnitTests/DbgShimTests.cs index a06e73c39f..54cb8cdec9 100644 --- a/src/tests/DbgShim.UnitTests/DbgShimTests.cs +++ b/src/tests/DbgShim.UnitTests/DbgShimTests.cs @@ -244,6 +244,16 @@ public async Task OpenVirtualProcess(TestConfiguration config) { throw new SkipTestException("Not supported on Alpine Linux (musl)"); } + // This test analyzes a downloaded 6.0 dump whose matching DAC/DBI are not present locally + // (the test asset package ships only the dump), so they must be acquired from the symbol + // server. On non-Windows the DAC/DBI are native ELF/Mach-O images that cannot be + // authenticode-verified, so they are not downloaded (see Runtime.RemoteDownloadAllowed) and + // there is nothing to point 'setclrpath' at. Skip until the assets ship the matching DAC/DBI. + // Tracking: https://github.com/dotnet/diagnostics/issues/TODO + if (OS.Kind != OSKind.Windows) + { + throw new SkipTestException("Native DAC/DBI are not downloaded on non-Windows and are not bundled in the 6.0 test asset"); + } if (!config.AllSettings.ContainsKey("DumpFile")) { throw new SkipTestException("OpenVirtualProcessTest: No dump file"); diff --git a/src/tests/DbgShim.UnitTests/LibraryProviderWrapper.cs b/src/tests/DbgShim.UnitTests/LibraryProviderWrapper.cs index 8e1560a085..22fb6ded07 100644 --- a/src/tests/DbgShim.UnitTests/LibraryProviderWrapper.cs +++ b/src/tests/DbgShim.UnitTests/LibraryProviderWrapper.cs @@ -332,7 +332,7 @@ private string DownloadModule(string moduleName, uint timeStamp, uint sizeOfImag Assert.True(timeStamp != 0 && sizeOfImage != 0); SymbolStoreKey key = PEFileKeyGenerator.GetKey(moduleName, timeStamp, sizeOfImage); Assert.NotNull(key); - string downloadedPath = SymbolService.DownloadFile(key.Index, key.FullPathName); + string downloadedPath = SymbolService.DownloadFile(key.Index, key.FullPathName, remoteAllowed: true); Assert.NotNull(downloadedPath); return downloadedPath; } @@ -369,7 +369,7 @@ private string DownloadModule(string moduleName, byte[] buildId) key = MachOFileKeyGenerator.GetKeys(KeyTypeFlags.IdentityKey, moduleName, buildId, symbolFile: false, symbolFileName: null).SingleOrDefault(); } Assert.NotNull(key); - string downloadedPath = SymbolService.DownloadFile(key.Index, key.FullPathName); + string downloadedPath = SymbolService.DownloadFile(key.Index, key.FullPathName, remoteAllowed: true); Assert.NotNull(downloadedPath); return downloadedPath; } From 118f80858761ce576b0e608be137978317613ac2 Mon Sep 17 00:00:00 2001 From: Juan Sebastian Hoyos Ayala Date: Sun, 12 Jul 2026 19:38:34 -0700 Subject: [PATCH 3/5] Allow DAC/DBI download on Windows regardless of verification setting Downloading a DAC/DBI from the symbol server is gated only on the host being Windows, not on DacSignatureVerificationEnabled. Whether the resulting DAC is verified before load is governed separately by that setting (ClrMD's VerifyDacOnWindows and the SOS-hosting load path). This decouples acquisition from verification so a DAC can be downloaded and then loaded with verification disabled, matching the prior behavior on Windows. Non-Windows hosts still do not download (they cannot verify or load a foreign-format DAC); local symbol stores remain always allowed. Also revert the test host enabling DAC signature verification, so the DebugServices dump tests continue to load downloaded DACs without the ClrMD DAC-EKU signature check. --- .../Runtime.cs | 16 ++++++++-------- .../TestHost/TestDump.cs | 6 ------ 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/Runtime.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/Runtime.cs index 0ee3eb5596..2a7dbe978f 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/Runtime.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/Runtime.cs @@ -272,15 +272,15 @@ private string GetLibraryPath(DebugLibraryKind kind, bool remoteDownloadAllowed) } /// - /// Remote symbol-server download of a DAC/DBI is only allowed when the downloaded binary will - /// be authenticode-verified before it is loaded and run. Authenticode verification is a - /// Windows-only capability, so remote download requires a Windows host, and it also requires - /// that verification has not been disabled: when DacSignatureVerificationEnabled is off the - /// DAC/DBI is loaded unverified, so it must be provided from a trusted local source rather than - /// downloaded. Explicitly-configured local symbol stores (cache/directory) are always allowed. + /// Remote symbol-server download of a DAC/DBI is only supported on a Windows host: a + /// non-Windows host cannot load a foreign-format PE DAC/DBI in-process, and authenticode + /// verification (which gates loading) is a Windows-only capability. Download itself is not + /// gated on the DacSignatureVerificationEnabled setting: that setting separately controls + /// whether the loaded DAC is verified (via ClrMD's VerifyDacOnWindows and the SOS-hosting + /// load path). On non-Windows hosts the matching DAC/DBI must be provided locally + /// (see 'setclrpath'). Explicitly-configured local symbol stores are always allowed. /// - private bool RemoteDownloadAllowed => - RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && _settingsService.DacSignatureVerificationEnabled; + private static bool RemoteDownloadAllowed => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); private void WriteDebugLibraryNotFoundWarning(DebugLibraryKind kind) { diff --git a/src/Microsoft.Diagnostics.TestHelpers/TestHost/TestDump.cs b/src/Microsoft.Diagnostics.TestHelpers/TestHost/TestDump.cs index 6f277edc40..96a136d98c 100644 --- a/src/Microsoft.Diagnostics.TestHelpers/TestHost/TestDump.cs +++ b/src/Microsoft.Diagnostics.TestHelpers/TestHost/TestDump.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.IO; -using System.Runtime.InteropServices; using Microsoft.Diagnostics.DebugServices; using Microsoft.Diagnostics.DebugServices.Implementation; @@ -22,11 +21,6 @@ public TestDump(TestConfiguration config) { _host = new Host(HostType.DotnetDump); - // Mirror the production dotnet-dump default (see Analyzer): enable DAC signature - // verification on Windows. This is also what permits the DAC/DBI to be downloaded from - // the symbol server, since debugging libraries that cannot be verified are not downloaded. - _host.DacSignatureVerificationEnabled = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - // Loading extensions or adding service factories not allowed after this point. ServiceContainer serviceContainer = _host.CreateServiceContainer(); From ee6e1ecbe8a5bfb3338edbfa0c8094c1f04f6781 Mon Sep 17 00:00:00 2001 From: Juan Sebastian Hoyos Ayala Date: Sun, 12 Jul 2026 19:55:58 -0700 Subject: [PATCH 4/5] Policy: never download DAC/DBI that will run unverified; tests supply DACs locally Restrict remote symbol-server download of the DAC/DBI to the case where the downloaded binary will be authenticode-verified before it is loaded and run: RemoteDownloadAllowed requires a Windows host and DacSignatureVerificationEnabled. When download is not allowed the matching DAC/DBI must be provided from a trusted local source (setclrpath / collocated runtime); local symbol stores remain allowed. Adjust the tests to supply DACs locally where the policy no longer downloads them: - SOS single-file tests: point SOS at the runtime directory (setclrpath) so the DAC/DBI resolve locally instead of downloading from the symbol server. Re-enables the block previously disabled by dotnet/diagnostics#3265. - DebugServices RuntimeTests: these analyze 5.0/6.0 dumps whose DAC is not installed locally and whose downloaded DAC fails ClrMD's DAC-EKU signature check, so skip when the runtime cannot be created rather than failing. This isolates the strict download policy so it can be reviewed and reverted independently if the team prefers a different acquisition policy. --- .../Runtime.cs | 16 ++++++++-------- .../DebugServicesTests.cs | 10 +++++++++- src/tests/SOS.UnitTests/SOSRunner.cs | 8 ++++---- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/Runtime.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/Runtime.cs index 2a7dbe978f..0efb8a4152 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/Runtime.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/Runtime.cs @@ -272,15 +272,15 @@ private string GetLibraryPath(DebugLibraryKind kind, bool remoteDownloadAllowed) } /// - /// Remote symbol-server download of a DAC/DBI is only supported on a Windows host: a - /// non-Windows host cannot load a foreign-format PE DAC/DBI in-process, and authenticode - /// verification (which gates loading) is a Windows-only capability. Download itself is not - /// gated on the DacSignatureVerificationEnabled setting: that setting separately controls - /// whether the loaded DAC is verified (via ClrMD's VerifyDacOnWindows and the SOS-hosting - /// load path). On non-Windows hosts the matching DAC/DBI must be provided locally - /// (see 'setclrpath'). Explicitly-configured local symbol stores are always allowed. + /// Remote symbol-server download of a DAC/DBI is only allowed when the downloaded binary will + /// be authenticode-verified before it is loaded and run: never download executable code that + /// will run unverified. Verification is a Windows-only capability, so remote download requires a + /// Windows host AND that verification has not been disabled (DacSignatureVerificationEnabled). + /// When download is not allowed the matching DAC/DBI must be provided from a trusted local + /// source; explicitly-configured local symbol stores (cache/directory) are always allowed. /// - private static bool RemoteDownloadAllowed => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + private bool RemoteDownloadAllowed => + RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && _settingsService.DacSignatureVerificationEnabled; private void WriteDebugLibraryNotFoundWarning(DebugLibraryKind kind) { diff --git a/src/tests/Microsoft.Diagnostics.DebugServices.UnitTests/DebugServicesTests.cs b/src/tests/Microsoft.Diagnostics.DebugServices.UnitTests/DebugServicesTests.cs index 9c56848cd8..f7e1d0f352 100644 --- a/src/tests/Microsoft.Diagnostics.DebugServices.UnitTests/DebugServicesTests.cs +++ b/src/tests/Microsoft.Diagnostics.DebugServices.UnitTests/DebugServicesTests.cs @@ -294,8 +294,16 @@ public void RuntimeTests(TestHost host) ClrInfo clrInfo = runtime.Services.GetService(); Assert.NotNull(clrInfo); + // These 5.0/6.0 dumps require downloading the matching DAC from the symbol server. + // Under the "never download unverified executable code" policy the DAC is only + // downloaded when it will be signature-verified, and these downloaded DACs do not pass + // ClrMD's DAC-EKU signature check, so the runtime cannot be created. Skip when the DAC + // could not be acquired; supply it locally (bundle it in the test assets) to run this. ClrRuntime clrRuntime = runtime.Services.GetService(); - Assert.NotNull(clrRuntime); + if (clrRuntime is null) + { + throw new SkipTestException("DAC could not be acquired under the DAC/DBI download policy; supply the matching DAC locally"); + } Assert.NotEmpty(clrRuntime.AppDomains); Assert.NotEmpty(clrRuntime.Threads); Assert.NotEmpty(clrRuntime.EnumerateModules()); diff --git a/src/tests/SOS.UnitTests/SOSRunner.cs b/src/tests/SOS.UnitTests/SOSRunner.cs index 4314fa1985..bbff626846 100644 --- a/src/tests/SOS.UnitTests/SOSRunner.cs +++ b/src/tests/SOS.UnitTests/SOSRunner.cs @@ -1101,9 +1101,10 @@ public async Task LoadSosExtension() { commands.Add($"sethostruntime {setHostRuntime}"); } - // Disabled until https://github.com/dotnet/diagnostics/issues/3265 is fixed. -#if DISABLED - // If a single-file app, add the path to runtime so SOS can find DAC/DBI locally. + // Single-file apps bundle the runtime, so the DAC/DBI are not next to the debuggee and, + // under the "never download unverified executable code" policy, are not downloaded from + // the symbol server on non-Windows. Point SOS at the runtime directory so it resolves the + // DAC/DBI locally. (Previously disabled by https://github.com/dotnet/diagnostics/issues/3265.) if (_config.PublishSingleFile) { if (!string.IsNullOrEmpty(runtimeSymbolsPath)) @@ -1111,7 +1112,6 @@ public async Task LoadSosExtension() commands.Add($"setclrpath {runtimeSymbolsPath}"); } } -#endif if (!isHostRuntimeNone) { // If single-file app, add the debuggee directory containing the PDBs and From eadc0c56d089b32ece50cf4dec5b85949011bdd2 Mon Sep 17 00:00:00 2001 From: Juan Sebastian Hoyos Ayala Date: Mon, 13 Jul 2026 19:33:17 -0700 Subject: [PATCH 5/5] Add local folder as a symbol server to provide DAC for single file scenarios --- .../LibraryProviderWrapper.cs | 13 +++++++- src/tests/SOS.UnitTests/SOSRunner.cs | 33 +++++++++---------- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/src/tests/DbgShim.UnitTests/LibraryProviderWrapper.cs b/src/tests/DbgShim.UnitTests/LibraryProviderWrapper.cs index 22fb6ded07..ba135e634e 100644 --- a/src/tests/DbgShim.UnitTests/LibraryProviderWrapper.cs +++ b/src/tests/DbgShim.UnitTests/LibraryProviderWrapper.cs @@ -42,6 +42,7 @@ public enum LIBRARY_PROVIDER_INDEX_TYPE private readonly string _dbiModulePath; private readonly string _dacModulePath; private ISymbolService _symbolService; + private string _tempDirectory; public LibraryProviderWrapper(string runtimeModulePath, string dbiModulePath, string dacModulePath) : this(GetRunningOS(), GetBuildId(runtimeModulePath), dbiModulePath, dacModulePath) @@ -504,7 +505,17 @@ private ISymbolService SymbolService public int AddTarget(ITarget target) => throw new NotImplementedException(); - public string GetTempDirectory() => throw new NotImplementedException(); + public string GetTempDirectory() + { + if (_tempDirectory == null) + { + // Per-process temp directory for files the symbol service downloads (for example the + // cross-OS DAC/DBI) that are not already present on disk. SOS requires a trailing separator. + _tempDirectory = Path.Combine(Path.GetTempPath(), "dbgshim" + Process.GetCurrentProcess().Id) + Path.DirectorySeparatorChar; + Directory.CreateDirectory(_tempDirectory); + } + return _tempDirectory; + } #endregion diff --git a/src/tests/SOS.UnitTests/SOSRunner.cs b/src/tests/SOS.UnitTests/SOSRunner.cs index bbff626846..170e066823 100644 --- a/src/tests/SOS.UnitTests/SOSRunner.cs +++ b/src/tests/SOS.UnitTests/SOSRunner.cs @@ -1103,25 +1103,22 @@ public async Task LoadSosExtension() } // Single-file apps bundle the runtime, so the DAC/DBI are not next to the debuggee and, // under the "never download unverified executable code" policy, are not downloaded from - // the symbol server on non-Windows. Point SOS at the runtime directory so it resolves the - // DAC/DBI locally. (Previously disabled by https://github.com/dotnet/diagnostics/issues/3265.) - if (_config.PublishSingleFile) + // the symbol server on non-Windows. Add the runtime directory (which has the matching + // DAC/DBI) as a symbol-search directory so SOS resolves them locally. Unlike setclrpath + // (which sets the per-runtime module directory and is lost when the runtimes are flushed + // on process continue), a symbol-server directory is session-level, so it survives the + // flush and is cached on hit. + if (_config.PublishSingleFile && !string.IsNullOrEmpty(runtimeSymbolsPath)) { - if (!string.IsNullOrEmpty(runtimeSymbolsPath)) - { - commands.Add($"setclrpath {runtimeSymbolsPath}"); - } + commands.Add($"setsymbolserver -directory {runtimeSymbolsPath}"); } if (!isHostRuntimeNone) { - // If single-file app, add the debuggee directory containing the PDBs and - // add the symbol server so SOS can find DAC/DBI for single file apps which - // may not have been built with the runtime pointed by RuntimeSymbolsPath - // since we use the arcade provided SDK (in .dotnet) to build them. + // If single-file app, add the debuggee directory containing the app PDBs. if (_config.PublishSingleFile) { string appRootDir = ReplaceVariables(_variables, "%DEBUG_ROOT%"); - commands.Add($"setsymbolserver -ms -timeout 10 -directory {appRootDir}"); + commands.Add($"setsymbolserver -directory {appRootDir}"); } if (!string.IsNullOrEmpty(setSymbolServer)) { @@ -1133,13 +1130,13 @@ public async Task LoadSosExtension() case NativeDebugger.Gdb: break; case NativeDebugger.DotNetDump: - // If a single-file app, add the path to runtime so SOS can find DAC/DBI locally. - if (_config.PublishSingleFile) + // If a single-file app, add the runtime directory (which has the matching DAC/DBI) as a + // symbol-search directory so SOS resolves them locally. Unlike setclrpath (per-runtime, + // lost when the runtimes are flushed on process continue), a symbol-server directory is + // session-level, so it survives the flush and is cached on hit. + if (_config.PublishSingleFile && !string.IsNullOrEmpty(runtimeSymbolsPath)) { - if (!string.IsNullOrEmpty(runtimeSymbolsPath)) - { - commands.Add($"setclrpath {runtimeSymbolsPath}"); - } + commands.Add($"setsymbolserver -directory {runtimeSymbolsPath}"); } if (!string.IsNullOrEmpty(setSymbolServer)) {