From 51c1f2beee2ffae2f8341f996c7fcafc05104a42 Mon Sep 17 00:00:00 2001 From: Juan Hoyos <19413848+hoyosjs@users.noreply.github.com> Date: Tue, 9 Jun 2026 21:23:06 -0700 Subject: [PATCH 1/7] Basic packaging changes --- eng/CdacPackageItems.props | 24 ------ eng/InstallNativePackages.targets | 8 +- eng/Versions.props | 8 ++ .../GenerateManifest/Directory.Build.props | 13 ++++ .../GenerateManifest/Directory.Build.targets | 3 + src/SOS/SOS.Package/SOS.Package.csproj | 2 +- src/SOS/SOS.Package/SOS.Symbol.Package.csproj | 60 ++++++++++----- .../pkg/Microsoft.Diagnostics.DbgShim.props | 11 +++ src/sos-packaging.props | 74 ++++++++++++++----- 9 files changed, 137 insertions(+), 66 deletions(-) delete mode 100644 eng/CdacPackageItems.props create mode 100644 src/SOS/SOS.Package/GenerateManifest/Directory.Build.props create mode 100644 src/SOS/SOS.Package/GenerateManifest/Directory.Build.targets diff --git a/eng/CdacPackageItems.props b/eng/CdacPackageItems.props deleted file mode 100644 index 65e548d057..0000000000 --- a/eng/CdacPackageItems.props +++ /dev/null @@ -1,24 +0,0 @@ - - - - <_cdacPackageVersion Condition="'$(TargetRid)' == 'win-x64'">$(runtimewinx64MicrosoftDotNetCdacTransportVersion) - <_cdacPackageVersion Condition="'$(TargetRid)' == 'win-arm64'">$(runtimewinarm64MicrosoftDotNetCdacTransportVersion) - <_cdacPackageVersion Condition="'$(TargetRid)' == 'linux-x64'">$(runtimelinuxx64MicrosoftDotNetCdacTransportVersion) - <_cdacPackageVersion Condition="'$(TargetRid)' == 'linux-arm64'">$(runtimelinuxarm64MicrosoftDotNetCdacTransportVersion) - <_cdacPackageVersion Condition="'$(TargetRid)' == 'osx-x64'">$(runtimeosxx64MicrosoftDotNetCdacTransportVersion) - <_cdacPackageVersion Condition="'$(TargetRid)' == 'osx-arm64'">$(runtimeosxarm64MicrosoftDotNetCdacTransportVersion) - - - - - - - - - - \ No newline at end of file diff --git a/eng/InstallNativePackages.targets b/eng/InstallNativePackages.targets index f8804fd7ab..938fc1d743 100644 --- a/eng/InstallNativePackages.targets +++ b/eng/InstallNativePackages.targets @@ -21,10 +21,14 @@ - + + + + diff --git a/eng/Versions.props b/eng/Versions.props index 7f2d6dfefb..c39d6d3c7d 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -14,6 +14,14 @@ true true + + true + + diff --git a/src/SOS/SOS.Package/GenerateManifest/Directory.Build.targets b/src/SOS/SOS.Package/GenerateManifest/Directory.Build.targets new file mode 100644 index 0000000000..333e562a8b --- /dev/null +++ b/src/SOS/SOS.Package/GenerateManifest/Directory.Build.targets @@ -0,0 +1,3 @@ + + + diff --git a/src/SOS/SOS.Package/SOS.Package.csproj b/src/SOS/SOS.Package/SOS.Package.csproj index f9688559c9..121655af78 100644 --- a/src/SOS/SOS.Package/SOS.Package.csproj +++ b/src/SOS/SOS.Package/SOS.Package.csproj @@ -41,7 +41,7 @@ - + diff --git a/src/SOS/SOS.Package/SOS.Symbol.Package.csproj b/src/SOS/SOS.Package/SOS.Symbol.Package.csproj index dbb9daae4b..eb76c10f46 100644 --- a/src/SOS/SOS.Package/SOS.Symbol.Package.csproj +++ b/src/SOS/SOS.Package/SOS.Symbol.Package.csproj @@ -11,6 +11,12 @@ tools true false + + + <_PackAllTargetRids Condition="'$(SingleTargetRidPackage)' != 'true'">true @@ -19,65 +25,79 @@ $(SOSPackagePathPrefix)/lib - + $(SOSPackagePathPrefix)/win-x64 - - - - + $(SOSPackagePathPrefix)/win-x86 - + $(SOSPackagePathPrefix)/win-arm64 - + $(SOSPackagePathPrefix)/linux-x64 - + $(SOSPackagePathPrefix)/linux-x64 - + $(SOSPackagePathPrefix)/linux-musl-x64 - + $(SOSPackagePathPrefix)/linux-musl-x64 - + $(SOSPackagePathPrefix)/linux-arm - + $(SOSPackagePathPrefix)/linux-arm - + $(SOSPackagePathPrefix)/linux-arm64 - + $(SOSPackagePathPrefix)/linux-arm64 - + $(SOSPackagePathPrefix)/linux-musl-arm64 - + $(SOSPackagePathPrefix)/linux-musl-arm64 - + $(SOSPackagePathPrefix)/osx-x64 - + $(SOSPackagePathPrefix)/osx-x64 - + $(SOSPackagePathPrefix)/osx-arm64 - + $(SOSPackagePathPrefix)/osx-arm64 diff --git a/src/dbgshim/pkg/Microsoft.Diagnostics.DbgShim.props b/src/dbgshim/pkg/Microsoft.Diagnostics.DbgShim.props index 9c22d95636..ed22809dbd 100644 --- a/src/dbgshim/pkg/Microsoft.Diagnostics.DbgShim.props +++ b/src/dbgshim/pkg/Microsoft.Diagnostics.DbgShim.props @@ -18,6 +18,17 @@ + + diff --git a/src/sos-packaging.props b/src/sos-packaging.props index 95e0d090ef..04b151341f 100644 --- a/src/sos-packaging.props +++ b/src/sos-packaging.props @@ -1,6 +1,17 @@ @@ -8,56 +19,81 @@ true - - + + + - + - + + + + - + + + - + + - - - + + + + + + + + - + + - - - + + + + + - - - + + + + + + - + + + - + + + $([MSBuild]::ValueOrDefault('%(FullPath)', '').Replace('linux-musl','linux')) From 2b5f735de5f173bd7f889051fe38e29ea121cf5d Mon Sep 17 00:00:00 2001 From: Juan Hoyos <19413848+hoyosjs@users.noreply.github.com> Date: Wed, 10 Jun 2026 00:50:51 -0700 Subject: [PATCH 2/7] Change cDAC loading paths and policies --- .../Host.cs | 4 +- .../Runtime.cs | 105 +++++++-- .../ServiceManager.cs | 3 +- .../IHostAssetResolver.cs | 29 +++ .../IRuntime.cs | 8 +- .../ISettingsService.cs | 17 +- .../Host/CommandFormatHelpers.cs | 3 +- .../Host/RuntimesCommand.cs | 24 +- src/SOS/SOS.Extensions/HostServices.cs | 2 +- src/SOS/SOS.Hosting/HostAssetResolver.cs | 61 +++++ src/SOS/SOS.Hosting/RuntimeWrapper.cs | 54 +++-- src/SOS/SOS.Hosting/SOS.Hosting.csproj | 1 - src/SOS/SOS.Hosting/SOSLibrary.cs | 52 +++-- src/SOS/SOS.Hosting/SOSPackageLayout.cs | 107 +++++++++ src/SOS/SOS.InstallHelper/InstallHelper.cs | 46 +--- .../SOS.InstallHelper.csproj | 4 + src/SOS/Strike/clrma/managedanalysis.cpp | 6 +- src/SOS/Strike/platform/runtimeimpl.cpp | 219 +++++++++++++++--- src/SOS/Strike/platform/runtimeimpl.h | 15 ++ src/SOS/Strike/util.cpp | 6 +- 20 files changed, 586 insertions(+), 180 deletions(-) create mode 100644 src/Microsoft.Diagnostics.DebugServices/IHostAssetResolver.cs create mode 100644 src/SOS/SOS.Hosting/HostAssetResolver.cs create mode 100644 src/SOS/SOS.Hosting/SOSPackageLayout.cs diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/Host.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/Host.cs index 212714d6b5..0b066c5309 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/Host.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/Host.cs @@ -118,9 +118,7 @@ public string GetTempDirectory() public virtual bool DacSignatureVerificationEnabled { get; set; } - public bool UseContractReader { get; set; } - - public bool ForceUseContractReader { get; set; } + public bool? UseCDac { get; set; } #endregion diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/Runtime.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/Runtime.cs index 05db19b715..ab236ab87a 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/Runtime.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/Runtime.cs @@ -20,6 +20,7 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation public class Runtime : IRuntime, IDisposable { private readonly ClrInfo _clrInfo; + private readonly IHostAssetResolver _hostAssetResolver; private readonly ISettingsService _settingsService; private readonly ISymbolService _symbolService; private Version _runtimeVersion; @@ -36,6 +37,7 @@ public Runtime(IServiceProvider services, int id, ClrInfo clrInfo) Target = services.GetService() ?? throw new DiagnosticsException("Dump or live session target required"); Id = id; _clrInfo = clrInfo ?? throw new ArgumentNullException(nameof(clrInfo)); + _hostAssetResolver = services.GetService() ?? throw new ArgumentException("IHostAssetResolver required"); _settingsService = services.GetService() ?? throw new ArgumentException("ISettingsService required"); _symbolService = services.GetService() ?? throw new ArgumentException("ISymbolService required"); @@ -100,27 +102,8 @@ public Version RuntimeVersion } } - public string GetCDacFilePath() - { - if (_cdacFilePath is null) - { - if (_settingsService.UseContractReader || _settingsService.ForceUseContractReader) - { - _cdacFilePath = GetLibraryPath(DebugLibraryKind.CDac); - } - } - return _cdacFilePath; - } - public string GetDacFilePath(out bool verifySignature) { - if (_settingsService.ForceUseContractReader) - { - // Don't verify signature when using the CDAC and don't change the cached value - // because it only applies to the regular DAC in _dacFilePath. - verifySignature = false; - return GetCDacFilePath(); - } if (_dacFilePath is null) { _dacFilePath = GetLibraryPath(DebugLibraryKind.Dac); @@ -133,6 +116,26 @@ public string GetDacFilePath(out bool verifySignature) return _dacFilePath; } + public string GetCDacFilePath() + { + // ShouldUseCDac() evaluates the cDAC loading policy. When it returns false the caller + // uses the in-box DAC from GetDacFilePath instead. + if (!ShouldUseCDac()) + { + return null; + } + + // 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); + if (_cdacFilePath is null && _settingsService.UseCDac == true) + { + // The cDAC was explicitly forced but isn't bundled with this tool. + throw new DiagnosticsException($"The cDAC was explicitly requested but no matching cDAC is available for this runtime: {RuntimeModule.FileName}"); + } + return _cdacFilePath; + } + public string GetDbiFilePath() { _dbiFilePath ??= GetLibraryPath(DebugLibraryKind.Dbi); @@ -141,12 +144,59 @@ public string GetDbiFilePath() #endregion + /// + /// The minimum runtime major version that supports the cDAC. + /// + private const int MinCDacRuntimeMajorVersion = 11; + + /// + /// Evaluates the cDAC loading policy for this runtime. This is the single place that + /// decides whether the diagnostics tool should load the cDAC itself in place of the + /// in-box DAC, based on the setting and the + /// target runtime version. + /// + private bool ShouldUseCDac() + { + return _settingsService.UseCDac switch + { + false => false, // Never load the cDAC. + true => true, // Always use the cDAC, regardless of the runtime version. Availability is + // checked by the caller (a missing forced cDAC is a hard error). + _ => ShouldUseCDacByDefault(), // No explicit setting: evaluate the default policy. + }; + } + + /// + /// The default cDAC policy used when is not set. + /// + private bool ShouldUseCDacByDefault() + { + // When DOTNET_ENABLE_CDAC is requested, the in-box (legacy) DAC loads and drives the + // cDAC contract reader itself, including its own dac-vs-cdac fallback/comparison + // (see CDAC_NO_FALLBACK). Defer to that mechanism rather than loading the cDAC + // directly so those scenarios (for example, the runtime's cDAC test pipeline that + // points at a freshly built cDAC via -liveruntimedir) keep working. + if (Environment.GetEnvironmentVariable("DOTNET_ENABLE_CDAC") == "1" + || Environment.GetEnvironmentVariable("COMPlus_ENABLE_CDAC") == "1") + { + return false; + } + + // Default policy: use the cDAC only for runtimes that support it. This needs to be + // changed to consider native AOT and singlefile. This is a dummy policy for work + // we will offload to dbgshim. + return RuntimeVersion is not null && RuntimeVersion.Major >= MinCDacRuntimeMajorVersion; + } + /// /// Create ClrRuntime instance /// private ClrRuntime CreateRuntime() { - string dacFilePath = GetDacFilePath(out _); + // Prefer the cDAC for the ClrMD data-access path when policy selects it; fall back to the in-box DAC. + // We ignore the dac verification param since it's already set as part of the CLRMD DataTarget creation + // now (it's a global setting to the session). + string dacFilePath = GetCDacFilePath() ?? GetDacFilePath(out _); if (dacFilePath is not null) { Trace.TraceInformation($"Creating ClrRuntime #{Id} {dacFilePath}"); @@ -187,6 +237,13 @@ 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. + if (libraryInfo.Kind == DebugLibraryKind.CDac) + { + continue; + } if (libraryInfo.ArchivedUnder != SymbolProperties.None) { libraryPath = DownloadFile(libraryInfo); @@ -206,7 +263,11 @@ private string GetLocalPath(DebugLibraryInfo libraryInfo) string localFilePath; if (libraryInfo.Kind == DebugLibraryKind.CDac) { - localFilePath = libraryInfo.FileName; + // The cDAC ships next to the native sos module. Ask the host asset resolver where it + // is rather than reasoning about layouts here (ClrMD's DebuggingLibraries entry points + // at the managed-assembly base directory, so it is ignored). The shared existence + // check below verifies the path, so the in-box DAC is used when the cDAC isn't bundled. + localFilePath = _hostAssetResolver?.GetCDacPath(); } else { @@ -219,7 +280,7 @@ private string GetLocalPath(DebugLibraryInfo libraryInfo) localFilePath = Path.Combine(Path.GetDirectoryName(RuntimeModule.FileName), Path.GetFileName(libraryInfo.FileName)); } } - if (!File.Exists(localFilePath)) + if (localFilePath is null || !File.Exists(localFilePath)) { localFilePath = null; } diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/ServiceManager.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/ServiceManager.cs index ae69ea69ef..2e42bfc3f5 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/ServiceManager.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/ServiceManager.cs @@ -360,8 +360,7 @@ private sealed class ExtensionLoadContext : AssemblyLoadContext "Microsoft.FileFormats", "Microsoft.SymbolStore", "SOS.Extensions", - "SOS.Hosting", - "SOS.InstallHelper" + "SOS.Hosting" }; private static readonly string _defaultAssembliesPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); diff --git a/src/Microsoft.Diagnostics.DebugServices/IHostAssetResolver.cs b/src/Microsoft.Diagnostics.DebugServices/IHostAssetResolver.cs new file mode 100644 index 0000000000..deb660a6b8 --- /dev/null +++ b/src/Microsoft.Diagnostics.DebugServices/IHostAssetResolver.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.DebugServices +{ + /// + /// Answers questions about where the host's assets live — the native binaries the host ships + /// (the native sos module, the cDAC, DiaSymReader, …). The directory of those assets is + /// host-specific: a native debugger host supplies it (the SOS hosting layer feeds it from the + /// host's sos module location), while in-process hosts (dotnet-dump) derive it from the tool's + /// package layout. Runtimes and other services query this resolver instead of reasoning about + /// layouts themselves. + /// + public interface IHostAssetResolver + { + /// + /// The directory containing the host's native binaries (the native sos module and the + /// cDAC that ships next to it). + /// + string NativeBinariesDirectory { get; } + + /// + /// The full path to where the cDAC native library (mscordaccore_universal) ships for the + /// current host (next to the native sos module). The path is not probed; the caller is + /// expected to check existence (the cDAC is not bundled in, for example, release builds). + /// + string GetCDacPath(); + } +} diff --git a/src/Microsoft.Diagnostics.DebugServices/IRuntime.cs b/src/Microsoft.Diagnostics.DebugServices/IRuntime.cs index 6bf7f05ef6..9400736d17 100644 --- a/src/Microsoft.Diagnostics.DebugServices/IRuntime.cs +++ b/src/Microsoft.Diagnostics.DebugServices/IRuntime.cs @@ -60,13 +60,15 @@ public interface IRuntime string RuntimeModuleDirectory { get; set; } /// - /// Returns the DAC file path + /// Returns the DAC file path to use for this runtime. /// - /// returns if the DAC signature should be verified + /// returns whether the returned DAC requires signature verification. string GetDacFilePath(out bool verifySignature); /// - /// Returns the CDac file path if enabled by global settings + /// Returns the cDAC (mscordaccore_universal) file path to use for this runtime, or null + /// when the cDAC should not be used (policy disabled or unsupported runtime) or isn't + /// available. /// string GetCDacFilePath(); diff --git a/src/Microsoft.Diagnostics.DebugServices/ISettingsService.cs b/src/Microsoft.Diagnostics.DebugServices/ISettingsService.cs index 60ad98c338..2e6c42bc32 100644 --- a/src/Microsoft.Diagnostics.DebugServices/ISettingsService.cs +++ b/src/Microsoft.Diagnostics.DebugServices/ISettingsService.cs @@ -17,13 +17,16 @@ public interface ISettingsService bool DacSignatureVerificationEnabled { get; set; } /// - /// If true, uses the CDAC contract reader if available. + /// Controls whether the cDAC is used in place of the in-box DAC: + /// + /// null (default): evaluate policy and fall back. The cDAC is used + /// when the target runtime supports it and a matching cDAC is available next + /// to the diagnostics tool; otherwise the in-box DAC is used. + /// true: always use the cDAC. Runtime construction fails if no + /// matching cDAC is available. + /// false: always use the in-box DAC. The cDAC is never loaded. + /// /// - bool UseContractReader { get; set; } - - /// - /// If true, always use the CDAC contract reader even when not requested - /// - bool ForceUseContractReader { get; set; } + bool? UseCDac { get; set; } } } diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/Host/CommandFormatHelpers.cs b/src/Microsoft.Diagnostics.ExtensionCommands/Host/CommandFormatHelpers.cs index 7b55377c45..15c3ef2860 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/Host/CommandFormatHelpers.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/Host/CommandFormatHelpers.cs @@ -20,8 +20,7 @@ public static void DisplaySettingService(this CommandBase command) { ISettingsService settingsService = command.Services.GetService() ?? throw new DiagnosticsException("Settings service required"); command.Console.WriteLine("Settings:"); - command.Console.WriteLine($"-> Use CDAC contract reader: {settingsService.UseContractReader}"); - command.Console.WriteLine($"-> Force use CDAC contract reader: {settingsService.ForceUseContractReader}"); + command.Console.WriteLine($"-> Use cDAC: {settingsService.UseCDac switch { true => "true", false => "false", _ => "policy (default)" }}"); command.Console.WriteLine($"-> DAC signature verification check enabled: {settingsService.DacSignatureVerificationEnabled}"); } diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/Host/RuntimesCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/Host/RuntimesCommand.cs index 83c70cef29..53b1aa49f2 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/Host/RuntimesCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/Host/RuntimesCommand.cs @@ -36,11 +36,8 @@ public class RuntimesCommand : CommandBase [Option(Name = "--all", Aliases = new string[] { "-a" }, Help = "Forces all runtimes to be enumerated.")] public bool All { get; set; } - [Option(Name = "--usecdac", Help = "Use the CDAC if available and requested (true/false).")] - public bool? UseContractReader { get; set; } - - [Option(Name = "--forceusecdac", Help = "Always use the CDAC (true/false).")] - public bool? ForceUseContractReader { get; set; } + [Option(Name = "--usecdac", Help = "Controls cDAC usage: true (always), false (never), or policy (default: use for supported runtimes when bundled).")] + public string UseCDac { get; set; } [Option(Name = "--DacSignatureVerification", Aliases = new string[] { "-v" }, Help = "Enforce the proper DAC certificate signing when loaded (true/false).")] public bool? DacSignatureVerification { get; set; } @@ -53,16 +50,15 @@ public override void Invoke() } bool flush = false; - if (UseContractReader.HasValue) - { - SettingsService.UseContractReader = UseContractReader.Value; - flush = true; - } - - if (ForceUseContractReader.HasValue) + if (UseCDac is not null) { - SettingsService.UseContractReader = ForceUseContractReader.Value; - SettingsService.ForceUseContractReader = ForceUseContractReader.Value; + SettingsService.UseCDac = UseCDac.ToLowerInvariant() switch + { + "true" => true, + "false" => false, + "policy" or "default" => (bool?)null, + _ => throw new DiagnosticsException($"Invalid --usecdac value '{UseCDac}'. Expected true, false, or policy."), + }; flush = true; } diff --git a/src/SOS/SOS.Extensions/HostServices.cs b/src/SOS/SOS.Extensions/HostServices.cs index be9e5f89ee..af4c6dc5e9 100644 --- a/src/SOS/SOS.Extensions/HostServices.cs +++ b/src/SOS/SOS.Extensions/HostServices.cs @@ -22,7 +22,7 @@ namespace SOS.Extensions /// /// The extension services Wrapper the native hosts are given /// - public sealed unsafe class HostServices : COMCallableIUnknown, SOSLibrary.ISOSModule + public sealed class HostServices : COMCallableIUnknown, SOSLibrary.ISOSModule { private static readonly Guid IID_IHostServices = new("27B2CB8D-BDEE-4CBD-B6EF-75880D76D46F"); diff --git a/src/SOS/SOS.Hosting/HostAssetResolver.cs b/src/SOS/SOS.Hosting/HostAssetResolver.cs new file mode 100644 index 0000000000..968a105078 --- /dev/null +++ b/src/SOS/SOS.Hosting/HostAssetResolver.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Runtime.InteropServices; +using Microsoft.Diagnostics.DebugServices; +using Microsoft.Diagnostics.Shared; + +namespace SOS.Hosting; + +/// +/// The host-registered . Resolves assets (native binaries, the +/// bundled cDAC) to the directory the host actually loaded sos from. When a native debugger host +/// supplies its sos module location () that location wins; +/// otherwise the host loaded sos itself (e.g. dotnet-dump) and the directory comes from this +/// tool's package layout. Either way the cDAC ships in that same directory. +/// +public sealed class HostAssetResolver : IHostAssetResolver +{ + private HostAssetResolver(string nativeBinariesDirectory) + { + NativeBinariesDirectory = nativeBinariesDirectory; + } + + public string NativeBinariesDirectory { get; } + + private static readonly string s_cDACBinaryName = ComputeCDacHostBinaryName(); + + private static string ComputeCDacHostBinaryName() + { + const string baseName = "mscordaccore_universal"; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return baseName + ".dll"; + } + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return "lib" + baseName + ".dylib"; + } + return "lib" + baseName + ".so"; + } + + public string GetCDacPath() + { + return Path.Combine(NativeBinariesDirectory, s_cDACBinaryName); + } + + [ServiceExport(Scope = ServiceScope.Global)] + public static IHostAssetResolver Create([ServiceImport(Optional = true)] SOSLibrary.ISOSModule sosModule) + { + // A native debugger host that loaded sos for us is the authoritative source for where + // the native binaries (and the cDAC next to them) live. Otherwise fall back to this + // tool's package layout. + string nativeBinariesDirectory = sosModule?.SOSPath; + if (string.IsNullOrEmpty(nativeBinariesDirectory)) + { + nativeBinariesDirectory = SOSPackageLayout.GetNativeBinariesDirectory(); + } + return new HostAssetResolver(nativeBinariesDirectory); + } +} diff --git a/src/SOS/SOS.Hosting/RuntimeWrapper.cs b/src/SOS/SOS.Hosting/RuntimeWrapper.cs index ac37d23e01..708161c99d 100644 --- a/src/SOS/SOS.Hosting/RuntimeWrapper.cs +++ b/src/SOS/SOS.Hosting/RuntimeWrapper.cs @@ -238,22 +238,22 @@ private int GetClrDataProcess( return HResult.E_INVALIDARG; } *ppClrDataProcess = IntPtr.Zero; - if ((flags & ClrDataProcessFlags.UseCDac) != 0) + // Prefer the cDAC for the data-access (IXCLRDataProcess) path when the runtime policy + // selects it (GetCDacFilePath returns non-null); fall back to the in-box DAC otherwise. + // The ICorDebug/DBI path (CreateCorDebugProcess) always uses the in-box DAC. The flags + // parameter is retained for the native IRuntime contract but no longer consulted here. + if (_cdacDataProcess == IntPtr.Zero) { - if (_cdacDataProcess == IntPtr.Zero) + try { - try - { - _cdacDataProcess = CreateClrDataProcess(GetCDacHandle()); - } - catch (Exception ex) - { - Trace.TraceError(ex.ToString()); - } + _cdacDataProcess = CreateClrDataProcess(GetCDacHandle()); + } + catch (Exception ex) + { + Trace.TraceError(ex.ToString()); } - *ppClrDataProcess = _cdacDataProcess; } - // Fallback to regular DAC instance if CDac isn't enabled or there where errors creating the instance + *ppClrDataProcess = _cdacDataProcess; if (*ppClrDataProcess == IntPtr.Zero) { if (_clrDataProcess == IntPtr.Zero) @@ -518,7 +518,13 @@ private IntPtr GetDacHandle() { if (_dacHandle == IntPtr.Zero) { - _dacHandle = GetDacHandle(useCDac: false); + string dacFilePath = _runtime.GetDacFilePath(out bool verifySignature); + if (dacFilePath == null) + { + Trace.TraceError($"Could not find matching DAC for this runtime: {_runtime.RuntimeModule.FileName}"); + return IntPtr.Zero; + } + _dacHandle = LoadDacLibrary(dacFilePath, verifySignature); } return _dacHandle; } @@ -527,27 +533,27 @@ private IntPtr GetCDacHandle() { if (_cdacHandle == IntPtr.Zero) { - _cdacHandle = GetDacHandle(useCDac: true); + string cdacFilePath = _runtime.GetCDacFilePath(); + if (cdacFilePath == null) + { + // The cDAC isn't selected for this runtime; the caller falls back to the in-box DAC. + return IntPtr.Zero; + } + // The cDAC ships in the signed tool install directory, so it is never signature-verified. + _cdacHandle = LoadDacLibrary(cdacFilePath, verifySignature: false); } return _cdacHandle; } - private IntPtr GetDacHandle(bool useCDac) + private static IntPtr LoadDacLibrary(string dacFilePath, bool verifySignature) { - bool verifySignature = false; - string dacFilePath = useCDac ? _runtime.GetCDacFilePath() : _runtime.GetDacFilePath(out verifySignature); - if (dacFilePath == null) - { - Trace.TraceError($"Could not find matching DAC {dacFilePath ?? ""} {useCDac} for this runtime: {_runtime.RuntimeModule.FileName}"); - return IntPtr.Zero; - } IntPtr dacHandle = IntPtr.Zero; IDisposable fileLock = null; try { if (verifySignature) { - Trace.TraceInformation($"Verifying DAC signing and cert {dacFilePath} {useCDac}"); + Trace.TraceInformation($"Verifying DAC signing and cert {dacFilePath}"); // Check if the DAC cert is valid before loading if (!AuthenticodeUtil.VerifyDacDll(dacFilePath, out fileLock)) @@ -561,7 +567,7 @@ private IntPtr GetDacHandle(bool useCDac) } catch (Exception ex) when (ex is DllNotFoundException or BadImageFormatException) { - Trace.TraceError($"LoadLibrary({dacFilePath}) {useCDac} FAILED {ex}"); + Trace.TraceError($"LoadLibrary({dacFilePath}) FAILED {ex}"); return IntPtr.Zero; } } diff --git a/src/SOS/SOS.Hosting/SOS.Hosting.csproj b/src/SOS/SOS.Hosting/SOS.Hosting.csproj index bb438ecf78..54f1132d8a 100644 --- a/src/SOS/SOS.Hosting/SOS.Hosting.csproj +++ b/src/SOS/SOS.Hosting/SOS.Hosting.csproj @@ -17,7 +17,6 @@ - diff --git a/src/SOS/SOS.Hosting/SOSLibrary.cs b/src/SOS/SOS.Hosting/SOSLibrary.cs index d7bed24b2e..e9e0f8927d 100644 --- a/src/SOS/SOS.Hosting/SOSLibrary.cs +++ b/src/SOS/SOS.Hosting/SOSLibrary.cs @@ -4,10 +4,10 @@ using System; using System.Diagnostics; using System.IO; -using System.Reflection; using System.Runtime.InteropServices; using Microsoft.Diagnostics.DebugServices; using Microsoft.Diagnostics.Runtime.Utilities; +using Microsoft.Diagnostics.Shared; namespace SOS.Hosting { @@ -16,21 +16,24 @@ namespace SOS.Hosting /// public sealed class SOSLibrary : IDisposable { - /// - /// Provides the SOS module path and handle - /// - public interface ISOSModule - { - /// - /// The SOS module path - /// - string SOSPath { get; } + /// + /// Provided by a native debugger host to tell SOS hosting where the native sos module was + /// loaded from and its handle. This is the source where sos (and the cDAC that + /// ships next to it) comes from. When absent (in-process hosts such as dotnet-dump that load + /// sos themselves), the directory is derived from the tool's package layout instead. + /// + public interface ISOSModule + { + /// + /// The directory containing the native sos module (and the cDAC next to it). + /// + string SOSPath { get; } - /// - /// The SOS module handle - /// - IntPtr SOSHandle { get; } - } + /// + /// The native sos module handle. + /// + IntPtr SOSHandle { get; } + } [UnmanagedFunctionPointer(CallingConvention.Winapi)] private delegate int SOSCommandDelegate( @@ -67,12 +70,12 @@ private delegate int SOSInitializeDelegate( public string SOSPath { get; set; } [ServiceExport(Scope = ServiceScope.Global)] - public static SOSLibrary TryCreate(IHost host, [ServiceImport(Optional = true)] ISOSModule sosModule) + public static SOSLibrary TryCreate(IHost host, IHostAssetResolver assetResolver, [ServiceImport(Optional = true)] ISOSModule sosModule) { SOSLibrary sosLibrary = null; try { - sosLibrary = new SOSLibrary(host, sosModule); + sosLibrary = new SOSLibrary(host, assetResolver, sosModule); sosLibrary.Initialize(); } catch @@ -86,10 +89,16 @@ public static SOSLibrary TryCreate(IHost host, [ServiceImport(Optional = true)] /// /// Create an instance of the hosting class /// - /// target instance - /// sos library info or null - private SOSLibrary(IHost host, ISOSModule sosModule) + /// the host instance + /// resolves where the native sos binaries live (the host's sos + /// directory, or this tool's package layout) + /// the host-loaded sos module (handle/ownership), or null when this + /// host loads sos itself (dotnet-dump) + private SOSLibrary(IHost host, IHostAssetResolver assetResolver, ISOSModule sosModule) { + // The asset resolver is the single source of truth for the native binaries directory; it + // already accounts for a host-supplied sos location. ISOSModule, when present, only tells + // us the host already loaded sos so we reuse its handle instead of loading/unloading it. if (sosModule is not null) { SOSPath = sosModule.SOSPath; @@ -97,8 +106,7 @@ private SOSLibrary(IHost host, ISOSModule sosModule) } else { - string rid = InstallHelper.GetRid(); - SOSPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), rid); + SOSPath = assetResolver.NativeBinariesDirectory; _uninitializeLibrary = true; } _hostWrapper = new HostWrapper(host); diff --git a/src/SOS/SOS.Hosting/SOSPackageLayout.cs b/src/SOS/SOS.Hosting/SOSPackageLayout.cs new file mode 100644 index 0000000000..7d0c7b9c48 --- /dev/null +++ b/src/SOS/SOS.Hosting/SOSPackageLayout.cs @@ -0,0 +1,107 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace Microsoft.Diagnostics.Shared; + +/// +/// Describes the on-disk layout of the diagnostics tool packages (dotnet-dump, +/// dotnet-sos, SOS.Package, dbgshim) relative to the assembly this type is compiled +/// into. +/// +/// A package contains two kinds of binaries that are loaded by the analyzer host: +/// +/// +/// Native binaries that are loaded directly into the analyzer host process and +/// therefore must match the host's OS and processor architecture (sos.dll / +/// libsosplugin, mscordaccore_universal, DiaSymReader, …). They live +/// in a host-specific subfolder of the managed assembly directory (e.g. +/// tools/<tfm>/any/win-x64/). The folder name is the analyzer host's +/// runtime identifier — that's the layout the build emits. +/// +/// +/// SOS managed extension assemblies that dotnet-sos install copies into the +/// SOS install directory (Microsoft.Diagnostics.ExtensionCommands.dll, etc.). They +/// live in the lib/ sibling subfolder and are not OS/arch specific. +/// +/// +/// +/// This file is compile-included by SOS.Hosting and SOS.InstallHelper +/// so they agree on the layout. Because it is compiled into each consumer, the +/// package base directory is the directory of the consuming assembly. +/// +internal static class SOSPackageLayout +{ + /// + /// The directory of the package this type is compiled into. All package-relative + /// paths are rooted here. + /// + private static string s_packageBaseDirectory = ComputePackageBaseDirectory(); + + /// + /// Returns the directory containing the native binaries for this package, + /// targeting (or the current host's architecture + /// when is null). Used when the host needs + /// to address binaries for a non-current architecture (for example, when + /// dotnet-sos install -a arm64 is invoked from an x64 host). + /// + public static string GetNativeBinariesDirectory(Architecture? architecture = null) + => Path.Combine(s_packageBaseDirectory, GetHostNativeBinariesFolderName(architecture)); + + /// + /// Returns the directory containing the SOS managed extension assemblies for this + /// package. + /// + public static string GetManagedBinariesDirectory() + => Path.Combine(s_packageBaseDirectory, "lib"); + + private static string ComputePackageBaseDirectory() + { + string location = typeof(SOSPackageLayout).Assembly.Location; + return Path.GetDirectoryName(location) + ?? throw new InvalidOperationException($"Cannot resolve package base directory: {typeof(SOSPackageLayout).Assembly.GetName().Name} has no on-disk location."); + } + + /// + /// The package-relative subfolder for native binaries targeting the current host + /// OS and the supplied (or the current process + /// architecture when is null). + /// + private static string GetHostNativeBinariesFolderName(Architecture? architecture) + => ComputeHostNativeBinariesFolderName(architecture); + + private static string ComputeHostNativeBinariesFolderName(Architecture? architecture) + { + string os; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + os = "win"; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + os = "osx"; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + os = "linux"; + try + { + if (File.ReadAllText("/etc/os-release").Contains("ID=alpine")) + { + os = "linux-musl"; + } + } + catch (Exception ex) when (ex is FileNotFoundException or DirectoryNotFoundException or IOException) + { + } + } + else + { + throw new PlatformNotSupportedException($"Unsupported operating system: {RuntimeInformation.OSDescription}"); + } + return $"{os}-{(architecture ?? RuntimeInformation.ProcessArchitecture).ToString().ToLowerInvariant()}"; + } +} diff --git a/src/SOS/SOS.InstallHelper/InstallHelper.cs b/src/SOS/SOS.InstallHelper/InstallHelper.cs index ed72e4b8c9..1f72dfaecb 100644 --- a/src/SOS/SOS.InstallHelper/InstallHelper.cs +++ b/src/SOS/SOS.InstallHelper/InstallHelper.cs @@ -5,9 +5,9 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.Reflection; using System.Runtime.InteropServices; using System.Security; +using Microsoft.Diagnostics.Shared; namespace SOS { @@ -55,7 +55,6 @@ public sealed class InstallHelper public InstallHelper(Action writeLine, Architecture? architecture = null) { m_writeLine = writeLine; - string rid = GetRid(architecture); string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) @@ -63,8 +62,9 @@ public InstallHelper(Action writeLine, Architecture? architecture = null LLDBInitFile = Path.Combine(home, ".lldbinit"); } InstallLocation = Path.GetFullPath(Path.Combine(home, ".dotnet", "sos")); - SOSNativeSourcePath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), rid); - SOSManagedSourcePath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "lib"); + + SOSNativeSourcePath = SOSPackageLayout.GetNativeBinariesDirectory(architecture); + SOSManagedSourcePath = SOSPackageLayout.GetManagedBinariesDirectory(); } /// @@ -306,44 +306,6 @@ private static void RetryOperation(string errorMessage, Action operation) } } - /// - /// Returns the RID - /// - /// architecture to install or if null using the current process architecture - public static string GetRid(Architecture? architecture = null) - { - string os = null; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - os = "win"; - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - os = "osx"; - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - os = "linux"; - try - { - string ostype = File.ReadAllText("/etc/os-release"); - if (ostype.Contains("ID=alpine")) - { - os = "linux-musl"; - } - } - catch (Exception ex) when (ex is FileNotFoundException or DirectoryNotFoundException or IOException) - { - } - } - if (os == null) - { - throw new SOSInstallerException($"Unsupported operating system {RuntimeInformation.OSDescription}"); - } - string architectureString = (architecture.HasValue ? architecture : RuntimeInformation.ProcessArchitecture).ToString().ToLowerInvariant(); - return $"{os}-{architectureString}"; - } - private static void CopyFiles(string sourcePath, string destinationPath) { foreach (string path in Directory.EnumerateDirectories(sourcePath)) diff --git a/src/SOS/SOS.InstallHelper/SOS.InstallHelper.csproj b/src/SOS/SOS.InstallHelper/SOS.InstallHelper.csproj index 315833e657..7f336113b2 100644 --- a/src/SOS/SOS.InstallHelper/SOS.InstallHelper.csproj +++ b/src/SOS/SOS.InstallHelper/SOS.InstallHelper.csproj @@ -10,4 +10,8 @@ true false + + + + diff --git a/src/SOS/Strike/clrma/managedanalysis.cpp b/src/SOS/Strike/clrma/managedanalysis.cpp index 365cc39c4d..9aee1f0762 100644 --- a/src/SOS/Strike/clrma/managedanalysis.cpp +++ b/src/SOS/Strike/clrma/managedanalysis.cpp @@ -7,8 +7,8 @@ extern bool IsWindowsTarget(); extern "C" IXCLRDataProcess * GetClrDataFromDbgEng(); _Use_decl_annotations_ -ClrmaManagedAnalysis::ClrmaManagedAnalysis() : - m_lRefs(1), +ClrmaManagedAnalysis::ClrmaManagedAnalysis() : + m_lRefs(1), m_pointerSize(0), m_fileSeparator(0), m_processorType(0), @@ -266,6 +266,8 @@ ClrmaManagedAnalysis::AssociateClient( } if (FAILED(hr = runtime->GetClrDataProcess(IRuntime::ClrDataProcessFlags::UseCDac, &m_clrData))) { + TraceInformation("AssociateClient Runtime based DAC retrieval failed with code %08x, falling back to CLRMA\n", hr); + m_clrData = GetClrDataFromDbgEng(); if (m_clrData == nullptr) { diff --git a/src/SOS/Strike/platform/runtimeimpl.cpp b/src/SOS/Strike/platform/runtimeimpl.cpp index 3f356315a9..0826075013 100644 --- a/src/SOS/Strike/platform/runtimeimpl.cpp +++ b/src/SOS/Strike/platform/runtimeimpl.cpp @@ -27,7 +27,7 @@ #define CORDBG_E_NO_IMAGE_AVAILABLE EMAKEHR(0x1c64) -typedef HRESULT (STDAPICALLTYPE *OpenVirtualProcessImpl2FnPtr)(ULONG64 clrInstanceId, +typedef HRESULT (STDAPICALLTYPE *OpenVirtualProcessImpl2FnPtr)(ULONG64 clrInstanceId, IUnknown * pDataTarget, LPCWSTR pDacModulePath, CLR_DEBUGGING_VERSION * pMaxDebuggerSupportedVersion, @@ -35,7 +35,7 @@ typedef HRESULT (STDAPICALLTYPE *OpenVirtualProcessImpl2FnPtr)(ULONG64 clrInsta IUnknown ** ppInstance, CLR_DEBUGGING_PROCESS_FLAGS * pdwFlags); -typedef HRESULT (STDAPICALLTYPE *OpenVirtualProcessImplFnPtr)(ULONG64 clrInstanceId, +typedef HRESULT (STDAPICALLTYPE *OpenVirtualProcessImplFnPtr)(ULONG64 clrInstanceId, IUnknown * pDataTarget, HMODULE hDacDll, CLR_DEBUGGING_VERSION * pMaxDebuggerSupportedVersion, @@ -43,7 +43,7 @@ typedef HRESULT (STDAPICALLTYPE *OpenVirtualProcessImplFnPtr)(ULONG64 clrInstan IUnknown ** ppInstance, CLR_DEBUGGING_PROCESS_FLAGS * pdwFlags); -typedef HRESULT (STDAPICALLTYPE *OpenVirtualProcess2FnPtr)(ULONG64 clrInstanceId, +typedef HRESULT (STDAPICALLTYPE *OpenVirtualProcess2FnPtr)(ULONG64 clrInstanceId, IUnknown * pDataTarget, HMODULE hDacDll, REFIID riid, @@ -104,7 +104,7 @@ static HRESULT GetSingleFileInfo(ITarget* target, PULONG pModuleIndex, PULONG64 continue; } } - else + else { hr = debuggerServices->GetOffsetBySymbol(index, symbolName, &symbolAddress); if (FAILED(hr)) { @@ -176,11 +176,11 @@ HRESULT Runtime::CreateInstance(ITarget* target, RuntimeConfiguration configurat // If the previous operations were successful, create the Runtime instance if (SUCCEEDED(hr)) { - if (moduleSize > 0) + if (moduleSize > 0) { *ppRuntime = new Runtime(target, configuration, moduleIndex, moduleAddress, moduleSize, runtimeInfo); } - else + else { ExtOut("Runtime (%s) module size == 0\n", runtimeModuleName); hr = E_INVALIDARG; @@ -204,8 +204,10 @@ Runtime::Runtime(ITarget* target, RuntimeConfiguration configuration, ULONG inde m_runtimeInfo(runtimeInfo), m_runtimeDirectory(nullptr), m_dacFilePath(nullptr), + m_cdacFilePath(nullptr), m_dbiFilePath(nullptr), m_clrDataProcess(nullptr), + m_cdacDataProcess(nullptr), m_pCorDebugProcess(nullptr) { _ASSERTE(index != -1); @@ -240,6 +242,11 @@ Runtime::~Runtime() free((void*)m_dacFilePath); m_dacFilePath = nullptr; } + if (m_cdacFilePath != nullptr) + { + free((void*)m_cdacFilePath); + m_cdacFilePath = nullptr; + } if (m_dbiFilePath != nullptr) { free((void*)m_dbiFilePath); @@ -256,6 +263,11 @@ Runtime::~Runtime() m_clrDataProcess->Release(); m_clrDataProcess = nullptr; } + if (m_cdacDataProcess != nullptr) + { + m_cdacDataProcess->Release(); + m_cdacDataProcess = nullptr; + } } /**********************************************************************\ @@ -299,6 +311,65 @@ LPCSTR Runtime::GetDacFilePath() return m_dacFilePath; } +#ifndef FEATURE_PAL +extern HMODULE g_hInstance; +#else +// A file-local anchor used to resolve the directory of the SOS module via dladdr. +static void CDacModuleAnchor() {} +#endif + +/**********************************************************************\ + * Returns the cDAC (mscordaccore_universal) module path bundled next to + * sos in the diagnostics tool package, or nullptr when it isn't present. + * The cDAC is shipped with the tool and is never downloaded. +\**********************************************************************/ +LPCSTR Runtime::GetCDacFilePath() +{ + if (m_cdacFilePath == nullptr) + { + // The cDAC lives in the same directory as the loaded sos module (the host's platform + // subfolder of the package), not in the target runtime directory. + ArrayHolder szSOSModulePath = new char[MAX_LONGPATH + 1]; +#ifdef FEATURE_PAL + Dl_info info; + if (dladdr((void*)&CDacModuleAnchor, &info) == 0 || info.dli_fname == nullptr) + { + ExtDbgOut("GetCDacFilePath: dladdr failed to locate the sos module\n"); + return nullptr; + } + strcpy_s(szSOSModulePath.GetPtr(), MAX_LONGPATH, info.dli_fname); +#else + if (GetModuleFileNameA(g_hInstance, szSOSModulePath, MAX_LONGPATH) == 0) + { + ExtDbgOut("GetCDacFilePath: GetModuleFileNameA failed %08x\n", HRESULT_FROM_WIN32(GetLastError())); + return nullptr; + } +#endif + std::string cdacModulePath(szSOSModulePath.GetPtr()); + size_t lastSlash = cdacModulePath.rfind(DIRECTORY_SEPARATOR_CHAR_A); + if (lastSlash == std::string::npos) + { + ExtDbgOut("GetCDacFilePath: failed to parse sos module directory from %s\n", cdacModulePath.c_str()); + return nullptr; + } + cdacModulePath.erase(lastSlash + 1); + cdacModulePath.append(NETCORE_CDAC_DLL_NAME_A); + + // The cDAC must exist on disk next to sos; it is never downloaded. When it is not + // bundled (for example, RIDs without a cDAC), callers fall back to the in-box DAC. +#ifdef FEATURE_PAL + bool exists = access(cdacModulePath.c_str(), F_OK) == 0; +#else + bool exists = GetFileAttributesA(cdacModulePath.c_str()) != INVALID_FILE_ATTRIBUTES; +#endif + if (exists) + { + m_cdacFilePath = _strdup(cdacModulePath.c_str()); + } + } + return m_cdacFilePath; +} + /**********************************************************************\ * Returns the DBI module path to the rest of SOS \**********************************************************************/ @@ -333,6 +404,10 @@ void Runtime::Flush() { m_clrDataProcess->Flush(); } + if (m_cdacDataProcess != nullptr) + { + m_cdacDataProcess->Flush(); + } } //---------------------------------------------------------------------------- @@ -360,7 +435,7 @@ HRESULT Runtime::QueryInterface( ULONG Runtime::AddRef() { - LONG ref = InterlockedIncrement(&m_ref); + LONG ref = InterlockedIncrement(&m_ref); return ref; } @@ -423,6 +498,28 @@ LPCSTR Runtime::GetRuntimeDirectory() \**********************************************************************/ HRESULT Runtime::GetClrDataProcess(ClrDataProcessFlags flags, IXCLRDataProcess** ppClrDataProcess) { + // When the cDAC is requested (e.g. by CLRMA or the main SOS DAC-load path) and the policy + // selects it (supported runtime version, DOTNET_ENABLE_CDAC not deferring to the in-box DAC), + // prefer it for the data-access path and fall back to the in-box DAC if it isn't bundled or + // fails to initialize. + if ((flags & ClrDataProcessFlags::UseCDac) != 0 && ShouldUseCDac()) + { + if (m_cdacDataProcess == nullptr) + { + LPCSTR cdacFilePath = GetCDacFilePath(); + if (cdacFilePath != nullptr) + { + m_cdacDataProcess = CreateClrDataProcessInstance(cdacFilePath); + } + } + if (m_cdacDataProcess != nullptr) + { + *ppClrDataProcess = m_cdacDataProcess; + return S_OK; + } + // Fall through to the DAC. + } + if (m_clrDataProcess == nullptr) { *ppClrDataProcess = nullptr; @@ -432,39 +529,97 @@ HRESULT Runtime::GetClrDataProcess(ClrDataProcessFlags flags, IXCLRDataProcess** { return CORDBG_E_NO_IMAGE_AVAILABLE; } - HMODULE hdac = LoadLibraryA(dacFilePath); - if (hdac == NULL) + m_clrDataProcess = CreateClrDataProcessInstance(dacFilePath); + if (m_clrDataProcess == nullptr) { - ExtDbgOut("LoadLibraryA(%s) FAILED %08x\n", dacFilePath, HRESULT_FROM_WIN32(GetLastError())); return CORDBG_E_MISSING_DEBUGGER_EXPORTS; } - PFN_CLRDataCreateInstance pfnCLRDataCreateInstance = (PFN_CLRDataCreateInstance)GetProcAddress(hdac, "CLRDataCreateInstance"); - if (pfnCLRDataCreateInstance == nullptr) - { - FreeLibrary(hdac); - return CORDBG_E_MISSING_DEBUGGER_EXPORTS; - } - ICLRDataTarget *target = new DataTarget(GetModuleAddress()); - HRESULT hr = pfnCLRDataCreateInstance(__uuidof(IXCLRDataProcess), target, (void**)&m_clrDataProcess); - if (FAILED(hr)) - { - m_clrDataProcess = nullptr; - return hr; - } - ULONG32 flags = 0; - m_clrDataProcess->GetOtherNotificationFlags(&flags); - flags |= (CLRDATA_NOTIFY_ON_MODULE_LOAD | CLRDATA_NOTIFY_ON_MODULE_UNLOAD | CLRDATA_NOTIFY_ON_EXCEPTION); - m_clrDataProcess->SetOtherNotificationFlags(flags); } *ppClrDataProcess = m_clrDataProcess; return S_OK; } +// The minimum runtime major version that supports the cDAC. +static const DWORD MinCDacRuntimeMajorVersion = 11; + +// Returns true if the named environment variable is set to "1". +static bool IsEnvironmentVariableSetToOne(const char* name) +{ + char buffer[16]; + DWORD length = GetEnvironmentVariableA(name, buffer, ARRAY_SIZE(buffer)); + return length > 0 && length < ARRAY_SIZE(buffer) && strcmp(buffer, "1") == 0; +} + +/**********************************************************************\ + * Evaluates the cDAC loading policy for this runtime. +\**********************************************************************/ +bool Runtime::ShouldUseCDac() +{ + // When DOTNET_ENABLE_CDAC is requested, the in-box (legacy) DAC loads and drives the cDAC + // contract reader itself (including its own dac-vs-cdac fallback/comparison). Defer to that + // mechanism rather than loading the cDAC directly so those scenarios keep working. + if (IsEnvironmentVariableSetToOne("DOTNET_ENABLE_CDAC") || IsEnvironmentVariableSetToOne("COMPlus_ENABLE_CDAC")) + { + return false; + } + + // Use the cDAC only for runtimes that support it (.NET 11+). + VS_FIXEDFILEINFO fileInfo; + if (FAILED(GetEEVersion(&fileInfo, nullptr, 0))) + { + return false; + } + DWORD majorVersion = (fileInfo.dwFileVersionMS >> 16) & 0xFFFF; + return majorVersion >= MinCDacRuntimeMajorVersion; +} + +/**********************************************************************\ + * Loads the given DAC/cDAC module and creates an IXCLRDataProcess from it. + * Returns nullptr on failure. +\**********************************************************************/ +IXCLRDataProcess* Runtime::CreateClrDataProcessInstance(LPCSTR dacFilePath) +{ + HMODULE hdac = LoadLibraryA(dacFilePath); + if (hdac == NULL) + { + ExtDbgOut("LoadLibraryA(%s) FAILED %08x\n", dacFilePath, HRESULT_FROM_WIN32(GetLastError())); + return nullptr; + } + PFN_CLRDataCreateInstance pfnCLRDataCreateInstance = (PFN_CLRDataCreateInstance)GetProcAddress(hdac, "CLRDataCreateInstance"); + if (pfnCLRDataCreateInstance == nullptr) + { + FreeLibrary(hdac); + return nullptr; + } + ICLRDataTarget *target = new DataTarget(GetModuleAddress()); + IXCLRDataProcess* clrDataProcess = nullptr; + HRESULT hr = pfnCLRDataCreateInstance(__uuidof(IXCLRDataProcess), target, (void**)&clrDataProcess); + if (FAILED(hr)) + { + // CLRDataCreateInstance only AddRefs the data target on success; release our reference + // (created at ref count 0) to delete it, and unload the module. + target->AddRef(); + target->Release(); + FreeLibrary(hdac); + return nullptr; + } + // Best-effort: enable module load/unload and exception notifications so SOS flushes its caches + // across stop states when the cDAC/DAC is used against a live target. Ignore failures (the + // cDAC may not implement these yet). + ULONG32 notificationFlags = 0; + if (SUCCEEDED(clrDataProcess->GetOtherNotificationFlags(¬ificationFlags))) + { + notificationFlags |= (CLRDATA_NOTIFY_ON_MODULE_LOAD | CLRDATA_NOTIFY_ON_MODULE_UNLOAD | CLRDATA_NOTIFY_ON_EXCEPTION); + clrDataProcess->SetOtherNotificationFlags(notificationFlags); + } + return clrDataProcess; +} + /**********************************************************************\ - * Loads and initializes the public ICorDebug interfaces. This should be - * called at least once per debugger stop state to ensure that the + * Loads and initializes the public ICorDebug interfaces. This should be + * called at least once per debugger stop state to ensure that the * interface is available and that it doesn't hold stale data. Calling - * it more than once isn't an error, but does have perf overhead from + * it more than once isn't an error, but does have perf overhead from * needlessly flushing memory caches. \**********************************************************************/ HRESULT Runtime::GetCorDebugInterface(ICorDebugProcess** ppCorDebugProcess) @@ -516,7 +671,7 @@ HRESULT Runtime::GetCorDebugInterface(ICorDebugProcess** ppCorDebugProcess) return hr; } const char* dbiFilePath = GetDbiFilePath(); - if (dbiFilePath == nullptr) + if (dbiFilePath == nullptr) { ExtErr("Could not find matching DBI\n"); return CORDBG_E_NO_IMAGE_AVAILABLE; @@ -553,7 +708,7 @@ HRESULT Runtime::GetCorDebugInterface(ICorDebugProcess** ppCorDebugProcess) } #ifdef FEATURE_PAL // On Linux/MacOS the DAC module handle needs to be re-created using the DAC PAL instance - // before being passed to DBI's OpenVirtualProcess* implementation. The DBI and DAC share + // before being passed to DBI's OpenVirtualProcess* implementation. The DBI and DAC share // the same PAL where dbgshim has it's own. LoadLibraryWFnPtr loadLibraryWFn = (LoadLibraryWFnPtr)GetProcAddress(hDac, "LoadLibraryW"); if (loadLibraryWFn != nullptr) diff --git a/src/SOS/Strike/platform/runtimeimpl.h b/src/SOS/Strike/platform/runtimeimpl.h index 6d8994eaeb..2d6e664327 100644 --- a/src/SOS/Strike/platform/runtimeimpl.h +++ b/src/SOS/Strike/platform/runtimeimpl.h @@ -38,6 +38,10 @@ #define DESKTOP_DAC_DLL_NAME_W MAKEDLLNAME_W(W("mscordacwks")) #define DESKTOP_DAC_DLL_NAME_A MAKEDLLNAME_A("mscordacwks") +// The cDAC (mscordaccore_universal) ships next to sos in the diagnostics tool package. +// MAKEDLLNAME_A applies the platform-specific prefix/suffix (e.g. .dll, lib*.so, lib*.dylib). +#define NETCORE_CDAC_DLL_NAME_A MAKEDLLNAME_A("mscordaccore_universal") + extern IRuntime* g_pRuntime; // Returns the runtime configuration as a string @@ -121,8 +125,10 @@ class Runtime : public IRuntime RuntimeInfo* m_runtimeInfo; LPCSTR m_runtimeDirectory; LPCSTR m_dacFilePath; + LPCSTR m_cdacFilePath; LPCSTR m_dbiFilePath; IXCLRDataProcess* m_clrDataProcess; + IXCLRDataProcess* m_cdacDataProcess; ICorDebugProcess* m_pCorDebugProcess; Runtime(ITarget* target, RuntimeConfiguration configuration, ULONG index, ULONG64 address, ULONG64 size, RuntimeInfo* runtimeInfo); @@ -143,6 +149,13 @@ class Runtime : public IRuntime } } + // Loads the given DAC/cDAC module and creates an IXCLRDataProcess from it (nullptr on failure). + IXCLRDataProcess* CreateClrDataProcessInstance(LPCSTR dacFilePath); + + // Evaluates the cDAC loading policy: cDAC is used for supported runtimes (.NET 11+) unless + // DOTNET_ENABLE_CDAC requests that the in-box DAC drive the cDAC contract reader itself. + bool ShouldUseCDac(); + public: static HRESULT CreateInstance(ITarget* target, RuntimeConfiguration configuration, Runtime** ppRuntime); @@ -150,6 +163,8 @@ class Runtime : public IRuntime LPCSTR GetDacFilePath(); + LPCSTR GetCDacFilePath(); + LPCSTR GetDbiFilePath(); void DisplayStatus(); diff --git a/src/SOS/Strike/util.cpp b/src/SOS/Strike/util.cpp index 8ead614499..5b7e172097 100644 --- a/src/SOS/Strike/util.cpp +++ b/src/SOS/Strike/util.cpp @@ -3795,7 +3795,7 @@ class SOSDacInterface15Simulator : public ISOSDacInterface15 HRESULT LoadClrDebugDll(void) { _ASSERTE(g_pRuntime != nullptr); - HRESULT hr = g_pRuntime->GetClrDataProcess(IRuntime::ClrDataProcessFlags::None, &g_clrData); + HRESULT hr = g_pRuntime->GetClrDataProcess(IRuntime::ClrDataProcessFlags::UseCDac, &g_clrData); if (FAILED(hr)) { g_clrData = GetClrDataFromDbgEng(); @@ -5322,7 +5322,7 @@ WString DmlEscape(const WString &input) const WCHAR *str = input.c_str(); size_t len = input.length(); WString result; - + for (size_t i = 0; i < len; i++) { // Ampersand must be escaped FIRST to avoid double-escaping @@ -5346,7 +5346,7 @@ WString DmlEscape(const WString &input) result += temp; } } - + return result; } From ecdd2b407f33f9f8f6980da7a081f9de5e77cd84 Mon Sep 17 00:00:00 2001 From: Juan Hoyos <19413848+hoyosjs@users.noreply.github.com> Date: Thu, 11 Jun 2026 04:17:39 -0700 Subject: [PATCH 3/7] Implement ICLRContractLocator in native runtime and signing override in CLRMD for cdac + dac scenarios --- .../Runtime.cs | 5 ++- .../RuntimeProvider.cs | 27 ++++++++++++- src/SOS/Strike/platform/datatarget.cpp | 32 ++++++++++++++- src/SOS/Strike/platform/datatarget.h | 10 ++++- src/SOS/Strike/platform/runtimeimpl.cpp | 39 +++++++++++++++++-- src/SOS/Strike/platform/runtimeimpl.h | 8 +++- 6 files changed, 110 insertions(+), 11 deletions(-) diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/Runtime.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/Runtime.cs index ab236ab87a..1739fce5dc 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/Runtime.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/Runtime.cs @@ -37,7 +37,10 @@ public Runtime(IServiceProvider services, int id, ClrInfo clrInfo) Target = services.GetService() ?? throw new DiagnosticsException("Dump or live session target required"); Id = id; _clrInfo = clrInfo ?? throw new ArgumentNullException(nameof(clrInfo)); - _hostAssetResolver = services.GetService() ?? throw new ArgumentException("IHostAssetResolver required"); + // IHostAssetResolver is optional: it is registered by the SOS hosting layer to locate + // the bundled cDAC. When absent (hosts without SOS.Hosting, e.g. some test hosts), cDAC + // resolution returns null and the in-box DAC is used. + _hostAssetResolver = services.GetService(); _settingsService = services.GetService() ?? throw new ArgumentException("ISettingsService required"); _symbolService = services.GetService() ?? throw new ArgumentException("ISymbolService required"); diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/RuntimeProvider.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/RuntimeProvider.cs index 00eb74c6cd..84572786c1 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/RuntimeProvider.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/RuntimeProvider.cs @@ -3,6 +3,8 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; using Microsoft.Diagnostics.Runtime; namespace Microsoft.Diagnostics.DebugServices.Implementation @@ -33,10 +35,33 @@ public IEnumerable EnumerateRuntimes(int startingRuntimeId, RuntimeEnu // not flushed when the Target/RuntimeService is flushed; they are all disposed and the list cleared. They are // all re-created the next time the IRuntime or ClrRuntime instance is queried. ISettingsService settingsService = _services.GetService(); + bool verifyDac = settingsService?.DacSignatureVerificationEnabled ?? true; + + // The cDAC (mscordaccore_universal) ships inside the (signed) diagnostics tool package and + // carries no individual DAC signature, so it cannot satisfy ClrMD's signature check. Trust it + // the same way the native and SOS-hosting cDAC load paths do (load without verification), while + // still verifying the in-box DAC. We trust ONLY the exact cDAC path the host resolver provides + // (the bundled binary next to sos); matching by file name alone would let a name-hijacked DLL + // loaded from elsewhere (target runtime dir, symbol cache, ...) bypass verification. + string trustedCDacPath = _services.GetService()?.GetCDacPath(); + string normalizedTrustedCDacPath = string.IsNullOrEmpty(trustedCDacPath) ? null : Path.GetFullPath(trustedCDacPath); + StringComparison pathComparison = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; + DataTarget dataTarget = new(_services.GetService(), new DataTargetOptions() { ForceCompleteRuntimeEnumeration = (flags & RuntimeEnumerationFlags.All) != 0, - VerifyDacOnWindows = settingsService?.DacSignatureVerificationEnabled ?? true + VerifyDacOnWindows = verifyDac, + // Takes priority over VerifyDacOnWindows: skip verification only for the exact bundled cDAC. + DacSignatureVerificationOverride = (dacFilePath) => + { + if (normalizedTrustedCDacPath is not null + && !string.IsNullOrEmpty(dacFilePath) + && string.Equals(Path.GetFullPath(dacFilePath), normalizedTrustedCDacPath, pathComparison)) + { + return false; + } + return verifyDac; + } }); for (int i = 0; i < dataTarget.ClrVersions.Length; i++) { diff --git a/src/SOS/Strike/platform/datatarget.cpp b/src/SOS/Strike/platform/datatarget.cpp index 63a059b536..b0068fb8e6 100644 --- a/src/SOS/Strike/platform/datatarget.cpp +++ b/src/SOS/Strike/platform/datatarget.cpp @@ -11,9 +11,10 @@ #define IMAGE_FILE_MACHINE_AMD64 0x8664 // AMD64 (K8) -DataTarget::DataTarget(ULONG64 baseAddress) : +DataTarget::DataTarget(ULONG64 baseAddress, ULONG64 contractDescriptorAddress) : m_ref(0), - m_baseAddress(baseAddress) + m_baseAddress(baseAddress), + m_contractDescriptorAddress(contractDescriptorAddress) { } @@ -55,6 +56,12 @@ DataTarget::QueryInterface( AddRef(); return S_OK; } + else if (InterfaceId == IID_ICLRContractLocator) + { + *Interface = (ICLRContractLocator*)this; + AddRef(); + return S_OK; + } else { *Interface = NULL; @@ -385,3 +392,24 @@ DataTarget::GetRuntimeBase( *baseAddress = m_baseAddress; return S_OK; } + +// ICLRContractLocator + +HRESULT STDMETHODCALLTYPE +DataTarget::GetContractDescriptor( + /* [out] */ CLRDATA_ADDRESS* contractAddress) +{ + if (contractAddress == nullptr) + { + return E_INVALIDARG; + } + // The contract descriptor address is resolved by the runtime (which knows the target OS, the + // runtime module index, and has a memory-reading callback) and handed to us at construction. + // The cDAC requires it via ICLRContractLocator; the legacy DAC never queries this interface. + if (m_contractDescriptorAddress == 0) + { + return E_FAIL; + } + *contractAddress = m_contractDescriptorAddress; + return S_OK; +} diff --git a/src/SOS/Strike/platform/datatarget.h b/src/SOS/Strike/platform/datatarget.h index 369cc290ce..418f8f0e2d 100644 --- a/src/SOS/Strike/platform/datatarget.h +++ b/src/SOS/Strike/platform/datatarget.h @@ -1,14 +1,15 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -class DataTarget : public ICLRDataTarget2, ICorDebugDataTarget4, ICLRMetadataLocator, ICLRRuntimeLocator +class DataTarget : public ICLRDataTarget2, ICorDebugDataTarget4, ICLRMetadataLocator, ICLRRuntimeLocator, ICLRContractLocator { private: LONG m_ref; // Reference count. ULONG64 m_baseAddress; // Runtime base address + ULONG64 m_contractDescriptorAddress; // cDAC contract descriptor address (0 if not applicable/resolved) public: - DataTarget(ULONG64 baseAddress); + DataTarget(ULONG64 baseAddress, ULONG64 contractDescriptorAddress = 0); virtual ~DataTarget() {} // IUnknown. @@ -120,4 +121,9 @@ class DataTarget : public ICLRDataTarget2, ICorDebugDataTarget4, ICLRMetadataLoc virtual HRESULT STDMETHODCALLTYPE GetRuntimeBase( /* [out] */ CLRDATA_ADDRESS* baseAddress); + + // ICLRContractLocator + + virtual HRESULT STDMETHODCALLTYPE GetContractDescriptor( + /* [out] */ CLRDATA_ADDRESS* contractAddress); }; \ No newline at end of file diff --git a/src/SOS/Strike/platform/runtimeimpl.cpp b/src/SOS/Strike/platform/runtimeimpl.cpp index 0826075013..b51b9cdc68 100644 --- a/src/SOS/Strike/platform/runtimeimpl.cpp +++ b/src/SOS/Strike/platform/runtimeimpl.cpp @@ -509,7 +509,7 @@ HRESULT Runtime::GetClrDataProcess(ClrDataProcessFlags flags, IXCLRDataProcess** LPCSTR cdacFilePath = GetCDacFilePath(); if (cdacFilePath != nullptr) { - m_cdacDataProcess = CreateClrDataProcessInstance(cdacFilePath); + m_cdacDataProcess = CreateClrDataProcessInstance(cdacFilePath, GetContractDescriptorAddress()); } } if (m_cdacDataProcess != nullptr) @@ -529,7 +529,7 @@ HRESULT Runtime::GetClrDataProcess(ClrDataProcessFlags flags, IXCLRDataProcess** { return CORDBG_E_NO_IMAGE_AVAILABLE; } - m_clrDataProcess = CreateClrDataProcessInstance(dacFilePath); + m_clrDataProcess = CreateClrDataProcessInstance(dacFilePath, 0); if (m_clrDataProcess == nullptr) { return CORDBG_E_MISSING_DEBUGGER_EXPORTS; @@ -577,7 +577,7 @@ bool Runtime::ShouldUseCDac() * Loads the given DAC/cDAC module and creates an IXCLRDataProcess from it. * Returns nullptr on failure. \**********************************************************************/ -IXCLRDataProcess* Runtime::CreateClrDataProcessInstance(LPCSTR dacFilePath) +IXCLRDataProcess* Runtime::CreateClrDataProcessInstance(LPCSTR dacFilePath, ULONG64 contractDescriptorAddress) { HMODULE hdac = LoadLibraryA(dacFilePath); if (hdac == NULL) @@ -591,7 +591,7 @@ IXCLRDataProcess* Runtime::CreateClrDataProcessInstance(LPCSTR dacFilePath) FreeLibrary(hdac); return nullptr; } - ICLRDataTarget *target = new DataTarget(GetModuleAddress()); + ICLRDataTarget *target = new DataTarget(GetModuleAddress(), contractDescriptorAddress); IXCLRDataProcess* clrDataProcess = nullptr; HRESULT hr = pfnCLRDataCreateInstance(__uuidof(IXCLRDataProcess), target, (void**)&clrDataProcess); if (FAILED(hr)) @@ -615,6 +615,37 @@ IXCLRDataProcess* Runtime::CreateClrDataProcessInstance(LPCSTR dacFilePath) return clrDataProcess; } +/**********************************************************************\ + * Resolves the address of the cDAC contract descriptor export + * (DotNetRuntimeContractDescriptor) in the runtime module, or 0 if it + * can't be located. Mirrors the export lookup in GetSingleFileInfo: the + * cross-platform reader-based lookup for ELF/Mach-O targets and the + * debugger's symbol resolution for Windows (PE) targets. +\**********************************************************************/ +ULONG64 Runtime::GetContractDescriptorAddress() +{ + const char* symbolName = "DotNetRuntimeContractDescriptor"; + ULONG64 symbolAddress = 0; + if (m_target->GetOperatingSystem() == ITarget::OperatingSystem::Linux || + m_target->GetOperatingSystem() == ITarget::OperatingSystem::OSX) + { + if (!::TryGetSymbolWithCallback(ReaderReadMemory, m_address, symbolName, &symbolAddress)) + { + return 0; + } + } + else + { + IDebuggerServices* debuggerServices = GetDebuggerServices(); + if (debuggerServices == nullptr || + FAILED(debuggerServices->GetOffsetBySymbol(m_index, symbolName, &symbolAddress))) + { + return 0; + } + } + return symbolAddress; +} + /**********************************************************************\ * Loads and initializes the public ICorDebug interfaces. This should be * called at least once per debugger stop state to ensure that the diff --git a/src/SOS/Strike/platform/runtimeimpl.h b/src/SOS/Strike/platform/runtimeimpl.h index 2d6e664327..5c4e4d3fa9 100644 --- a/src/SOS/Strike/platform/runtimeimpl.h +++ b/src/SOS/Strike/platform/runtimeimpl.h @@ -150,7 +150,13 @@ class Runtime : public IRuntime } // Loads the given DAC/cDAC module and creates an IXCLRDataProcess from it (nullptr on failure). - IXCLRDataProcess* CreateClrDataProcessInstance(LPCSTR dacFilePath); + // contractDescriptorAddress is the cDAC contract descriptor address (0 for the in-box DAC, which + // does not use it). + IXCLRDataProcess* CreateClrDataProcessInstance(LPCSTR dacFilePath, ULONG64 contractDescriptorAddress); + + // Resolves the address of the cDAC contract descriptor export (DotNetRuntimeContractDescriptor) + // in the runtime module, or 0 if it can't be found. The cDAC requires this via ICLRContractLocator. + ULONG64 GetContractDescriptorAddress(); // Evaluates the cDAC loading policy: cDAC is used for supported runtimes (.NET 11+) unless // DOTNET_ENABLE_CDAC requests that the in-box DAC drive the cDAC contract reader itself. From d0cf0f85cb6de6599b31e3fc5daa75d9d9124118 Mon Sep 17 00:00:00 2001 From: Juan Sebastian Hoyos Ayala Date: Wed, 17 Jun 2026 10:57:15 -0700 Subject: [PATCH 4/7] Disable tests failing from dotnet/diagnostics#5883 --- src/tests/SOS.UnitTests/SOS.cs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/tests/SOS.UnitTests/SOS.cs b/src/tests/SOS.UnitTests/SOS.cs index a8eb0ddb61..110c8ab27f 100644 --- a/src/tests/SOS.UnitTests/SOS.cs +++ b/src/tests/SOS.UnitTests/SOS.cs @@ -332,11 +332,8 @@ await SOSTestHelpers.RunTest( [SkippableTheory, MemberData(nameof(SOSTestHelpers.Configurations), MemberType = typeof(SOSTestHelpers))] public async Task StackTests(TestConfiguration config) { - if (config.RuntimeFrameworkVersionMajor == 10) - { - // The clrstack -i command regressed on .NET 10 win-x86, so skip this test for now. - SOSTestHelpers.SkipIfWinX86(config); - } + // Tracking: https://github.com/dotnet/diagnostics/issues/5883 (dotnet/runtime#129456) + SOSTestHelpers.SkipIfWinX86(config); await SOSTestHelpers.RunTest( config, @@ -507,6 +504,8 @@ public SOSGCTests(ITestOutputHelper output) public async Task GCTests(TestConfiguration config) { SOSTestHelpers.SkipIfArm(config); + // Tracking: https://github.com/dotnet/diagnostics/issues/5883 (dotnet/runtime#129456) + SOSTestHelpers.SkipIfWinX86(config); // Live only await SOSTestHelpers.RunTest( @@ -525,6 +524,8 @@ public async Task GCPOHTests(TestConfiguration config) { throw new SkipTestException("This test validates POH behavior, which was introduced in .net 5"); } + // Tracking: https://github.com/dotnet/diagnostics/issues/5883 (dotnet/runtime#129456) + SOSTestHelpers.SkipIfWinX86(config); await SOSTestHelpers.RunTest( config, debuggeeName: "GCPOH", @@ -709,6 +710,8 @@ public async Task VarargPInvokeInteropMD(TestConfiguration config) { throw new SkipTestException("Test only supports CDB and therefore only runs on Windows"); } + // Tracking: https://github.com/dotnet/diagnostics/issues/5883 (dotnet/runtime#129456) + SOSTestHelpers.SkipIfWinX86(config); await SOSTestHelpers.RunTest( config, @@ -862,11 +865,8 @@ public async Task StackAndOtherTests(TestConfiguration config) { throw new SkipTestException("Single-file DAC signature verification failure with CDB (https://github.com/dotnet/diagnostics/issues/5757)"); } - if (config.RuntimeFrameworkVersionMajor == 10) - { - // The clrstack -i -a command regressed on .NET 10 win-x86, so skip this test for now. - SOSTestHelpers.SkipIfWinX86(config); - } + // Tracking: https://github.com/dotnet/diagnostics/issues/5883 (dotnet/runtime#129456) + SOSTestHelpers.SkipIfWinX86(config); foreach (TestConfiguration currentConfig in TestRunner.EnumeratePdbTypeConfigs(config)) { From 63250349245bfb1cff6c649011ee7cdda0038382 Mon Sep 17 00:00:00 2001 From: Juan Sebastian Hoyos Ayala Date: Wed, 17 Jun 2026 11:14:15 -0700 Subject: [PATCH 5/7] Relax line number against issue dotnet/diagnostics#5884 --- src/tests/SOS.UnitTests/Scripts/StackAndOtherTests.script | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/tests/SOS.UnitTests/Scripts/StackAndOtherTests.script b/src/tests/SOS.UnitTests/Scripts/StackAndOtherTests.script index 0db28bdd9e..fa3336be07 100644 --- a/src/tests/SOS.UnitTests/Scripts/StackAndOtherTests.script +++ b/src/tests/SOS.UnitTests/Scripts/StackAndOtherTests.script @@ -67,7 +67,7 @@ SOSCOMMAND:SOSStatus SOSCOMMAND:ClrStack VERIFY:.*OS Thread Id:\s+0x\s+.* VERIFY:\s+Child\s+SP\s+IP\s+Call Site\s+ -VERIFY:.*\s+\s+.*\s+SymbolTestApp\.Program\.Foo4\(System\.String\)\s+\[(?i:.*[\\|/]SymbolTestApp\.cs) @ 57\]\s* +VERIFY:.*\s+\s+.*\s+SymbolTestApp\.Program\.Foo4\(System\.String\)\s+\[(?i:.*[\\|/]SymbolTestApp\.cs) @ (53|57)\]\s* VERIFY:\s+\s+\s+SymbolTestApp\.Program\.Foo2\(.*\)\s+\[(?i:.*[\\|/]SymbolTestApp\.cs) @ 32\]\s* VERIFY:\s+\s+\s+SymbolTestApp\.Program\.Foo1\(.*\)\s+\[(?i:.*[\\|/]SymbolTestApp\.cs) @ 27\]\s* VERIFY:\s+\s+\s+SymbolTestApp\.Program\.Main\(.*\)\s+\[(?i:.*[\\|/]SymbolTestApp\.cs) @ 22\]\s* @@ -89,7 +89,7 @@ VERIFY:\s+....* SOSCOMMAND:ClrStack -f VERIFY:.*OS Thread Id:\s+0x\s+.* VERIFY:\s+Child\s+SP\s+IP\s+Call Site\s+ -VERIFY:\s+\s+\s+SymbolTestApp\.(dll|exe)!SymbolTestApp\.Program\.Foo4\(System\.String\)\s+\+\s+\s+\[(?i:.*[\\|/]SymbolTestApp\.cs) @ 57\]\s* +VERIFY:\s+\s+\s+SymbolTestApp\.(dll|exe)!SymbolTestApp\.Program\.Foo4\(System\.String\)\s+\+\s+\s+\[(?i:.*[\\|/]SymbolTestApp\.cs) @ (53|57)\]\s* VERIFY:\s+\s+\s+SymbolTestApp\.(dll|exe)!SymbolTestApp\.Program\.Foo2\(.*\)\s+\+\s+\s+\[(?i:.*[\\|/]SymbolTestApp\.cs) @ 32\]\s* VERIFY:\s+\s+\s+SymbolTestApp\.(dll|exe)!SymbolTestApp\.Program\.Foo1\(.*\)\s+\+\s+\s+\[(?i:.*[\\|/]SymbolTestApp\.cs) @ 27\]\s* VERIFY:\s+\s+\s+SymbolTestApp\.(dll|exe)!SymbolTestApp\.Program\.Main\(.*\)\s+\+\s+\s+\[(?i:.*[\\|/]SymbolTestApp\.cs) @ 22\]\s* @@ -98,7 +98,7 @@ VERIFY:\s+\s+\s+SymbolTestApp\.(dll|exe)!SymbolTestApp\.Program\ SOSCOMMAND:ClrStack -a VERIFY:.*OS Thread Id:\s+0x\s+.* VERIFY:\s+Child\s+SP\s+IP\s+Call Site\s+ -VERIFY:.*\s+\s+\s+SymbolTestApp\.Program\.Foo4\(System\.String\)\s+\[(?i:.*[\\|/]SymbolTestApp\.cs) @ 57\]\s* +VERIFY:.*\s+\s+\s+SymbolTestApp\.Program\.Foo4\(System\.String\)\s+\[(?i:.*[\\|/]SymbolTestApp\.cs) @ (53|57)\]\s* VERIFY:\s+PARAMETERS:\s+ VERIFY:\s+dllPath \(0x\) = 0x\s+ VERIFY:.*\s+LOCALS:\s+ @@ -112,7 +112,7 @@ SOSCOMMAND:ClrStack -r VERIFY:.*OS Thread Id:\s+0x\s+.* VERIFY:\s+Child\s+SP\s+IP\s+Call Site\s+ -VERIFY:.*\s+\s+\s+SymbolTestApp\.Program\.Foo4\(System\.String\)\s+\[(?i:.*[\\|/]SymbolTestApp\.cs) @ 57\]\s* +VERIFY:.*\s+\s+\s+SymbolTestApp\.Program\.Foo4\(System\.String\)\s+\[(?i:.*[\\|/]SymbolTestApp\.cs) @ (53|57)\]\s* IFDEF:ARM VERIFY:\s+r0=\s+r1=\s+r2=\s+ ENDIF:ARM From 9559aef332abb48dc8fb4e1c80ff371363b66e8f Mon Sep 17 00:00:00 2001 From: Juan Sebastian Hoyos Ayala Date: Wed, 17 Jun 2026 21:39:20 -0700 Subject: [PATCH 6/7] Refactor cDAC handling: replace UseCDac property with CDacLoadPolicy enum --- .../Host.cs | 2 +- .../Runtime.cs | 16 +++++------ .../CDacLoadPolicy.cs | 28 +++++++++++++++++++ .../ISettingsService.cs | 13 ++------- .../Host/CommandFormatHelpers.cs | 2 +- .../Host/RuntimesCommand.cs | 8 +++--- 6 files changed, 45 insertions(+), 24 deletions(-) create mode 100644 src/Microsoft.Diagnostics.DebugServices/CDacLoadPolicy.cs diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/Host.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/Host.cs index 0b066c5309..d502b96943 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/Host.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/Host.cs @@ -118,7 +118,7 @@ public string GetTempDirectory() public virtual bool DacSignatureVerificationEnabled { get; set; } - public bool? UseCDac { get; set; } + public CDacLoadPolicy CDacLoadPolicy { get; set; } #endregion diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/Runtime.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/Runtime.cs index 1739fce5dc..4c362a4de8 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/Runtime.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/Runtime.cs @@ -131,7 +131,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); - if (_cdacFilePath is null && _settingsService.UseCDac == true) + if (_cdacFilePath is null && _settingsService.CDacLoadPolicy == CDacLoadPolicy.UseCDac) { // The cDAC was explicitly forced but isn't bundled with this tool. throw new DiagnosticsException($"The cDAC was explicitly requested but no matching cDAC is available for this runtime: {RuntimeModule.FileName}"); @@ -155,22 +155,22 @@ public string GetDbiFilePath() /// /// Evaluates the cDAC loading policy for this runtime. This is the single place that /// decides whether the diagnostics tool should load the cDAC itself in place of the - /// in-box DAC, based on the setting and the + /// in-box DAC, based on the setting and the /// target runtime version. /// private bool ShouldUseCDac() { - return _settingsService.UseCDac switch + return _settingsService.CDacLoadPolicy switch { - false => false, // Never load the cDAC. - true => true, // Always use the cDAC, regardless of the runtime version. Availability is - // checked by the caller (a missing forced cDAC is a hard error). - _ => ShouldUseCDacByDefault(), // No explicit setting: evaluate the default policy. + CDacLoadPolicy.UseLegacyDac => false, // Never load the cDAC. + CDacLoadPolicy.UseCDac => true, // Always use the cDAC, regardless of the runtime version. Availability is + // checked by the caller (a missing forced cDAC is a hard error). + _ => ShouldUseCDacByDefault(), // No explicit setting: evaluate the default policy. }; } /// - /// The default cDAC policy used when is not set. + /// The default cDAC policy used when is not set. /// private bool ShouldUseCDacByDefault() { diff --git a/src/Microsoft.Diagnostics.DebugServices/CDacLoadPolicy.cs b/src/Microsoft.Diagnostics.DebugServices/CDacLoadPolicy.cs new file mode 100644 index 0000000000..ff187888fd --- /dev/null +++ b/src/Microsoft.Diagnostics.DebugServices/CDacLoadPolicy.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.DebugServices +{ + /// + /// Controls whether the cDAC is used in place of the in-box DAC. + /// + public enum CDacLoadPolicy + { + /// + /// Evaluate policy and fall back. The cDAC is used when the target runtime supports it + /// and a matching cDAC is available next to the diagnostics tool; otherwise the in-box + /// DAC is used. + /// + Default, + + /// + /// Always use the cDAC. Runtime construction fails if no matching cDAC is available. + /// + UseCDac, + + /// + /// Always use the in-box DAC. The cDAC is never loaded. + /// + UseLegacyDac, + } +} diff --git a/src/Microsoft.Diagnostics.DebugServices/ISettingsService.cs b/src/Microsoft.Diagnostics.DebugServices/ISettingsService.cs index 2e6c42bc32..031d28a8b9 100644 --- a/src/Microsoft.Diagnostics.DebugServices/ISettingsService.cs +++ b/src/Microsoft.Diagnostics.DebugServices/ISettingsService.cs @@ -17,16 +17,9 @@ public interface ISettingsService bool DacSignatureVerificationEnabled { get; set; } /// - /// Controls whether the cDAC is used in place of the in-box DAC: - /// - /// null (default): evaluate policy and fall back. The cDAC is used - /// when the target runtime supports it and a matching cDAC is available next - /// to the diagnostics tool; otherwise the in-box DAC is used. - /// true: always use the cDAC. Runtime construction fails if no - /// matching cDAC is available. - /// false: always use the in-box DAC. The cDAC is never loaded. - /// + /// Controls whether the cDAC is used in place of the in-box DAC. See + /// for the individual policy values. /// - bool? UseCDac { get; set; } + CDacLoadPolicy CDacLoadPolicy { get; set; } } } diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/Host/CommandFormatHelpers.cs b/src/Microsoft.Diagnostics.ExtensionCommands/Host/CommandFormatHelpers.cs index 15c3ef2860..9eb92edcd8 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/Host/CommandFormatHelpers.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/Host/CommandFormatHelpers.cs @@ -20,7 +20,7 @@ public static void DisplaySettingService(this CommandBase command) { ISettingsService settingsService = command.Services.GetService() ?? throw new DiagnosticsException("Settings service required"); command.Console.WriteLine("Settings:"); - command.Console.WriteLine($"-> Use cDAC: {settingsService.UseCDac switch { true => "true", false => "false", _ => "policy (default)" }}"); + command.Console.WriteLine($"-> Use cDAC: {settingsService.CDacLoadPolicy switch { CDacLoadPolicy.UseCDac => "true", CDacLoadPolicy.UseLegacyDac => "false", _ => "policy (default)" }}"); command.Console.WriteLine($"-> DAC signature verification check enabled: {settingsService.DacSignatureVerificationEnabled}"); } diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/Host/RuntimesCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/Host/RuntimesCommand.cs index 53b1aa49f2..c65b18d206 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/Host/RuntimesCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/Host/RuntimesCommand.cs @@ -52,11 +52,11 @@ public override void Invoke() bool flush = false; if (UseCDac is not null) { - SettingsService.UseCDac = UseCDac.ToLowerInvariant() switch + SettingsService.CDacLoadPolicy = UseCDac.ToLowerInvariant() switch { - "true" => true, - "false" => false, - "policy" or "default" => (bool?)null, + "true" => CDacLoadPolicy.UseCDac, + "false" => CDacLoadPolicy.UseLegacyDac, + "policy" or "default" => CDacLoadPolicy.Default, _ => throw new DiagnosticsException($"Invalid --usecdac value '{UseCDac}'. Expected true, false, or policy."), }; flush = true; From e8921a942859ee6107eb247fd4b1903ef43a3b3c Mon Sep 17 00:00:00 2001 From: Juan Sebastian Hoyos Ayala Date: Wed, 17 Jun 2026 21:41:33 -0700 Subject: [PATCH 7/7] Front load DAC to work around ordering issue --- src/SOS/SOS.Hosting/RuntimeWrapper.cs | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/SOS/SOS.Hosting/RuntimeWrapper.cs b/src/SOS/SOS.Hosting/RuntimeWrapper.cs index 708161c99d..a704534190 100644 --- a/src/SOS/SOS.Hosting/RuntimeWrapper.cs +++ b/src/SOS/SOS.Hosting/RuntimeWrapper.cs @@ -389,6 +389,20 @@ private IntPtr CreateCorDebugProcess() Trace.TraceError($"Could not find matching DBI {dbiFilePath ?? ""} for this runtime: {_runtime.RuntimeModule.FileName}"); return IntPtr.Zero; } + + // Load the in-box DAC before the DBI. The DBI has a hard load-time dependency on the in-box DAC + // (libmscordaccore.so / mscordaccore.dll is a NEEDED import resolved next to the runtime). + // as it's the PAL provider for the debugger process. For senarios where the DBI is not collocated with the DAC + // (e.x. single-file), each is downloaded into its own symbol-cache directory, so the loader can only satisfy the DBI's dependency if the DAC is + // already resident in the process. When the cDAC serves the data-access path the in-box DAC is otherwise never loaded, so load it explicitly here first. + // This also verifies the DAC signature before the DBI is passed the DAC path or handle. + IntPtr dacHandle = GetDacHandle(); + if (dacHandle == IntPtr.Zero) + { + return IntPtr.Zero; + } + string dacFilePath = _runtime.GetDacFilePath(out bool _); + if (_dbiHandle == IntPtr.Zero) { try @@ -415,16 +429,6 @@ private IntPtr CreateCorDebugProcess() int hresult = 0; try { - // This will verify the DAC signature if needed before DBI is passed the DAC path or handle - IntPtr dacHandle = GetDacHandle(); - if (dacHandle == IntPtr.Zero) - { - return IntPtr.Zero; - } - - // The DAC was verified in the GetDacHandle call above. Ignore the verifySignature parameter here. - string dacFilePath = _runtime.GetDacFilePath(out bool _); - OpenVirtualProcessImpl2Delegate openVirtualProcessImpl2 = SOSHost.GetDelegateFunction(_dbiHandle, "OpenVirtualProcessImpl2"); if (openVirtualProcessImpl2 != null) {