diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/CdacHResults.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/CdacHResults.cs
new file mode 100644
index 00000000000000..a453cc2627a7de
--- /dev/null
+++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/CdacHResults.cs
@@ -0,0 +1,66 @@
+// 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.DataContractReader;
+
+///
+/// cDAC-specific HRESULTs surfaced when creating a data-access interface
+/// for a target fails for a reason this cDAC can describe precisely. This covers two stages of
+/// creation: locating and reading the target's contract descriptor (the DESCRIPTOR codes),
+/// and eager validation that this cDAC can service the contracts the descriptor advertises (the
+/// CONTRACT codes, see
+/// ).
+///
+///
+/// The customer bit (bit 29, 0x20000000) is set on every value so these codes can never
+/// collide with system or CLR (corerror.h) HRESULTs. All values are failure codes, so
+/// existing FAILED(hr) checks in native callers continue to behave correctly. Native code
+/// and tooling that wants to distinguish the failure modes (for example, to decide whether to fall
+/// back to the brittle DAC) can compare against the literal values documented here.
+///
+public static class CdacHResults
+{
+ ///
+ /// A contract required to service the SOS interface is not advertised by the target's
+ /// contract descriptor. The target runtime predates the contract or does not expose it.
+ ///
+ public const int CDAC_E_CONTRACT_NOT_ADVERTISED = unchecked((int)0xA0DAC001);
+
+ ///
+ /// The target advertises a required contract at a version this cDAC does not recognize,
+ /// typically because the target runtime is newer than this cDAC.
+ ///
+ public const int CDAC_E_CONTRACT_UNRECOGNIZED = unchecked((int)0xA0DAC002);
+
+ ///
+ /// The target advertises a required contract at a version this cDAC recognizes but
+ /// intentionally does not implement, typically because the target runtime is too old.
+ ///
+ public const int CDAC_E_CONTRACT_UNSUPPORTED = unchecked((int)0xA0DAC003);
+
+ ///
+ /// A required contract could not be validated and the cause could not be classified as one of
+ /// the more specific CONTRACT codes above. This is the default HRESULT carried by the
+ /// base type.
+ ///
+ public const int CDAC_E_CONTRACT_UNAVAILABLE = unchecked((int)0xA0DAC004);
+
+ ///
+ /// No usable cDAC contract descriptor could be located for the target: the contract locator
+ /// did not produce a descriptor address, the descriptor header could not be read, or the bytes
+ /// at the descriptor address are not a contract descriptor (bad magic). This typically means the
+ /// target is not a cDAC-capable .NET runtime (for example, a non-.NET target or a runtime that
+ /// predates the cDAC contract descriptor), and tooling should fall back to the brittle DAC
+ /// rather than treat it as a hard error.
+ ///
+ public const int CDAC_E_DESCRIPTOR_NOT_FOUND = unchecked((int)0xA0DAC011);
+
+ ///
+ /// A cDAC contract descriptor was found but could not be consumed: a field could not be read,
+ /// the embedded JSON failed to parse, or the descriptor content is internally inconsistent
+ /// (for example, duplicate names or an out-of-range pointer-data index). This indicates a
+ /// corrupt dump or a descriptor format this cDAC cannot understand, and is distinct from a
+ /// recognized-but-unsupported contract version ().
+ ///
+ public const int CDAC_E_DESCRIPTOR_MALFORMED = unchecked((int)0xA0DAC012);
+}
diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ContractNotAvailableException.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ContractNotAvailableException.cs
new file mode 100644
index 00000000000000..bf41f12e12705a
--- /dev/null
+++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ContractNotAvailableException.cs
@@ -0,0 +1,109 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+
+namespace Microsoft.Diagnostics.DataContractReader;
+
+///
+/// Exception for failures to retrieve a data contract from a target. The concrete subclasses
+/// describe the specific cause; this base type is also thrown directly for an unclassified
+/// validation failure. Each concrete subclass sets a distinct value on
+/// so both the eager validation path
+/// ()
+/// and the lazy SOS data-access path surface the same self-describing failure code with no wrapping
+/// or translation. The base defaults to so an
+/// unclassified failure still carries a cDAC-specific failure code rather than S_OK.
+///
+public class ContractNotAvailableException : Exception
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The name of the requested contract.
+ /// The target-advertised version of the requested contract, or if the target did not advertise the contract.
+ /// The exception message.
+ public ContractNotAvailableException(string contractName, string? contractVersion, string message)
+ : base(message)
+ {
+ ContractName = contractName;
+ ContractVersion = contractVersion;
+ HResult = CdacHResults.CDAC_E_CONTRACT_UNAVAILABLE;
+ }
+
+ ///
+ /// Gets the name of the requested contract.
+ ///
+ public string ContractName { get; }
+
+ ///
+ /// Gets the target-advertised version of the requested contract, or if the target did not advertise the contract.
+ ///
+ public string? ContractVersion { get; }
+}
+
+///
+/// Exception thrown when the target's contract descriptor does not advertise the requested contract.
+///
+public sealed class ContractMissingException : ContractNotAvailableException
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The name of the requested contract.
+ public ContractMissingException(string contractName)
+ : base(contractName, null, $"Contract '{contractName}' is not advertised by the target.")
+ {
+ HResult = CdacHResults.CDAC_E_CONTRACT_NOT_ADVERTISED;
+ }
+}
+
+///
+/// Exception thrown when the target advertises the requested contract, but this cDAC cannot provide an implementation for the advertised version.
+///
+public abstract class ContractUnsupportedException : ContractNotAvailableException
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The name of the requested contract.
+ /// The target-advertised version of the requested contract.
+ /// The exception message.
+ protected ContractUnsupportedException(string contractName, string contractVersion, string message)
+ : base(contractName, contractVersion, message)
+ { }
+}
+
+///
+/// Exception thrown when the target advertises a contract version that this cDAC does not recognize, typically because the target runtime is newer than this cDAC.
+///
+public sealed class ContractUnrecognizedException : ContractUnsupportedException
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The name of the requested contract.
+ /// The target-advertised version of the requested contract.
+ public ContractUnrecognizedException(string contractName, string contractVersion)
+ : base(contractName, contractVersion, $"Contract '{contractName}' version {contractVersion} is advertised by the target but is not recognized by this cDAC.")
+ {
+ HResult = CdacHResults.CDAC_E_CONTRACT_UNRECOGNIZED;
+ }
+}
+
+///
+/// Exception thrown when the target advertises a contract version that this cDAC recognizes but intentionally does not implement, typically because the target runtime is too old.
+///
+public sealed class ContractObsoleteException : ContractUnsupportedException
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The name of the requested contract.
+ /// The target-advertised version of the requested contract.
+ public ContractObsoleteException(string contractName, string contractVersion)
+ : base(contractName, contractVersion, $"Contract '{contractName}' version {contractVersion} is advertised by the target and recognized by this cDAC, but is intentionally not implemented.")
+ {
+ HResult = CdacHResults.CDAC_E_CONTRACT_UNSUPPORTED;
+ }
+}
diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ContractRegistry.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ContractRegistry.cs
index 0c2e0aa833e760..cccea8cb6724cd 100644
--- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ContractRegistry.cs
+++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ContractRegistry.cs
@@ -157,19 +157,19 @@ public abstract class ContractRegistry
///
/// When this method returns , contains the requested contract instance; otherwise, .
///
- ///
- /// When this method returns , contains a human-readable explanation of why the contract could not be retrieved; otherwise, .
+ ///
+ /// When this method returns , contains the exception that describes why the contract could not be retrieved; otherwise, .
///
///
/// if the requested contract is present and was retrieved successfully; if the contract is not present or registered"/>.
///
- public abstract bool TryGetContract([NotNullWhen(true)] out TContract contract, out string? failureReason) where TContract : IContract;
+ public abstract bool TryGetContract([NotNullWhen(true)] out TContract contract, [NotNullWhen(false)] out System.Exception? failureException) where TContract : IContract;
public TContract GetContract() where TContract : IContract
{
- if (!TryGetContract(out TContract contract, out string? failureReason))
+ if (!TryGetContract(out TContract contract, out System.Exception? failureException))
{
- throw new NotImplementedException($"Contract '{typeof(TContract).Name}' is not supported by the target. Reason: {failureReason ?? "no reason provided"}");
+ throw failureException ?? new ContractMissingException(TContract.Name);
}
return contract;
}
@@ -179,6 +179,31 @@ public bool TryGetContract([NotNullWhen(true)] out TContract contract
return TryGetContract(out contract, out _);
}
+ ///
+ /// Determines whether this cDAC can provide the requested contract for the target. Implementations
+ /// should resolve the target-advertised version against the registered implementations only, without
+ /// instantiating the contract, so that validation performs no target memory reads (safe on partial or
+ /// triage dumps) and never triggers contract-to-contract chaining.
+ ///
+ /// The contract type to validate.
+ ///
+ /// When this method returns , contains the exception describing why the
+ /// contract cannot be provided; otherwise, .
+ ///
+ ///
+ /// if the contract is advertised by the target and a matching implementation
+ /// is registered; otherwise, .
+ ///
+ ///
+ /// The default implementation delegates to ,
+ /// which instantiates the contract and therefore does read target memory. Registries that need the
+ /// no-instantiation guarantee above must override this method (CachingContractRegistry does).
+ ///
+ public virtual bool TryValidate([NotNullWhen(false)] out System.Exception? failureException) where TContract : IContract
+ {
+ return TryGetContract(out _, out failureException);
+ }
+
///
/// Register a contract implementation for a specific version.
/// External packages use this to add contract versions or entirely new contract interfaces.
@@ -186,6 +211,12 @@ public bool TryGetContract([NotNullWhen(true)] out TContract contract
public abstract void Register(string version, Func creator)
where TContract : IContract;
+ ///
+ /// Register a contract version that is recognized but intentionally not implemented.
+ ///
+ public abstract void RegisterUnsupported(string version)
+ where TContract : IContract;
+
///
/// Flush all cached data held by contracts in this registry for the given
/// . Called when the target process state may have changed
diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs
index 1456316dc93bce..3720399748bf24 100644
--- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs
+++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs
@@ -339,6 +339,16 @@ public readonly record struct FieldInfo
///
public abstract ContractRegistry Contracts { get; }
+ ///
+ /// when the named sub-descriptor has been published by the target and
+ /// parsed into the registry (or was never advertised). A sub-descriptor is written lazily by a
+ /// subordinate module (for example the GC), so a target attached very early - before that module
+ /// publishes its sub-descriptor address (see dotnet/runtime#128215) - reports
+ /// for that name until a later picks it up.
+ ///
+ /// The sub-descriptor name (for example "GC").
+ public virtual bool IsSubDescriptorResolved(string name) => true;
+
///
/// Clear cached data held by this target for the given .
/// Called when the target process state may have changed (e.g. on resume).
diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs
index c654da1595ab90..0b0173b118cf7f 100644
--- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs
+++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs
@@ -80,4 +80,125 @@ public static void Register(ContractRegistry registry)
registry.Register("c1", static t => new RuntimeMutableTypeSystem_1(t));
}
+
+ ///
+ /// Eagerly validates that every contract required by the cDAC data-access interfaces can be
+ /// provided for the target. Contract availability is checked without instantiating the
+ /// contracts; is read to determine the target operating system so
+ /// that OS-specific contracts are validated only when the target platform actually uses them.
+ /// In-box (main-descriptor) contracts are required unconditionally. Contracts published by a
+ /// sub-descriptor are version-checked always, but their absence is tolerated while their
+ /// sub-descriptor is still pending.
+ ///
+ /// The target being validated (source of the contract registry and
+ /// sub-descriptor resolution state).
+ ///
+ /// Thrown for the first required contract that cannot be provided. The concrete exception type
+ /// and its identify the failure:
+ /// /
+ /// if the target does not advertise a required contract,
+ /// /
+ /// if the advertised version is unknown to this cDAC, or
+ /// /
+ /// if the advertised version is recognized but intentionally unimplemented.
+ ///
+ public static void ValidateForDataAccess(Target target)
+ {
+ ContractRegistry registry = target.Contracts;
+
+ // In-box (main-descriptor) contract accesses across the ISOSDac* and IXCLRData* surface that
+ // SOSDacImpl exposes. These live in the main descriptor, present as soon as the runtime module
+ // is loaded, so they are required eagerly and unconditionally - a genuinely-missing one is a
+ // serviceability failure even at early attach. IObjectiveCMarshal is intentionally omitted:
+ // SOS reaches it through TryGetContract so its absence degrades gracefully rather than faulting.
+ Validate(registry);
+ Validate(registry);
+ Validate(registry);
+ Validate(registry);
+ Validate(registry);
+ Validate(registry);
+ Validate(registry);
+ Validate(registry);
+ Validate(registry);
+ Validate(registry);
+ Validate(registry);
+ Validate(registry);
+ Validate(registry);
+ Validate(registry);
+ Validate(registry);
+ Validate(registry);
+ Validate(registry);
+ Validate(registry);
+ Validate(registry);
+ Validate(registry);
+ Validate(registry);
+ Validate(registry);
+ Validate(registry);
+
+ // Transitive contract accesses from the implementations above.
+ Validate(registry); // IComWrappers: ComWrappers_1.cs
+ Validate(registry); // IStackWalk: StackWalk_1.cs
+ Validate(registry); // IAuxiliarySymbols/IPrecodeStubs: CodePointerUtils.cs, PrecodeStubs_Common.cs
+ Validate(registry); // ILoader: Loader_1.cs
+
+ // Operating-system-specific in-box contracts, gated on the target's platform. IRuntimeInfo is
+ // in the main descriptor (present at attach), so reading the OS here is safe. These contracts
+ // are advertised only where the runtime is built for that platform, so the gate keeps them
+ // from being required where the runtime never advertises them - this is genuine absence, not a
+ // sub-descriptor deferral.
+ RuntimeInfoOperatingSystem targetOperatingSystem = registry.RuntimeInfo.GetTargetOperatingSystem();
+ if (targetOperatingSystem == RuntimeInfoOperatingSystem.Windows)
+ {
+ // IBuiltInCOM is only advertised on runtimes built with classic COM interop (Windows).
+ Validate(registry); // SOSDacImpl.cs GetCCWData/GetRCWData/etc.
+ Validate(registry); // SOSDacImpl.cs GetClrWatsonBuckets
+ }
+
+ // DBI-only in-box contract: used by the DacDbi path (Legacy/Dbi/DacDbiImpl.cs).
+ // Since cDAC is all-or-nothing for some tools debugger, a target either
+ // exposes a fully serviceable set of contracts, or we fall back/fail.
+ Validate(registry); // DacDbiImpl.cs (edit-and-continue mutable type system)
+
+ // Sub-descriptor-provided contracts. Only validated if they have been published.
+ // A version this cDAC cannot service is always rejected, but a missing one is rejected only once the
+ // sub-descriptor is resolved. Defer and let the tool APIs see a degradation to E_NOTIMPL.
+ ValidateSubDescriptorContract(target);
+
+ static void Validate(ContractRegistry registry) where TContract : IContract
+ {
+ if (registry.TryValidate(out System.Exception? failure))
+ {
+ return;
+ }
+
+ // TryValidate reports the failure through a contract availability exception that already
+ // carries the appropriate cDAC HRESULT, so rethrow it directly. The null-coalescing arm
+ // only guards a registry that violates the TryValidate contract (false without a failure).
+ throw failure ?? new ContractNotAvailableException(
+ TContract.Name,
+ contractVersion: null,
+ message: $"Contract '{TContract.Name}' validation failed but no reason was reported.");
+ }
+
+ static void ValidateSubDescriptorContract(Target target) where TContract : IContract
+ {
+ if (target.Contracts.TryValidate(out System.Exception? failure))
+ {
+ return;
+ }
+
+ // A version this cDAC cannot service (unrecognized or obsolete) is always a failure, even
+ // during early attach. A not-advertised contract is a failure only once the sub-descriptor
+ // that publishes it has resolved; while that provider is still pending the contract may yet
+ // be published, so defer rather than fail creation.
+ bool providerResolved = target.IsSubDescriptorResolved(TContract.Name);
+ if (failure is ContractUnrecognizedException or ContractObsoleteException || providerResolved)
+ {
+ throw failure ?? new ContractNotAvailableException(
+ TContract.Name,
+ contractVersion: null,
+ message: $"Contract '{TContract.Name}' validation failed but no reason was reported.");
+ }
+ }
+ }
}
diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/CachingContractRegistry.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/CachingContractRegistry.cs
index 4ca7582501f6b5..285d50712d3aa6 100644
--- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/CachingContractRegistry.cs
+++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/CachingContractRegistry.cs
@@ -19,6 +19,7 @@ internal sealed class CachingContractRegistry : ContractRegistry
private readonly Dictionary _contracts = [];
private readonly Dictionary<(Type, string), Func> _creators = [];
+ private readonly HashSet<(Type, string)> _unsupportedVersions = [];
private readonly Target _target;
private readonly TryGetContractVersionDelegate _tryGetContractVersion;
@@ -38,28 +39,23 @@ public override void Register(string version, Func
_creators[(typeof(TContract), version)] = t => creator(t);
}
- public override bool TryGetContract([NotNullWhen(true)] out TContract contract, out string? failureReason)
+ public override void RegisterUnsupported(string version)
+ {
+ _unsupportedVersions.Add((typeof(TContract), version));
+ }
+
+ public override bool TryGetContract([NotNullWhen(true)] out TContract contract, [NotNullWhen(false)] out System.Exception? failureException)
{
contract = default!;
- failureReason = null;
+ failureException = null;
if (_contracts.TryGetValue(typeof(TContract), out IContract? cached))
{
contract = (TContract)cached;
return true;
}
- Func? creator;
- if (_tryGetContractVersion(TContract.Name, out string? version))
- {
- if (!_creators.TryGetValue((typeof(TContract), version), out creator))
- {
- failureReason = $"Target supports contract '{typeof(TContract).Name}' version {version}, but no implementation is registered for that version.";
- return false;
- }
- }
- else if (!_creators.TryGetValue((typeof(TContract), string.Empty), out creator))
+ if (!TryResolveCreator(typeof(TContract), TContract.Name, out Func? creator, out failureException))
{
- failureReason = $"Target does not support contract '{typeof(TContract).Name}'.";
return false;
}
@@ -73,6 +69,61 @@ public override bool TryGetContract([NotNullWhen(true)] out TContract
return true;
}
+ public override bool TryValidate([NotNullWhen(false)] out System.Exception? failureException)
+ {
+ failureException = null;
+
+ // An already-instantiated contract is, by definition, supported.
+ if (_contracts.ContainsKey(typeof(TContract)))
+ {
+ return true;
+ }
+
+ // Presence-only validation: confirm this cDAC can provide the contract at the version the
+ // target advertises, but never invoke the creator. Instantiating a contract can read target
+ // memory and chain into other contracts, and a partial capture (for example a minidump) may
+ // legitimately be missing the data a given contract needs while still being fully usable for
+ // others (stack walks and similar). Reading target memory here could spuriously fail
+ // validation and reject an otherwise serviceable dump, so resolve the creator without running it.
+ return TryResolveCreator(typeof(TContract), TContract.Name, out _, out failureException);
+ }
+
+ ///
+ /// Classifies whether a registered creator exists for the target-advertised version of a
+ /// contract, without invoking it. Shared by
+ /// and .
+ ///
+ private bool TryResolveCreator(
+ Type contractType,
+ string contractName,
+ [NotNullWhen(true)] out Func? creator,
+ [NotNullWhen(false)] out System.Exception? failureException)
+ {
+ creator = null;
+ failureException = null;
+
+ if (!_tryGetContractVersion(contractName, out string? version))
+ {
+ if (_creators.TryGetValue((contractType, string.Empty), out creator))
+ {
+ return true;
+ }
+
+ failureException = new ContractMissingException(contractName);
+ return false;
+ }
+
+ if (!_creators.TryGetValue((contractType, version), out creator))
+ {
+ failureException = _unsupportedVersions.Contains((contractType, version))
+ ? new ContractObsoleteException(contractName, version)
+ : new ContractUnrecognizedException(contractName, version);
+ return false;
+ }
+
+ return true;
+ }
+
public override void Flush(FlushScope scope)
{
foreach (IContract contract in _contracts.Values)
diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs
index c9e6f53c922ba4..60de4df06858ae 100644
--- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs
+++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs
@@ -9,6 +9,7 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
+using System.Text.Json;
using Microsoft.Diagnostics.DataContractReader.Data;
using Microsoft.Diagnostics.DataContractReader.Contracts;
using System.Collections.Frozen;
@@ -45,7 +46,9 @@ private readonly struct Configuration
// pointer slot only ever transitions null -> real-address. Once a slot holds a
// non-null sub-descriptor address it never changes again, so we never need to
// re-validate already-loaded sub-descriptors and we can safely drop a slot from this.
- private readonly List _pendingSubDescriptors = [];
+ // Each entry keeps the sub-descriptor's name so a specific provider (for example the GC) can be
+ // queried for whether it has been published yet (IsSubDescriptorResolved).
+ private readonly List<(string Name, TargetPointer Slot)> _pendingSubDescriptors = [];
private IReadOnlyDictionary _contracts = new Dictionary();
private IReadOnlyDictionary _globals = new Dictionary();
@@ -54,6 +57,19 @@ private readonly struct Configuration
public override ContractRegistry Contracts { get; }
public override DataCache ProcessedData { get; }
+ // A named sub-descriptor is resolved once its slot has been parsed (dropped from the pending
+ // list) - or was never advertised at all. A slot that is still pending (the subordinate module
+ // has not published its sub-descriptor address yet) reports false until a later Flush picks it up.
+ public override bool IsSubDescriptorResolved(string name)
+ {
+ foreach ((string pendingName, TargetPointer _) in _pendingSubDescriptors)
+ {
+ if (pendingName == name)
+ return false;
+ }
+ return true;
+ }
+
public delegate int ReadFromTargetDelegate(ulong address, Span bufferToFill);
public delegate int WriteToTargetDelegate(ulong address, Span bufferToWrite);
public delegate int GetTargetThreadContextDelegate(uint threadId, uint contextFlags, Span bufferToFill);
@@ -73,30 +89,19 @@ private readonly struct Configuration
/// A callback to set a thread's context
/// A callback to allocate virtual memory in the target
/// Registration actions that populate the contract registry (e.g., )
- /// The target object.
- /// If a target instance could be created, true; otherwise, false.
- public static bool TryCreate(
+ /// The target object.
+ public static ContractDescriptorTarget Create(
ulong contractDescriptor,
ReadFromTargetDelegate readFromTarget,
WriteToTargetDelegate writeToTarget,
GetTargetThreadContextDelegate getThreadContext,
SetTargetThreadContextDelegate setThreadContext,
AllocVirtualDelegate allocVirtual,
- Action[] contractRegistrations,
- [NotNullWhen(true)] out ContractDescriptorTarget? target)
+ Action[] contractRegistrations)
{
DataTargetDelegates dataTargetDelegates = new DataTargetDelegates(readFromTarget, writeToTarget, getThreadContext, setThreadContext, allocVirtual);
- if (TryReadContractDescriptor(
- contractDescriptor,
- dataTargetDelegates,
- out Descriptor descriptor))
- {
- target = new ContractDescriptorTarget(descriptor, dataTargetDelegates, contractRegistrations);
- return true;
- }
-
- target = null;
- return false;
+ Descriptor descriptor = ReadContractDescriptor(contractDescriptor, dataTargetDelegates);
+ return new ContractDescriptorTarget(descriptor, dataTargetDelegates, contractRegistrations);
}
///
@@ -158,12 +163,12 @@ public override void Flush(FlushScope scope)
private void AddDescriptor(Descriptor descriptor)
{
_descriptors.Add(descriptor);
- foreach (TargetPointer pSubDescriptor in GetSubDescriptors(descriptor))
+ foreach ((string name, TargetPointer pSubDescriptor) in GetSubDescriptors(descriptor))
{
if (pSubDescriptor == TargetPointer.Null)
continue;
- _pendingSubDescriptors.Add(pSubDescriptor);
+ _pendingSubDescriptors.Add((name, pSubDescriptor));
}
}
@@ -179,19 +184,14 @@ private void BuildDescriptors(bool forceBuild = false)
for (int i = _pendingSubDescriptors.Count - 1; i >= 0; i--)
{
- TargetPointer pendingSubDescriptor = _pendingSubDescriptors[i];
+ (_, TargetPointer pendingSubDescriptor) = _pendingSubDescriptors[i];
if (TryReadPointer(pendingSubDescriptor, out TargetPointer subDescriptorAddress)
&& subDescriptorAddress != TargetPointer.Null)
{
_pendingSubDescriptors.RemoveAt(i);
- if (TryReadContractDescriptor(
- subDescriptorAddress.Value,
- _dataTargetDelegates,
- out Descriptor subDescriptor))
- {
- AddDescriptor(subDescriptor);
- }
+ Descriptor subDescriptor = ReadContractDescriptor(subDescriptorAddress.Value, _dataTargetDelegates);
+ AddDescriptor(subDescriptor);
}
}
} while (_descriptors.Count > loopDescriptorCount);
@@ -216,14 +216,16 @@ private void BuildDescriptors(bool forceBuild = false)
{
if (descriptor.Config.IsLittleEndian != _config.IsLittleEndian ||
descriptor.Config.PointerSize != _config.PointerSize)
- throw new InvalidOperationException("All descriptors must have the same endianness and pointer size.");
+ {
+ throw DescriptorMalformed("All descriptors must have the same endianness and pointer size.");
+ }
// Read contracts and add to map
foreach ((string name, string version) in descriptor.ContractDescriptor.Contracts ?? [])
{
if (contracts.ContainsKey(name))
{
- throw new InvalidOperationException($"Duplicate contract name '{name}' found in contract descriptor.");
+ throw DescriptorMalformed($"Duplicate contract name '{name}' found in contract descriptor.");
}
contracts[name] = version;
}
@@ -249,7 +251,7 @@ private void BuildDescriptors(bool forceBuild = false)
if (seenTypeNames.Contains(name))
{
- throw new InvalidOperationException($"Duplicate type name '{name}' found in contract descriptor.");
+ throw DescriptorMalformed($"Duplicate type name '{name}' found in contract descriptor.");
}
seenTypeNames.Add(name);
@@ -263,14 +265,14 @@ private void BuildDescriptors(bool forceBuild = false)
foreach ((string name, ContractDescriptorParser.GlobalDescriptor global) in descriptor.ContractDescriptor.Globals)
{
if (seenGlobalNames.Contains(name))
- throw new InvalidOperationException($"Duplicate global name '{name}' found in contract descriptor.");
+ throw DescriptorMalformed($"Duplicate global name '{name}' found in contract descriptor.");
seenGlobalNames.Add(name);
if (global.Indirect)
{
if (global.NumericValue.Value >= (ulong)descriptor.PointerData.Length)
- throw new InvalidOperationException($"Invalid pointer data index {global.NumericValue.Value}.");
+ throw DescriptorMalformed($"Invalid pointer data index {global.NumericValue.Value}.");
globals[name] = new GlobalValue
{
@@ -311,43 +313,50 @@ private struct Descriptor
public TargetPointer[] PointerData { get; init; }
}
- private static IEnumerable GetSubDescriptors(Descriptor descriptor)
+ private static IEnumerable<(string Name, TargetPointer Slot)> GetSubDescriptors(Descriptor descriptor)
{
foreach (KeyValuePair subDescriptor in descriptor.ContractDescriptor?.SubDescriptors ?? [])
{
if (subDescriptor.Value.Indirect)
{
if (subDescriptor.Value.NumericValue.Value >= (ulong)descriptor.PointerData.Length)
- throw new InvalidOperationException($"Invalid pointer data index {subDescriptor.Value.NumericValue.Value}.");
+ throw DescriptorMalformed($"Invalid pointer data index {subDescriptor.Value.NumericValue.Value}.");
- yield return descriptor.PointerData[(int)subDescriptor.Value.NumericValue];
+ yield return (subDescriptor.Key, descriptor.PointerData[(int)subDescriptor.Value.NumericValue]);
}
}
}
// See docs/design/datacontracts/contract-descriptor.md
- private static bool TryReadContractDescriptor(
+ // Failure constructing a target from its contract descriptor surfaces as a FormatException so
+ // existing callers and tests keep working, but the HResult is set to a cDAC-specific code so
+ // tooling can distinguish "no descriptor / not a cDAC target" from "descriptor present but
+ // corrupt". The boundary entry points propagate Exception.HResult when it is a failure code.
+ private static FormatException DescriptorNotFound(string message) =>
+ new(message) { HResult = CdacHResults.CDAC_E_DESCRIPTOR_NOT_FOUND };
+
+ private static FormatException DescriptorMalformed(string message, System.Exception? innerException = null) =>
+ new(message, innerException) { HResult = CdacHResults.CDAC_E_DESCRIPTOR_MALFORMED };
+
+ private static Descriptor ReadContractDescriptor(
ulong address,
- DataTargetDelegates dataTargetDelegates,
- out Descriptor descriptor)
+ DataTargetDelegates dataTargetDelegates)
{
- descriptor = default;
-
// Magic - uint64_t
Span buffer = stackalloc byte[sizeof(ulong)];
if (dataTargetDelegates.ReadFromTarget(address, buffer) < 0)
- return false;
+ throw DescriptorNotFound($"Failed to read contract descriptor header at 0x{address:x8}.");
address += sizeof(ulong);
ReadOnlySpan magicLE = "DNCCDAC\0"u8;
ReadOnlySpan magicBE = "\0CADCCND"u8;
bool isLittleEndian = buffer.SequenceEqual(magicLE);
if (!isLittleEndian && !buffer.SequenceEqual(magicBE))
- return false;
+ throw DescriptorNotFound("Contract descriptor has an invalid magic value.");
// Flags - uint32_t
if (!TryRead(address, isLittleEndian, dataTargetDelegates, out uint flags))
- return false;
+ throw DescriptorMalformed($"Failed to read contract descriptor flags at 0x{address:x8}.");
address += sizeof(uint);
@@ -358,19 +367,19 @@ private static bool TryReadContractDescriptor(
// Descriptor size - uint32_t
if (!TryRead(address, config.IsLittleEndian, dataTargetDelegates, out uint descriptorSize))
- return false;
+ throw DescriptorMalformed($"Failed to read contract descriptor size at 0x{address:x8}.");
address += sizeof(uint);
// Descriptor - char*
if (!TryReadPointer(address, config, dataTargetDelegates, out TargetPointer descriptorAddr))
- return false;
+ throw DescriptorMalformed($"Failed to read contract descriptor JSON pointer at 0x{address:x8}.");
address += (uint)pointerSize;
// Pointer data count - uint32_t
if (!TryRead(address, config.IsLittleEndian, dataTargetDelegates, out uint pointerDataCount))
- return false;
+ throw DescriptorMalformed($"Failed to read contract descriptor pointer data count at 0x{address:x8}.");
address += sizeof(uint);
@@ -379,35 +388,52 @@ private static bool TryReadContractDescriptor(
// Pointer data - uintptr_t*
if (!TryReadPointer(address, config, dataTargetDelegates, out TargetPointer pointerDataAddr))
- return false;
+ throw DescriptorMalformed($"Failed to read contract descriptor pointer data pointer at 0x{address:x8}.");
// Read descriptor
+ if (descriptorSize > int.MaxValue)
+ throw DescriptorMalformed($"Contract descriptor size {descriptorSize} is too large.");
+
Span descriptorBuffer = descriptorSize <= StackAllocByteThreshold
? stackalloc byte[(int)descriptorSize]
: new byte[(int)descriptorSize];
if (dataTargetDelegates.ReadFromTarget(descriptorAddr.Value, descriptorBuffer) < 0)
- return false;
+ throw DescriptorMalformed($"Failed to read contract descriptor JSON at 0x{descriptorAddr.Value:x8}.");
- ContractDescriptorParser.ContractDescriptor? contractDescriptor = ContractDescriptorParser.ParseCompact(descriptorBuffer);
+ ContractDescriptorParser.ContractDescriptor? contractDescriptor;
+ try
+ {
+ contractDescriptor = ContractDescriptorParser.ParseCompact(descriptorBuffer);
+ }
+ catch (JsonException ex)
+ {
+ throw DescriptorMalformed("Failed to parse contract descriptor JSON.", ex);
+ }
+ catch (InvalidOperationException ex)
+ {
+ throw DescriptorMalformed("Failed to parse contract descriptor JSON.", ex);
+ }
if (contractDescriptor is null)
- return false;
+ throw DescriptorMalformed("Contract descriptor JSON parsed to null.");
// Read pointer data
- TargetPointer[] pointerData = new TargetPointer[pointerDataCount];
- for (int i = 0; i < pointerDataCount; i++)
+ if (pointerDataCount > int.MaxValue)
+ throw DescriptorMalformed($"Contract descriptor pointer data count {pointerDataCount} is too large.");
+
+ int pointerDataLength = (int)pointerDataCount;
+ TargetPointer[] pointerData = new TargetPointer[pointerDataLength];
+ for (int i = 0; i < pointerDataLength; i++)
{
if (!TryReadPointer(pointerDataAddr.Value + (uint)(i * pointerSize), config, dataTargetDelegates, out pointerData[i]))
- return false;
+ throw DescriptorMalformed($"Failed to read contract descriptor pointer data entry {i}.");
}
- descriptor = new Descriptor
+ return new Descriptor
{
Config = config,
ContractDescriptor = contractDescriptor,
PointerData = pointerData
};
-
- return true;
}
public override int PointerSize => _config.PointerSize;
diff --git a/src/native/managed/cdac/mscordaccore_universal/Entrypoints.cs b/src/native/managed/cdac/mscordaccore_universal/Entrypoints.cs
index 52378dd3a7c87a..c9f3db79c5e7f0 100644
--- a/src/native/managed/cdac/mscordaccore_universal/Entrypoints.cs
+++ b/src/native/managed/cdac/mscordaccore_universal/Entrypoints.cs
@@ -85,8 +85,7 @@ private static unsafe int Init(
};
}
- // TODO: [cdac] Better error code/details
- if (!ContractDescriptorTarget.TryCreate(
+ ContractDescriptorTarget target = ContractDescriptorTarget.Create(
descriptor,
(address, buffer) =>
{
@@ -130,9 +129,7 @@ private static unsafe int Init(
},
setThreadContextDelegate,
allocDelegate,
- [Contracts.CoreCLRContracts.Register],
- out ContractDescriptorTarget? target))
- return -1;
+ [Contracts.CoreCLRContracts.Register]);
GCHandle gcHandle = GCHandle.Alloc(target);
*handle = GCHandle.ToIntPtr(gcHandle);
@@ -184,6 +181,12 @@ private static unsafe int CreateSosInterface(IntPtr handle, IntPtr legacyImplPtr
object? legacyImpl = legacyImplPtr != IntPtr.Zero
? ComInterfaceMarshaller.ConvertToManaged((void*)legacyImplPtr)
: null;
+
+ // Without a legacy implementation to absorb individually-unimplemented APIs, validate
+ // the complete data-access contract set before publishing the interface.
+ if (legacyImpl is null)
+ Contracts.CoreCLRContracts.ValidateForDataAccess(target);
+
Legacy.SOSDacImpl impl = new(target, legacyImpl);
nint ptr = (nint)ComInterfaceMarshaller.ConvertToUnmanaged(impl);
*obj = ptr;
@@ -351,7 +354,10 @@ private static unsafe int CLRDataCreateInstanceCore(Guid* pIID, IntPtr /*ICLRDat
if (hr != 0)
{
throw new InvalidOperationException(
- $"{nameof(ICLRContractLocator)} failed to fetch the contract descriptor with HRESULT: 0x{hr:x}.");
+ $"{nameof(ICLRContractLocator)} failed to fetch the contract descriptor with HRESULT: 0x{hr:x}.")
+ {
+ HResult = CdacHResults.CDAC_E_DESCRIPTOR_NOT_FOUND
+ };
}
return CreateInstanceFromContractDescriptorCore(pIID, legacyTarget, contractAddress, legacyImpl, iface);
@@ -387,7 +393,7 @@ private static unsafe int CreateInstanceFromContractDescriptorCore(Guid* pIID, o
};
}
- if (!ContractDescriptorTarget.TryCreate(
+ ContractDescriptorTarget target = ContractDescriptorTarget.Create(
contractAddress,
(address, buffer) =>
{
@@ -434,11 +440,12 @@ private static unsafe int CreateInstanceFromContractDescriptorCore(Guid* pIID, o
}
},
allocVirtual,
- [Contracts.CoreCLRContracts.Register],
- out ContractDescriptorTarget? target))
- {
- return -1;
- }
+ [Contracts.CoreCLRContracts.Register]);
+
+ // Without a legacy implementation to absorb individually-unimplemented APIs, validate
+ // the complete data-access contract set before publishing the interface.
+ if (legacyImpl is null)
+ Contracts.CoreCLRContracts.ValidateForDataAccess(target);
Legacy.SOSDacImpl impl = new(target, legacyImpl);
void* ccw = ComInterfaceMarshaller.ConvertToUnmanaged(impl);
@@ -460,7 +467,7 @@ private static unsafe ContractDescriptorTarget CreateTargetFromCorDebugDataTarge
$"Data target does not implement {nameof(ICorDebugDataTarget)}", nameof(targetObject));
ICorDebugMutableDataTarget? mutableDataTarget = targetObject as ICorDebugMutableDataTarget;
- if (!ContractDescriptorTarget.TryCreate(
+ return ContractDescriptorTarget.Create(
contractAddress,
(address, buffer) =>
{
@@ -542,13 +549,6 @@ private static unsafe ContractDescriptorTarget CreateTargetFromCorDebugDataTarge
allocatedAddress = 0;
return HResults.E_NOTIMPL;
},
- [Contracts.CoreCLRContracts.Register],
- out ContractDescriptorTarget? target))
- {
- throw new InvalidOperationException(
- $"Failed to create a {nameof(ContractDescriptorTarget)} from the contract descriptor at 0x{contractAddress:x}.");
- }
-
- return target!;
+ [Contracts.CoreCLRContracts.Register]);
}
}
diff --git a/src/native/managed/cdac/scripts/DumpHelpers.cs b/src/native/managed/cdac/scripts/DumpHelpers.cs
index e4b1c93d7e7326..9f7c6eedaeb7d7 100644
--- a/src/native/managed/cdac/scripts/DumpHelpers.cs
+++ b/src/native/managed/cdac/scripts/DumpHelpers.cs
@@ -48,19 +48,14 @@ public static ContractDescriptorTarget CreateCdacTarget(DataTarget dt)
{
ulong contractAddr = FindContractDescriptor(dt);
- if (!ContractDescriptorTarget.TryCreate(
- contractAddr,
- (ulong address, Span buffer) => dt.DataReader.Read(address, buffer) == buffer.Length ? 0 : -1,
- (ulong address, Span buffer) => -1,
- (uint threadId, uint contextFlags, Span buffer) =>
- dt.DataReader.GetThreadContext(threadId, contextFlags, buffer) ? 0 : -1,
- (uint threadId, ReadOnlySpan context) => -1,
- [CoreCLRContracts.Register],
- out ContractDescriptorTarget? target))
- {
- throw new InvalidOperationException("Failed to create cDAC target.");
- }
-
- return target;
+ return ContractDescriptorTarget.Create(
+ contractAddr,
+ (ulong address, Span buffer) => dt.DataReader.Read(address, buffer) == buffer.Length ? 0 : -1,
+ (ulong address, Span buffer) => -1,
+ (uint threadId, uint contextFlags, Span buffer) =>
+ dt.DataReader.GetThreadContext(threadId, contextFlags, buffer) ? 0 : -1,
+ (uint threadId, ReadOnlySpan context) => -1,
+ (ulong _, out ulong _) => throw new NotImplementedException("Scripts do not provide AllocVirtual"),
+ [CoreCLRContracts.Register]);
}
}
diff --git a/src/native/managed/cdac/tests/DataGenerator/TestTarget.cs b/src/native/managed/cdac/tests/DataGenerator/TestTarget.cs
index 6d6f6544b17575..da18f2b7abc335 100644
--- a/src/native/managed/cdac/tests/DataGenerator/TestTarget.cs
+++ b/src/native/managed/cdac/tests/DataGenerator/TestTarget.cs
@@ -271,22 +271,25 @@ public TestContractRegistry(IManagedTypeSource managedTypeSource)
public override IManagedTypeSource ManagedTypeSource => _managedTypeSource;
- public override bool TryGetContract([NotNullWhen(true)] out TContract contract, out string? failureReason)
+ public override bool TryGetContract([NotNullWhen(true)] out TContract contract, [NotNullWhen(false)] out System.Exception? failureException)
{
if (typeof(TContract) == typeof(IManagedTypeSource))
{
contract = (TContract)_managedTypeSource;
- failureReason = null;
+ failureException = null;
return true;
}
contract = default!;
- failureReason = "Not registered in TestContractRegistry.";
+ failureException = new ContractMissingException(TContract.Name);
return false;
}
public override void Register(string version, Func creator)
=> throw new NotImplementedException();
+ public override void RegisterUnsupported(string version)
+ => throw new NotImplementedException();
+
public override void Flush(FlushScope scope) { }
}
diff --git a/src/native/managed/cdac/tests/TestInfrastructure/ContractDescriptor/ContractDescriptorBuilder.cs b/src/native/managed/cdac/tests/TestInfrastructure/ContractDescriptor/ContractDescriptorBuilder.cs
index e525d16c6eb865..8c18c5181f679f 100644
--- a/src/native/managed/cdac/tests/TestInfrastructure/ContractDescriptor/ContractDescriptorBuilder.cs
+++ b/src/native/managed/cdac/tests/TestInfrastructure/ContractDescriptor/ContractDescriptorBuilder.cs
@@ -29,13 +29,16 @@ public class DescriptorBuilder(ContractDescriptorBuilder parent)
private bool _created;
private readonly ContractDescriptorBuilder _parent = parent;
- private IReadOnlyCollection? _contracts;
+ private IReadOnlyDictionary? _contracts;
private IDictionary? _types;
private IReadOnlyCollection<(string Name, ulong? Value, uint? IndirectIndex, string? StringValue, string? TypeName)>? _globals;
private IReadOnlyCollection<(string Name, ulong? Value, uint? IndirectIndex, string? StringValue, string? TypeName)>? _subDescriptors;
private IReadOnlyCollection? _indirectValues;
public DescriptorBuilder SetContracts(IReadOnlyCollection contracts)
+ => SetContracts(contracts.ToDictionary(static c => c, static _ => "c1"));
+
+ public DescriptorBuilder SetContracts(IReadOnlyDictionary contracts)
{
if (_contracts is not null)
throw new InvalidOperationException("Contracts already set");
@@ -139,9 +142,9 @@ private string MakeContractsJson()
if (_contracts is null || _contracts.Count == 0)
return string.Empty;
StringBuilder sb = new();
- foreach (var c in _contracts)
+ foreach ((string name, string version) in _contracts)
{
- sb.Append($"\"{c}\": \"c1\",");
+ sb.Append($"\"{name}\": \"{version}\",");
}
Debug.Assert(sb.Length > 0);
sb.Length--; // remove trailing comma
@@ -202,13 +205,78 @@ private string MakeContractsJson()
}
}
- public bool TryCreateTarget(DescriptorBuilder descriptor, [NotNullWhen(true)] out ContractDescriptorTarget? target, Action[]? contractRegistrations = null)
+ public bool TryCreateTarget(
+ DescriptorBuilder descriptor,
+ [NotNullWhen(true)] out ContractDescriptorTarget? target,
+ params Action[] additionalContractRegistrations)
+ {
+ try
+ {
+ target = CreateTarget(descriptor, additionalContractRegistrations);
+ return true;
+ }
+ catch (Exception)
+ {
+ target = null;
+ return false;
+ }
+ }
+
+ public ContractDescriptorTarget CreateTarget(DescriptorBuilder descriptor, params Action[] additionalContractRegistrations)
{
if (_created)
throw new InvalidOperationException("Context already created");
_created = true;
ulong contractDescriptorAddress = descriptor.CreateSubDescriptor(ContractDescriptorAddr, JsonDescriptorAddr, ContractPointerDataAddr);
MockMemorySpace.MemoryContext memoryContext = GetMemoryContext();
- return ContractDescriptorTarget.TryCreate(contractDescriptorAddress, memoryContext.ReadFromTarget, memoryContext.WriteToTarget, (_, _, _) => throw new NotImplementedException("Tests do not provide GetTargetThreadContext"), (_, _) => throw new NotImplementedException("Tests do not provide SetTargetThreadContext"), (ulong _, out ulong _) => throw new NotImplementedException("Tests do not provide AllocVirtual"), contractRegistrations ?? [Contracts.CoreCLRContracts.Register], out target);
+ Action[] contractRegistrations = [Contracts.CoreCLRContracts.Register, .. additionalContractRegistrations];
+
+ return ContractDescriptorTarget.Create(
+ contractDescriptorAddress,
+ memoryContext.ReadFromTarget,
+ memoryContext.WriteToTarget,
+ (_, _, _) => throw new NotImplementedException("Tests do not provide GetTargetThreadContext"),
+ (_, _) => throw new NotImplementedException("Tests do not provide SetTargetThreadContext"),
+ (ulong _, out ulong _) => throw new NotImplementedException("Tests do not provide AllocVirtual"),
+ contractRegistrations);
+ }
+
+ public ContractDescriptorTarget CreateTargetFromRawDescriptor(byte[] descriptor, byte[] descriptorJson, byte[] pointerData)
+ {
+ if (_created)
+ throw new InvalidOperationException("Context already created");
+ _created = true;
+
+ AddHeapFragment(new MockMemorySpace.HeapFragment
+ {
+ Address = ContractDescriptorAddr,
+ Data = descriptor,
+ Name = "ContractDescriptor"
+ });
+ AddHeapFragment(new MockMemorySpace.HeapFragment
+ {
+ Address = JsonDescriptorAddr,
+ Data = descriptorJson,
+ Name = "JsonDescriptor"
+ });
+ if (pointerData.Length > 0)
+ {
+ AddHeapFragment(new MockMemorySpace.HeapFragment
+ {
+ Address = ContractPointerDataAddr,
+ Data = pointerData,
+ Name = "PointerData"
+ });
+ }
+
+ MockMemorySpace.MemoryContext memoryContext = GetMemoryContext();
+ return ContractDescriptorTarget.Create(
+ ContractDescriptorAddr,
+ memoryContext.ReadFromTarget,
+ memoryContext.WriteToTarget,
+ (_, _, _) => throw new NotImplementedException("Tests do not provide GetTargetThreadContext"),
+ (_, _) => throw new NotImplementedException("Tests do not provide SetTargetThreadContext"),
+ (ulong _, out ulong _) => throw new NotImplementedException("Tests do not provide AllocVirtual"),
+ [Contracts.CoreCLRContracts.Register]);
}
}
diff --git a/src/native/managed/cdac/tests/TestInfrastructure/DumpTestBase.cs b/src/native/managed/cdac/tests/TestInfrastructure/DumpTestBase.cs
index c98afbe8b41c96..865402813cb6b9 100644
--- a/src/native/managed/cdac/tests/TestInfrastructure/DumpTestBase.cs
+++ b/src/native/managed/cdac/tests/TestInfrastructure/DumpTestBase.cs
@@ -108,17 +108,14 @@ protected void InitializeDumpTest(TestConfiguration config, string debuggeeName,
_host = ClrMdDumpHost.Open(dumpPath, GetSymbolPaths(debuggeeName, versionDir));
ulong contractDescriptor = _host.FindContractDescriptorAddress();
- bool created = ContractDescriptorTarget.TryCreate(
+ _target = ContractDescriptorTarget.Create(
contractDescriptor,
_host.ReadFromTarget,
writeToTarget: static (_, _) => -1,
_host.GetThreadContext,
setThreadContext: static (_, _) => -1,
allocVirtual: static (ulong _, out ulong _) => throw new NotImplementedException("Dump tests do not provide AllocVirtual"),
- [Contracts.CoreCLRContracts.Register],
- out _target);
-
- Assert.True(created, $"Failed to create ContractDescriptorTarget from dump: {dumpPath}");
+ [Contracts.CoreCLRContracts.Register]);
}
///
@@ -134,17 +131,14 @@ protected void InitializeDumpTestFromPath(string dumpPath)
_host = ClrMdDumpHost.Open(dumpPath, []);
ulong contractDescriptor = _host.FindContractDescriptorAddress();
- bool created = ContractDescriptorTarget.TryCreate(
+ _target = ContractDescriptorTarget.Create(
contractDescriptor,
_host.ReadFromTarget,
writeToTarget: static (_, _) => -1,
_host.GetThreadContext,
setThreadContext: static (_, _) => -1,
allocVirtual: static (ulong _, out ulong _) => throw new NotImplementedException("Dump tests do not provide AllocVirtual"),
- [Contracts.CoreCLRContracts.Register],
- out _target);
-
- Assert.True(created, $"Failed to create ContractDescriptorTarget from dump: {dumpPath}");
+ [Contracts.CoreCLRContracts.Register]);
}
public void Dispose()
diff --git a/src/native/managed/cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs b/src/native/managed/cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs
index d064f5eda7d272..f016f625003d72 100644
--- a/src/native/managed/cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs
+++ b/src/native/managed/cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs
@@ -604,6 +604,7 @@ public sealed class TestContractRegistry : ContractRegistry
private readonly Dictionary _versions = new();
private readonly Dictionary _mocks = new();
private readonly Dictionary _resolved = new();
+ private readonly HashSet<(Type, string)> _unsupportedVersions = new();
private Target _target = null!;
public void SetTarget(Target target) => _target = target;
@@ -617,10 +618,13 @@ public void SetMock(TContract mock) where TContract : IContract
public override void Register(string version, Func creator)
=> _creators[(typeof(TContract), version)] = t => creator(t);
- public override bool TryGetContract([NotNullWhen(true)] out TContract contract, out string? failureReason)
+ public override void RegisterUnsupported(string version)
+ => _unsupportedVersions.Add((typeof(TContract), version));
+
+ public override bool TryGetContract([NotNullWhen(true)] out TContract contract, [NotNullWhen(false)] out System.Exception? failureException)
{
contract = default!;
- failureReason = null;
+ failureException = null;
if (_resolved.TryGetValue(typeof(TContract), out var cached))
{
contract = (TContract)cached;
@@ -636,7 +640,9 @@ public override bool TryGetContract([NotNullWhen(true)] out TContract
{
if (!_creators.TryGetValue((typeof(TContract), version), out var creator))
{
- failureReason = $"Target supports contract '{typeof(TContract).Name}' version {version}, but no implementation is registered for that version.";
+ failureException = _unsupportedVersions.Contains((typeof(TContract), version))
+ ? new ContractObsoleteException(TContract.Name, version)
+ : new ContractUnrecognizedException(TContract.Name, version);
return false;
}
@@ -644,7 +650,7 @@ public override bool TryGetContract([NotNullWhen(true)] out TContract
}
else
{
- failureReason = $"Contract '{typeof(TContract).Name}' is not supported by the target.";
+ failureException = new ContractMissingException(TContract.Name);
return false;
}
diff --git a/src/native/managed/cdac/tests/UnitTests/ContractDescriptor/TargetTests.SubDescriptors.cs b/src/native/managed/cdac/tests/UnitTests/ContractDescriptor/TargetTests.SubDescriptors.cs
index d0a2a9ba2abca5..f64280ef7df2f6 100644
--- a/src/native/managed/cdac/tests/UnitTests/ContractDescriptor/TargetTests.SubDescriptors.cs
+++ b/src/native/managed/cdac/tests/UnitTests/ContractDescriptor/TargetTests.SubDescriptors.cs
@@ -229,4 +229,153 @@ public void SubDescriptor_Multiple_Breadth(MockTarget.Architecture arch)
Assert.Equal(expectedValue, globalStringValue);
}
}
+
+ // Builds a target whose primary descriptor advertises the given contracts and references a GC
+ // sub-descriptor whose pointer slot still reads null. This models early attach: the GC has not
+ // yet published its sub-descriptor address (dotnet/runtime#128215), so the slot stays pending and
+ // IsSubDescriptorResolved("GC") is false.
+ private static ContractDescriptorTarget CreatePendingGCSubDescriptorTarget(
+ MockTarget.Architecture arch, IReadOnlyDictionary contracts)
+ {
+ TargetTestHelpers targetTestHelpers = new(arch);
+ ContractDescriptorBuilder builder = new(targetTestHelpers);
+
+ // A pointer slot that reads null models a sub-descriptor the target has not published yet.
+ uint pendingPointerAddr = 0x12465312;
+ byte[] nullPointerBytes = new byte[targetTestHelpers.PointerSize];
+ targetTestHelpers.WritePointer(nullPointerBytes, 0);
+ builder.AddHeapFragment(new MockMemorySpace.HeapFragment
+ {
+ Address = pendingPointerAddr,
+ Data = nullPointerBytes,
+ Name = "PendingGCSubDescriptorPointer"
+ });
+
+ ContractDescriptorBuilder.DescriptorBuilder primaryDescriptor = new(builder);
+ primaryDescriptor
+ .SetSubDescriptors([("GC", 1u)])
+ .SetIndirectValues([0, pendingPointerAddr])
+ .SetContracts(new Dictionary(contracts));
+
+ Assert.True(builder.TryCreateTarget(primaryDescriptor, out ContractDescriptorTarget? target));
+ Assert.False(target.IsSubDescriptorResolved("GC"));
+ return target;
+ }
+
+ // The in-box (main-descriptor) contracts required for data access, minus the sub-descriptor-only
+ // IGC, each advertised at the version CoreCLRContracts registers.
+ private static Dictionary RequiredContractsWithoutGC()
+ => s_requiredDataAccessContracts
+ .Where(static c => c != "GC")
+ .ToDictionary(static c => c, static c => "c1");
+
+ [Theory]
+ [ClassData(typeof(MockTarget.StdArch))]
+ public void ValidateForDataAccess_PendingGCSubDescriptor_MissingGC_DoesNotThrow(MockTarget.Architecture arch)
+ {
+ // Every in-box contract is present, IGC is not advertised, and its GC sub-descriptor is still
+ // pending. IGC's absence is deferred (the GC may publish it after a later Flush), so a target
+ // attached this early must not be rejected.
+ ContractDescriptorTarget target = CreatePendingGCSubDescriptorTarget(arch, RequiredContractsWithoutGC());
+
+ Contracts.CoreCLRContracts.ValidateForDataAccess(target);
+ }
+
+ [Theory]
+ [ClassData(typeof(MockTarget.StdArch))]
+ public void ValidateForDataAccess_PendingGCSubDescriptor_MissingInBoxContract_Throws(MockTarget.Architecture arch)
+ {
+ // A pending GC sub-descriptor only defers IGC. An in-box contract (Loader) is still required
+ // unconditionally, so its absence rejects the target even during early attach.
+ Dictionary contracts = RequiredContractsWithoutGC();
+ contracts.Remove("Loader");
+ ContractDescriptorTarget target = CreatePendingGCSubDescriptorTarget(arch, contracts);
+
+ ContractMissingException ex = Assert.Throws(
+ () => Contracts.CoreCLRContracts.ValidateForDataAccess(target));
+ Assert.Equal("Loader", ex.ContractName);
+ }
+
+ // Builds a target whose GC sub-descriptor is fully published (resolved): the primary descriptor's
+ // sub-descriptor pointer slot reads a real address, so BuildDescriptors parses the sub-descriptor
+ // and IsSubDescriptorResolved("GC") is true. The main descriptor advertises mainContracts; the GC
+ // sub-descriptor advertises gcSubContracts (where IGC lives in a real runtime).
+ private static ContractDescriptorTarget CreateResolvedGCSubDescriptorTarget(
+ MockTarget.Architecture arch,
+ IReadOnlyDictionary mainContracts,
+ IReadOnlyDictionary gcSubContracts)
+ {
+ TargetTestHelpers targetTestHelpers = new(arch);
+ ContractDescriptorBuilder builder = new(targetTestHelpers);
+
+ uint gcSubDescriptorAddr = 0x12345678;
+ uint gcSubDescriptorJsonAddr = 0x12445678;
+ uint gcSubDescriptorPointerDataAddr = 0x12545678;
+ uint gcSubDescriptorPointerAddr = 0x12465312;
+
+ ContractDescriptorBuilder.DescriptorBuilder gcSubDescriptor = new(builder);
+ gcSubDescriptor
+ .SetGlobals(SubDescriptorGlobals)
+ .SetContracts(new Dictionary(gcSubContracts));
+ gcSubDescriptor.CreateSubDescriptor(gcSubDescriptorAddr, gcSubDescriptorJsonAddr, gcSubDescriptorPointerDataAddr);
+
+ byte[] pointerDataBytes = new byte[targetTestHelpers.PointerSize];
+ targetTestHelpers.WritePointer(pointerDataBytes, gcSubDescriptorAddr);
+ builder.AddHeapFragment(new MockMemorySpace.HeapFragment
+ {
+ Address = gcSubDescriptorPointerAddr,
+ Data = pointerDataBytes,
+ Name = "ResolvedGCSubDescriptorPointer"
+ });
+
+ ContractDescriptorBuilder.DescriptorBuilder primaryDescriptor = new(builder);
+ primaryDescriptor
+ .SetSubDescriptors([("GC", 1u)])
+ .SetIndirectValues([0, gcSubDescriptorPointerAddr])
+ .SetContracts(new Dictionary(mainContracts));
+
+ Assert.True(builder.TryCreateTarget(primaryDescriptor, out ContractDescriptorTarget? target));
+ Assert.True(target.IsSubDescriptorResolved("GC"));
+ return target;
+ }
+
+ [Theory]
+ [ClassData(typeof(MockTarget.StdArch))]
+ public void ValidateForDataAccess_ResolvedGCSubDescriptor_ValidGC_DoesNotThrow(MockTarget.Architecture arch)
+ {
+ // The GC sub-descriptor is published and advertises IGC at a supported version, so the target
+ // is fully serviceable.
+ ContractDescriptorTarget target = CreateResolvedGCSubDescriptorTarget(
+ arch, RequiredContractsWithoutGC(), new Dictionary { ["GC"] = "c1" });
+
+ Contracts.CoreCLRContracts.ValidateForDataAccess(target);
+ }
+
+ [Theory]
+ [ClassData(typeof(MockTarget.StdArch))]
+ public void ValidateForDataAccess_ResolvedGCSubDescriptor_MissingGC_Throws(MockTarget.Architecture arch)
+ {
+ // The GC sub-descriptor is published but does not advertise IGC. Deferral no longer applies
+ // once the provider has resolved, so the absent IGC now rejects the target.
+ ContractDescriptorTarget target = CreateResolvedGCSubDescriptorTarget(
+ arch, RequiredContractsWithoutGC(), new Dictionary());
+
+ ContractMissingException ex = Assert.Throws(
+ () => Contracts.CoreCLRContracts.ValidateForDataAccess(target));
+ Assert.Equal("GC", ex.ContractName);
+ }
+
+ [Theory]
+ [ClassData(typeof(MockTarget.StdArch))]
+ public void ValidateForDataAccess_ResolvedGCSubDescriptor_UnrecognizedGCVersion_Throws(MockTarget.Architecture arch)
+ {
+ // The GC sub-descriptor is published and advertises IGC at a version this cDAC cannot service.
+ // A version that has actually been read is always a failure.
+ ContractDescriptorTarget target = CreateResolvedGCSubDescriptorTarget(
+ arch, RequiredContractsWithoutGC(), new Dictionary { ["GC"] = "c99" });
+
+ ContractUnrecognizedException ex = Assert.Throws(
+ () => Contracts.CoreCLRContracts.ValidateForDataAccess(target));
+ Assert.Equal("GC", ex.ContractName);
+ }
}
diff --git a/src/native/managed/cdac/tests/UnitTests/ContractDescriptor/TargetTests.cs b/src/native/managed/cdac/tests/UnitTests/ContractDescriptor/TargetTests.cs
index 64f76a941cfa6e..e498303a476161 100644
--- a/src/native/managed/cdac/tests/UnitTests/ContractDescriptor/TargetTests.cs
+++ b/src/native/managed/cdac/tests/UnitTests/ContractDescriptor/TargetTests.cs
@@ -235,6 +235,361 @@ public void ReadUtf16String(MockTarget.Architecture arch)
Assert.Equal(expected, actual);
}
+ [Theory]
+ [ClassData(typeof(MockTarget.StdArch))]
+ public void Create_InvalidDescriptorJson_ThrowsFormatException(MockTarget.Architecture arch)
+ {
+ TargetTestHelpers targetTestHelpers = new(arch);
+ ContractDescriptorBuilder builder = new(targetTestHelpers);
+ byte[] descriptor = new byte[ContractDescriptorHelpers.Size(targetTestHelpers.Arch.Is64Bit)];
+ byte[] descriptorJson = "{ invalid json"u8.ToArray();
+ ContractDescriptorHelpers.Fill(descriptor, targetTestHelpers.Arch, descriptorJson.Length, 0xdddddddd, 0, 0xeeeeeeee);
+
+ FormatException ex = Assert.Throws(() => builder.CreateTargetFromRawDescriptor(descriptor, descriptorJson, []));
+ Assert.IsType(ex.InnerException);
+ Assert.Equal(CdacHResults.CDAC_E_DESCRIPTOR_MALFORMED, ex.HResult);
+ }
+
+ [Theory]
+ [ClassData(typeof(MockTarget.StdArch))]
+ public void Create_InvalidMagic_ThrowsDescriptorNotFound(MockTarget.Architecture arch)
+ {
+ TargetTestHelpers targetTestHelpers = new(arch);
+ ContractDescriptorBuilder builder = new(targetTestHelpers);
+ byte[] descriptor = new byte[ContractDescriptorHelpers.Size(targetTestHelpers.Arch.Is64Bit)];
+ byte[] descriptorJson = "{}"u8.ToArray();
+ ContractDescriptorHelpers.Fill(descriptor, targetTestHelpers.Arch, descriptorJson.Length, 0xdddddddd, 0, 0xeeeeeeee);
+ // Corrupt the magic so the bytes at the descriptor address are not a recognized contract descriptor.
+ descriptor[0] ^= 0xFF;
+
+ FormatException ex = Assert.Throws(() => builder.CreateTargetFromRawDescriptor(descriptor, descriptorJson, []));
+ Assert.Equal(CdacHResults.CDAC_E_DESCRIPTOR_NOT_FOUND, ex.HResult);
+ }
+
+ [Theory]
+ [ClassData(typeof(MockTarget.StdArch))]
+ public void Create_InvalidPointerDataIndex_ThrowsFormatException(MockTarget.Architecture arch)
+ {
+ TargetTestHelpers targetTestHelpers = new(arch);
+ ContractDescriptorBuilder builder = new(targetTestHelpers);
+ ContractDescriptorBuilder.DescriptorBuilder descriptorBuilder = new(builder);
+ descriptorBuilder.SetGlobals([("invalid", null, (uint?)1, null, "pointer")])
+ .SetIndirectValues([0]);
+
+ FormatException ex = Assert.Throws(() => builder.CreateTarget(descriptorBuilder));
+ Assert.Equal(CdacHResults.CDAC_E_DESCRIPTOR_MALFORMED, ex.HResult);
+ }
+
+ [Theory]
+ [ClassData(typeof(MockTarget.StdArch))]
+ public void TryCreateTarget_InvalidPointerDataIndex_ReturnsFalse(MockTarget.Architecture arch)
+ {
+ TargetTestHelpers targetTestHelpers = new(arch);
+ ContractDescriptorBuilder builder = new(targetTestHelpers);
+ ContractDescriptorBuilder.DescriptorBuilder descriptorBuilder = new(builder);
+ descriptorBuilder.SetGlobals([("invalid", null, (uint?)1, null, "pointer")])
+ .SetIndirectValues([0]);
+
+ Assert.False(builder.TryCreateTarget(descriptorBuilder, out _));
+ }
+
+ [Theory]
+ [ClassData(typeof(MockTarget.StdArch))]
+ public void GetContract_MissingContract_ThrowsContractMissingException(MockTarget.Architecture arch)
+ {
+ TargetTestHelpers targetTestHelpers = new(arch);
+ ContractDescriptorBuilder builder = new(targetTestHelpers);
+ ContractDescriptorBuilder.DescriptorBuilder descriptorBuilder = new(builder);
+ descriptorBuilder.SetContracts([]);
+
+ Assert.True(builder.TryCreateTarget(descriptorBuilder, out ContractDescriptorTarget? target));
+
+ ContractMissingException ex = Assert.Throws(() => target.Contracts.RuntimeInfo);
+ Assert.Equal("RuntimeInfo", ex.ContractName);
+ Assert.Null(ex.ContractVersion);
+ Assert.Equal(CdacHResults.CDAC_E_CONTRACT_NOT_ADVERTISED, ex.HResult);
+ }
+
+ [Theory]
+ [ClassData(typeof(MockTarget.StdArch))]
+ public void GetContract_UnrecognizedVersion_ThrowsContractUnrecognizedException(MockTarget.Architecture arch)
+ {
+ TargetTestHelpers targetTestHelpers = new(arch);
+ ContractDescriptorBuilder builder = new(targetTestHelpers);
+ ContractDescriptorBuilder.DescriptorBuilder descriptorBuilder = new(builder);
+ descriptorBuilder.SetContracts(new Dictionary { ["RuntimeInfo"] = "unsupported-version" });
+
+ Assert.True(builder.TryCreateTarget(
+ descriptorBuilder,
+ out ContractDescriptorTarget? target));
+
+ ContractUnrecognizedException ex = Assert.Throws(() => target.Contracts.RuntimeInfo);
+ Assert.Equal("RuntimeInfo", ex.ContractName);
+ Assert.Equal("unsupported-version", ex.ContractVersion);
+ Assert.Equal(CdacHResults.CDAC_E_CONTRACT_UNRECOGNIZED, ex.HResult);
+ }
+
+ [Theory]
+ [ClassData(typeof(MockTarget.StdArch))]
+ public void GetContract_ObsoleteVersion_ThrowsContractObsoleteException(MockTarget.Architecture arch)
+ {
+ TargetTestHelpers targetTestHelpers = new(arch);
+ ContractDescriptorBuilder builder = new(targetTestHelpers);
+ ContractDescriptorBuilder.DescriptorBuilder descriptorBuilder = new(builder);
+ descriptorBuilder.SetContracts(new Dictionary { ["RuntimeInfo"] = "obsolete-version" });
+
+ Assert.True(builder.TryCreateTarget(
+ descriptorBuilder,
+ out ContractDescriptorTarget? target,
+ registry => registry.RegisterUnsupported("obsolete-version")));
+
+ ContractObsoleteException ex = Assert.Throws(() => target.Contracts.RuntimeInfo);
+ Assert.Equal("RuntimeInfo", ex.ContractName);
+ Assert.Equal("obsolete-version", ex.ContractVersion);
+ Assert.Equal(CdacHResults.CDAC_E_CONTRACT_UNSUPPORTED, ex.HResult);
+ }
+
+ [Theory]
+ [ClassData(typeof(MockTarget.StdArch))]
+ public void TryGetContract_UnrecognizedVersion_ReturnsContractUnrecognizedException(MockTarget.Architecture arch)
+ {
+ TargetTestHelpers targetTestHelpers = new(arch);
+ ContractDescriptorBuilder builder = new(targetTestHelpers);
+ ContractDescriptorBuilder.DescriptorBuilder descriptorBuilder = new(builder);
+ descriptorBuilder.SetContracts(new Dictionary { ["RuntimeInfo"] = "unsupported-version" });
+
+ Assert.True(builder.TryCreateTarget(descriptorBuilder, out ContractDescriptorTarget? target));
+
+ Assert.False(target.Contracts.TryGetContract(out _, out System.Exception? failureException));
+ ContractUnrecognizedException ex = Assert.IsType(failureException);
+ Assert.Equal("RuntimeInfo", ex.ContractName);
+ Assert.Equal("unsupported-version", ex.ContractVersion);
+ }
+
+ // The contracts required by the data-access interfaces, advertised at the versions
+ // CoreCLRContracts registers. Mirrors CoreCLRContracts.ValidateForDataAccess.
+ private static readonly string[] s_requiredDataAccessContracts =
+ [
+ "AuxiliarySymbols", "BuiltInCOM", "CodeNotifications", "CodeVersions", "ComWrappers",
+ "ConditionalWeakTable", "DacStreams", "Debugger", "DebugInfo", "EcmaMetadata", "Exception",
+ "ExecutionManager", "FeatureFlags", "GC", "GCInfo", "Loader", "Notifications", "Object",
+ "PlatformMetadata", "PrecodeStubs", "ReJIT", "RuntimeInfo", "RuntimeMutableTypeSystem",
+ "RuntimeTypeSystem", "SHash", "Signature", "StackWalk", "StressLog", "SyncBlock", "Thread",
+ ];
+
+ // A string-valued "OperatingSystem" contract-descriptor global, used to drive the target
+ // platform that ValidateForDataAccess reads when deciding which OS-specific contracts to require.
+ private static readonly (string Name, ulong? Value, string? StringValue, string? TypeName)[] s_windowsOperatingSystemGlobal =
+ [("OperatingSystem", null, "windows", "string")];
+ private static readonly (string Name, ulong? Value, string? StringValue, string? TypeName)[] s_unixOperatingSystemGlobal =
+ [("OperatingSystem", null, "unix", "string")];
+
+ [Theory]
+ [ClassData(typeof(MockTarget.StdArch))]
+ public void TryValidate_RegisteredVersion_ReturnsTrue(MockTarget.Architecture arch)
+ {
+ TargetTestHelpers targetTestHelpers = new(arch);
+ ContractDescriptorBuilder builder = new(targetTestHelpers);
+ ContractDescriptorBuilder.DescriptorBuilder descriptorBuilder = new(builder);
+ descriptorBuilder.SetContracts(new Dictionary { ["RuntimeInfo"] = "c1" });
+
+ Assert.True(builder.TryCreateTarget(descriptorBuilder, out ContractDescriptorTarget? target));
+
+ Assert.True(target.Contracts.TryValidate(out System.Exception? failure));
+ Assert.Null(failure);
+ }
+
+ [Theory]
+ [ClassData(typeof(MockTarget.StdArch))]
+ public void TryValidate_DoesNotInstantiateContract(MockTarget.Architecture arch)
+ {
+ // GCInfo's creator reads RuntimeInfo from target memory. Advertise GCInfo but omit RuntimeInfo:
+ // TryValidate must succeed (a creator is registered) without invoking it, whereas a real
+ // GetContract would chain into RuntimeInfo and fail.
+ TargetTestHelpers targetTestHelpers = new(arch);
+ ContractDescriptorBuilder builder = new(targetTestHelpers);
+ ContractDescriptorBuilder.DescriptorBuilder descriptorBuilder = new(builder);
+ descriptorBuilder.SetContracts(new Dictionary { ["GCInfo"] = "c1" });
+
+ Assert.True(builder.TryCreateTarget(descriptorBuilder, out ContractDescriptorTarget? target));
+
+ Assert.True(target.Contracts.TryValidate(out System.Exception? failure));
+ Assert.Null(failure);
+ Assert.Throws(() => target.Contracts.GCInfo);
+ }
+
+ [Theory]
+ [ClassData(typeof(MockTarget.StdArch))]
+ public void TryValidate_MissingContract_ReturnsContractMissingException(MockTarget.Architecture arch)
+ {
+ TargetTestHelpers targetTestHelpers = new(arch);
+ ContractDescriptorBuilder builder = new(targetTestHelpers);
+ ContractDescriptorBuilder.DescriptorBuilder descriptorBuilder = new(builder);
+ descriptorBuilder.SetContracts([]);
+
+ Assert.True(builder.TryCreateTarget(descriptorBuilder, out ContractDescriptorTarget? target));
+
+ Assert.False(target.Contracts.TryValidate(out System.Exception? failure));
+ Assert.IsType(failure);
+ }
+
+ [Theory]
+ [ClassData(typeof(MockTarget.StdArch))]
+ public void ValidateForDataAccess_AllRequiredPresent_DoesNotThrow(MockTarget.Architecture arch)
+ {
+ TargetTestHelpers targetTestHelpers = new(arch);
+ ContractDescriptorBuilder builder = new(targetTestHelpers);
+ ContractDescriptorBuilder.DescriptorBuilder descriptorBuilder = new(builder);
+ descriptorBuilder.SetContracts(s_requiredDataAccessContracts);
+
+ Assert.True(builder.TryCreateTarget(descriptorBuilder, out ContractDescriptorTarget? target));
+
+ Contracts.CoreCLRContracts.ValidateForDataAccess(target);
+ }
+
+ [Theory]
+ [ClassData(typeof(MockTarget.StdArch))]
+ public void ValidateForDataAccess_MissingRequiredContract_ThrowsNotAdvertised(MockTarget.Architecture arch)
+ {
+ TargetTestHelpers targetTestHelpers = new(arch);
+ ContractDescriptorBuilder builder = new(targetTestHelpers);
+ ContractDescriptorBuilder.DescriptorBuilder descriptorBuilder = new(builder);
+ descriptorBuilder.SetContracts(s_requiredDataAccessContracts.Where(static c => c != "RuntimeInfo").ToArray());
+
+ Assert.True(builder.TryCreateTarget(descriptorBuilder, out ContractDescriptorTarget? target));
+
+ ContractMissingException ex = Assert.Throws(
+ () => Contracts.CoreCLRContracts.ValidateForDataAccess(target));
+ Assert.Equal(CdacHResults.CDAC_E_CONTRACT_NOT_ADVERTISED, ex.HResult);
+ Assert.Equal("RuntimeInfo", ex.ContractName);
+ }
+
+ [Theory]
+ [ClassData(typeof(MockTarget.StdArch))]
+ public void ValidateForDataAccess_MissingTransitiveContract_ThrowsNotAdvertised(MockTarget.Architecture arch)
+ {
+ string[] transitiveDependencies = ["ConditionalWeakTable", "Debugger", "SHash"];
+
+ foreach (string missingContract in transitiveDependencies)
+ {
+ TargetTestHelpers targetTestHelpers = new(arch);
+ ContractDescriptorBuilder builder = new(targetTestHelpers);
+ ContractDescriptorBuilder.DescriptorBuilder descriptorBuilder = new(builder);
+ descriptorBuilder.SetContracts(s_requiredDataAccessContracts.Where(c => c != missingContract).ToArray());
+
+ Assert.True(builder.TryCreateTarget(descriptorBuilder, out ContractDescriptorTarget? target));
+
+ ContractMissingException ex = Assert.Throws(
+ () => Contracts.CoreCLRContracts.ValidateForDataAccess(target));
+ Assert.Equal(CdacHResults.CDAC_E_CONTRACT_NOT_ADVERTISED, ex.HResult);
+ Assert.Equal(missingContract, ex.ContractName);
+ }
+ }
+
+ [Theory]
+ [ClassData(typeof(MockTarget.StdArch))]
+ public void ValidateForDataAccess_UnrecognizedRequiredVersion_ThrowsUnrecognized(MockTarget.Architecture arch)
+ {
+ TargetTestHelpers targetTestHelpers = new(arch);
+ ContractDescriptorBuilder builder = new(targetTestHelpers);
+ ContractDescriptorBuilder.DescriptorBuilder descriptorBuilder = new(builder);
+ Dictionary contracts = s_requiredDataAccessContracts.ToDictionary(static c => c, static _ => "c1");
+ contracts["RuntimeInfo"] = "version-from-the-future";
+ descriptorBuilder.SetContracts(contracts);
+
+ Assert.True(builder.TryCreateTarget(descriptorBuilder, out ContractDescriptorTarget? target));
+
+ ContractUnrecognizedException ex = Assert.Throws(
+ () => Contracts.CoreCLRContracts.ValidateForDataAccess(target));
+ Assert.Equal(CdacHResults.CDAC_E_CONTRACT_UNRECOGNIZED, ex.HResult);
+ Assert.Equal("RuntimeInfo", ex.ContractName);
+ Assert.Equal("version-from-the-future", ex.ContractVersion);
+ }
+
+ [Theory]
+ [ClassData(typeof(MockTarget.StdArch))]
+ public void ValidateForDataAccess_DeprecatedContractReference_ThrowsUnsupported(MockTarget.Architecture arch)
+ {
+ TargetTestHelpers targetTestHelpers = new(arch);
+ ContractDescriptorBuilder builder = new(targetTestHelpers);
+ ContractDescriptorBuilder.DescriptorBuilder descriptorBuilder = new(builder);
+ Dictionary contracts = s_requiredDataAccessContracts.ToDictionary(static c => c, static _ => "c1");
+ contracts["RuntimeInfo"] = "deprecated-version";
+ descriptorBuilder.SetContracts(contracts);
+
+ // Model a target descriptor that still references a contract version this cDAC recognizes
+ // as deprecated. Production registrations can use the same mechanism when a version retires.
+ Assert.True(builder.TryCreateTarget(
+ descriptorBuilder,
+ out ContractDescriptorTarget? target,
+ registry => registry.RegisterUnsupported("deprecated-version")));
+
+ ContractObsoleteException ex = Assert.Throws(
+ () => Contracts.CoreCLRContracts.ValidateForDataAccess(target));
+ Assert.Equal(CdacHResults.CDAC_E_CONTRACT_UNSUPPORTED, ex.HResult);
+ Assert.Equal("RuntimeInfo", ex.ContractName);
+ Assert.Equal("deprecated-version", ex.ContractVersion);
+ }
+
+ [Theory]
+ [ClassData(typeof(MockTarget.StdArch))]
+ public void ValidateForDataAccess_WindowsTarget_RequiresWindowsErrorReporting(MockTarget.Architecture arch)
+ {
+ TargetTestHelpers targetTestHelpers = new(arch);
+ ContractDescriptorBuilder builder = new(targetTestHelpers);
+ ContractDescriptorBuilder.DescriptorBuilder descriptorBuilder = new(builder);
+
+ // A Windows target advertising every cross-platform contract but not the Windows-only
+ // WindowsErrorReporting must still fail validation, since SOS reaches it unconditionally
+ // (GetClrWatsonBuckets) on Windows.
+ descriptorBuilder
+ .SetContracts(s_requiredDataAccessContracts)
+ .SetGlobals(s_windowsOperatingSystemGlobal);
+
+ Assert.True(builder.TryCreateTarget(descriptorBuilder, out ContractDescriptorTarget? target));
+
+ ContractMissingException ex = Assert.Throws(
+ () => Contracts.CoreCLRContracts.ValidateForDataAccess(target));
+ Assert.Equal(CdacHResults.CDAC_E_CONTRACT_NOT_ADVERTISED, ex.HResult);
+ Assert.Equal("WindowsErrorReporting", ex.ContractName);
+ }
+
+ [Theory]
+ [ClassData(typeof(MockTarget.StdArch))]
+ public void ValidateForDataAccess_WindowsTargetWithWindowsErrorReporting_DoesNotThrow(MockTarget.Architecture arch)
+ {
+ TargetTestHelpers targetTestHelpers = new(arch);
+ ContractDescriptorBuilder builder = new(targetTestHelpers);
+ ContractDescriptorBuilder.DescriptorBuilder descriptorBuilder = new(builder);
+
+ descriptorBuilder
+ .SetContracts([.. s_requiredDataAccessContracts, "WindowsErrorReporting"])
+ .SetGlobals(s_windowsOperatingSystemGlobal);
+
+ Assert.True(builder.TryCreateTarget(descriptorBuilder, out ContractDescriptorTarget? target));
+
+ Contracts.CoreCLRContracts.ValidateForDataAccess(target);
+ }
+
+ [Theory]
+ [ClassData(typeof(MockTarget.StdArch))]
+ public void ValidateForDataAccess_NonWindowsTarget_DoesNotRequireWindowsErrorReporting(MockTarget.Architecture arch)
+ {
+ TargetTestHelpers targetTestHelpers = new(arch);
+ ContractDescriptorBuilder builder = new(targetTestHelpers);
+ ContractDescriptorBuilder.DescriptorBuilder descriptorBuilder = new(builder);
+
+ // A non-Windows target that omits WindowsErrorReporting must validate cleanly: the guard
+ // must not require a contract the target platform never uses.
+ descriptorBuilder
+ .SetContracts(s_requiredDataAccessContracts)
+ .SetGlobals(s_unixOperatingSystemGlobal);
+
+ Assert.True(builder.TryCreateTarget(descriptorBuilder, out ContractDescriptorTarget? target));
+
+ Contracts.CoreCLRContracts.ValidateForDataAccess(target);
+ }
+
private static void ValidateGlobals(
ContractDescriptorTarget target,
(string Name, ulong Value, string? Type)[] globals,