diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/Runtime.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/Runtime.cs index 28c444bb62..0efb8a4152 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, remoteDownloadAllowed: RemoteDownloadAllowed); + 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, remoteDownloadAllowed: 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, remoteDownloadAllowed: RemoteDownloadAllowed); + 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 remoteDownloadAllowed) { Architecture currentArch = RuntimeInformation.ProcessArchitecture; string libraryPath = null; @@ -233,15 +252,14 @@ private string GetLibraryPath(DebugLibraryKind kind) 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) { - libraryPath = DownloadFile(libraryInfo); + libraryPath = DownloadFile(libraryInfo, remoteDownloadAllowed); if (libraryPath is not null) { break; @@ -253,6 +271,31 @@ private string GetLibraryPath(DebugLibraryKind kind) return libraryPath; } + /// + /// 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 bool RemoteDownloadAllowed => + RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && _settingsService.DacSignatureVerificationEnabled; + + private void WriteDebugLibraryNotFoundWarning(DebugLibraryKind kind) + { + if (RemoteDownloadAllowed) + { + return; + } + string library = kind == DebugLibraryKind.Dbi ? "DBI" : "DAC"; + _consoleService?.WriteWarning( + $"Could not find matching {library} for runtime: {RuntimeModule.FileName}{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}"); + } + private string GetLocalPath(DebugLibraryInfo libraryInfo) { string localFilePath; @@ -282,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; @@ -348,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 @@ -408,7 +451,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 +462,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.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/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.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.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/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/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/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/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..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"); @@ -261,7 +271,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/DbgShim.UnitTests/LibraryProviderWrapper.cs b/src/tests/DbgShim.UnitTests/LibraryProviderWrapper.cs index 8e1560a085..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) @@ -332,7 +333,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 +370,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; } @@ -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/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/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")); + } } diff --git a/src/tests/SOS.UnitTests/SOSRunner.cs b/src/tests/SOS.UnitTests/SOSRunner.cs index 4314fa1985..170e066823 100644 --- a/src/tests/SOS.UnitTests/SOSRunner.cs +++ b/src/tests/SOS.UnitTests/SOSRunner.cs @@ -1101,27 +1101,24 @@ 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. - if (_config.PublishSingleFile) + // 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. 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}"); } -#endif 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)) {