From ae543e36bcf56a5ec34d842693b915399d3dec3a Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Tue, 29 Sep 2026 12:54:13 -0400 Subject: [PATCH 1/9] Add managed cDAC memory enumeration and collector host Implement shared dump collection, explicit activation and runtime image lookup, and auxiliary-symbol coverage with managed tests and documentation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: de3eab4e-b090-430a-8e30-6b204fb1519a --- .../design/datacontracts/RuntimeTypeSystem.md | 11 - .../Target.cs | 5 + .../Contracts/PrecodeStubs_Common.cs | 24 +- .../Context/IPlatformAgnosticContext.cs | 39 +- .../StackWalk/FrameHandling/FrameHelpers.cs | 33 +- .../CoreCLRContracts.cs | 14 +- .../TypeValidation.cs | 21 +- .../TargetArchitectureSupport.cs | 42 + .../Entrypoints/EntrypointHelpers.cs | 188 ++ .../EnumMemory/DumpCreator.cs | 349 ++++ .../EnumMemory/MemoryRegionEnumerator.cs | 141 ++ .../EnumMemory/MethodCollector.cs | 129 ++ .../EnumMemory/MiniMetadataWriter.cs | 88 + .../EnumMemory/ObjectCollector.cs | 285 +++ .../EnumMemory/RuntimeModuleInfo.cs | 239 +++ .../ICLRData.cs | 39 +- .../SOSDacImpl.ICLRDataEnumMemoryRegions.cs | 19 +- .../SOSDacImpl.cs | 11 +- .../ContractDescriptorTarget.cs | 108 +- src/native/managed/cdac/README.md | 68 + src/native/managed/cdac/cdac.slnx | 1 + src/native/managed/cdac/gen/Emitter.cs | 32 +- .../mscordaccore_enummemory/Entrypoints.cs | 62 + .../GenerateWindowsVersionResource.ps1 | 113 ++ .../mscordaccore_enummemory.csproj | 85 + .../mscordaccore_universal/Entrypoints.cs | 185 +- .../managed/cdac/scripts/DescriptorCommand.cs | 2 +- .../managed/cdac/scripts/DumpHelpers.cs | 27 +- .../tests/DataGenerator/DataGeneratorTests.cs | 24 +- .../cdac/tests/DataGenerator/TestTarget.cs | 1 + .../DumpTests/AuxiliarySymbolsDumpTests.cs | 18 +- .../tests/TestInfrastructure/ClrMdDumpHost.cs | 10 +- .../tests/TestInfrastructure/DumpTestBase.cs | 10 +- .../TestPlaceholderTarget.cs | 1 + .../ContractDescriptor/TargetTests.cs | 2 +- .../cdac/tests/UnitTests/EnumMemoryTests.cs | 1782 +++++++++++++++++ .../UnitTests/FunctionTableAccessTests.cs | 22 - ...iagnostics.DataContractReader.Tests.csproj | 1 + 38 files changed, 3910 insertions(+), 321 deletions(-) create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/TargetArchitectureSupport.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Entrypoints/EntrypointHelpers.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/DumpCreator.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/MemoryRegionEnumerator.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/MethodCollector.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/MiniMetadataWriter.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/ObjectCollector.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/RuntimeModuleInfo.cs create mode 100644 src/native/managed/cdac/mscordaccore_enummemory/Entrypoints.cs create mode 100644 src/native/managed/cdac/mscordaccore_enummemory/GenerateWindowsVersionResource.ps1 create mode 100644 src/native/managed/cdac/mscordaccore_enummemory/mscordaccore_enummemory.csproj create mode 100644 src/native/managed/cdac/tests/UnitTests/EnumMemoryTests.cs diff --git a/docs/design/datacontracts/RuntimeTypeSystem.md b/docs/design/datacontracts/RuntimeTypeSystem.md index d153b01d5776a9..84bccadacef69a 100644 --- a/docs/design/datacontracts/RuntimeTypeSystem.md +++ b/docs/design/datacontracts/RuntimeTypeSystem.md @@ -589,17 +589,7 @@ static class RuntimeTypeSystem_1_Helpers | `InstantiatedMethodDesc` | `PerInstInfo` | `pointer` | The pointer to the method's type arguments | | `LayoutEEClass` | `LayoutInfo` | `EEClassLayoutInfo` | Inline `EEClassLayoutInfo` for a type with layout. Only the offset is used - the reader constructs an `EEClassLayoutInfo` at that offset from the `EEClass` address. Only valid when `EEClass.VMFlags` has `VMFLAG_HASLAYOUT` | | `LoaderAllocator` | `CreationNumber` | `uint64` | Monotonically-increasing creation number assigned to each collectible LoaderAllocator. | -| `LoaderAllocator` | `DynamicHelpersStubHeap` | `pointer` | Dynamic-helper stub heap (optional, present when ReadyToRun dynamic-helper stubs are enabled) | -| `LoaderAllocator` | `ExecutableHeap` | `pointer` | Executable-code heap | -| `LoaderAllocator` | `FixupPrecodeHeap` | `pointer` | Fixup-precode heap (optional, present when fixup precodes are supported) | -| `LoaderAllocator` | `HighFrequencyHeap` | `pointer` | High-frequency allocation heap | | `LoaderAllocator` | `IsCollectible` | `uint8` | Non-zero if the LoaderAllocator is collectible. | -| `LoaderAllocator` | `LowFrequencyHeap` | `pointer` | Low-frequency allocation heap | -| `LoaderAllocator` | `NewStubPrecodeHeap` | `pointer` | New-stub-precode heap (optional, absent with portable entry points) | -| `LoaderAllocator` | `ObjectHandle` | `ObjectHandle` | Handle to the managed loader allocator object | -| `LoaderAllocator` | `ReferenceCount` | `uint32` | Reference count of the loader allocator | -| `LoaderAllocator` | `StaticsHeap` | `pointer` | Heap containing statics-related allocations | -| `LoaderAllocator` | `VirtualCallStubManager` | `pointer` | Pointer to the virtual-call stub manager | | `MethodDesc` | *(type size)* | `uint32` | Base size for mcIL classification | | `MethodDesc` | `ChunkIndex` | `uint8` | Offset of this MethodDesc relative to the end of its containing MethodDescChunk - in multiples of MethodDescAlignment | | `MethodDesc` | `CodeData` | `pointer` | Pointer to per-method code data containing entry-point and code-versioning state | @@ -636,7 +626,6 @@ static class RuntimeTypeSystem_1_Helpers | `NativeCodeSlot` | *(type size)* | `uint32` | Size of the native code slot, added when HasNativeCodeSlot flag is set | | `NonVtableSlot` | *(type size)* | `uint32` | Size of the non-vtable slot, added when HasNonVtableSlot flag is set | | `Object` | *(type size)* | `uint32` | Size in bytes of the fixed Object portion through its MethodTable pointer | -| `Object` | `m_pMethTab` | `pointer` | Method table for the object | | `ParamTypeDesc` | `TypeArg` | `pointer` | Associated type argument | | `PInvokeMethodDesc` | *(type size)* | `uint32` | Base size for mcPInvoke classification | | `Signature` | `SignatureLength` | `uint32` | Length in bytes of the raw signature blob. | 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 3720399748bf24..6e526df75533f9 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs @@ -27,6 +27,11 @@ public abstract class Target /// public abstract bool IsLittleEndian { get; } + /// Attempts to locate the runtime image in the target. + /// When this method returns, contains the image base, or a null target pointer if unavailable. + /// if the image base is available; otherwise, . + public abstract bool TryGetRuntimeImageBase(out TargetPointer imageBase); + /// /// Fills a buffer with the context of the given thread /// diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/PrecodeStubs_Common.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/PrecodeStubs_Common.cs index b80cf28877df7e..854182ebd856c1 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/PrecodeStubs_Common.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/PrecodeStubs_Common.cs @@ -31,8 +31,9 @@ internal interface IPrecodeStubsContractCommonApi internal class PrecodeStubsCommon : IPrecodeStubs where TPrecodeStubsImplementation : IPrecodeStubsContractCommonApi { private readonly Target _target; - private readonly CodePointerFlags _codePointerFlags; - internal readonly Data.PrecodeMachineDescriptor MachineDescriptor; + + internal Data.PrecodeMachineDescriptor MachineDescriptor => + _target.ProcessedData.GetOrAdd(_target.Contracts.PlatformMetadata.GetPrecodeMachineDescriptor()); protected Target Target => _target; @@ -103,19 +104,19 @@ internal override TargetPointer GetMethodDesc(Target target, Data.PrecodeMachine internal TargetPointer CodePointerReadableInstrPointer(TargetCodePointer codePointer) { - if (_codePointerFlags.HasFlag(CodePointerFlags.HasArm32ThumbBit)) + CodePointerFlags codePointerFlags = _target.Contracts.PlatformMetadata.GetCodePointerFlags(); + if (codePointerFlags.HasFlag(CodePointerFlags.HasArm32ThumbBit)) { return codePointer.AsTargetPointer & ~1ul; } - if (_codePointerFlags.HasFlag(CodePointerFlags.HasArm64PtrAuth)) + if (codePointerFlags.HasFlag(CodePointerFlags.HasArm64PtrAuth)) { throw new NotImplementedException("CodePointerReadableInstrPointer for ARM64 with pointer authentication"); } - Debug.Assert(_codePointerFlags == 0); + Debug.Assert(codePointerFlags == 0); return codePointer.AsTargetPointer; } - internal ValidPrecode GetPrecodeFromEntryPoint(TargetCodePointer entryPoint) { TargetPointer instrPointer = CodePointerReadableInstrPointer(entryPoint); @@ -142,10 +143,6 @@ internal ValidPrecode GetPrecodeFromEntryPoint(TargetCodePointer entryPoint) public PrecodeStubsCommon(Target target) { _target = target; - IPlatformMetadata pm = target.Contracts.PlatformMetadata; - TargetPointer descAddr = pm.GetPrecodeMachineDescriptor(); - MachineDescriptor = target.ProcessedData.GetOrAdd(descAddr); - _codePointerFlags = pm.GetCodePointerFlags(); } TargetPointer IPrecodeStubs.GetMethodDescFromStubAddress(TargetCodePointer entryPoint) @@ -158,22 +155,23 @@ TargetPointer IPrecodeStubs.GetMethodDescFromStubAddress(TargetCodePointer entry TargetPointer IPrecodeStubs.GetPrecodeEntryPointFromInteriorAddress(TargetCodePointer interiorAddress, bool isFixupPrecode) { TargetPointer instrPointer = CodePointerReadableInstrPointer(interiorAddress); + Data.PrecodeMachineDescriptor machineDescriptor = MachineDescriptor; uint stubSize; if (isFixupPrecode) { - if (MachineDescriptor.FixupStubPrecodeSize is not byte fixupSize || fixupSize == 0) + if (machineDescriptor.FixupStubPrecodeSize is not byte fixupSize || fixupSize == 0) throw new InvalidOperationException("FixupPrecode size not available"); stubSize = fixupSize; } else { - if (MachineDescriptor.StubPrecodeSize is not byte stubPrecodeSize || stubPrecodeSize == 0) + if (machineDescriptor.StubPrecodeSize is not byte stubPrecodeSize || stubPrecodeSize == 0) throw new InvalidOperationException("StubPrecode size not available"); stubSize = stubPrecodeSize; } - ulong pageMask = MachineDescriptor.StubCodePageSize - 1; + ulong pageMask = machineDescriptor.StubCodePageSize - 1; ulong pageBase = instrPointer.Value & ~pageMask; ulong offset = instrPointer.Value - pageBase; ulong entryPointAddress = pageBase + (offset / stubSize) * stubSize; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformAgnosticContext.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformAgnosticContext.cs index d0cd2264d1772d..25158889d14598 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformAgnosticContext.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformAgnosticContext.cs @@ -42,17 +42,32 @@ public interface IPlatformAgnosticContext public static IPlatformAgnosticContext GetContextForPlatform(Target target) { IRuntimeInfo runtimeInfo = target.Contracts.RuntimeInfo; - return runtimeInfo.GetTargetArchitecture() switch - { - RuntimeInfoArchitecture.X86 => new ContextHolder(), - RuntimeInfoArchitecture.X64 => new ContextHolder(), - RuntimeInfoArchitecture.Arm => new ContextHolder(), - RuntimeInfoArchitecture.Arm64 => new ContextHolder(), - RuntimeInfoArchitecture.LoongArch64 => new ContextHolder(), - RuntimeInfoArchitecture.RiscV64 => new ContextHolder(), - RuntimeInfoArchitecture.Wasm => new ContextHolder(), - RuntimeInfoArchitecture.Unknown => throw new InvalidOperationException($"Processor architecture is required for creating a platform specific context and is not provided by the target"), - _ => throw new InvalidOperationException($"Unsupported architecture {runtimeInfo.GetTargetArchitecture()}"), - }; + RuntimeInfoArchitecture architecture = runtimeInfo.GetTargetArchitecture(); + + if (TargetArchitectureSupport.IsX86Supported && architecture == RuntimeInfoArchitecture.X86) + return new ContextHolder(); + + if (TargetArchitectureSupport.IsX64Supported && architecture == RuntimeInfoArchitecture.X64) + return new ContextHolder(); + + if (TargetArchitectureSupport.IsArmSupported && architecture == RuntimeInfoArchitecture.Arm) + return new ContextHolder(); + + if (TargetArchitectureSupport.IsArm64Supported && architecture == RuntimeInfoArchitecture.Arm64) + return new ContextHolder(); + + if (TargetArchitectureSupport.IsLoongArch64Supported && architecture == RuntimeInfoArchitecture.LoongArch64) + return new ContextHolder(); + + if (TargetArchitectureSupport.IsRiscV64Supported && architecture == RuntimeInfoArchitecture.RiscV64) + return new ContextHolder(); + + if (TargetArchitectureSupport.IsWasmSupported && architecture == RuntimeInfoArchitecture.Wasm) + return new ContextHolder(); + + throw new InvalidOperationException( + architecture == RuntimeInfoArchitecture.Unknown + ? "Processor architecture is required for creating a platform specific context and is not provided by the target" + : $"Unsupported architecture {architecture}"); } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs index b1c59af11e2e8a..47418144e2dbb2 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs @@ -358,17 +358,28 @@ public bool IsExceptionHandlingHelperInlinedCallFrame(TargetPointer framePtr) private IPlatformFrameHandler GetFrameHandler(IPlatformAgnosticContext context) { - return context switch - { - ContextHolder contextHolder => new X86FrameHandler(_target, contextHolder), - ContextHolder contextHolder => new AMD64FrameHandler(_target, contextHolder), - ContextHolder contextHolder => new ARMFrameHandler(_target, contextHolder), - ContextHolder contextHolder => new ARM64FrameHandler(_target, contextHolder), - ContextHolder contextHolder => new RISCV64FrameHandler(_target, contextHolder), - ContextHolder contextHolder => new LoongArch64FrameHandler(_target, contextHolder), - ContextHolder contextHolder => new WasmFrameHandler(_target, contextHolder), - _ => throw new InvalidOperationException("Unsupported context type"), - }; + if (TargetArchitectureSupport.IsX86Supported && context is ContextHolder x86Context) + return new X86FrameHandler(_target, x86Context); + + if (TargetArchitectureSupport.IsX64Supported && context is ContextHolder x64Context) + return new AMD64FrameHandler(_target, x64Context); + + if (TargetArchitectureSupport.IsArmSupported && context is ContextHolder armContext) + return new ARMFrameHandler(_target, armContext); + + if (TargetArchitectureSupport.IsArm64Supported && context is ContextHolder arm64Context) + return new ARM64FrameHandler(_target, arm64Context); + + if (TargetArchitectureSupport.IsRiscV64Supported && context is ContextHolder riscV64Context) + return new RISCV64FrameHandler(_target, riscV64Context); + + if (TargetArchitectureSupport.IsLoongArch64Supported && context is ContextHolder loongArch64Context) + return new LoongArch64FrameHandler(_target, loongArch64Context); + + if (TargetArchitectureSupport.IsWasmSupported && context is ContextHolder wasmContext) + return new WasmFrameHandler(_target, wasmContext); + + throw new InvalidOperationException($"Unsupported context type {context.GetType()}"); } private static bool InlinedCallFrameHasActiveCall(Data.InlinedCallFrame frame) 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 808d6350d69001..92c6f06c6558c8 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs @@ -62,13 +62,13 @@ public static void Register(ContractRegistry registry) RuntimeInfoArchitecture arch = t.Contracts.RuntimeInfo.GetTargetArchitecture(); return arch switch { - RuntimeInfoArchitecture.X64 => new GCInfo_1(t), - RuntimeInfoArchitecture.X86 => new GCInfoX86_1(t), - RuntimeInfoArchitecture.Arm64 => new GCInfo_1(t), - RuntimeInfoArchitecture.Arm => new GCInfo_1(t), - RuntimeInfoArchitecture.LoongArch64 => new GCInfo_1(t), - RuntimeInfoArchitecture.RiscV64 => new GCInfo_1(t), - RuntimeInfoArchitecture.Wasm => new GCInfo_1(t), + RuntimeInfoArchitecture.X64 when TargetArchitectureSupport.IsX64Supported => new GCInfo_1(t), + RuntimeInfoArchitecture.X86 when TargetArchitectureSupport.IsX86Supported => new GCInfoX86_1(t), + RuntimeInfoArchitecture.Arm64 when TargetArchitectureSupport.IsArm64Supported => new GCInfo_1(t), + RuntimeInfoArchitecture.Arm when TargetArchitectureSupport.IsArmSupported => new GCInfo_1(t), + RuntimeInfoArchitecture.LoongArch64 when TargetArchitectureSupport.IsLoongArch64Supported => new GCInfo_1(t), + RuntimeInfoArchitecture.RiscV64 when TargetArchitectureSupport.IsRiscV64Supported => new GCInfo_1(t), + RuntimeInfoArchitecture.Wasm when TargetArchitectureSupport.IsWasmSupported => new GCInfo_1(t), _ => default(GCInfo), }; }); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/TypeValidation.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/TypeValidation.cs index 68d43c42a1e8b3..729dd1ed88541d 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/TypeValidation.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/TypeValidation.cs @@ -76,7 +76,8 @@ internal TargetPointer CanonMT } } - internal readonly bool ValidateReadable() => ValidateDataReadable(_target, Address); + internal readonly bool ValidateReadable() => new MethodTable(_target, Address).TryReadAllFields(); + internal TargetPointer ParentMethodTable => _target.ReadPointer(Address + (ulong)_type.Fields[nameof(ParentMethodTable)].Offset); } @@ -96,7 +97,7 @@ internal NonValidatedEEClass(Target target, TargetPointer eeClassPointer) internal TargetPointer MethodTable => _target.ReadPointer(Address + (ulong)_type.Fields[nameof(MethodTable)].Offset); - internal readonly bool ValidateReadable() => ValidateDataReadable(_target, Address); + internal readonly bool ValidateReadable() => new EEClass(_target, Address).TryReadAllFields(); } internal static NonValidatedMethodTable GetMethodTableData(Target target, TargetPointer methodTablePointer) @@ -200,22 +201,6 @@ private bool ValidateMethodTableAdHoc(NonValidatedMethodTable methodTable) return true; } - private static bool ValidateDataReadable(Target target, TargetPointer dataAddress) where T : IData - { - try - { - T dataClass = T.Create(target, dataAddress); - // Fields are read lazily, so force a full read to validate that the - // entire structure is readable (an unreadable field throws below). - (dataClass as IReadableData)?.EnsureAllFieldsRead(); - return true; - } - catch (VirtualReadException) - { - return false; - } - } - private TargetPointer GetClassThrowing(NonValidatedMethodTable methodTable) { TargetPointer eeClassOrCanonMT = methodTable.EEClassOrCanonMT; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/TargetArchitectureSupport.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/TargetArchitectureSupport.cs new file mode 100644 index 00000000000000..6f16b24ca74c45 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/TargetArchitectureSupport.cs @@ -0,0 +1,42 @@ +// 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.Diagnostics.CodeAnalysis; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts; + +internal static class TargetArchitectureSupport +{ + private const string ArmSwitchName = "Microsoft.Diagnostics.DataContractReader.Architecture.Arm"; + private const string Arm64SwitchName = "Microsoft.Diagnostics.DataContractReader.Architecture.Arm64"; + private const string LoongArch64SwitchName = "Microsoft.Diagnostics.DataContractReader.Architecture.LoongArch64"; + private const string RiscV64SwitchName = "Microsoft.Diagnostics.DataContractReader.Architecture.RiscV64"; + private const string WasmSwitchName = "Microsoft.Diagnostics.DataContractReader.Architecture.Wasm"; + private const string X64SwitchName = "Microsoft.Diagnostics.DataContractReader.Architecture.X64"; + private const string X86SwitchName = "Microsoft.Diagnostics.DataContractReader.Architecture.X86"; + + [FeatureSwitchDefinition(ArmSwitchName)] + public static bool IsArmSupported { get; } = IsEnabled(ArmSwitchName); + + [FeatureSwitchDefinition(Arm64SwitchName)] + public static bool IsArm64Supported { get; } = IsEnabled(Arm64SwitchName); + + [FeatureSwitchDefinition(LoongArch64SwitchName)] + public static bool IsLoongArch64Supported { get; } = IsEnabled(LoongArch64SwitchName); + + [FeatureSwitchDefinition(RiscV64SwitchName)] + public static bool IsRiscV64Supported { get; } = IsEnabled(RiscV64SwitchName); + + [FeatureSwitchDefinition(WasmSwitchName)] + public static bool IsWasmSupported { get; } = IsEnabled(WasmSwitchName); + + [FeatureSwitchDefinition(X64SwitchName)] + public static bool IsX64Supported { get; } = IsEnabled(X64SwitchName); + + [FeatureSwitchDefinition(X86SwitchName)] + public static bool IsX86Supported { get; } = IsEnabled(X86SwitchName); + + private static bool IsEnabled(string switchName) => + !AppContext.TryGetSwitch(switchName, out bool isEnabled) || isEnabled; +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Entrypoints/EntrypointHelpers.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Entrypoints/EntrypointHelpers.cs new file mode 100644 index 00000000000000..15fee234aeedec --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Entrypoints/EntrypointHelpers.cs @@ -0,0 +1,188 @@ +// 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.Runtime.InteropServices; +using System.Runtime.InteropServices.Marshalling; +using System.Threading; +using Microsoft.Diagnostics.DataContractReader.Contracts; + +namespace Microsoft.Diagnostics.DataContractReader.Legacy; + +/// +/// Provides shared activation helpers for native cDAC entrypoints. +/// +public static unsafe class EntrypointHelpers +{ + /// + /// Creates a cDAC instance exposing the requested COM interface. + /// + /// A pointer to the requested interface identifier. + /// An interface pointer. + /// An optional legacy pointer, or zero. + /// When this method returns, contains the requested interface pointer on success, or null on failure. The caller must release a successful result. + /// The nonzero target contract descriptor address. + /// The runtime image base address, or when unavailable. + /// An HRESULT indicating whether activation succeeded. + public static int CreateInstance(Guid* pIID, IntPtr pDataTarget, IntPtr legacyImplPtr, void** iface, ulong contractAddress, TargetPointer runtimeImageBase = default) + { + if (iface == null) + return HResults.E_INVALIDARG; + + *iface = null; + if (pIID == null || pDataTarget == IntPtr.Zero || contractAddress == 0) + return HResults.E_INVALIDARG; + + try + { + ICLRDataTarget dataTarget = ComInterfaceMarshaller.ConvertToManaged((void*)pDataTarget)!; + ContractDescriptorTarget target = CreateTarget(dataTarget, contractAddress, runtimeImageBase); + return CreateSosInterface(target, legacyImplPtr, *pIID, iface); + } + catch (System.Exception ex) + { + int hr = ex.HResult; + return hr < 0 ? hr : HResults.E_FAIL; + } + } + + /// + /// Locates the runtime image through the runtime locator, falling back to the data target's module lookup. + /// + /// The data target used to locate the runtime module. + /// When this method returns successfully, contains the runtime image base; otherwise, contains zero. + /// if a nonzero runtime image base was found; otherwise, . + public static bool TryGetRuntimeImageBase(ICLRDataTarget dataTarget, out TargetPointer address) + { + const string RuntimeModuleName = "coreclr.dll"; + + address = TargetPointer.Null; + ulong imageBase = 0; + int hr = HResults.S_OK; + if (dataTarget is not ICLRRuntimeLocator runtimeLocator || runtimeLocator.GetRuntimeBase(&imageBase) != HResults.S_OK) + { + imageBase = 0; + hr = dataTarget.GetImageBase(RuntimeModuleName, &imageBase); + } + if (hr != HResults.S_OK || imageBase == 0) + return false; + + address = new(imageBase); + return true; + } + + /// + /// Finds the contract descriptor through the data target's contract locator. + /// + /// The data target queried for . + /// When this method returns, contains the descriptor address on success, or zero on failure. + /// if the locator returns a nonzero descriptor address; otherwise, . + public static bool TryGetContractDescriptorAddress(ICLRDataTarget dataTarget, out TargetPointer address) + { + address = TargetPointer.Null; + ulong contractAddress = 0; + if (dataTarget is not ICLRContractLocator contractLocator + || contractLocator.GetContractDescriptor(&contractAddress) != HResults.S_OK + || contractAddress == 0) + { + return false; + } + + address = new(contractAddress); + return true; + } + + private static ContractDescriptorTarget CreateTarget(ICLRDataTarget dataTarget, ulong contractAddress, TargetPointer runtimeImageBase) + { + ContractDescriptorTarget.AllocVirtualDelegate allocVirtual = (ulong size, out ulong allocatedAddress) => + { + allocatedAddress = 0; + return HResults.E_NOTIMPL; + }; + + if (dataTarget is ICLRDataTarget2 dataTarget2) + { + const uint MemCommit = 0x1000; + const uint PageReadWrite = 0x04; + + allocVirtual = (ulong size, out ulong allocatedAddress) => + { + ClrDataAddress address; + int result = dataTarget2.AllocVirtual(0, (uint)size, MemCommit, PageReadWrite, &address); + allocatedAddress = address.Value; + return result; + }; + } + + return ContractDescriptorTarget.Create( + contractAddress, + (address, buffer) => + { + fixed (byte* bufferPointer = buffer) + { + uint bytesRead; + int hr = dataTarget.ReadVirtual(address, bufferPointer, (uint)buffer.Length, &bytesRead); + return hr < 0 || bytesRead == (uint)buffer.Length ? hr : HResults.E_FAIL; + } + }, + (address, buffer) => + { + fixed (byte* bufferPointer = buffer) + { + uint bytesWritten; + return dataTarget.WriteVirtual(address, bufferPointer, (uint)buffer.Length, &bytesWritten); + } + }, + dataTarget.GetThreadContext, + (threadId, context) => SetThreadContext(dataTarget, threadId, context), + allocVirtual, + [CoreCLRContracts.Register], + runtimeImageBase); + } + + private static int SetThreadContext(ICLRDataTarget dataTarget, uint threadId, ReadOnlySpan context) + { + const nuint ContextAlignment = 16; + fixed (byte* contextPointer = context) + { + if (((nuint)contextPointer & (ContextAlignment - 1)) == 0) + return dataTarget.SetThreadContext(threadId, (uint)context.Length, contextPointer); + + byte* alignedBuffer = (byte*)NativeMemory.AlignedAlloc((nuint)context.Length, ContextAlignment); + try + { + context.CopyTo(new Span(alignedBuffer, context.Length)); + return dataTarget.SetThreadContext(threadId, (uint)context.Length, alignedBuffer); + } + finally + { + NativeMemory.AlignedFree(alignedBuffer); + } + } + } + + private static int CreateSosInterface(ContractDescriptorTarget target, IntPtr legacyImplPtr, Guid iid, void** iface) + { + Lock apiLock = new(); + CoreCLRContracts.ValidateForDataAccess(target, apiLock); + object? legacyImpl = legacyImplPtr != IntPtr.Zero + ? ComInterfaceMarshaller.ConvertToManaged((void*)legacyImplPtr) + : null; + + SOSDacImpl impl = new(target, legacyImpl, apiLock); + void* ccw = ComInterfaceMarshaller.ConvertToUnmanaged(impl); + try + { + int hr = Marshal.QueryInterface((nint)ccw, iid, out nint requestedInterface); + if (hr < 0) + return hr; + + *iface = (void*)requestedInterface; + return HResults.S_OK; + } + finally + { + ComInterfaceMarshaller.Free(ccw); + } + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/DumpCreator.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/DumpCreator.cs new file mode 100644 index 00000000000000..5474b4e90e8a94 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/DumpCreator.cs @@ -0,0 +1,349 @@ +// 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.Collections.Generic; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.DataContractReader.Contracts.Extensions; +using Microsoft.Diagnostics.DataContractReader.Legacy; +using ContractModuleHandle = Microsoft.Diagnostics.DataContractReader.Contracts.ModuleHandle; + +namespace Microsoft.Diagnostics.DataContractReader.Legacy.EnumMemory; + +internal sealed class DumpCreator +{ + private const int MaxSyncBlocks = 1_000_000; + private const int MaxThreads = 1_000_000; + + private readonly Target _target; + private readonly bool _includeHeap; + private readonly MemoryRegionEmitter _emitter; + private readonly HashSet _loaderAllocators = []; + private readonly MethodCollector _methods; + private readonly ObjectCollector _objects; + + public DumpCreator( + Target target, + CLRDataEnumMemoryFlags flags, + MemoryRegionEmitter emitter) + { + _target = target; + _includeHeap = flags == CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_HEAP2; + _emitter = emitter; + _methods = new(target); + _objects = new(target, emitter, _methods, flags == CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_TRIAGE); + } + + public void EnumerateMemoryRegions() + { + TryEnumerate(EnumerateRuntimeModule); + TryEnumerate(EnumerateStatics); + TryEnumerate(EnumerateDebugger); + TryEnumerate(EnumerateModules); + TryEnumerate(EnumerateThreads); + + if (_includeHeap) + { + TryEnumerate(EnumerateGC); + TryEnumerate(EnumerateCodeAndLoaderHeaps); + TryEnumerate(EnumerateSyncBlocks); + TryEnumerate(EnumerateAuxiliarySymbols); + TryEnumerate(EnumerateStressLog); + } + + TryEnumerate(WriteMiniMetadata); + } + + private void EnumerateRuntimeModule() + { + if (RuntimeModuleInfo.TryCreate(_target, out RuntimeModuleInfo runtimeModule)) + { + foreach (TargetSpan range in runtimeModule.EnumerateMemoryRegions()) + _emitter.Add(range.Address.Value, range.Size); + } + } + + private static void TryEnumerate(Action enumerate) + { + try + { + enumerate(); + } + catch (System.Exception ex) when (ex.HResult != HResults.COR_E_OPERATIONCANCELED) + { + // Dump creation is best-effort because the target may be partially unreadable or corrupt. + } + } + + private void EnumerateStatics() + { + IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem; + rts.GetWellKnownMethodTable(WellKnownMethodTable.Array); + rts.GetWellKnownMethodTable(WellKnownMethodTable.Canon); + rts.GetWellKnownMethodTable(WellKnownMethodTable.Exception); + rts.GetWellKnownMethodTable(WellKnownMethodTable.Free); + rts.GetWellKnownMethodTable(WellKnownMethodTable.Object); + rts.GetWellKnownMethodTable(WellKnownMethodTable.String); + } + + private void EnumerateDebugger() + { + IDebugger debugger = _target.Contracts.Debugger; + debugger.TryGetDebuggerData(out _); + debugger.GetAttachStateFlags(); + _target.Contracts.ReJIT.IsEnabled(); + } + + private void EnumerateModules() + { + ILoader loader = _target.Contracts.Loader; + IEcmaMetadata ecmaMetadata = _target.Contracts.EcmaMetadata; + + TargetPointer appDomain = loader.GetAppDomain(); + loader.GetRootAssembly(); + IEnumerable modules = loader.GetModuleHandles( + appDomain, + AssemblyIterationFlags.IncludeLoaded | AssemblyIterationFlags.IncludeExecution); + + foreach (ContractModuleHandle module in modules) + { + loader.GetModule(module); + loader.GetAssembly(module); + TargetPointer peAssembly = loader.GetPEAssembly(module); + ModuleFlags flags = loader.GetFlags(module); + loader.GetSimpleName(module); + loader.GetPath(module); + loader.GetFileName(module); + loader.GetILBase(module); + loader.GetModuleLookupMapBase(module, ModuleLookupMapKind.FieldDefToDesc); + loader.GetModuleLookupMapBase(module, ModuleLookupMapKind.ManifestModuleReferences); + loader.GetModuleLookupMapBase(module, ModuleLookupMapKind.MemberRefToDesc); + loader.GetModuleLookupMapBase(module, ModuleLookupMapKind.MethodDefToDesc); + loader.GetModuleLookupMapBase(module, ModuleLookupMapKind.TypeDefToMethodTable); + loader.GetModuleLookupMapBase(module, ModuleLookupMapKind.TypeRefToMethodTable); + + if (loader.TryGetLoadedImageContents(module, out _, out _, out _)) + _emitter.RegisterMetadataRange(ecmaMetadata.GetReadOnlyMetadataAddress(module)); + + if (flags.HasFlag(ModuleFlags.ReflectionEmit)) + _emitter.RegisterMetadataRange(ecmaMetadata.GetReadWriteSavedMetadataAddress(module)); + + // Smaller dumps do not include in-memory symbols unless they are otherwise referenced. + if (loader.TryGetSymbolStream(module, out TargetPointer symbolBuffer, out uint symbolSize) && _includeHeap) + _emitter.Add(symbolBuffer.Value, symbolSize); + + if (peAssembly != TargetPointer.Null) + ecmaMetadata.HasReadWriteMetadata(peAssembly); + + TargetPointer loaderAllocator = loader.GetLoaderAllocator(module); + if (loaderAllocator != TargetPointer.Null) + _loaderAllocators.Add(loaderAllocator); + + loader.IsProbeExtensionResultValid(module); + } + + TargetPointer globalLoaderAllocator = loader.GetGlobalLoaderAllocator(); + if (globalLoaderAllocator != TargetPointer.Null) + { + _loaderAllocators.Add(globalLoaderAllocator); + loader.GetHighFrequencyHeap(globalLoaderAllocator); + loader.GetLowFrequencyHeap(globalLoaderAllocator); + } + + foreach (ContractModuleHandle module in loader.GetModuleHandles( + appDomain, + AssemblyIterationFlags.IncludeLoading | AssemblyIterationFlags.IncludeLoaded | AssemblyIterationFlags.IncludeExecution)) + { + loader.IsAssemblyLoaded(module); + } + + foreach (ContractModuleHandle _ in loader.GetModuleHandles(appDomain, AssemblyIterationFlags.IncludeFailedToLoad)) + { + } + } + + private void EnumerateThreads() + { + IThread thread = _target.Contracts.Thread; + ThreadStoreData threadStore = thread.GetThreadStoreData(); + _ = thread.GetThreadCounts(); + HashSet visited = []; + TargetPointer threadAddress = threadStore.FirstThread; + + for (int i = 0; threadAddress != TargetPointer.Null && i < MaxThreads; i++) + { + if (!visited.Add(threadAddress)) + break; + + ThreadData threadData = thread.GetThreadData(threadAddress); + thread.GetThreadAllocContext(threadAddress, out _, out _); + EnumerateStack(threadData); + if (threadData.LastThrownObjectHandle != TargetPointer.Null) + { + TargetPointer exceptionObject = + _target.ReadPointer(threadData.LastThrownObjectHandle.Value); + _objects.EnumerateObject(exceptionObject); + } + + threadAddress = threadData.NextThread; + } + } + + private void EnumerateStack(ThreadData threadData) + { + IStackWalk stackWalk = _target.Contracts.StackWalk; + foreach (IStackDataFrameHandle frame in stackWalk.CreateStackWalk(threadData)) + { + TargetCodePointer instructionPointer = stackWalk.GetInstructionPointer(frame); + TargetPointer methodDesc = stackWalk.GetMethodDescPtr(frame); + _methods.CaptureMethod(methodDesc, instructionPointer); + } + } + + private void WriteMiniMetadata() + { + Dictionary names = new(_methods.Names); + foreach ((TargetPointer address, string name) in _objects.Names) + names.TryAdd(address, name); + + MiniMetadataWriter.Write(_target, _emitter, names); + } + + private void EnumerateGC() + { + IGC gc = _target.Contracts.GC; + if (!gc.GetGCStructuresValid()) + return; + + string[] identifiers = gc.GetGCIdentifiers(); + gc.GetGCHeapCount(); + gc.GetMaxGeneration(); + gc.GetGCBounds(out _, out _); + gc.GetCurrentGCState(); + gc.TryGetGCDynamicAdaptationMode(out _); + gc.GetGlobalMechanisms(); + gc.GetGlobalAllocationContext(out _, out _); + + if (Array.IndexOf(identifiers, GCIdentifiers.Workstation) >= 0) + { + gc.GetHeapData(); + gc.GetOomData(); + } + else + { + foreach (TargetPointer heap in gc.GetGCHeaps()) + { + gc.GetHeapData(heap); + gc.GetOomData(heap); + } + } + + foreach ((GCHeapSegmentInfo segment, _) in gc.EnumerateAllSegments()) + { + if (segment.End > segment.Start) + _emitter.Add(segment.Start.Value, segment.End.Value - segment.Start.Value); + } + + AddGCMemoryRegions(gc.GetHandleTableMemoryRegions()); + AddGCMemoryRegions(gc.GetGCBookkeepingMemoryRegions()); + AddGCMemoryRegions(gc.GetGCFreeRegions()); + } + + private void AddGCMemoryRegions(IReadOnlyList regions) + { + foreach (GCMemoryRegionData region in regions) + _emitter.Add(region.Start.Value, region.Size); + } + + private void EnumerateCodeAndLoaderHeaps() + { + IExecutionManager executionManager = _target.Contracts.ExecutionManager; + foreach (JitManagerKind kind in new[] { JitManagerKind.EE, JitManagerKind.Interpreter }) + { + foreach (ICodeHeapInfo codeHeap in executionManager.GetCodeHeapInfos(kind)) + { + switch (codeHeap) + { + case HostCodeHeapInfo host when host.CurrentAddress > host.BaseAddress: + _emitter.Add(host.BaseAddress.Value, host.CurrentAddress.Value - host.BaseAddress.Value); + break; + case LoaderCodeHeapInfo loader: + AddLoaderHeapBlocks(loader.LoaderHeapAddress); + break; + } + } + } + + ILoader loaderContract = _target.Contracts.Loader; + foreach (TargetPointer loaderAllocator in _loaderAllocators) + { + IReadOnlyDictionary heaps = + loaderContract.GetLoaderAllocatorHeaps(loaderAllocator); + foreach (TargetPointer loaderHeap in heaps.Values) + AddLoaderHeapBlocks(loaderHeap); + } + } + + private void AddLoaderHeapBlocks(TargetPointer loaderHeap) + { + if (loaderHeap == TargetPointer.Null) + return; + + ILoader loader = _target.Contracts.Loader; + foreach (LoaderHeapBlock block in loader.EnumerateLoaderHeapBlocks(loaderHeap)) + _emitter.Add(block.Address.Value, block.Size.Value); + } + + private void EnumerateSyncBlocks() + { + ISyncBlock syncBlock = _target.Contracts.SyncBlock; + IThread thread = _target.Contracts.Thread; + uint count = syncBlock.GetSyncBlockCount(); + for (uint i = 1; i <= count; i++) + { + TargetPointer address = syncBlock.GetSyncBlock(i); + if (address == TargetPointer.Null || syncBlock.IsSyncBlockFree(i)) + continue; + + syncBlock.GetSyncBlockObject(i); + if (syncBlock.TryGetLockInfo(address, out uint owningThreadId, out _)) + thread.IdToThread(owningThreadId); + syncBlock.GetAdditionalThreadCount(address); + syncBlock.GetBuiltInComData(address, out _, out _, out _); + } + + TargetPointer cleanup = syncBlock.GetSyncBlockFromCleanupList(); + TraverseTargetLinkedList(cleanup, syncBlock.GetNextSyncBlock); + } + + private void EnumerateAuxiliarySymbols() + { + foreach ((TargetCodePointer _, string _) in _target.Contracts.AuxiliarySymbols.EnumerateAuxiliarySymbols()) + { + } + } + + internal static void TraverseTargetLinkedList( + TargetPointer current, + Func getNext) + { + HashSet visited = []; + for (int i = 0; + current != TargetPointer.Null && i < MaxSyncBlocks && visited.Add(current); + i++) + { + current = getNext(current); + } + } + + private void EnumerateStressLog() + { + IStressLog stressLog = _target.Contracts.StressLog; + if (!stressLog.HasStressLog()) + return; + + StressLogData data = stressLog.GetStressLogData(); + foreach (StressLogMemoryRange range in stressLog.GetStressLogMemoryRanges(data)) + _emitter.Add(range.Start.Value, range.Size); + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/MemoryRegionEnumerator.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/MemoryRegionEnumerator.cs new file mode 100644 index 00000000000000..52c76e25c4b18c --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/MemoryRegionEnumerator.cs @@ -0,0 +1,141 @@ +// 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.Collections.Generic; +using System.Runtime.InteropServices; + +namespace Microsoft.Diagnostics.DataContractReader.Legacy.EnumMemory; + +internal static unsafe class MemoryRegionEnumerator +{ + private const uint MiniDumpWithPrivateReadWriteMemory = 0x200; + private const uint MiniDumpWithFullAuxiliaryState = 0x8000; + private const uint MiniDumpFilterTriage = 0x100000; + + public static int Enumerate(Target target, void* callback, uint miniDumpFlags) + { + if (target is not ContractDescriptorTarget descriptorTarget) + return HResults.E_NOTIMPL; + + target.Flush(FlushScope.All); + var emitter = new MemoryRegionEmitter((nint)callback, (uint)target.PointerSize); + using IDisposable readScope = descriptorTarget.RegisterReadCallback((address, size) => + { + if (emitter.ShouldEmitTargetRead(address, size)) + emitter.Add(address, size); + }); + foreach (TargetSpan range in descriptorTarget.EnumerateDescriptorMemory()) + emitter.Add(range.Address.Value, range.Size); + + new DumpCreator(target, GetDumpFlags(miniDumpFlags), emitter).EnumerateMemoryRegions(); + return emitter.Result; + } + + internal static CLRDataEnumMemoryFlags GetDumpFlags(uint miniDumpFlags) + { + if ((miniDumpFlags & MiniDumpWithPrivateReadWriteMemory) != 0) + return CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_HEAP2; + if ((miniDumpFlags & MiniDumpWithFullAuxiliaryState) != 0) + return CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT; + if ((miniDumpFlags & MiniDumpFilterTriage) != 0) + return CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_TRIAGE; + + return CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_MINI; + } +} + +internal sealed unsafe class MemoryRegionEmitter(nint callback, uint pointerSize) +{ + private static readonly Guid s_ICLRDataEnumMemoryRegionsCallback2_Iid = new("3721A26F-8B91-4D98-A388-DB17B356FADB"); + + private readonly delegate* unmanaged[MemberFunction] _queryInterface = + (delegate* unmanaged[MemberFunction])(*(nint**)callback)[0]; + // ICLRDataEnumMemoryRegionsCallback::EnumMemoryRegion follows the three IUnknown vtable slots. + private readonly delegate* unmanaged[MemberFunction] _enumMemoryRegion = + (delegate* unmanaged[MemberFunction])(*(nint**)callback)[3]; + private readonly List _metadataRanges = []; + + public int Result { get; private set; } + + public void Add(ulong address, uint size) => Add(address, (ulong)size); + + public void Add(ulong address, ulong size) + { + if (address == 0 || size == 0) + return; + + while (size != 0) + { + uint chunkSize = (uint)Math.Min(size, uint.MaxValue); + int hr = _enumMemoryRegion(callback, ToClrDataAddress(address), chunkSize); + if (hr == HResults.COR_E_OPERATIONCANCELED) + Marshal.ThrowExceptionForHR(hr); + + if (hr < 0 && Result >= 0) + Result = hr; + + address = checked(address + chunkSize); + size -= chunkSize; + } + } + + public void RegisterMetadataRange(TargetSpan range) + { + if (range.Address != TargetPointer.Null && range.Size != 0) + _metadataRanges.Add(range); + } + + public bool ShouldEmitTargetRead(ulong address, ulong size) + { + foreach (TargetSpan range in _metadataRanges) + { + if (address < range.Address.Value) + continue; + + ulong offset = address - range.Address.Value; + if (offset <= range.Size && size <= range.Size - offset) + return false; + } + + return true; + } + + public bool Update(ulong address, ReadOnlySpan buffer) + { + nint callback2 = 0; + Guid iid = s_ICLRDataEnumMemoryRegionsCallback2_Iid; + int hr = _queryInterface(callback, &iid, &callback2); + + if (hr < 0 || callback2 == 0) + return false; + + try + { + delegate* unmanaged[MemberFunction] updateMemoryRegion = + (delegate* unmanaged[MemberFunction])(*(nint**)callback2)[4]; + fixed (byte* bufferPointer = buffer) + { + hr = updateMemoryRegion( + callback2, + ToClrDataAddress(address), + (uint)buffer.Length, + bufferPointer); + } + + if (hr < 0) + return false; + + return true; + } + finally + { + delegate* unmanaged[MemberFunction] release = + (delegate* unmanaged[MemberFunction])(*(nint**)callback2)[2]; + release(callback2); + } + } + + internal ulong ToClrDataAddress(ulong address) => + pointerSize == sizeof(uint) ? (ulong)(long)(int)address : address; +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/MethodCollector.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/MethodCollector.cs new file mode 100644 index 00000000000000..5de6e59d6ea175 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/MethodCollector.cs @@ -0,0 +1,129 @@ +// 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.Collections.Generic; +using System.Text; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.DataContractReader.Contracts.Extensions; +using Microsoft.Diagnostics.DataContractReader.Legacy; +using ContractModuleHandle = Microsoft.Diagnostics.DataContractReader.Contracts.ModuleHandle; + +namespace Microsoft.Diagnostics.DataContractReader.Legacy.EnumMemory; + +internal sealed class MethodCollector(Target target) +{ + private readonly Target _target = target; + private readonly HashSet _captured = []; + private readonly HashSet _capturedCodeBlocks = []; + private readonly Dictionary _names = []; + + public IReadOnlyDictionary Names => _names; + + public void CaptureMethod(TargetPointer methodDesc) + => CaptureMethod(methodDesc, TargetCodePointer.Null); + + public void CaptureMethod(TargetPointer methodDesc, TargetCodePointer instructionPointer) + { + if (instructionPointer != TargetCodePointer.Null) + EnumerateCodeDependencies(instructionPointer); + + if (methodDesc == TargetPointer.Null || !_captured.Add(methodDesc)) + return; + + EnumerateMethodDependencies(methodDesc); + EnumerateMethodDescDataDependencies(methodDesc); + CacheMethodName(methodDesc); + } + + private void EnumerateCodeDependencies(TargetCodePointer instructionPointer) + { + IExecutionManager executionManager = _target.Contracts.ExecutionManager; + if (executionManager.GetCodeBlockHandle(instructionPointer) is not CodeBlockHandle codeBlock + || !_capturedCodeBlocks.Add(codeBlock.Address)) + return; + + executionManager.GetGCInfo(codeBlock, out _, out _); + + IDebugInfo debugInfo = _target.Contracts.DebugInfo; + if (!debugInfo.HasDebugInfo(instructionPointer)) + return; + + foreach (OffsetMapping _ in debugInfo.GetMethodNativeMap(instructionPointer, preferUninstrumented: true, out _)) + { + } + + foreach (DebugVarInfo _ in debugInfo.GetMethodVarInfo(instructionPointer, out _)) + { + } + } + + private void EnumerateMethodDependencies(TargetPointer methodDesc) + { + IRuntimeTypeSystem types = _target.Contracts.RuntimeTypeSystem; + MethodDescHandle method = types.GetMethodDescHandle(methodDesc); + if (types.IsNoMetadataMethod(method, out _)) + return; + + uint token = types.GetMethodToken(method); + TargetPointer methodTable = types.GetMethodTable(method); + TargetPointer module = types.GetModule(types.GetTypeHandle(methodTable)); + ILoader loader = _target.Contracts.Loader; + ContractModuleHandle moduleHandle = loader.GetModuleHandleFromModulePtr(module); + loader.GetAssembly(moduleHandle); + loader.GetFlags(moduleHandle); + loader.GetPath(moduleHandle); + loader.IsProbeExtensionResultValid(moduleHandle); + loader.GetModuleLookupMapElement(moduleHandle, ModuleLookupMapKind.MethodDefToDesc, token, out _); + loader.GetILHeader(moduleHandle, token); + } + + private void EnumerateMethodDescDataDependencies(TargetPointer methodDesc) + { + IRuntimeTypeSystem types = _target.Contracts.RuntimeTypeSystem; + MethodDescHandle method = types.GetMethodDescHandle(methodDesc); + ICodeVersions codeVersions = _target.Contracts.CodeVersions; + NativeCodeVersionHandle codeVersion = codeVersions.GetActiveNativeCodeVersion(methodDesc); + + types.IsDynamicMethod(method); + types.GetSlotNumber(method); + if (codeVersion.Valid) + { + TargetCodePointer nativeCode = codeVersions.GetNativeCode(codeVersion); + if (nativeCode != TargetCodePointer.Null) + _target.Contracts.PrecodeStubs.GetInterpreterCodeFromInterpreterPrecodeIfPresent(nativeCode); + + codeVersions.GetGCStressCodeCopy(codeVersion); + } + if (types.HasNativeCodeSlot(method)) + types.GetAddressOfNativeCodeSlot(method); + } + + private void CacheMethodName(TargetPointer methodDesc) + { + if (_names.ContainsKey(methodDesc)) + return; + + IRuntimeTypeSystem types = _target.Contracts.RuntimeTypeSystem; + MethodDescHandle method = types.GetMethodDescHandle(methodDesc); + if (types.IsNoMetadataMethod(method, out _) && !types.IsILStub(method)) + return; + + try + { + StringBuilder name = new(); + TypeNameBuilder.AppendMethodInternal( + _target, + name, + method, + TypeNameFormat.FormatSignature | + TypeNameFormat.FormatNamespace | + TypeNameFormat.FormatFullInst); + if (name.Length != 0) + _names.Add(methodDesc, name.ToString()); + } + catch (System.Exception ex) when (ex.HResult != HResults.COR_E_OPERATIONCANCELED) + { + } + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/MiniMetadataWriter.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/MiniMetadataWriter.cs new file mode 100644 index 00000000000000..14d1fc997f7437 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/MiniMetadataWriter.cs @@ -0,0 +1,88 @@ +// 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.Buffers.Binary; +using System.Collections.Generic; +using System.Text; +using Microsoft.Diagnostics.DataContractReader.Contracts; + +namespace Microsoft.Diagnostics.DataContractReader.Legacy.EnumMemory; + +internal static class MiniMetadataWriter +{ + private const uint MiniMetadataSignature = 0x6d727473; + private const uint EENameStreamSignature = 0x614e4545; + private const int StreamsHeaderSize = 12; + private const int EENameStreamHeaderSize = 8; + + public static void Write( + Target target, + MemoryRegionEmitter emitter, + IReadOnlyDictionary names) + { + if (names.Count == 0) + return; + + TargetPointer bufferAddress = + target.ReadPointer(target.ReadGlobalPointer(Constants.Globals.MiniMetaDataBuffAddress)); + uint capacity = + target.Read(target.ReadGlobalPointer(Constants.Globals.MiniMetaDataBuffMaxSize)); + if (bufferAddress == TargetPointer.Null + || capacity < StreamsHeaderSize + EENameStreamHeaderSize) + { + return; + } + + byte[] buffer = new byte[capacity]; + int offset = StreamsHeaderSize + EENameStreamHeaderSize; + uint count = 0; + + foreach ((TargetPointer address, string name) in names) + { + int nameSize = Encoding.UTF8.GetByteCount(name); + int entrySize = checked(target.PointerSize + nameSize + 1); + if (offset > buffer.Length - entrySize) + break; + + WritePointer(buffer.AsSpan(offset, target.PointerSize), address.Value, target); + offset += target.PointerSize; + offset += Encoding.UTF8.GetBytes(name, buffer.AsSpan(offset, nameSize)); + buffer[offset++] = 0; + count++; + } + + WriteUInt32(buffer.AsSpan(0, sizeof(uint)), MiniMetadataSignature, target); + WriteUInt32(buffer.AsSpan(4, sizeof(uint)), checked((uint)offset), target); + WriteUInt32(buffer.AsSpan(8, sizeof(uint)), 1, target); + WriteUInt32(buffer.AsSpan(StreamsHeaderSize, sizeof(uint)), EENameStreamSignature, target); + WriteUInt32(buffer.AsSpan(StreamsHeaderSize + 4, sizeof(uint)), count, target); + + emitter.Add(bufferAddress.Value, checked((uint)offset)); + emitter.Update(bufferAddress.Value, buffer.AsSpan(0, offset)); + } + + private static void WritePointer(Span destination, ulong value, Target target) + { + if (target.PointerSize == sizeof(uint)) + { + WriteUInt32(destination, checked((uint)value), target); + } + else if (target.IsLittleEndian) + { + BinaryPrimitives.WriteUInt64LittleEndian(destination, value); + } + else + { + BinaryPrimitives.WriteUInt64BigEndian(destination, value); + } + } + + private static void WriteUInt32(Span destination, uint value, Target target) + { + if (target.IsLittleEndian) + BinaryPrimitives.WriteUInt32LittleEndian(destination, value); + else + BinaryPrimitives.WriteUInt32BigEndian(destination, value); + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/ObjectCollector.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/ObjectCollector.cs new file mode 100644 index 00000000000000..810d17b37f02b0 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/ObjectCollector.cs @@ -0,0 +1,285 @@ +// 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.Buffers.Binary; +using System.Collections.Generic; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using System.Runtime.InteropServices; +using System.Text; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.DataContractReader.Legacy; + +namespace Microsoft.Diagnostics.DataContractReader.Legacy.EnumMemory; + +internal sealed class ObjectCollector( + Target target, + MemoryRegionEmitter emitter, + MethodCollector methods, + bool isTriage) +{ + private const ulong MaxObjectSize = 64 * 1024 * 1024; + private const int MaxInnerExceptionCount = 256; + private const int MaxTypeTraversalDepth = 1_024; + + private readonly Target _target = target; + private readonly MemoryRegionEmitter _emitter = emitter; + private readonly MethodCollector _methods = methods; + private readonly Dictionary _names = []; + private readonly Stack _pendingObjects = []; + private readonly HashSet _visitedObjects = []; + private int _remainingInnerExceptions; + + public IReadOnlyDictionary Names => _names; + + public void EnumerateObject(TargetPointer objectAddress) + { + _remainingInnerExceptions = MaxInnerExceptionCount; + _pendingObjects.Push(objectAddress); + try + { + while (_pendingObjects.TryPop(out TargetPointer pendingObject)) + EnumerateObjectCore(pendingObject); + } + finally + { + _pendingObjects.Clear(); + } + } + + private void EnumerateObjectCore(TargetPointer objectAddress) + { + if (objectAddress == TargetPointer.Null || !_visitedObjects.Add(objectAddress)) + { + return; + } + + try + { + IObject objects = _target.Contracts.Object; + IRuntimeTypeSystem types = _target.Contracts.RuntimeTypeSystem; + ulong objectSize = objects.GetSize(objectAddress); + if (objectSize == 0 || objectSize > MaxObjectSize) + return; + + _emitter.Add(objectAddress.Value, objectSize); + objects.GetSyncBlockAddress(objectAddress); + + TargetPointer methodTable = objects.GetMethodTableAddress(objectAddress); + ITypeHandle type = types.GetTypeHandle(methodTable); + bool isException = EnumerateTypeHierarchy(type); + + if (methodTable == types.GetWellKnownMethodTable(WellKnownMethodTable.String)) + { + objects.GetStringValue(objectAddress); + } + else if (types.IsArray(type, out _)) + { + EnumerateArray(objects, objectAddress, type); + } + + if (_target.Contracts.FeatureFlags.IsEnabled(RuntimeFeature.COMInterop)) + objects.GetBuiltInComData(objectAddress, out _, out _, out _); + + if (isException) + EnumerateExceptionData(objectAddress); + + CacheMethodTableName(methodTable); + } + catch (System.Exception ex) when (ex.HResult != HResults.COR_E_OPERATIONCANCELED) + { + } + } + + private bool EnumerateTypeHierarchy(ITypeHandle type) + { + IRuntimeTypeSystem types = _target.Contracts.RuntimeTypeSystem; + TargetPointer exceptionMethodTable = + types.GetWellKnownMethodTable(WellKnownMethodTable.Exception); + bool isException = false; + + for (int depth = 0; type.Address != TargetPointer.Null && depth < MaxTypeTraversalDepth; depth++) + { + EnumerateMethodTableData(type); + isException |= type.Address == exceptionMethodTable; + + TargetPointer parentMethodTable = types.GetParentMethodTable(type); + if (parentMethodTable == TargetPointer.Null) + break; + + type = types.GetTypeHandle(parentMethodTable); + } + + return isException; + } + + private void EnumerateMethodTableData(ITypeHandle type) + { + IRuntimeTypeSystem types = _target.Contracts.RuntimeTypeSystem; + types.GetBaseSize(type); + types.GetComponentSize(type); + if (types.IsFreeObjectMethodTable(type)) + return; + + types.GetModule(type); + types.GetCanonicalMethodTable(type); + types.GetParentMethodTable(type); + types.GetNumInterfaces(type); + types.GetNumMethods(type); + types.GetTypeDefToken(type); + types.GetTypeDefTypeAttributes(type); + types.ContainsGCPointers(type); + types.IsDynamicStatics(type); + } + + private void EnumerateArray(IObject objects, TargetPointer objectAddress, ITypeHandle type) + { + IRuntimeTypeSystem types = _target.Contracts.RuntimeTypeSystem; + objects.GetArrayData(objectAddress, out _, out _, out _, out _, out _); + ITypeHandle elementType = types.GetTypeParam(type); + types.GetSignatureCorElementType(elementType); + for (int depth = 0; depth < MaxTypeTraversalDepth && types.IsArray(elementType, out _); depth++) + elementType = types.GetTypeParam(elementType); + } + + private void EnumerateExceptionData(TargetPointer exceptionObject) + { + IException exceptions = _target.Contracts.Exception; + ExceptionData data = exceptions.GetExceptionData(exceptionObject); + if (data.InnerException != TargetPointer.Null && _remainingInnerExceptions > 0) + { + _remainingInnerExceptions--; + _pendingObjects.Push(data.InnerException); + } + EnumerateStackTraceString(data.StackTraceString); + if (data.RemoteStackTraceString != TargetPointer.Null && + (!isTriage || !ExceptionTypeOverridesStackTraceGetter(exceptionObject))) + { + EnumerateStackTraceString(data.RemoteStackTraceString); + } + _pendingObjects.Push(data.WatsonBuckets); + _pendingObjects.Push(data.StackTrace); + if (!isTriage) + _pendingObjects.Push(data.Message); + + foreach (ExceptionStackFrameInfo frame in exceptions.GetExceptionStackFrames(exceptionObject)) + _methods.CaptureMethod(frame.MethodDesc); + } + + private bool ExceptionTypeOverridesStackTraceGetter(TargetPointer exceptionObject) + { + IRuntimeTypeSystem types = _target.Contracts.RuntimeTypeSystem; + TargetPointer methodTable = _target.Contracts.Object.GetMethodTableAddress(exceptionObject); + TargetPointer exceptionMethodTable = types.GetWellKnownMethodTable(WellKnownMethodTable.Exception); + if (methodTable == exceptionMethodTable) + return false; + + ITypeHandle exceptionType = types.GetTypeHandle(exceptionMethodTable); + ITypeHandle objectType = types.GetTypeHandle(types.GetWellKnownMethodTable(WellKnownMethodTable.Object)); + ITypeHandle derivedType = types.GetTypeHandle(methodTable); + Contracts.ModuleHandle module = _target.Contracts.Loader.GetModuleHandleFromModulePtr(types.GetModule(exceptionType)); + MetadataReader metadata = _target.Contracts.EcmaMetadata.GetMetadata(module) + ?? throw new InvalidOperationException("Exception metadata is required to identify the StackTrace getter."); + + ushort numSlots = types.GetNumVtableSlots(exceptionType); + for (ushort slot = types.GetNumVtableSlots(objectType); slot < numSlots; slot++) + { + TargetPointer methodDesc = types.GetMethodDescForSlot(exceptionType, slot); + if (methodDesc == TargetPointer.Null) + continue; + + uint token = types.GetMethodToken(types.GetMethodDescHandle(methodDesc)); + MethodDefinition method = metadata.GetMethodDefinition(MetadataTokens.MethodDefinitionHandle((int)(token & 0x00FFFFFF))); + if (metadata.StringComparer.Equals(method.Name, "get_StackTrace")) + return types.GetMethodDescForSlot(derivedType, slot) != methodDesc; + } + + throw new InvalidOperationException("Could not find the StackTrace getter on System.Exception."); + } + + private void EnumerateStackTraceString(TargetPointer address) + { + if (address == TargetPointer.Null) + return; + + EnumerateObjectCore(address); + if (!isTriage) + return; + + IObject objects = _target.Contracts.Object; + objects.GetStringData(address, out uint length, out uint offsetToFirstChar); + if (length == 0 || length > MaxObjectSize / sizeof(char)) + return; + + char[] buffer = new char[checked((int)length)]; + Span bytes = MemoryMarshal.AsBytes(buffer.AsSpan()); + _target.ReadBuffer(address + offsetToFirstChar, bytes); + if (_target.IsLittleEndian != BitConverter.IsLittleEndian) + { + for (int i = 0; i < buffer.Length; i++) + buffer[i] = (char)BinaryPrimitives.ReverseEndianness((ushort)buffer[i]); + } + StripFileInfoFromStackTrace(buffer); + if (_target.IsLittleEndian != BitConverter.IsLittleEndian) + { + for (int i = 0; i < buffer.Length; i++) + buffer[i] = (char)BinaryPrimitives.ReverseEndianness((ushort)buffer[i]); + } + // Native DAC also uses the optional update callback, leaving the string length unchanged. + _emitter.Update(address.Value + offsetToFirstChar, bytes); + } + + internal static void StripFileInfoFromStackTrace(Span buffer) + { + // Mirror StripFileInfoFromStackTrace in vm/excep.cpp, including trailing-text removal. + int depth = 0; + int written = 0; + int lastMethodEnd = 0; + for (int i = 0; i < buffer.Length; i++) + { + char c = buffer[i]; + buffer[written++] = c; + if (c == '(') + depth++; + else if (c == ')') + { + if (depth == 1) + { + lastMethodEnd = written; + while (i + 1 < buffer.Length && buffer[i + 1] is not '\r' and not '\n') + i++; + } + depth--; + } + } + + buffer[lastMethodEnd..].Clear(); + } + + private void CacheMethodTableName(TargetPointer methodTable) + { + if (methodTable == TargetPointer.Null || _names.ContainsKey(methodTable)) + return; + + try + { + IRuntimeTypeSystem types = _target.Contracts.RuntimeTypeSystem; + ITypeHandle type = types.GetTypeHandle(methodTable); + if (types.IsFreeObjectMethodTable(type)) + return; + + StringBuilder name = new(); + TypeNameBuilder.AppendType( + _target, + name, + type, + TypeNameFormat.FormatNamespace | TypeNameFormat.FormatFullInst); + if (name.Length != 0) + _names.Add(methodTable, name.ToString()); + } + catch (System.Exception ex) when (ex.HResult != HResults.COR_E_OPERATIONCANCELED) + { + } + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/RuntimeModuleInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/RuntimeModuleInfo.cs new file mode 100644 index 00000000000000..3203a001675a3b --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/RuntimeModuleInfo.cs @@ -0,0 +1,239 @@ +// 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.Buffers.Binary; +using System.Collections.Generic; + +namespace Microsoft.Diagnostics.DataContractReader.Legacy.EnumMemory; + +/// +/// Describes a runtime module's PE headers, directories, and exports. +/// +public sealed class RuntimeModuleInfo +{ + private const int DosHeaderSize = 64; + private const int PeHeaderSize = 264; + private const int OptionalHeaderOffset = 24; + private const int SizeOfHeadersOffset = OptionalHeaderOffset + 60; + private const int Pe32DataDirectoryOffset = OptionalHeaderOffset + 96; + private const int Pe32PlusDataDirectoryOffset = OptionalHeaderOffset + 112; + private const int ExportDirectoryIndex = 0; + private const int ResourceDirectoryIndex = 2; + private const int DebugDirectoryIndex = 6; + private const ushort DosSignature = 0x5a4d; + private const ushort Pe32Magic = 0x10b; + private const ushort Pe32PlusMagic = 0x20b; + private const uint PeSignature = 0x00004550; + + /// + /// Reads memory from the target process. + /// + /// The target address to read. + /// The buffer to fill with target memory. + /// if the entire buffer was read; otherwise, . + public delegate bool TryReadMemory(ulong address, Span buffer); + + private RuntimeModuleInfo(ulong imageBase, uint sizeOfHeaders, TargetDirectory exportDirectory, TargetDirectory resourceDirectory, TargetDirectory debugDirectory) + { + ImageBase = imageBase; + SizeOfHeaders = sizeOfHeaders; + ExportDirectory = exportDirectory; + ResourceDirectory = resourceDirectory; + DebugDirectory = debugDirectory; + } + + internal ulong ImageBase { get; } + internal uint SizeOfHeaders { get; } + private TargetDirectory ExportDirectory { get; } + private TargetDirectory ResourceDirectory { get; } + private TargetDirectory DebugDirectory { get; } + + internal IEnumerable EnumerateMemoryRegions() + { + if (ImageBase != 0 && SizeOfHeaders != 0) + yield return new TargetSpan(ImageBase, SizeOfHeaders); + if (DebugDirectory.Address != 0 && DebugDirectory.Size != 0) + yield return new TargetSpan(DebugDirectory.Address, DebugDirectory.Size); + if (ResourceDirectory.Address != 0 && ResourceDirectory.Size != 0) + yield return new TargetSpan(ResourceDirectory.Address, ResourceDirectory.Size); + if (ExportDirectory.Address != 0 && ExportDirectory.Size != 0) + yield return new TargetSpan(ExportDirectory.Address, ExportDirectory.Size); + } + + internal static bool TryCreate(Target target, out RuntimeModuleInfo module) + { + module = null!; + if (!target.TryGetRuntimeImageBase(out TargetPointer imageBase)) + return false; + + try + { + return TryCreate(imageBase, (address, buffer) => + { + target.ReadBuffer(address, buffer); + return true; + }, out module); + } + catch (VirtualReadException) + { + return false; + } + } + + /// + /// Reads the runtime module's PE headers and directories. + /// + /// The runtime module's image base address. + /// The callback used to read target memory. + /// When this method returns, contains the module information on success, or on failure. + /// if the headers were read successfully or the image is not PE; otherwise, . + public static bool TryCreate(ulong imageBase, TryReadMemory readMemory, out RuntimeModuleInfo module) + { + module = null!; + Span dosHeader = stackalloc byte[DosHeaderSize]; + if (!readMemory(imageBase, dosHeader)) + return false; + + if (BinaryPrimitives.ReadUInt16LittleEndian(dosHeader) != DosSignature) + { + module = new(imageBase, 0, default, default, default); + return true; + } + + int peHeaderOffset = BinaryPrimitives.ReadInt32LittleEndian(dosHeader[0x3c..]); + if (peHeaderOffset < 0 || !TryAdd(imageBase, (uint)peHeaderOffset, out ulong peHeaderAddress)) + return false; + + Span peHeader = stackalloc byte[PeHeaderSize]; + if (!readMemory(peHeaderAddress, peHeader) + || BinaryPrimitives.ReadUInt32LittleEndian(peHeader) != PeSignature) + { + return false; + } + + int dataDirectoryOffset = BinaryPrimitives.ReadUInt16LittleEndian(peHeader[OptionalHeaderOffset..]) switch + { + Pe32Magic => Pe32DataDirectoryOffset, + Pe32PlusMagic => Pe32PlusDataDirectoryOffset, + _ => -1, + }; + if (dataDirectoryOffset < 0) + return false; + + uint sizeOfHeaders = BinaryPrimitives.ReadUInt32LittleEndian(peHeader[SizeOfHeadersOffset..]); + if (sizeOfHeaders == 0) + return false; + + module = new( + imageBase, + sizeOfHeaders, + GetDirectory(peHeader, imageBase, dataDirectoryOffset, ExportDirectoryIndex), + GetDirectory(peHeader, imageBase, dataDirectoryOffset, ResourceDirectoryIndex), + GetDirectory(peHeader, imageBase, dataDirectoryOffset, DebugDirectoryIndex)); + return true; + } + + /// + /// Finds an exported symbol in the runtime module. + /// + /// The callback used to read target memory. + /// The UTF-8 symbol name without a null terminator. + /// When this method returns successfully, contains the exported symbol's address. + /// if the symbol was found; otherwise, . + public bool TryGetExport(TryReadMemory readMemory, ReadOnlySpan symbolName, out ulong address) + { + address = 0; + if (ExportDirectory.Size < 40) + return false; + + Span exportDirectory = stackalloc byte[40]; + if (!readMemory(ExportDirectory.Address, exportDirectory)) + return false; + + uint functionCount = BinaryPrimitives.ReadUInt32LittleEndian(exportDirectory[20..]); + uint nameCount = BinaryPrimitives.ReadUInt32LittleEndian(exportDirectory[24..]); + uint functionTableRva = BinaryPrimitives.ReadUInt32LittleEndian(exportDirectory[28..]); + uint nameTableRva = BinaryPrimitives.ReadUInt32LittleEndian(exportDirectory[32..]); + uint ordinalTableRva = BinaryPrimitives.ReadUInt32LittleEndian(exportDirectory[36..]); + if (functionCount > ExportDirectory.Size / sizeof(uint) + || nameCount > ExportDirectory.Size / sizeof(uint)) + { + return false; + } + + byte[] targetName = new byte[symbolName.Length + 1]; + Span namePointer = stackalloc byte[sizeof(uint)]; + Span ordinalBuffer = stackalloc byte[sizeof(ushort)]; + Span functionRvaBuffer = stackalloc byte[sizeof(uint)]; + for (uint nameIndex = 0; nameIndex < nameCount; nameIndex++) + { + if (!TryAdd(ImageBase, nameTableRva, nameIndex, sizeof(uint), out ulong namePointerAddress) + || !readMemory(namePointerAddress, namePointer)) + { + return false; + } + + uint nameRva = BinaryPrimitives.ReadUInt32LittleEndian(namePointer); + if (nameRva == 0 || !TryAdd(ImageBase, nameRva, out ulong nameAddress) + || !readMemory(nameAddress, targetName)) + { + continue; + } + + if (!targetName.AsSpan(0, symbolName.Length).SequenceEqual(symbolName) + || targetName[^1] != 0) + { + continue; + } + + if (!TryAdd(ImageBase, ordinalTableRva, nameIndex, sizeof(ushort), out ulong ordinalAddress) + || !readMemory(ordinalAddress, ordinalBuffer)) + { + return false; + } + + ushort ordinal = BinaryPrimitives.ReadUInt16LittleEndian(ordinalBuffer); + if (ordinal >= functionCount + || !TryAdd(ImageBase, functionTableRva, ordinal, sizeof(uint), out ulong functionAddress) + || !readMemory(functionAddress, functionRvaBuffer)) + { + return false; + } + + uint functionRva = BinaryPrimitives.ReadUInt32LittleEndian(functionRvaBuffer); + return functionRva != 0 && TryAdd(ImageBase, functionRva, out address); + } + + return false; + } + + private static TargetDirectory GetDirectory(ReadOnlySpan peHeader, ulong imageBase, int dataDirectoryOffset, int directoryIndex) + { + int entryOffset = dataDirectoryOffset + (directoryIndex * 2 * sizeof(uint)); + uint rva = BinaryPrimitives.ReadUInt32LittleEndian(peHeader[entryOffset..]); + uint size = BinaryPrimitives.ReadUInt32LittleEndian(peHeader[(entryOffset + sizeof(uint))..]); + return rva != 0 && size != 0 && TryAdd(imageBase, rva, out ulong address) + ? new(address, size) + : default; + } + + private static bool TryAdd(ulong baseAddress, uint offset, out ulong address) + { + address = baseAddress + offset; + return address >= baseAddress; + } + + private static bool TryAdd(ulong baseAddress, uint tableRva, uint index, uint elementSize, out ulong address) + { + ulong tableOffset = (ulong)tableRva + ((ulong)index * elementSize); + address = baseAddress + tableOffset; + return tableOffset >= tableRva && address >= baseAddress; + } + + private readonly struct TargetDirectory(ulong address, uint size) + { + public ulong Address { get; } = address; + public uint Size { get; } = size; + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ICLRData.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ICLRData.cs index 2e81d2e00a9f63..e46f33b118139e 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ICLRData.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ICLRData.cs @@ -10,12 +10,21 @@ namespace Microsoft.Diagnostics.DataContractReader.Legacy; // This file contains managed declarations for the IXCLRData interfaces. // See src/coreclr/inc/clrdata.idl +public enum CLRDataEnumMemoryFlags +{ + CLRDATA_ENUM_MEM_DEFAULT = 0x0, + CLRDATA_ENUM_MEM_MINI = CLRDATA_ENUM_MEM_DEFAULT, + CLRDATA_ENUM_MEM_HEAP = 0x1, + CLRDATA_ENUM_MEM_TRIAGE = 0x2, + CLRDATA_ENUM_MEM_HEAP2 = 0x3, +} + [GeneratedComInterface] [Guid("471c35b4-7c2f-4ef0-a945-00f8c38056f1")] public unsafe partial interface ICLRDataEnumMemoryRegions { [PreserveSig] - int EnumMemoryRegions(/*ICLRDataEnumMemoryRegionsCallback*/ void* callback, uint miniDumpFlags, /*CLRDataEnumMemoryFlags*/ int clrFlags); + int EnumMemoryRegions(/*ICLRDataEnumMemoryRegionsCallback*/ void* callback, uint miniDumpFlags, CLRDataEnumMemoryFlags clrFlags); } [GeneratedComInterface] @@ -56,6 +65,34 @@ public unsafe partial interface ICLRDataTarget int Request(uint reqCode, uint inBufferSize, byte* inBuffer, uint outBufferSize, byte* outBuffer); } +internal static unsafe class CLRDataTargetExtensions +{ + internal static int GetThreadContext(this ICLRDataTarget target, uint threadId, uint contextFlags, Span buffer) + { + const nuint ContextAlignment = 16; + fixed (byte* context = buffer) + { + if (((nuint)context & (ContextAlignment - 1)) == 0) + return target.GetThreadContext(threadId, contextFlags, (uint)buffer.Length, context); + + byte* alignedContext = (byte*)NativeMemory.AlignedAlloc((nuint)buffer.Length, ContextAlignment); + NativeMemory.Clear(alignedContext, (nuint)buffer.Length); + try + { + int hr = target.GetThreadContext(threadId, contextFlags, (uint)buffer.Length, alignedContext); + if (hr == HResults.S_OK) + new ReadOnlySpan(alignedContext, buffer.Length).CopyTo(buffer); + + return hr; + } + finally + { + NativeMemory.AlignedFree(alignedContext); + } + } + } +} + [GeneratedComInterface] [Guid("6d05fae3-189c-4630-a6dc-1c251e1c01ab")] public unsafe partial interface ICLRDataTarget2 : ICLRDataTarget diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.ICLRDataEnumMemoryRegions.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.ICLRDataEnumMemoryRegions.cs index 3708b8cfb19af8..22420ef51b5069 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.ICLRDataEnumMemoryRegions.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.ICLRDataEnumMemoryRegions.cs @@ -2,8 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; - using System.Threading; +using Microsoft.Diagnostics.DataContractReader.Legacy.EnumMemory; + namespace Microsoft.Diagnostics.DataContractReader.Legacy; /// @@ -12,10 +13,22 @@ namespace Microsoft.Diagnostics.DataContractReader.Legacy; /// public sealed unsafe partial class SOSDacImpl : ICLRDataEnumMemoryRegions { - int ICLRDataEnumMemoryRegions.EnumMemoryRegions(void* callback, uint miniDumpFlags, int clrFlags) + int ICLRDataEnumMemoryRegions.EnumMemoryRegions(void* callback, uint miniDumpFlags, CLRDataEnumMemoryFlags clrFlags) { using Lock.Scope scope = _apiLock.EnterScope(); - return HResults.E_NOTIMPL; + if (callback is null) + return HResults.E_INVALIDARG; + + try + { + // Like the native DAC, ignore the reserved clrFlags argument. + return MemoryRegionEnumerator.Enumerate(_target, callback, miniDumpFlags); + } + catch (Exception ex) + { + int hr = ex.HResult; + return hr < 0 ? hr : HResults.E_FAIL; + } } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs index 0083167031b474..cd6fbb088136ae 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -31,7 +31,7 @@ namespace Microsoft.Diagnostics.DataContractReader.Legacy; /// [GeneratedComClass] public sealed unsafe partial class SOSDacImpl - : ICustomQueryInterface, ISOSDacInterface, ISOSDacInterface2, ISOSDacInterface3, ISOSDacInterface4, ISOSDacInterface5, + : ISOSDacInterface, ISOSDacInterface2, ISOSDacInterface3, ISOSDacInterface4, ISOSDacInterface5, ISOSDacInterface6, ISOSDacInterface7, ISOSDacInterface8, ISOSDacInterface9, ISOSDacInterface10, ISOSDacInterface11, ISOSDacInterface12, ISOSDacInterface13, ISOSDacInterface14, ISOSDacInterface15, ISOSDacInterface16, ISOSDacInterface17 @@ -105,15 +105,6 @@ public SOSDacImpl(Target target, object? legacyObj, Lock apiLock) #endif } - CustomQueryInterfaceResult ICustomQueryInterface.GetInterface(ref Guid iid, out nint ppv) - { - ppv = default; - - return iid == typeof(ICLRDataEnumMemoryRegions).GUID - ? CustomQueryInterfaceResult.Failed - : CustomQueryInterfaceResult.NotHandled; - } - #region ISOSDacInterface int ISOSDacInterface.GetAppDomainConfigFile(ClrDataAddress appDomain, int count, char* configFile, uint* pNeeded) { diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs index 82748adff5b676..fb1c5d673e0900 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs @@ -12,7 +12,6 @@ using System.Text.Json; using Microsoft.Diagnostics.DataContractReader.Data; using Microsoft.Diagnostics.DataContractReader.Contracts; -using System.Collections.Frozen; namespace Microsoft.Diagnostics.DataContractReader; @@ -37,8 +36,9 @@ private readonly struct Configuration } private readonly Configuration _config; + private readonly TargetPointer _runtimeImageBase; - private readonly DataTargetDelegates _dataTargetDelegates; + private DataTargetDelegates _dataTargetDelegates; private readonly List _descriptors = []; @@ -51,9 +51,9 @@ private readonly struct Configuration // 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(); - private IReadOnlyDictionary _types = new Dictionary(); + private Dictionary _contracts = []; + private Dictionary _globals = []; + private Dictionary _types = []; public override ContractRegistry Contracts { get; } public override DataCache ProcessedData { get; } @@ -90,6 +90,7 @@ public override bool IsSubDescriptorResolved(string name) /// 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 runtime image base address, or when unavailable. This value is fixed for the lifetime of the target. /// The target object. public static ContractDescriptorTarget Create( ulong contractDescriptor, @@ -98,11 +99,12 @@ public static ContractDescriptorTarget Create( GetTargetThreadContextDelegate getThreadContext, SetTargetThreadContextDelegate setThreadContext, AllocVirtualDelegate allocVirtual, - Action[] contractRegistrations) + Action[] contractRegistrations, + TargetPointer runtimeImageBase = default) { DataTargetDelegates dataTargetDelegates = new DataTargetDelegates(readFromTarget, writeToTarget, getThreadContext, setThreadContext, allocVirtual); Descriptor descriptor = ReadContractDescriptor(contractDescriptor, dataTargetDelegates); - return new ContractDescriptorTarget(descriptor, dataTargetDelegates, contractRegistrations); + return new ContractDescriptorTarget(descriptor, dataTargetDelegates, contractRegistrations, runtimeImageBase); } /// @@ -118,6 +120,7 @@ public static ContractDescriptorTarget Create( /// Whether the target is little-endian /// The size of a pointer in bytes in the target process. /// Registration actions that populate the contract registry (e.g., ) + /// The runtime image base address, or when unavailable. This value is fixed for the lifetime of the target. /// The target object. public static ContractDescriptorTarget Create( ContractDescriptorParser.ContractDescriptor contractDescriptor, @@ -129,7 +132,8 @@ public static ContractDescriptorTarget Create( AllocVirtualDelegate allocVirtual, bool isLittleEndian, int pointerSize, - Action[]? contractRegistrations = null) + Action[]? contractRegistrations = null, + TargetPointer runtimeImageBase = default) { return new ContractDescriptorTarget( new Descriptor @@ -139,21 +143,61 @@ public static ContractDescriptorTarget Create( PointerData = globalPointerValues }, new DataTargetDelegates(readFromTarget, writeToTarget, getThreadContext, setThreadContext, allocVirtual), - contractRegistrations ?? []); + contractRegistrations ?? [], runtimeImageBase); } - private ContractDescriptorTarget(Descriptor mainDescriptor, DataTargetDelegates dataTargetDelegates, Action[] contractRegistrations) + private ContractDescriptorTarget(Descriptor mainDescriptor, DataTargetDelegates dataTargetDelegates, Action[] contractRegistrations, TargetPointer runtimeImageBase) { Contracts = new CachingContractRegistry(this, this.TryGetContractVersion, contractRegistrations); ProcessedData = new DataCache(this); _config = mainDescriptor.Config; + _runtimeImageBase = runtimeImageBase; _dataTargetDelegates = dataTargetDelegates; AddDescriptor(mainDescriptor); BuildDescriptors(forceBuild: true); } + public override bool TryGetRuntimeImageBase(out TargetPointer imageBase) + { + imageBase = _runtimeImageBase; + return imageBase != TargetPointer.Null; + } + + /// Reports successful target memory reads until the returned scope is disposed. + /// Receives successful memory reads as an address and size in bytes. + /// A scope that restores the previous reader when disposed. + /// Access to the target must be serialized. Nested scopes must be disposed in reverse order. + public IDisposable RegisterReadCallback(Action reportRead) + { + ReadCallbackScope scope = new(this, _dataTargetDelegates); + _dataTargetDelegates = _dataTargetDelegates.WithReadCallback(reportRead); + return scope; + } + + /// Enumerates the target memory backing the loaded descriptors and their sub-descriptor pointer slots. + /// The native descriptor headers, JSON, pointer tables, and pointer slots. + /// Does not read target memory. Externally supplied descriptors have no native backing ranges. + public IEnumerable EnumerateDescriptorMemory() + { + foreach (Descriptor descriptor in _descriptors) + { + if (descriptor.HeaderMemory.Size != 0) + yield return descriptor.HeaderMemory; + if (descriptor.JsonMemory.Size != 0) + yield return descriptor.JsonMemory; + if (descriptor.PointerDataMemory.Size != 0) + yield return descriptor.PointerDataMemory; + + foreach ((_, TargetPointer slot) in GetSubDescriptors(descriptor)) + { + if (slot != TargetPointer.Null) + yield return new TargetSpan(slot, (uint)PointerSize); + } + } + } + public override void Flush(FlushScope scope) { base.Flush(scope); @@ -301,9 +345,9 @@ private void BuildDescriptors(bool forceBuild = false) } } - _contracts = contracts.ToFrozenDictionary(); - _globals = globals.ToFrozenDictionary(); - _types = types.ToFrozenDictionary(); + _contracts = contracts; + _globals = globals; + _types = types; } private struct GlobalValue @@ -318,6 +362,9 @@ private struct Descriptor public Configuration Config { get; init; } public ContractDescriptorParser.ContractDescriptor ContractDescriptor { get; init; } public TargetPointer[] PointerData { get; init; } + public TargetSpan HeaderMemory { get; init; } + public TargetSpan JsonMemory { get; init; } + public TargetSpan PointerDataMemory { get; init; } } private static IEnumerable<(string Name, TargetPointer Slot)> GetSubDescriptors(Descriptor descriptor) @@ -349,6 +396,7 @@ private static Descriptor ReadContractDescriptor( ulong address, DataTargetDelegates dataTargetDelegates) { + ulong descriptorAddress = address; // Magic - uint64_t Span buffer = stackalloc byte[sizeof(ulong)]; if (dataTargetDelegates.ReadFromTarget(address, buffer) < 0) @@ -439,7 +487,10 @@ private static Descriptor ReadContractDescriptor( { Config = config, ContractDescriptor = contractDescriptor, - PointerData = pointerData + PointerData = pointerData, + HeaderMemory = new(new(descriptorAddress), address - descriptorAddress + (uint)pointerSize), + JsonMemory = new(descriptorAddr, descriptorSize), + PointerDataMemory = new(pointerDataAddr, (ulong)pointerDataCount * (uint)pointerSize), }; } @@ -989,6 +1040,20 @@ public void Clear() } } + private sealed class ReadCallbackScope(ContractDescriptorTarget target, DataTargetDelegates original) : IDisposable + { + private bool _disposed; + + public void Dispose() + { + if (_disposed) + return; + + target._dataTargetDelegates = original; + _disposed = true; + } + } + private readonly struct DataTargetDelegates( ReadFromTargetDelegate readFromTarget, WriteToTargetDelegate writeToTarget, @@ -1020,5 +1085,20 @@ public int AllocVirtual(ulong size, out ulong allocatedAddress) { return allocVirtual(size, out allocatedAddress); } + + public DataTargetDelegates WithReadCallback(Action reportRead) + { + ReadFromTargetDelegate reader = readFromTarget; + return new( + (address, buffer) => + { + int hr = reader(address, buffer); + if (hr >= 0) + reportRead(address, (ulong)buffer.Length); + + return hr; + }, + writeToTarget, getThreadContext, setThreadContext, allocVirtual); + } } } diff --git a/src/native/managed/cdac/README.md b/src/native/managed/cdac/README.md index e9d325dbc91ee1..871f0d204b457b 100644 --- a/src/native/managed/cdac/README.md +++ b/src/native/managed/cdac/README.md @@ -40,6 +40,73 @@ ISOSDacInterface* / IXCLRDataProcess (COM-style API surface) and algorithms each contract must implement. - **To write tests**: see the [tests README](tests/README.md). +## Dump collection + +`SOSDacImpl` implements `ICLRDataEnumMemoryRegions` using the shared collection +engine in `Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory`. +Both the universal cDAC and the separately built +`mscordaccore_enummemory` provider use this implementation. +The separate NativeAOT project controls its exports, architecture restrictions, +size settings, and installation without duplicating collection logic. + +Enumeration shares the SOS/process instance's COM identity and API lock. +`MemoryRegionEnumerator.Enumerate` requires a `ContractDescriptorTarget`, returning +`E_NOTIMPL` for other target implementations. Each call flushes all cached target data +and contract state, then uses `RegisterReadCallback` to report successful reads, +including string terminators. Collection uses the existing target and contract instances; +it does not copy registrations or construct a separate target. The scope restores the +previous reader on success, failure, or cancellation. + +`EnumerateDescriptorMemory` separately reports native descriptor headers (including +padding), JSON, pointer tables, and both resolved and pending sub-descriptor pointer slots. +It enumerates stored ranges without rereading descriptors. An externally supplied main +descriptor has no native backing memory, but its pointer slots and any native +sub-descriptors are still included. Flushing before installing the read callback discovers +newly published sub-descriptors without allowing reporting cancellation to interrupt parsing. +This works with both COM-data-target and callback-based activation. +Both target factories accept an optional `runtimeImageBase`, stored for the lifetime of +the target and preserved across flushes and read-callback scopes. The default value +indicates that the image base is unavailable. During collection, `TryGetRuntimeImageBase` +provides the stored runtime image address. The enummemory entrypoint resolves it once through +`EntrypointHelpers.TryGetRuntimeImageBase`, using `ICLRRuntimeLocator` and falling back to +`ICLRDataTarget.GetImageBase` when the locator is unavailable or does not return `S_OK`, +matching the native DAC's lookup order. Enummemory activation requires both a nonzero +descriptor address and a nonzero runtime image base; failure to obtain either fails activation. +Universal COM activation uses only `ICLRRuntimeLocator` and never falls back to +`GetImageBase`. An unsuccessful lookup leaves the image base unavailable for universal activation. +DacDbi activation uses the supplied `runtimeBase`. Inspection scripts use the image base +of the same module that exports the descriptor, including the NativeAOT application-module +fallback. Dump-test targets likewise use the image base of the descriptor's runtime module. +Runtime image headers and directories are then read through `Target` +and included in the dump, without storing image information on `SOSDacImpl`. +Callback-only native activation does not supply an image base and leaves image +mapping to the dump writer. + +Both NativeAOT hosts forward `CLRDataCreateInstance` to shared activation in +`EntrypointHelpers.CreateInstance` in Legacy, which requires a nonzero descriptor address +and accepts the optional runtime image base without performing further discovery. +The entrypoints obtain that address from `ICLRContractLocator`, which `createdump` +implements using the descriptor discovered during module enumeration. +The universal host requires this locator to return `S_OK` and a nonzero address. +The enummemory entrypoint first resolves the required runtime image base, then tries +the locator. If the locator is absent, returns a result other than `S_OK`, or returns +a zero address, enummemory falls back to the runtime's PE exports. This also supports +WER, which does not provide `ICLRContractLocator`. +The enummemory entrypoint performs that export lookup using the already-resolved image base and `RuntimeModuleInfo` in +`Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory`. +`RuntimeModuleInfo` owns the PE parsing and export lookup, and provides the runtime image +regions used by dump collection. Shared `EntrypointHelpers.TryGetContractDescriptorAddress` +uses only `ICLRContractLocator`. +The universal host's explicit-address dbgshim entrypoint bypasses descriptor discovery. + +The collector selects its collection mode from `miniDumpFlags`, as the native DAC does. The +`CLRDataEnumMemoryFlags` argument to `EnumMemoryRegions` is reserved and ignored. +Triage exception collection omits messages and remote stack traces from types +that override the `StackTrace` getter. Stack-trace strings have source-file +information removed through the optional `ICLRDataEnumMemoryRegionsCallback2` +update callback, when the dump writer supports it. This does not guarantee that +other memory selected by the dump writer is free of personal information. + ## Project structure | Directory | Purpose | @@ -49,6 +116,7 @@ ISOSDacInterface* / IXCLRDataProcess (COM-style API surface) | `Microsoft.Diagnostics.DataContractReader.Legacy` | `SOSDacImpl` — bridges `ISOSDacInterface*` COM APIs to contracts | | `Microsoft.Diagnostics.DataContractReader` | Contract/data descriptor parsing and `Target` construction | | `mscordaccore_universal` | Entry point that wires everything together | +| `mscordaccore_enummemory` | NativeAOT entrypoint host for the shared dump collector; produces `mscordaccore` | | `tests` | Unit tests with mock memory infrastructure | ## Contract specifications diff --git a/src/native/managed/cdac/cdac.slnx b/src/native/managed/cdac/cdac.slnx index 43221efbf6c7f2..7d3544fbc272bc 100644 --- a/src/native/managed/cdac/cdac.slnx +++ b/src/native/managed/cdac/cdac.slnx @@ -10,6 +10,7 @@ + diff --git a/src/native/managed/cdac/gen/Emitter.cs b/src/native/managed/cdac/gen/Emitter.cs index 9cc730a4e102e5..35855a5ad42c21 100644 --- a/src/native/managed/cdac/gen/Emitter.cs +++ b/src/native/managed/cdac/gen/Emitter.cs @@ -49,12 +49,7 @@ public static string Emit(CdacTypeModel model) } sb.AppendLine(BuildClassDoc(model)); - // A partial class may add interfaces from any declaration. Lazily-read - // types implement IReadableData so callers can force a full read. - if (hasInstanceMembers) - sb.AppendLine($"partial class {model.ClassName} : global::Microsoft.Diagnostics.DataContractReader.Data.IReadableData"); - else - sb.AppendLine($"partial class {model.ClassName}"); + sb.AppendLine($"partial class {model.ClassName}"); sb.AppendLine("{"); // Emit a static _typeNames array for LayoutSet.Resolve and ITypeHandle resolution. @@ -114,7 +109,7 @@ public static string Emit(CdacTypeModel model) EmitMemberProperty(sb, member); if (hasInstanceMembers) - EmitEnsureAllFieldsRead(sb, model); + EmitTryReadAllFields(sb, model); foreach (MemberModel member in model.Members) { @@ -364,19 +359,20 @@ private static void EmitDataDescriptorDependencyAttribute(StringBuilder sb, Memb } /// - /// Emits the implementation, - /// which touches every lazily-read member so a caller can eagerly force a full - /// read of the structure (used to validate that the whole struct is readable). + /// Emits TryReadAllFields, which touches every lazily-read member and + /// reports whether the entire structure was readable. /// - private static void EmitEnsureAllFieldsRead(StringBuilder sb, CdacTypeModel model) + private static void EmitTryReadAllFields(StringBuilder sb, CdacTypeModel model) { // Descriptor attributes short-circuit body analysis in CdacUsageGraph. // A custom initializer can also read globals, call helpers, and access // other Data properties, so let the analyzer walk this method's body. if (!model.Members.Any(member => member.Kind == MemberKind.CustomInit)) - EmitEnsureAllFieldsReadDependencyAttributes(sb, model); - sb.AppendLine(" void global::Microsoft.Diagnostics.DataContractReader.Data.IReadableData.EnsureAllFieldsRead()"); + EmitTryReadAllFieldsDependencyAttributes(sb, model); + sb.AppendLine(" internal bool TryReadAllFields()"); sb.AppendLine(" {"); + sb.AppendLine(" try"); + sb.AppendLine(" {"); foreach (MemberModel member in model.Members) { if (member.Kind == MemberKind.Field @@ -384,14 +380,20 @@ private static void EmitEnsureAllFieldsRead(StringBuilder sb, CdacTypeModel mode || member.Kind == MemberKind.InstanceDataStart || member.Kind == MemberKind.CustomInit) { - sb.AppendLine($" _ = {member.Name};"); + sb.AppendLine($" _ = {member.Name};"); } } + sb.AppendLine(" return true;"); + sb.AppendLine(" }"); + sb.AppendLine(" catch (VirtualReadException)"); + sb.AppendLine(" {"); + sb.AppendLine(" return false;"); + sb.AppendLine(" }"); sb.AppendLine(" }"); sb.AppendLine(); } - private static void EmitEnsureAllFieldsReadDependencyAttributes(StringBuilder sb, CdacTypeModel model) + private static void EmitTryReadAllFieldsDependencyAttributes(StringBuilder sb, CdacTypeModel model) { (string FieldName, string NativeType)[] fields = model.Members .Where(member => diff --git a/src/native/managed/cdac/mscordaccore_enummemory/Entrypoints.cs b/src/native/managed/cdac/mscordaccore_enummemory/Entrypoints.cs new file mode 100644 index 00000000000000..7feb599ebb0887 --- /dev/null +++ b/src/native/managed/cdac/mscordaccore_enummemory/Entrypoints.cs @@ -0,0 +1,62 @@ +// 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.Runtime.InteropServices; +using System.Runtime.InteropServices.Marshalling; +using Microsoft.Diagnostics.DataContractReader.Legacy; +using Microsoft.Diagnostics.DataContractReader.Legacy.EnumMemory; + +namespace Microsoft.Diagnostics.DataContractReader; + +internal static class Entrypoints +{ + [UnmanagedCallersOnly(EntryPoint = "CLRDataCreateInstance")] + private static unsafe int CLRDataCreateInstance(Guid* pIID, IntPtr pDataTarget, void** iface) + { + if (iface == null) + return HResults.E_INVALIDARG; + + *iface = null; + if (pIID == null || pDataTarget == IntPtr.Zero) + return HResults.E_INVALIDARG; + + try + { + ICLRDataTarget dataTarget = ComInterfaceMarshaller.ConvertToManaged((void*)pDataTarget)!; + + if (!EntrypointHelpers.TryGetRuntimeImageBase(dataTarget, out TargetPointer runtimeImageBase)) + return HResults.E_FAIL; + + // WER does not provide ICLRContractLocator, so locate the descriptor through the runtime's PE exports if necessary. + EntrypointHelpers.TryGetContractDescriptorAddress(dataTarget, out TargetPointer contractAddress); + if (contractAddress == TargetPointer.Null) + { + RuntimeModuleInfo.TryReadMemory readMemory = (address, buffer) => TryReadTarget(dataTarget, address, buffer); + if (!RuntimeModuleInfo.TryCreate(runtimeImageBase, readMemory, out RuntimeModuleInfo module) + || !module.TryGetExport(readMemory, "DotNetRuntimeContractDescriptor"u8, out ulong exportAddress)) + { + return CdacHResults.CDAC_E_DESCRIPTOR_NOT_FOUND; + } + contractAddress = new(exportAddress); + } + + return EntrypointHelpers.CreateInstance(pIID, pDataTarget, IntPtr.Zero, iface, contractAddress, runtimeImageBase); + } + catch (Exception ex) + { + int hr = ex.HResult; + return hr < 0 ? hr : HResults.E_FAIL; + } + } + + private static unsafe bool TryReadTarget(ICLRDataTarget dataTarget, ulong address, Span buffer) + { + fixed (byte* bufferPointer = buffer) + { + uint bytesRead; + return dataTarget.ReadVirtual(address, bufferPointer, (uint)buffer.Length, &bytesRead) >= 0 + && bytesRead == (uint)buffer.Length; + } + } +} diff --git a/src/native/managed/cdac/mscordaccore_enummemory/GenerateWindowsVersionResource.ps1 b/src/native/managed/cdac/mscordaccore_enummemory/GenerateWindowsVersionResource.ps1 new file mode 100644 index 00000000000000..1d2a5440ace2d6 --- /dev/null +++ b/src/native/managed/cdac/mscordaccore_enummemory/GenerateWindowsVersionResource.ps1 @@ -0,0 +1,113 @@ +param ( + [Parameter(Mandatory = $true)] + [string] $outputPath, + + [Parameter(Mandatory = $true)] + [string] $fileVersion, + + [Parameter(Mandatory = $true)] + [string] $productVersion, + + [Parameter(Mandatory = $true)] + [string] $companyName, + + [Parameter(Mandatory = $true)] + [string] $productName, + + [Parameter(Mandatory = $true)] + [string] $legalCopyright +) + +function Escape-ResourceString { + param ( + [Parameter(Mandatory = $true)] + [string] $value + ) + + return $value.Replace('\', '\\').Replace('"', '\"') +} + +$versionParts = $fileVersion.Split('.') +if ($versionParts.Length -ne 4) { + throw "The file version '$fileVersion' does not contain four components." +} + +foreach ($part in $versionParts) { + [uint16] $parsedPart = 0 + if (-not [uint16]::TryParse($part, [ref] $parsedPart)) { + throw "The file version component '$part' is not a valid unsigned 16-bit integer." + } +} + +$kitsRoot = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows Kits\Installed Roots').KitsRoot10 +$resourceCompiler = Get-ChildItem -Path (Join-Path $kitsRoot 'bin\*\x64\rc.exe') | + Sort-Object -Property { [version] $_.Directory.Parent.Name } -Descending | + Select-Object -First 1 +if ($null -eq $resourceCompiler) { + throw "Unable to locate the Windows SDK resource compiler under '$kitsRoot'." +} + +$sdkVersion = $resourceCompiler.Directory.Parent.Name +$sdkIncludeRoot = Join-Path $kitsRoot "Include\$sdkVersion" +$sdkIncludeDirectories = @('shared', 'um', 'ucrt', 'winrt') | + ForEach-Object { Join-Path $sdkIncludeRoot $_ } +foreach ($includeDirectory in $sdkIncludeDirectories) { + if (-not [System.IO.Directory]::Exists($includeDirectory)) { + throw "Unable to locate the Windows SDK include directory '$includeDirectory'." + } +} + +$outputDirectory = Split-Path -Parent $outputPath +[System.IO.Directory]::CreateDirectory($outputDirectory) | Out-Null +$resourceSource = Join-Path $outputDirectory 'mscordaccore.version.rc' +$numericVersion = $versionParts -join ',' + +$resourceContents = @" +#include + +1 VERSIONINFO +FILEVERSION $numericVersion +PRODUCTVERSION $numericVersion +FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +FILEFLAGS 0 +FILEOS VOS__WINDOWS32 +FILETYPE VFT_DLL +FILESUBTYPE VFT2_UNKNOWN +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904B0" + BEGIN + VALUE "CompanyName", "$(Escape-ResourceString $companyName)" + VALUE "FileDescription", "mscordaccore" + VALUE "FileVersion", "$(Escape-ResourceString $fileVersion)" + VALUE "InternalName", "mscordaccore.dll" + VALUE "LegalCopyright", "$(Escape-ResourceString $legalCopyright)" + VALUE "OriginalFilename", "mscordaccore.dll" + VALUE "ProductName", "$(Escape-ResourceString $productName)" + VALUE "ProductVersion", "$(Escape-ResourceString $productVersion)" + END + END + + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0409, 1200 + END +END +"@ + +[System.IO.File]::WriteAllText( + $resourceSource, + $resourceContents, + [System.Text.UnicodeEncoding]::new($false, $true)) + +$resourceCompilerArguments = @('/nologo', '/fo', $outputPath) +foreach ($includeDirectory in $sdkIncludeDirectories) { + $resourceCompilerArguments += @('/i', $includeDirectory) +} +$resourceCompilerArguments += $resourceSource + +& $resourceCompiler.FullName $resourceCompilerArguments +if ($LASTEXITCODE -ne 0) { + throw "The Windows SDK resource compiler failed with exit code $LASTEXITCODE." +} diff --git a/src/native/managed/cdac/mscordaccore_enummemory/mscordaccore_enummemory.csproj b/src/native/managed/cdac/mscordaccore_enummemory/mscordaccore_enummemory.csproj new file mode 100644 index 00000000000000..5d06b0a58b5b16 --- /dev/null +++ b/src/native/managed/cdac/mscordaccore_enummemory/mscordaccore_enummemory.csproj @@ -0,0 +1,85 @@ + + + true + + + + + $(LibPrefix)mscordaccore + false + $(CDacTfm) + Microsoft.Diagnostics.DataContractReader + enable + true + false + true + false + Size + false + true + + + + <_TargetHostArchitecture Condition="'$(RuntimeIdentifier)' != ''">$(RuntimeIdentifier.Substring($([MSBuild]::Add($(RuntimeIdentifier.LastIndexOf('-')), 1)))) + <_TargetHostArchitecture Condition="'$(_TargetHostArchitecture)' == ''">$(TargetArchitecture) + + + + + + + + + + + + + + + $(IntermediateOutputPath)mscordaccore.version.res + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/native/managed/cdac/mscordaccore_universal/Entrypoints.cs b/src/native/managed/cdac/mscordaccore_universal/Entrypoints.cs index 54d42dcc1bde8d..2f1416af2ee307 100644 --- a/src/native/managed/cdac/mscordaccore_universal/Entrypoints.cs +++ b/src/native/managed/cdac/mscordaccore_universal/Entrypoints.cs @@ -9,7 +9,10 @@ namespace Microsoft.Diagnostics.DataContractReader; -internal static class Entrypoints +/// +/// Provides native entrypoints and debugger target construction for the universal cDAC. +/// +public static class Entrypoints { private sealed class CdacHandle { @@ -264,7 +267,7 @@ private static unsafe int CreateDacDbiInterface(IntPtr handle, IntPtr legacyImpl [UnmanagedCallersOnly(EntryPoint = "CLRDataCreateInstanceWithFallback")] private static unsafe int CLRDataCreateInstanceWithFallback(Guid* pIID, IntPtr /*ICLRDataTarget*/ pLegacyTarget, IntPtr pLegacyImpl, void** iface) { - return CLRDataCreateInstanceImpl(pIID, pLegacyTarget, pLegacyImpl, iface); + return CreateInstance(pIID, pLegacyTarget, pLegacyImpl, iface); } [UnmanagedCallersOnly(EntryPoint = "DacDbiInterfaceInstance")] @@ -297,7 +300,7 @@ private static unsafe int DacDbiInterfaceInstance( ICorDebugDataTarget dataTarget = UniqueComInterfaceMarshaller.ConvertToManaged((void*)pTarget)!; dataTargetComObject = (ComObject)(object)dataTarget; - ContractDescriptorTarget target = CreateTargetFromCorDebugDataTarget(dataTarget, contractDescriptorAddress); + ContractDescriptorTarget target = CreateTargetFromCorDebugDataTarget(dataTarget, contractDescriptorAddress, runtimeBase); Legacy.DacDbiImpl impl = new(target, legacyObj: null, apiLock: new Lock(), dataTargetComObject: dataTargetComObject); *iface = ComInterfaceMarshaller.ConvertToUnmanaged(impl); dataTargetComObject = null; @@ -322,39 +325,31 @@ private static unsafe int DacDbiInterfaceInstance( [UnmanagedCallersOnly(EntryPoint = "CLRDataCreateInstance")] private static unsafe int CLRDataCreateInstance(Guid* pIID, IntPtr /*ICLRDataTarget*/ pLegacyTarget, void** iface) { - return CLRDataCreateInstanceImpl(pIID, pLegacyTarget, IntPtr.Zero, iface); + return CreateInstance(pIID, pLegacyTarget, IntPtr.Zero, iface); } - // Creates a cDAC data-access instance from an explicit contract descriptor address, - // so the data target does not need to implement ICLRContractLocator. - [UnmanagedCallersOnly(EntryPoint = "DbgShimCreateInstanceFromContractDescriptor")] - private static unsafe int DbgShimCreateInstanceFromContractDescriptor(Guid* pIID, IntPtr /*ICLRDataTarget*/ pLegacyTarget, ulong contractDescriptorAddr, void** iface) + private static unsafe int CreateInstance(Guid* pIID, IntPtr pDataTarget, IntPtr legacyImplPtr, void** iface, TargetPointer contractAddress = default) { - if (pLegacyTarget == IntPtr.Zero || contractDescriptorAddr == 0 || iface == null) + if (iface == null) return HResults.E_INVALIDARG; + *iface = null; + if (pIID == null || pDataTarget == IntPtr.Zero) + return HResults.E_INVALIDARG; try { - object legacyTarget = ComInterfaceMarshaller.ConvertToManaged((void*)pLegacyTarget)!; - return CreateInstanceFromContractDescriptorCore(pIID, legacyTarget, contractDescriptorAddr, legacyImplPtr: IntPtr.Zero, new Lock(), iface); - } - catch (Exception ex) - { - int hr = ex.HResult; - return hr < 0 ? hr : HResults.E_FAIL; - } - } + ICLRDataTarget dataTarget = ComInterfaceMarshaller.ConvertToManaged((void*)pDataTarget)!; + if (contractAddress == TargetPointer.Null && !EntrypointHelpers.TryGetContractDescriptorAddress(dataTarget, out contractAddress)) + { + return CdacHResults.CDAC_E_DESCRIPTOR_NOT_FOUND; + } - private static unsafe int CLRDataCreateInstanceImpl(Guid* pIID, IntPtr /*ICLRDataTarget*/ pLegacyTarget, IntPtr pLegacyImpl, void** iface) - { - if (pLegacyTarget == IntPtr.Zero || iface == null) - return HResults.E_INVALIDARG; - *iface = null; + ulong runtimeImageBase = 0; + if (dataTarget is ICLRRuntimeLocator runtimeLocator && runtimeLocator.GetRuntimeBase(&runtimeImageBase) != HResults.S_OK) + runtimeImageBase = 0; - try - { - return CLRDataCreateInstanceCore(pIID, pLegacyTarget, pLegacyImpl, new Lock(), iface); + return EntrypointHelpers.CreateInstance(pIID, pDataTarget, legacyImplPtr, iface, contractAddress, runtimeImageBase); } catch (Exception ex) { @@ -363,127 +358,28 @@ private static unsafe int CLRDataCreateInstanceImpl(Guid* pIID, IntPtr /*ICLRDat } } - private static unsafe int CLRDataCreateInstanceCore(Guid* pIID, IntPtr /*ICLRDataTarget*/ pLegacyTarget, IntPtr pLegacyImpl, Lock apiLock, void** iface) - { - object legacyTarget = ComInterfaceMarshaller.ConvertToManaged((void*)pLegacyTarget)!; - - ICLRContractLocator contractLocator = legacyTarget as ICLRContractLocator ?? throw new ArgumentException( - $"{nameof(pLegacyTarget)} does not implement {nameof(ICLRContractLocator)}", nameof(pLegacyTarget)); - - ulong contractAddress; - int hr = contractLocator.GetContractDescriptor(&contractAddress); - if (hr != 0) - { - throw new InvalidOperationException( - $"{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, pLegacyImpl, apiLock, iface); - } - - private static unsafe int CreateInstanceFromContractDescriptorCore(Guid* pIID, object legacyTarget, ulong contractAddress, IntPtr legacyImplPtr, Lock apiLock, void** iface) + // Creates a cDAC data-access instance from an explicit contract descriptor address, + // so the data target does not need to implement ICLRContractLocator. + [UnmanagedCallersOnly(EntryPoint = "DbgShimCreateInstanceFromContractDescriptor")] + private static unsafe int DbgShimCreateInstanceFromContractDescriptor(Guid* pIID, IntPtr /*ICLRDataTarget*/ pLegacyTarget, ulong contractDescriptorAddr, void** iface) { - ICLRDataTarget dataTarget = legacyTarget as ICLRDataTarget ?? throw new ArgumentException( - $"Data target does not implement {nameof(ICLRDataTarget)}", nameof(legacyTarget)); - - // Try to get ICLRDataTarget2 for memory allocation support (optional) - ICLRDataTarget2? dataTarget2 = legacyTarget as ICLRDataTarget2; - - // Build the allocVirtual delegate if the target supports ICLRDataTarget2 - ContractDescriptorTarget.AllocVirtualDelegate allocVirtual = (ulong size, out ulong allocatedAddress) => - { - allocatedAddress = 0; - return HResults.E_NOTIMPL; - }; - - if (dataTarget2 is not null) - { - // Windows virtual memory allocation flags used by ICLRDataTarget2::AllocVirtual. - const uint MEM_COMMIT = 0x1000; - const uint PAGE_READWRITE = 0x04; - - allocVirtual = (ulong size, out ulong allocatedAddress) => - { - ClrDataAddress addr; - int result = dataTarget2.AllocVirtual(0, (uint)size, MEM_COMMIT, PAGE_READWRITE, &addr); - allocatedAddress = (ulong)addr; - return result; - }; - } - - ContractDescriptorTarget target = ContractDescriptorTarget.Create( - contractAddress, - (address, buffer) => - { - fixed (byte* bufferPtr = buffer) - { - uint bytesRead; - return dataTarget.ReadVirtual(address, bufferPtr, (uint)buffer.Length, &bytesRead); - } - }, - (address, buffer) => - { - fixed (byte* bufferPtr = buffer) - { - uint bytesWritten; - return dataTarget.WriteVirtual(address, bufferPtr, (uint)buffer.Length, &bytesWritten); - } - }, - (threadId, contextFlags, bufferToFill) => - { - fixed (byte* bufferPtr = bufferToFill) - { - return dataTarget.GetThreadContext(threadId, contextFlags, (uint)bufferToFill.Length, bufferPtr); - } - }, - (threadId, context) => - { - fixed (byte* contextPtr = context) - { - if (((nuint)contextPtr & (ContextAlignment - 1)) == 0) - { - return dataTarget.SetThreadContext(threadId, (uint)context.Length, contextPtr); - } - - byte* alignedBuffer = (byte*)NativeMemory.AlignedAlloc((nuint)context.Length, ContextAlignment); - try - { - context.CopyTo(new Span(alignedBuffer, context.Length)); - return dataTarget.SetThreadContext(threadId, (uint)context.Length, alignedBuffer); - } - finally - { - NativeMemory.AlignedFree(alignedBuffer); - } - } - }, - allocVirtual, - [Contracts.CoreCLRContracts.Register]); - - Contracts.CoreCLRContracts.ValidateForDataAccess(target, apiLock); - - object? legacyImpl = legacyImplPtr != IntPtr.Zero - ? ComInterfaceMarshaller.ConvertToManaged((void*)legacyImplPtr) - : null; - - Legacy.SOSDacImpl impl = new(target, legacyImpl, apiLock); - void* ccw = ComInterfaceMarshaller.ConvertToUnmanaged(impl); - int hrQI = Marshal.QueryInterface((nint)ccw, *pIID, out nint ptrToIface); - - // Decrement reference count on ccw because QI incremented it - ComInterfaceMarshaller.Free(ccw); - - if (hrQI < 0) - return hrQI; + if (iface != null) + *iface = null; + if (contractDescriptorAddr == 0) + return HResults.E_INVALIDARG; - *iface = (void*)ptrToIface; - return 0; + return CreateInstance(pIID, pLegacyTarget, IntPtr.Zero, iface, contractDescriptorAddr); } - private static unsafe ContractDescriptorTarget CreateTargetFromCorDebugDataTarget(object targetObject, ulong contractAddress) + /// + /// Creates a contract descriptor target backed by a debugger data target. + /// + /// The object implementing . + /// The target contract descriptor address. + /// The runtime image base address, or zero when unavailable. + /// A target registered with the CoreCLR contracts. + /// does not implement . + public static unsafe ContractDescriptorTarget CreateTargetFromCorDebugDataTarget(object targetObject, ulong contractAddress, ulong runtimeBase) { ICorDebugDataTarget dataTarget = targetObject as ICorDebugDataTarget ?? throw new ArgumentException( $"Data target does not implement {nameof(ICorDebugDataTarget)}", nameof(targetObject)); @@ -571,6 +467,7 @@ private static unsafe ContractDescriptorTarget CreateTargetFromCorDebugDataTarge allocatedAddress = 0; return HResults.E_NOTIMPL; }, - [Contracts.CoreCLRContracts.Register]); + [Contracts.CoreCLRContracts.Register], + runtimeBase); } } diff --git a/src/native/managed/cdac/scripts/DescriptorCommand.cs b/src/native/managed/cdac/scripts/DescriptorCommand.cs index a9e850906b6f74..1b111fd55343f2 100644 --- a/src/native/managed/cdac/scripts/DescriptorCommand.cs +++ b/src/native/managed/cdac/scripts/DescriptorCommand.cs @@ -42,7 +42,7 @@ private int Run(ParseResult parse) private static void Execute(string dumpPath) { using DataTarget dt = DataTarget.LoadDump(dumpPath); - ulong contractAddr = DumpHelpers.FindContractDescriptor(dt); + ulong contractAddr = DumpHelpers.FindContractDescriptor(dt, out _); Console.WriteLine($"Dump: {dumpPath}"); Console.WriteLine($"Pointer size: {dt.DataReader.PointerSize}"); diff --git a/src/native/managed/cdac/scripts/DumpHelpers.cs b/src/native/managed/cdac/scripts/DumpHelpers.cs index 9f7c6eedaeb7d7..f55d575ef3655e 100644 --- a/src/native/managed/cdac/scripts/DumpHelpers.cs +++ b/src/native/managed/cdac/scripts/DumpHelpers.cs @@ -1,6 +1,8 @@ // 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.Linq; using Microsoft.Diagnostics.DataContractReader; using Microsoft.Diagnostics.DataContractReader.Contracts; using Microsoft.Diagnostics.Runtime; @@ -11,19 +13,24 @@ internal static class DumpHelpers { private static readonly string[] s_coreClrModuleNames = ["coreclr.dll", "libcoreclr.so", "libcoreclr.dylib"]; - public static ulong FindContractDescriptor(DataTarget dt) + public static ulong FindContractDescriptor(DataTarget dt, out ulong runtimeImageBase) { - // First pass: look in known CoreCLR modules. - // Second pass: check all remaining modules (covers NativeAOT where the export is in the app binary). + runtimeImageBase = 0; + // Prefer CoreCLR modules, retaining the first other export for NativeAOT application images. ulong fallback = 0; + ulong fallbackImageBase = 0; foreach (ModuleInfo module in dt.DataReader.EnumerateModules()) { ulong addr = module.GetExportSymbolAddress("DotNetRuntimeContractDescriptor"); if (addr == 0) continue; + ulong imageBase = module.ImageBase; if (dt.DataReader.PointerSize == 4) + { addr &= 0xFFFF_FFFF; + imageBase &= 0xFFFF_FFFF; + } string? fileName = module.FileName; if (fileName is not null) @@ -31,22 +38,31 @@ public static ulong FindContractDescriptor(DataTarget dt) int lastSep = Math.Max(fileName.LastIndexOf('/'), fileName.LastIndexOf('\\')); string name = lastSep >= 0 ? fileName[(lastSep + 1)..] : fileName; if (s_coreClrModuleNames.Contains(name, StringComparer.OrdinalIgnoreCase)) + { + runtimeImageBase = imageBase; return addr; + } } if (fallback == 0) + { fallback = addr; + fallbackImageBase = imageBase; + } } if (fallback != 0) + { + runtimeImageBase = fallbackImageBase; return fallback; + } throw new InvalidOperationException("Could not find DotNetRuntimeContractDescriptor export."); } public static ContractDescriptorTarget CreateCdacTarget(DataTarget dt) { - ulong contractAddr = FindContractDescriptor(dt); + ulong contractAddr = FindContractDescriptor(dt, out ulong runtimeImageBase); return ContractDescriptorTarget.Create( contractAddr, @@ -56,6 +72,7 @@ public static ContractDescriptorTarget CreateCdacTarget(DataTarget dt) 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]); + [CoreCLRContracts.Register], + runtimeImageBase); } } diff --git a/src/native/managed/cdac/tests/DataGenerator/DataGeneratorTests.cs b/src/native/managed/cdac/tests/DataGenerator/DataGeneratorTests.cs index ad00dcddad3bef..96d1e74550a638 100644 --- a/src/native/managed/cdac/tests/DataGenerator/DataGeneratorTests.cs +++ b/src/native/managed/cdac/tests/DataGenerator/DataGeneratorTests.cs @@ -87,31 +87,31 @@ public void GeneratedHelpersDeclareDescriptorDependencies() } [Fact] - public void EnsureAllFieldsReadDeclaresGeneratedPropertyDependencies() + public void TryReadAllFieldsDeclaresGeneratedPropertyDependencies() { - InterfaceMapping map = typeof(TestNative).GetInterfaceMap(typeof(IReadableData)); - int index = Array.IndexOf(map.InterfaceMethods, typeof(IReadableData).GetMethod(nameof(IReadableData.EnsureAllFieldsRead))!); - MethodInfo ensureAllFieldsRead = map.TargetMethods[index]; + MethodInfo tryReadAllFields = typeof(TestNative).GetMethod( + "TryReadAllFields", + BindingFlags.Instance | BindingFlags.NonPublic)!; Assert.Equal( [ (FieldName: "A", NativeType: "uint32"), (FieldName: "B", NativeType: "pointer"), ], - Dependencies(ensureAllFieldsRead) + Dependencies(tryReadAllFields) .Select(attribute => (attribute.FieldName, attribute.NativeType))); - Assert.False(UsesTypeSize(ensureAllFieldsRead)); + Assert.False(UsesTypeSize(tryReadAllFields)); } [Fact] - public void EnsureAllFieldsReadWithCustomInitHasNoShortCircuitingDependencies() + public void TryReadAllFieldsWithCustomInitHasNoShortCircuitingDependencies() { - InterfaceMapping map = typeof(TestCustomInit).GetInterfaceMap(typeof(IReadableData)); - int index = Array.IndexOf(map.InterfaceMethods, typeof(IReadableData).GetMethod(nameof(IReadableData.EnsureAllFieldsRead))!); - MethodInfo ensureAllFieldsRead = map.TargetMethods[index]; + MethodInfo tryReadAllFields = typeof(TestCustomInit).GetMethod( + "TryReadAllFields", + BindingFlags.Instance | BindingFlags.NonPublic)!; - Assert.Empty(Dependencies(ensureAllFieldsRead)); - Assert.False(UsesTypeSize(ensureAllFieldsRead)); + Assert.Empty(Dependencies(tryReadAllFields)); + Assert.False(UsesTypeSize(tryReadAllFields)); } [Fact] diff --git a/src/native/managed/cdac/tests/DataGenerator/TestTarget.cs b/src/native/managed/cdac/tests/DataGenerator/TestTarget.cs index da18f2b7abc335..d3438726b3e63f 100644 --- a/src/native/managed/cdac/tests/DataGenerator/TestTarget.cs +++ b/src/native/managed/cdac/tests/DataGenerator/TestTarget.cs @@ -257,6 +257,7 @@ public override bool TryReadGlobalPointer(string name, [NotNullWhen(true)] out T public override bool IsAlignedToPointerSize(TargetPointer pointer) => throw new NotImplementedException(); public override bool TryGetThreadContext(ulong threadId, uint contextFlags, Span buffer) => throw new NotImplementedException(); public override bool TrySetThreadContext(ulong threadId, ReadOnlySpan context) => throw new NotImplementedException(); + public override bool TryGetRuntimeImageBase(out TargetPointer imageBase) => throw new NotImplementedException(); // --- Stub ContractRegistry ------------------------------------- diff --git a/src/native/managed/cdac/tests/DumpTests/AuxiliarySymbolsDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/AuxiliarySymbolsDumpTests.cs index da2b1b76e558be..30ed1a57730ccf 100644 --- a/src/native/managed/cdac/tests/DumpTests/AuxiliarySymbolsDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/AuxiliarySymbolsDumpTests.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; -using Microsoft.Diagnostics.DataContractReader.Contracts; using Microsoft.Diagnostics.DataContractReader.TestInfrastructure; using Xunit; @@ -11,12 +10,11 @@ namespace Microsoft.Diagnostics.DataContractReader.DumpTests; public class AuxiliarySymbolsDumpTests : DumpTestBase { protected override string DebuggeeName => "BasicThreads"; - protected override string DumpType => "full"; [ConditionalTheory] [MemberData(nameof(TestConfigurations))] [SkipOnVersion("net10.0", "Allocation helpers are not included in the .NET 10 auxiliary symbol table")] - public void JitHelpersAreReachableByAddress(TestConfiguration config) + public void JitHelpersAreIncludedInHeapDump(TestConfiguration config) { InitializeDumpTest(config); @@ -35,21 +33,11 @@ public void JitHelpersAreReachableByAddress(TestConfiguration config) ]; Dictionary helpersByAddress = []; - TargetPointer table = Target.ReadGlobalPointer(Constants.Globals.AuxiliarySymbols); - uint count = Target.Read(Target.ReadGlobalPointer(Constants.Globals.AuxiliarySymbolCount)); - Target.TypeInfo typeInfo = Target.GetTypeInfo(DataType.AuxiliarySymbolInfo); - uint entrySize = typeInfo.Size!.Value; - int addressOffset = typeInfo.Fields["Address"].Offset; - int nameOffset = typeInfo.Fields["Name"].Offset; - - for (uint i = 0; i < count; i++) + foreach ((TargetCodePointer address, string name) in + Target.Contracts.AuxiliarySymbols.EnumerateAuxiliarySymbols()) { - TargetPointer entry = table + ((ulong)i * entrySize); - TargetPointer namePointer = Target.ReadPointer(entry + (ulong)nameOffset); - string name = Target.ReadUtf8String(namePointer); if (expectedHelpers.Remove(name)) { - TargetCodePointer address = Target.ReadCodePointer(entry + (ulong)addressOffset); Assert.NotEqual(TargetCodePointer.Null, address); helpersByAddress.TryAdd(address, name); } diff --git a/src/native/managed/cdac/tests/TestInfrastructure/ClrMdDumpHost.cs b/src/native/managed/cdac/tests/TestInfrastructure/ClrMdDumpHost.cs index 341a4b2a8c8a20..bf0dac6a6386ef 100644 --- a/src/native/managed/cdac/tests/TestInfrastructure/ClrMdDumpHost.cs +++ b/src/native/managed/cdac/tests/TestInfrastructure/ClrMdDumpHost.cs @@ -124,11 +124,13 @@ public int GetThreadContext(uint threadId, uint contextFlags, Span buffer) } /// - /// Locate the DotNetRuntimeContractDescriptor symbol address in the dump. + /// Locate the DotNetRuntimeContractDescriptor symbol address in the dump and return + /// the image base of the module that exports it. /// Uses ClrMD's built-in export resolution which handles PE, ELF, and Mach-O formats. /// - public ulong FindContractDescriptorAddress() + public ulong FindContractDescriptorAddress(out ulong runtimeImageBase) { + runtimeImageBase = 0; foreach (ModuleInfo module in _dataTarget.DataReader.EnumerateModules()) { string? fileName = module.FileName; @@ -146,11 +148,15 @@ public ulong FindContractDescriptorAddress() ulong address = module.GetExportSymbolAddress("DotNetRuntimeContractDescriptor"); if (address != 0) { + runtimeImageBase = module.ImageBase; // ClrMD may return addresses with spurious upper bits on 32-bit targets // (observed on ARM32 ELF). Mask to the target's pointer size. // https://github.com/microsoft/clrmd/issues/1407 if (_dataTarget.DataReader.PointerSize == 4) + { address &= 0xFFFF_FFFF; + runtimeImageBase &= 0xFFFF_FFFF; + } return address; } diff --git a/src/native/managed/cdac/tests/TestInfrastructure/DumpTestBase.cs b/src/native/managed/cdac/tests/TestInfrastructure/DumpTestBase.cs index 865402813cb6b9..f18f4b2ab2a82f 100644 --- a/src/native/managed/cdac/tests/TestInfrastructure/DumpTestBase.cs +++ b/src/native/managed/cdac/tests/TestInfrastructure/DumpTestBase.cs @@ -106,7 +106,7 @@ protected void InitializeDumpTest(TestConfiguration config, string debuggeeName, } _host = ClrMdDumpHost.Open(dumpPath, GetSymbolPaths(debuggeeName, versionDir)); - ulong contractDescriptor = _host.FindContractDescriptorAddress(); + ulong contractDescriptor = _host.FindContractDescriptorAddress(out ulong runtimeImageBase); _target = ContractDescriptorTarget.Create( contractDescriptor, @@ -115,7 +115,8 @@ protected void InitializeDumpTest(TestConfiguration config, string debuggeeName, _host.GetThreadContext, setThreadContext: static (_, _) => -1, allocVirtual: static (ulong _, out ulong _) => throw new NotImplementedException("Dump tests do not provide AllocVirtual"), - [Contracts.CoreCLRContracts.Register]); + [Contracts.CoreCLRContracts.Register], + runtimeImageBase); } /// @@ -129,7 +130,7 @@ protected void InitializeDumpTestFromPath(string dumpPath) throw new SkipTestException($"Dump not found: {dumpPath}"); _host = ClrMdDumpHost.Open(dumpPath, []); - ulong contractDescriptor = _host.FindContractDescriptorAddress(); + ulong contractDescriptor = _host.FindContractDescriptorAddress(out ulong runtimeImageBase); _target = ContractDescriptorTarget.Create( contractDescriptor, @@ -138,7 +139,8 @@ protected void InitializeDumpTestFromPath(string dumpPath) _host.GetThreadContext, setThreadContext: static (_, _) => -1, allocVirtual: static (ulong _, out ulong _) => throw new NotImplementedException("Dump tests do not provide AllocVirtual"), - [Contracts.CoreCLRContracts.Register]); + [Contracts.CoreCLRContracts.Register], + runtimeImageBase); } public void Dispose() diff --git a/src/native/managed/cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs b/src/native/managed/cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs index 7ef4525766ca53..68361e0e4f3c81 100644 --- a/src/native/managed/cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs +++ b/src/native/managed/cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs @@ -552,6 +552,7 @@ public override bool TryGetTypeInfo(string typeName, out Target.TypeInfo info) public override bool TryGetThreadContext(ulong threadId, uint contextFlags, Span bufferToFill) => throw new NotImplementedException(); public override bool TrySetThreadContext(ulong threadId, ReadOnlySpan context) => throw new NotImplementedException(); + public override bool TryGetRuntimeImageBase(out TargetPointer imageBase) => throw new NotImplementedException(); public override Target.IDataCache ProcessedData => _dataCache; public override ContractRegistry Contracts => _contractRegistry; diff --git a/src/native/managed/cdac/tests/UnitTests/ContractDescriptor/TargetTests.cs b/src/native/managed/cdac/tests/UnitTests/ContractDescriptor/TargetTests.cs index 66ded94c2f11f7..c1debd01928470 100644 --- a/src/native/managed/cdac/tests/UnitTests/ContractDescriptor/TargetTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/ContractDescriptor/TargetTests.cs @@ -386,7 +386,7 @@ public void TryGetContract_UnrecognizedVersion_ReturnsContractUnrecognizedExcept // The contracts required by the data-access interfaces, advertised at the versions // CoreCLRContracts registers. Mirrors CoreCLRContracts.ValidateForDataAccess. - private static readonly IReadOnlyDictionary s_requiredDataAccessContracts = + internal static readonly IReadOnlyDictionary s_requiredDataAccessContracts = new Dictionary { ["AuxiliarySymbols"] = "c1", diff --git a/src/native/managed/cdac/tests/UnitTests/EnumMemoryTests.cs b/src/native/managed/cdac/tests/UnitTests/EnumMemoryTests.cs new file mode 100644 index 00000000000000..8664d0165c4a00 --- /dev/null +++ b/src/native/managed/cdac/tests/UnitTests/EnumMemoryTests.cs @@ -0,0 +1,1782 @@ +// 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.Buffers.Binary; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.Marshalling; +using System.Text; +using System.Threading; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.DataContractReader.Legacy; +using Microsoft.Diagnostics.DataContractReader.Legacy.EnumMemory; +using Microsoft.Diagnostics.DataContractReader.TestInfrastructure; +using Microsoft.Diagnostics.DataContractReader.TestInfrastructure.ContractDescriptor; +using Microsoft.Diagnostics.Runtime; +using Moq; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +public unsafe partial class EnumMemoryTests +{ + [Theory] + [InlineData(false, 0x1234_5678_9000ul)] + [InlineData(true, 0x1234_5678_9000ul)] + [InlineData(false, 0ul)] + [InlineData(true, 0ul)] + public void RuntimeImageBase_PreservesProvidedAddressAcrossFlush(bool nativeDescriptor, ulong address) + { + TargetTestHelpers helpers = new(new() { IsLittleEndian = true, Is64Bit = true }); + ContractDescriptorBuilder builder = new(helpers); + ulong descriptorAddress = new ContractDescriptorBuilder.DescriptorBuilder(builder).CreateSubDescriptor(0x20000, 0x21000, 0x22000); + MockMemorySpace.MemoryContext memory = builder.GetMemoryContext(); + ContractDescriptorTarget target = nativeDescriptor + ? ContractDescriptorTarget.Create(descriptorAddress, memory.ReadFromTarget, memory.WriteToTarget, + (_, _, _) => HResults.E_NOTIMPL, (_, _) => HResults.E_NOTIMPL, + (ulong _, out ulong allocated) => { allocated = 0; return HResults.E_NOTIMPL; }, [], runtimeImageBase: address) + : ContractDescriptorTarget.Create(ContractDescriptorParser.ParseCompact("""{"version":2}"""u8), [], + memory.ReadFromTarget, memory.WriteToTarget, (_, _, _) => HResults.E_NOTIMPL, (_, _) => HResults.E_NOTIMPL, + (ulong _, out ulong allocated) => { allocated = 0; return HResults.E_NOTIMPL; }, + isLittleEndian: true, pointerSize: 8, runtimeImageBase: address); + + bool expected = address != 0; + Assert.Equal(expected, target.TryGetRuntimeImageBase(out TargetPointer actual)); + Assert.Equal(new TargetPointer(address), actual); + target.Flush(FlushScope.All); + Assert.Equal(expected, target.TryGetRuntimeImageBase(out actual)); + Assert.Equal(new TargetPointer(address), actual); + } + + [Theory] + [InlineData("locator", false)] + [InlineData("pe-exports", false)] + [InlineData("explicit", false)] + [InlineData("collector-locator", false)] + [InlineData("fallback", false)] + [InlineData("locator", true)] + [InlineData("pe-exports", true)] + [InlineData("explicit", true)] + [InlineData("collector-locator", true)] + [InlineData("fallback", true)] + public void Activation_DiscoversDescriptorAndRejectsShortReads(string discovery, bool shortRead) + { + TargetTestHelpers helpers = new(new() { IsLittleEndian = true, Is64Bit = true }); + ContractDescriptorBuilder builder = new(helpers); + builder.AddHeapFragment(new() { Address = 0x10000, Data = CreateRuntimeImage(is64Bit: true), Name = "RuntimeImage" }); + ulong descriptorAddress = new ContractDescriptorBuilder.DescriptorBuilder(builder) + .SetContracts(ContractDescriptor.TargetTests.s_requiredDataAccessContracts) + .SetGlobals([("OperatingSystem", null, "unix", "string")]) + .CreateSubDescriptor(0x12000, 0x13000, 0x14000); + MockMemorySpace.MemoryContext memory = builder.GetMemoryContext(); + RuntimeDataTarget dataTarget = discovery == "pe-exports" + ? new RuntimeDataTarget([]) { ReadMemory = memory.ReadFromTarget, ShortRead = shortRead } + : new ContractLocatorDataTarget { ContractAddress = descriptorAddress, ReadMemory = memory.ReadFromTarget, ShortRead = shortRead }; + int hr = Activate(dataTarget, typeof(ICLRDataEnumMemoryRegions).GUID, out nint instance, + discovery == "explicit" ? descriptorAddress : 0, discovery); + + if (shortRead) + { + Assert.True(hr < 0); + Assert.Equal(0, instance); + } + else + { + Assert.Equal(HResults.S_OK, hr); + Assert.NotEqual(0, instance); + try + { + Assert.Equal(HResults.S_OK, Marshal.QueryInterface(instance, typeof(IXCLRDataProcess).GUID, out nint process)); + Marshal.Release(process); + using RecordingCallback callback = new(supportsUpdates: false); + var enumerate = (delegate* unmanaged[MemberFunction])(*(nint**)instance)[3]; + Assert.Equal(HResults.S_OK, enumerate(instance, (void*)callback.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); + Assert.Equal(discovery is "pe-exports" or "collector-locator", callback.Ranges.Contains((dataTarget.ImageBase, 0x300u))); + } + finally + { + Marshal.Release(instance); + } + } + + Assert.Equal(discovery is "pe-exports" or "collector-locator" ? 1 : 0, dataTarget.ImageBaseLookups); + if (dataTarget is ContractLocatorDataTarget locator) + Assert.Equal(discovery == "explicit" ? 0 : 1, locator.ContractLookups); + } + + [Theory] + [InlineData(0ul)] + [InlineData(0x10000ul)] + public void Activation_HelperUsesProvidedImageBaseWithoutLookup(ulong runtimeImageBase) + { + ContractDescriptorBuilder builder = new(new(new() { IsLittleEndian = true, Is64Bit = true })); + builder.AddHeapFragment(new() { Address = 0x10000, Data = CreateRuntimeImage(is64Bit: true), Name = "RuntimeImage" }); + ulong descriptorAddress = new ContractDescriptorBuilder.DescriptorBuilder(builder) + .SetContracts(ContractDescriptor.TargetTests.s_requiredDataAccessContracts) + .SetGlobals([("OperatingSystem", null, "unix", "string")]) + .CreateSubDescriptor(0x12000, 0x13000, 0x14000); + RuntimeDataTarget dataTarget = new([]) { ReadMemory = builder.GetMemoryContext().ReadFromTarget }; + void* target = ComInterfaceMarshaller.ConvertToUnmanaged(dataTarget); + try + { + Guid iid = typeof(ICLRDataEnumMemoryRegions).GUID; + void* instance = null; + Assert.Equal(HResults.S_OK, EntrypointHelpers.CreateInstance(&iid, (nint)target, 0, &instance, descriptorAddress, runtimeImageBase)); + Assert.True(instance != null); + try + { + using RecordingCallback callback = new(supportsUpdates: false); + var enumerate = (delegate* unmanaged[MemberFunction])(*(nint**)instance)[3]; + Assert.Equal(HResults.S_OK, enumerate(instance, (void*)callback.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); + Assert.Equal(runtimeImageBase != 0, callback.Ranges.Contains((0x10000ul, 0x300u))); + } + finally + { + Marshal.Release((nint)instance); + } + Assert.Equal(0, dataTarget.ImageBaseLookups); + } + finally + { + ComInterfaceMarshaller.Free(target); + } + } + + [Theory] + [InlineData(HResults.E_FAIL, 0x10000ul, true)] + [InlineData(HResults.E_NOTIMPL, 0x10000ul, true)] + [InlineData(HResults.S_FALSE, 0x10000ul, true)] + [InlineData(HResults.S_OK, 0ul, true)] + [InlineData(HResults.E_FAIL, 0x10000ul, false)] + [InlineData(HResults.E_NOTIMPL, 0x10000ul, false)] + [InlineData(HResults.S_FALSE, 0x10000ul, false)] + [InlineData(HResults.S_OK, 0ul, false)] + public void Activation_CollectorRejectsUnavailableRuntimeImageBase(int result, ulong imageBase, bool hasContractLocator) + { + ContractDescriptorBuilder builder = new(new(new() { IsLittleEndian = true, Is64Bit = true })); + ulong descriptorAddress = new ContractDescriptorBuilder.DescriptorBuilder(builder) + .SetContracts(ContractDescriptor.TargetTests.s_requiredDataAccessContracts) + .CreateSubDescriptor(0x12000, 0x13000, 0x14000); + RuntimeDataTarget dataTarget = hasContractLocator + ? new ContractLocatorDataTarget { ContractAddress = descriptorAddress, ReadMemory = builder.GetMemoryContext().ReadFromTarget } + : new RuntimeDataTarget([]); + dataTarget.LookupResult = result; + dataTarget.ImageBase = imageBase; + int hr = Activate(dataTarget, typeof(ICLRDataEnumMemoryRegions).GUID, out nint instance, entrypoint: "collector-locator"); + + try + { + Assert.Equal(HResults.E_FAIL, hr); + Assert.Equal(0, instance); + Assert.Equal(1, dataTarget.ImageBaseLookups); + Assert.Equal(0, dataTarget.ReadCount); + if (dataTarget is ContractLocatorDataTarget locator) + Assert.Equal(0, locator.ContractLookups); + } + finally + { + if (instance != 0) + Marshal.Release(instance); + } + } + + [Theory] + [InlineData("locator", null, 0x10000ul)] + [InlineData("explicit", null, 0x10000ul)] + [InlineData("fallback", null, 0x10000ul)] + [InlineData("locator", HResults.S_OK, 0x10000ul)] + [InlineData("explicit", HResults.S_OK, 0x10000ul)] + [InlineData("fallback", HResults.S_OK, 0x10000ul)] + [InlineData("locator", HResults.E_FAIL, 0x10000ul)] + [InlineData("explicit", HResults.E_FAIL, 0x10000ul)] + [InlineData("fallback", HResults.E_FAIL, 0x10000ul)] + [InlineData("locator", HResults.S_FALSE, 0x10000ul)] + [InlineData("explicit", HResults.S_FALSE, 0x10000ul)] + [InlineData("fallback", HResults.S_FALSE, 0x10000ul)] + [InlineData("locator", HResults.S_OK, 0ul)] + [InlineData("explicit", HResults.S_OK, 0ul)] + [InlineData("fallback", HResults.S_OK, 0ul)] + public void Activation_UniversalUsesRuntimeLocatorWithoutImageBaseFallback(string entrypoint, int? locatorResult, ulong imageBase) + { + ContractDescriptorBuilder builder = new(new(new() { IsLittleEndian = true, Is64Bit = true })); + builder.AddHeapFragment(new() { Address = 0x10000, Data = CreateRuntimeImage(is64Bit: true), Name = "RuntimeImage" }); + ulong descriptorAddress = new ContractDescriptorBuilder.DescriptorBuilder(builder) + .SetContracts(ContractDescriptor.TargetTests.s_requiredDataAccessContracts) + .SetGlobals([("OperatingSystem", null, "unix", "string")]) + .CreateSubDescriptor(0x12000, 0x13000, 0x14000); + MockMemorySpace.MemoryContext memory = builder.GetMemoryContext(); + ContractLocatorDataTarget dataTarget = locatorResult is int result + ? new RuntimeAndContractLocatorDataTarget + { + ContractAddress = descriptorAddress, + ReadMemory = memory.ReadFromTarget, + RuntimeLookupResult = result, + ImageBase = imageBase, + } + : new ContractLocatorDataTarget { ContractAddress = descriptorAddress, ReadMemory = memory.ReadFromTarget }; + + Assert.Equal(HResults.S_OK, Activate(dataTarget, typeof(ICLRDataEnumMemoryRegions).GUID, out nint instance, + entrypoint == "explicit" ? descriptorAddress : 0, entrypoint)); + Assert.NotEqual(0, instance); + try + { + using RecordingCallback callback = new(supportsUpdates: false); + var enumerate = (delegate* unmanaged[MemberFunction])(*(nint**)instance)[3]; + Assert.Equal(HResults.S_OK, enumerate(instance, (void*)callback.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); + Assert.Equal(locatorResult == HResults.S_OK && imageBase != 0, callback.Ranges.Contains((0x10000ul, 0x300u))); + Assert.Equal(0, dataTarget.ImageBaseLookups); + Assert.Equal(locatorResult.HasValue ? 1 : 0, dataTarget.RuntimeBaseLookups); + } + finally + { + Marshal.Release(instance); + } + } + + [Theory] + [InlineData(HResults.S_OK, 0ul, "locator")] + [InlineData(HResults.S_FALSE, 0x12000ul, "locator")] + [InlineData(HResults.E_FAIL, 0x12000ul, "locator")] + [InlineData(HResults.S_OK, 0ul, "fallback")] + [InlineData(HResults.S_FALSE, 0x12000ul, "fallback")] + [InlineData(HResults.E_FAIL, 0x12000ul, "fallback")] + public void Activation_UniversalRejectsUnavailableLocatorWithoutFallingBack(int result, ulong address, string entrypoint) + { + ContractLocatorDataTarget dataTarget = new() { ContractResult = result, ContractAddress = address }; + Assert.Equal(CdacHResults.CDAC_E_DESCRIPTOR_NOT_FOUND, + Activate(dataTarget, typeof(ICLRDataEnumMemoryRegions).GUID, out nint instance, entrypoint: entrypoint)); + Assert.Equal(0, instance); + Assert.Equal(1, dataTarget.ContractLookups); + Assert.Equal(0, dataTarget.ImageBaseLookups); + Assert.Equal(0, dataTarget.ReadCount); + } + + [Theory] + [InlineData("locator")] + [InlineData("fallback")] + public void Activation_UniversalRequiresContractLocator(string entrypoint) + { + RuntimeDataTarget dataTarget = new(CreateRuntimeImage(is64Bit: true)); + Assert.Equal(CdacHResults.CDAC_E_DESCRIPTOR_NOT_FOUND, + Activate(dataTarget, typeof(ICLRDataEnumMemoryRegions).GUID, out nint instance, entrypoint: entrypoint)); + Assert.Equal(0, instance); + Assert.Equal(0, dataTarget.ImageBaseLookups); + Assert.Equal(0, dataTarget.ReadCount); + } + + [Theory] + [InlineData("iid")] + [InlineData("target")] + [InlineData("output")] + [InlineData("contract")] + public void Activation_HelperRejectsInvalidArguments(string argument) + { + RuntimeDataTarget dataTarget = new([]); + void* target = ComInterfaceMarshaller.ConvertToUnmanaged(dataTarget); + try + { + Guid iid = typeof(ICLRDataEnumMemoryRegions).GUID; + void* instance = (void*)1; + Assert.Equal(HResults.E_INVALIDARG, EntrypointHelpers.CreateInstance( + argument == "iid" ? null : &iid, + argument == "target" ? IntPtr.Zero : (nint)target, + IntPtr.Zero, + argument == "output" ? null : &instance, + argument == "contract" ? 0ul : 0x12000ul)); + if (argument != "output") + Assert.Equal(0, (nint)instance); + Assert.Equal(0, dataTarget.ImageBaseLookups); + Assert.Equal(0, dataTarget.ReadCount); + } + finally + { + ComInterfaceMarshaller.Free(target); + } + } + + [Theory] + [InlineData("iid", "locator")] + [InlineData("target", "locator")] + [InlineData("output", "locator")] + [InlineData("iid", "collector-locator")] + [InlineData("target", "collector-locator")] + [InlineData("output", "collector-locator")] + [InlineData("iid", "fallback")] + [InlineData("target", "fallback")] + [InlineData("output", "fallback")] + public void Activation_EntrypointRejectsNullArguments(string argument, string entrypoint) + { + RuntimeDataTarget dataTarget = new([]); + void* target = ComInterfaceMarshaller.ConvertToUnmanaged(dataTarget); + try + { + Guid iid = typeof(ICLRDataEnumMemoryRegions).GUID; + void* instance = (void*)1; + Assert.Equal(HResults.E_INVALIDARG, InvokeActivation( + argument == "iid" ? null : &iid, + argument == "target" ? IntPtr.Zero : (nint)target, + argument == "output" ? null : &instance, 0, entrypoint)); + if (argument != "output") + Assert.Equal(0, (nint)instance); + Assert.Equal(0, dataTarget.ImageBaseLookups); + Assert.Equal(0, dataTarget.ReadCount); + } + finally + { + ComInterfaceMarshaller.Free(target); + } + } + + private static int Activate(RuntimeDataTarget dataTarget, Guid iid, out nint instance, ulong contractAddress = 0, string entrypoint = "locator") + { + void* target = ComInterfaceMarshaller.ConvertToUnmanaged(dataTarget); + try + { + void* result = (void*)1; + int hr = InvokeActivation(&iid, (nint)target, &result, contractAddress, entrypoint); + instance = (nint)result; + return hr; + } + finally + { + ComInterfaceMarshaller.Free(target); + } + } + + private static int InvokeActivation(Guid* iid, nint target, void** instance, ulong contractAddress, string entrypoint) + { + Assembly assembly = entrypoint is "pe-exports" or "collector-locator" + ? Assembly.Load(OperatingSystem.IsWindows() ? "mscordaccore" : "libmscordaccore") + : typeof(Entrypoints).Assembly; + string methodName = entrypoint switch + { + "explicit" => "DbgShimCreateInstanceFromContractDescriptor", + "fallback" => "CLRDataCreateInstanceWithFallback", + _ => "CLRDataCreateInstance", + }; + Type type = assembly.GetType("Microsoft.Diagnostics.DataContractReader.Entrypoints", throwOnError: true)!; + MethodInfo method = type.GetMethod(methodName, BindingFlags.Static | BindingFlags.NonPublic)!; + Assert.NotNull(method); + nint address = method.MethodHandle.GetFunctionPointer(); + return entrypoint switch + { + "explicit" => ((delegate* unmanaged)address)(iid, target, contractAddress, instance), + "fallback" => ((delegate* unmanaged)address)(iid, target, 0, instance), + _ => ((delegate* unmanaged)address)(iid, target, instance), + }; + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void RuntimeImageBase_CorDebugTarget_PreservesProvidedBase(MockTarget.Architecture arch) + { + ulong expectedBase = arch.Is64Bit ? 0x1234_5678_9000UL : 0x9000_0000UL; + TargetTestHelpers helpers = new(arch); + ContractDescriptorBuilder builder = new(helpers); + ulong descriptorAddress = new ContractDescriptorBuilder.DescriptorBuilder(builder).CreateSubDescriptor(0x30000, 0x31000, 0x32000); + CorDebugDataTarget dataTarget = new(builder.GetMemoryContext()); + + Target target = Entrypoints.CreateTargetFromCorDebugDataTarget(dataTarget, descriptorAddress, expectedBase); + + Assert.True(target.TryGetRuntimeImageBase(out TargetPointer imageBase)); + Assert.Equal(expectedBase, imageBase.Value); + target.Flush(FlushScope.All); + Assert.True(target.TryGetRuntimeImageBase(out imageBase)); + Assert.Equal(expectedBase, imageBase.Value); + } + + [Theory] + [InlineData(4, @"C:\runtime\coreclr.dll")] + [InlineData(8, @"C:\runtime\coreclr.dll")] + [InlineData(4, "/runtime/libcoreclr.so")] + [InlineData(8, "/runtime/libcoreclr.so")] + [InlineData(4, "/runtime/libcoreclr.dylib")] + [InlineData(8, "/runtime/libcoreclr.dylib")] + public void RuntimeImageBase_ClrMdLookup_UsesDescriptorModule(int pointerSize, string runtimeModuleName) + { + const ulong RawImageBase = 0x1234_5678_9000_0000; + const ulong RawDescriptorAddress = RawImageBase + 0x1000; + var reader = new Mock(); + reader.SetupGet(r => r.PointerSize).Returns(pointerSize); + reader.SetupGet(r => r.Architecture).Returns(pointerSize == 4 ? Architecture.X86 : Architecture.X64); + reader.SetupGet(r => r.TargetPlatform).Returns(OSPlatform.Windows); + reader.Setup(r => r.EnumerateModules()).Returns( + [ + CreateModule(0x10000, "application", 0x11000), + CreateModule(0x20000, runtimeModuleName, 0), + CreateModule(RawImageBase, runtimeModuleName, RawDescriptorAddress), + ]); + using DataTarget dataTarget = new(reader.Object, new DataTargetOptions()); + ClrMdDumpHost host = Assert.IsType(Activator.CreateInstance( + typeof(ClrMdDumpHost), BindingFlags.Instance | BindingFlags.NonPublic, binder: null, + args: ["test.dmp", dataTarget, Array.Empty()], culture: null)); + + ulong descriptorAddress = host.FindContractDescriptorAddress(out ulong imageBase); + ulong mask = pointerSize == 4 ? uint.MaxValue : ulong.MaxValue; + Assert.Equal(RawDescriptorAddress & mask, descriptorAddress); + Assert.Equal(RawImageBase & mask, imageBase); + reader.Verify(r => r.EnumerateModules(), Times.Once); + } + + private static ModuleInfo CreateModule(ulong imageBase, string fileName, ulong descriptorAddress) + { + var module = new Mock(imageBase, fileName); + module.Setup(m => m.GetExportSymbolAddress("DotNetRuntimeContractDescriptor")).Returns(descriptorAddress); + return module.Object; + } + + private sealed class CorDebugDataTarget(MockMemorySpace.MemoryContext memory) : ICorDebugDataTarget + { + public ContractDescriptorTarget.GetTargetThreadContextDelegate? ReadThreadContext { get; init; } + + public int GetPlatform(int* platform) => throw new NotImplementedException(); + + public int GetThreadContext(uint threadId, uint contextFlags, uint contextSize, byte* context) + => ReadThreadContext is not null + ? ReadThreadContext(threadId, contextFlags, new Span(context, checked((int)contextSize))) + : throw new NotImplementedException(); + + public int ReadVirtual(ulong address, byte* buffer, uint bytesRequested, uint* bytesRead) + { + int hr = memory.ReadFromTarget(address, new Span(buffer, checked((int)bytesRequested))); + *bytesRead = hr >= 0 ? bytesRequested : 0; + return hr; + } + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void Enumeration_ReadsRuntimeImageAtProvidedBaseOnEveryCall(MockTarget.Architecture arch) + { + const ulong ImageBase = 0x10000; + TargetTestHelpers helpers = new(arch); + ContractDescriptorBuilder builder = new(helpers); + byte[] image = CreateRuntimeImage(arch.Is64Bit); + builder.AddHeapFragment(new() { Address = ImageBase, Data = image, Name = "RuntimeImage" }); + ulong descriptorAddress = new ContractDescriptorBuilder.DescriptorBuilder(builder).CreateSubDescriptor(0x30000, 0x31000, 0x32000); + MockMemorySpace.MemoryContext memory = builder.GetMemoryContext(); + Lock apiLock = new(); + int imageReadCount = 0; + bool imageReadUnderLock = true; + ContractDescriptorTarget target = ContractDescriptorTarget.Create(descriptorAddress, + (address, buffer) => + { + if (address >= ImageBase && address < ImageBase + (ulong)image.Length) + { + imageReadCount++; + imageReadUnderLock &= apiLock.IsHeldByCurrentThread; + } + return memory.ReadFromTarget(address, buffer); + }, + memory.WriteToTarget, (_, _, _) => HResults.E_NOTIMPL, (_, _) => HResults.E_NOTIMPL, + (ulong _, out ulong address) => { address = 0; return HResults.E_NOTIMPL; }, + [], runtimeImageBase: ImageBase); + ICLRDataEnumMemoryRegions impl = new SOSDacImpl(target, legacyObj: null, apiLock); + Assert.Equal(0, imageReadCount); + + for (int i = 0; i < 2; i++) + { + using RecordingCallback callback = new(supportsUpdates: false); + Assert.Equal(HResults.S_OK, impl.EnumMemoryRegions((void*)callback.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); + Assert.Equal(2 * (i + 1), imageReadCount); + Assert.True(imageReadUnderLock); + Assert.Contains((ImageBase, 64u), callback.Ranges); + Assert.Contains((ImageBase + 0x80, 264u), callback.Ranges); + Assert.Contains((ImageBase, 0x300u), callback.Ranges); + Assert.Contains((ImageBase + 0x400, 0x100u), callback.Ranges); + Assert.Contains((ImageBase + 0x600, 0x40u), callback.Ranges); + Assert.Contains((ImageBase + 0x700, 0x20u), callback.Ranges); + } + + using RecordingCallback cancelled = new(supportsUpdates: false) { CancelAtAddress = ImageBase }; + Assert.Equal(HResults.COR_E_OPERATIONCANCELED, impl.EnumMemoryRegions((void*)cancelled.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); + Assert.Equal(5, imageReadCount); + using RecordingCallback afterCancellation = new(supportsUpdates: false); + Assert.Equal(HResults.S_OK, impl.EnumMemoryRegions((void*)afterCancellation.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); + Assert.Contains((ImageBase, 0x300u), afterCancellation.Ranges); + Assert.Equal(7, imageReadCount); + } + + [Theory] + [InlineData(false, HResults.S_OK, HResults.S_OK, 0x10000ul, true)] + [InlineData(true, HResults.S_OK, HResults.E_FAIL, 0x10000ul, true)] + [InlineData(false, HResults.S_OK, HResults.E_FAIL, 0x10000ul, false)] + [InlineData(true, HResults.E_FAIL, HResults.E_FAIL, 0x10000ul, false)] + [InlineData(false, HResults.S_OK, HResults.S_FALSE, 0x10000ul, false)] + [InlineData(true, HResults.S_FALSE, HResults.S_FALSE, 0x10000ul, false)] + [InlineData(false, HResults.S_OK, HResults.S_OK, 0ul, false)] + [InlineData(true, HResults.S_OK, HResults.S_OK, 0ul, false)] + [InlineData(true, HResults.E_FAIL, HResults.S_OK, 0x10000ul, true)] + [InlineData(true, HResults.S_FALSE, HResults.S_OK, 0x10000ul, true)] + [InlineData(true, HResults.S_FALSE, HResults.E_FAIL, 0x10000ul, false)] + [InlineData(true, HResults.E_FAIL, HResults.S_OK, 0ul, false)] + public void RuntimeImageLookup_UsesHostCapabilityWithoutReadingHeaders(bool hasLocator, int locatorResult, int fallbackResult, ulong imageBase, bool expected) + { + RuntimeDataTarget dataTarget = hasLocator + ? new RuntimeLocatorDataTarget([]) { RuntimeLookupResult = locatorResult } + : new RuntimeDataTarget([]); + dataTarget.LookupResult = fallbackResult; + dataTarget.ImageBase = imageBase; + + bool actualResult = EntrypointHelpers.TryGetRuntimeImageBase(dataTarget, out TargetPointer address); + bool useFallback = !hasLocator || locatorResult != HResults.S_OK; + Assert.Equal(expected, actualResult); + Assert.Equal(expected ? new TargetPointer(imageBase) : TargetPointer.Null, address); + Assert.Equal(useFallback ? 1 : 0, dataTarget.ImageBaseLookups); + Assert.Equal(hasLocator ? 1 : 0, dataTarget.RuntimeBaseLookups); + Assert.Equal(0, dataTarget.ReadCount); + } + + [Theory] + [InlineData(false, false, null)] + [InlineData(true, false, null)] + [InlineData(false, true, null)] + [InlineData(true, true, null)] + [InlineData(false, false, HResults.S_OK)] + [InlineData(true, false, HResults.S_OK)] + [InlineData(false, true, HResults.S_OK)] + [InlineData(true, true, HResults.S_OK)] + [InlineData(false, false, HResults.S_FALSE)] + [InlineData(true, false, HResults.S_FALSE)] + [InlineData(false, true, HResults.S_FALSE)] + [InlineData(true, true, HResults.S_FALSE)] + [InlineData(false, false, HResults.E_FAIL)] + [InlineData(true, false, HResults.E_FAIL)] + [InlineData(false, true, HResults.E_FAIL)] + [InlineData(true, true, HResults.E_FAIL)] + public void RuntimeDescriptorBootstrap_FallsBackToPeExportsAndRejectsShortReads(bool is64Bit, bool shortRead, int? locatorResult) + { + ContractDescriptorBuilder builder = new(new(new() { IsLittleEndian = true, Is64Bit = is64Bit })); + builder.AddHeapFragment(new() { Address = 0x10000, Data = CreateRuntimeImage(is64Bit), Name = "RuntimeImage" }); + new ContractDescriptorBuilder.DescriptorBuilder(builder) + .SetContracts(ContractDescriptor.TargetTests.s_requiredDataAccessContracts) + .CreateSubDescriptor(0x12000, 0x13000, 0x14000); + RuntimeDataTarget dataTarget = locatorResult is int result + ? new ContractLocatorDataTarget + { + ContractResult = result, + ContractAddress = result == HResults.S_OK ? 0ul : 0x12000ul, + ReadMemory = builder.GetMemoryContext().ReadFromTarget, + ShortRead = shortRead, + } + : new RuntimeDataTarget([]) { ReadMemory = builder.GetMemoryContext().ReadFromTarget, ShortRead = shortRead }; + + int hr = Activate(dataTarget, typeof(ICLRDataEnumMemoryRegions).GUID, out nint instance, entrypoint: "pe-exports"); + try + { + Assert.Equal(shortRead ? CdacHResults.CDAC_E_DESCRIPTOR_NOT_FOUND : HResults.S_OK, hr); + Assert.Equal(!shortRead, instance != 0); + Assert.Equal(1, dataTarget.ImageBaseLookups); + if (dataTarget is ContractLocatorDataTarget locator) + Assert.Equal(1, locator.ContractLookups); + if (shortRead) + Assert.Equal(1, dataTarget.ReadCount); + } + finally + { + if (instance != 0) + Marshal.Release(instance); + } + } + + [Fact] + public void RuntimeDescriptorBootstrap_RejectsMissingImageBaseWithoutReading() + { + RuntimeDataTarget dataTarget = new(CreateRuntimeImage(is64Bit: true)) { ImageBase = 0 }; + + Assert.Equal(HResults.E_FAIL, Activate(dataTarget, typeof(ICLRDataEnumMemoryRegions).GUID, out nint instance, entrypoint: "pe-exports")); + Assert.Equal(0, instance); + Assert.Equal(1, dataTarget.ImageBaseLookups); + Assert.Equal(0, dataTarget.ReadCount); + } + + [Theory] + [InlineData("no-base", false, 0)] + [InlineData("unreadable-dos", false, 1)] + [InlineData("unreadable-pe", false, 2)] + [InlineData("not-pe", true, 1)] + [InlineData("negative-offset", false, 1)] + [InlineData("bad-signature", false, 2)] + [InlineData("bad-optional-header", false, 2)] + [InlineData("zero-headers", false, 2)] + public void RuntimeImageDiscovery_HandlesMissingOrInvalidImages(string scenario, bool expected, int expectedReads) + { + const ulong ImageBase = 0x10000; + byte[] image = CreateRuntimeImage(is64Bit: true); + switch (scenario) + { + case "not-pe": + image[0] = 0; + break; + case "negative-offset": + BinaryPrimitives.WriteInt32LittleEndian(image.AsSpan(0x3c), -1); + break; + case "bad-signature": + image[0x80] = 0; + break; + case "bad-optional-header": + BinaryPrimitives.WriteUInt16LittleEndian(image.AsSpan(0x80 + 24), 0); + break; + case "zero-headers": + BinaryPrimitives.WriteUInt32LittleEndian(image.AsSpan(0x80 + 24 + 60), 0); + break; + } + + ContractDescriptorBuilder builder = new(new(new() { Is64Bit = true, IsLittleEndian = true })); + builder.AddHeapFragment(new() { Address = ImageBase, Data = image, Name = "RuntimeImage" }); + ulong descriptorAddress = new ContractDescriptorBuilder.DescriptorBuilder(builder).CreateSubDescriptor(0x20000, 0x21000, 0x22000); + MockMemorySpace.MemoryContext memory = builder.GetMemoryContext(); + int reads = 0; + ContractDescriptorTarget target = ContractDescriptorTarget.Create(descriptorAddress, + (address, buffer) => + { + if (address >= ImageBase && address < ImageBase + (ulong)image.Length) + { + reads++; + if ((scenario == "unreadable-dos" && address == ImageBase) + || (scenario == "unreadable-pe" && address == ImageBase + 0x80)) + { + return HResults.E_FAIL; + } + } + return memory.ReadFromTarget(address, buffer); + }, + memory.WriteToTarget, (_, _, _) => HResults.E_NOTIMPL, (_, _) => HResults.E_NOTIMPL, + (ulong _, out ulong address) => { address = 0; return HResults.E_NOTIMPL; }, [], + runtimeImageBase: scenario == "no-base" ? TargetPointer.Null : new(ImageBase)); + + Assert.Equal(0, reads); + Assert.Equal(expected, RuntimeModuleInfo.TryCreate(target, out RuntimeModuleInfo module)); + Assert.Equal(expectedReads, reads); + if (expected) + { + Assert.Equal(ImageBase, module.ImageBase); + Assert.Equal(0u, module.SizeOfHeaders); + Assert.Empty(module.EnumerateMemoryRegions()); + } + else + { + Assert.Null(module); + } + } + + [Theory] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public void RuntimeImageRegions_ReturnsNonemptyRangesWithoutReading(bool is64Bit, bool emptyDirectories) + { + const ulong ImageBase = 0x10000; + byte[] image = CreateRuntimeImage(is64Bit); + if (emptyDirectories) + { + int directoryOffset = 0x80 + 24 + (is64Bit ? 112 : 96); + image.AsSpan(directoryOffset, 8).Clear(); + image.AsSpan(directoryOffset + 2 * 8, 4).Clear(); + image.AsSpan(directoryOffset + 6 * 8 + 4, 4).Clear(); + } + + int reads = 0; + Assert.True(RuntimeModuleInfo.TryCreate(ImageBase, (address, buffer) => + { + reads++; + image.AsSpan(checked((int)(address - ImageBase)), buffer.Length).CopyTo(buffer); + return true; + }, out RuntimeModuleInfo module)); + Assert.Equal(2, reads); + List expected = [new(ImageBase, 0x300)]; + if (!emptyDirectories) + { + expected.Add(new(ImageBase + 0x700, 0x20)); + expected.Add(new(ImageBase + 0x600, 0x40)); + expected.Add(new(ImageBase + 0x400, 0x100)); + } + + IEnumerable regions = module.EnumerateMemoryRegions(); + Assert.Equal(expected, regions); + Assert.Equal(expected, regions); + Assert.Equal(2, reads); + } + + private static byte[] CreateRuntimeImage(bool is64Bit) + { + byte[] image = new byte[0x1000]; + BinaryPrimitives.WriteUInt16LittleEndian(image, 0x5a4d); + BinaryPrimitives.WriteInt32LittleEndian(image.AsSpan(0x3c), 0x80); + Span peHeader = image.AsSpan(0x80); + BinaryPrimitives.WriteUInt32LittleEndian(peHeader, 0x00004550); + BinaryPrimitives.WriteUInt16LittleEndian(peHeader.Slice(24), is64Bit ? (ushort)0x20b : (ushort)0x10b); + BinaryPrimitives.WriteUInt32LittleEndian(peHeader.Slice(24 + 60), 0x300); + int directoryOffset = 24 + (is64Bit ? 112 : 96); + foreach ((int index, uint rva, uint size) in new[] { (0, 0x400u, 0x100u), (2, 0x600u, 0x40u), (6, 0x700u, 0x20u) }) + { + BinaryPrimitives.WriteUInt32LittleEndian(peHeader.Slice(directoryOffset + index * 8), rva); + BinaryPrimitives.WriteUInt32LittleEndian(peHeader.Slice(directoryOffset + index * 8 + 4), size); + } + + BinaryPrimitives.WriteUInt32LittleEndian(image.AsSpan(0x400 + 20), 1); + BinaryPrimitives.WriteUInt32LittleEndian(image.AsSpan(0x400 + 24), 1); + BinaryPrimitives.WriteUInt32LittleEndian(image.AsSpan(0x400 + 28), 0x440); + BinaryPrimitives.WriteUInt32LittleEndian(image.AsSpan(0x400 + 32), 0x444); + BinaryPrimitives.WriteUInt32LittleEndian(image.AsSpan(0x400 + 36), 0x448); + BinaryPrimitives.WriteUInt32LittleEndian(image.AsSpan(0x440), 0x2000); + BinaryPrimitives.WriteUInt32LittleEndian(image.AsSpan(0x444), 0x450); + "DotNetRuntimeContractDescriptor\0"u8.CopyTo(image.AsSpan(0x450)); + return image; + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Enumeration_SharesComIdentityWithAnalysisInterfaces(bool enumerateFirst) + { + Lock apiLock = new(); + TargetTestHelpers helpers = new(new() { IsLittleEndian = true, Is64Bit = true }); + ContractDescriptorBuilder builder = new(helpers); + ContractDescriptorTarget target = builder.CreateTarget(new(builder)); + SOSDacImpl impl = new(target, legacyObj: null, apiLock); + Assert.Equal(HResults.E_INVALIDARG, ((ICLRDataEnumMemoryRegions)impl).EnumMemoryRegions(null, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); + Assert.Equal(HResults.E_POINTER, ((IXCLRDataProcess3)impl).GetFunctionTable(default, 0, null, null, null)); + + void* entry = enumerateFirst + ? ComInterfaceMarshaller.ConvertToUnmanaged(impl) + : ComInterfaceMarshaller.ConvertToUnmanaged(impl); + try + { + Guid iidIUnknown = new("00000000-0000-0000-C000-000000000046"); + Assert.Equal(HResults.S_OK, Marshal.QueryInterface((nint)entry, in iidIUnknown, out nint identity)); + try + { + using RecordingCallback callback = new(supportsUpdates: false, apiLock); + foreach (Type interfaceType in new[] + { + typeof(ICLRDataEnumMemoryRegions), typeof(IXCLRDataProcess), typeof(IXCLRDataProcess2), + typeof(IXCLRDataProcess3), typeof(ISOSDacInterface), typeof(ISOSDacInterface17), + }) + { + Guid iid = interfaceType.GUID; + Assert.Equal(HResults.S_OK, Marshal.QueryInterface((nint)entry, in iid, out nint iface)); + try + { + Assert.Equal(HResults.S_OK, Marshal.QueryInterface(iface, in iidIUnknown, out nint interfaceIdentity)); + try + { + Assert.Equal(identity, interfaceIdentity); + } + finally + { + Marshal.Release(interfaceIdentity); + } + + Guid enumerationIid = typeof(ICLRDataEnumMemoryRegions).GUID; + Assert.Equal(HResults.S_OK, Marshal.QueryInterface(iface, in enumerationIid, out nint enumeration)); + try + { + var enumerate = (delegate* unmanaged[MemberFunction])(*(nint**)enumeration)[3]; + Assert.Equal(HResults.E_INVALIDARG, enumerate(enumeration, null, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); + callback.Regions.Clear(); + Assert.Equal(HResults.S_OK, enumerate(enumeration, (void*)callback.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); + Assert.NotEmpty(callback.Regions); + Assert.True(callback.WasLockHeld); + Assert.False(apiLock.IsHeldByCurrentThread); + } + finally + { + Marshal.Release(enumeration); + } + } + finally + { + Marshal.Release(iface); + } + } + } + finally + { + Marshal.Release(identity); + } + } + finally + { + if (enumerateFirst) + ComInterfaceMarshaller.Free(entry); + else + ComInterfaceMarshaller.Free(entry); + } + } + + [Theory] + [InlineData(true, true, true)] + [InlineData(true, false, true)] + [InlineData(false, true, true)] + [InlineData(false, false, true)] + [InlineData(true, true, false)] + [InlineData(true, false, false)] + [InlineData(false, true, false)] + [InlineData(false, false, false)] + public void Enumeration_FlushesCachesAndRestoresCallbacksOnEveryCall(bool nativeDescriptor, bool littleEndian, bool is64Bit) + { + const ulong PointerAddress = 0x1000; + TargetTestHelpers helpers = new(new() { IsLittleEndian = littleEndian, Is64Bit = is64Bit }); + ContractDescriptorBuilder builder = new(helpers); + byte[] pointerBytes = new byte[helpers.PointerSize]; + helpers.WritePointer(pointerBytes, 0x9000); + builder.AddHeapFragment(new() { Address = PointerAddress, Data = pointerBytes, Name = "CachedPointer" }); + ContractDescriptorBuilder.DescriptorBuilder descriptor = new(builder); + descriptor.SetContracts(["RuntimeTypeSystem"]); + ulong descriptorAddress = descriptor.CreateSubDescriptor(0x2000, 0x3000, 0x4000); + MockMemorySpace.MemoryContext memory = builder.GetMemoryContext(); + Action[] registrations = + [ + registry => registry.Register("c1", target => + { + TargetPointer? cached = null; + Mock types = new(); + types.Setup(t => t.GetWellKnownMethodTable(It.IsAny())) + .Returns(() => cached ??= target.ProcessedData.GetOrAdd(new(PointerAddress)).Value); + types.Setup(t => t.Flush(FlushScope.All)).Callback(() => cached = null); + return types.Object; + }) + ]; + ContractDescriptorTarget target = nativeDescriptor + ? ContractDescriptorTarget.Create(descriptorAddress, memory.ReadFromTarget, memory.WriteToTarget, + (_, _, _) => HResults.E_NOTIMPL, (_, _) => HResults.E_NOTIMPL, + (ulong _, out ulong address) => { address = 0; return HResults.E_NOTIMPL; }, []) + : ContractDescriptorTarget.Create( + ContractDescriptorParser.ParseCompact("""{"version":2,"contracts":{"RuntimeTypeSystem":"c1"}}"""u8), + [], memory.ReadFromTarget, memory.WriteToTarget, + (_, _, _) => HResults.E_NOTIMPL, (_, _) => HResults.E_NOTIMPL, + (ulong _, out ulong address) => { address = 0; return HResults.E_NOTIMPL; }, + littleEndian, helpers.PointerSize); + + Assert.False(target.TryGetRuntimeImageBase(out TargetPointer imageBase)); + Assert.Equal(TargetPointer.Null, imageBase); + + // Registrations added after target construction must also participate in collection. + foreach (Action register in registrations) + register(target.Contracts); + + Assert.Equal(new TargetPointer(0x9000), target.Contracts.RuntimeTypeSystem.GetWellKnownMethodTable(WellKnownMethodTable.Object)); + IRuntimeTypeSystem originalContract = target.Contracts.RuntimeTypeSystem; + CachedPointer originalData = target.ProcessedData.GetOrAdd(new(PointerAddress)); + ICLRDataEnumMemoryRegions impl = new SOSDacImpl(target, legacyObj: null, new()); + for (int i = 0; i < 2; i++) + { + using RecordingCallback callback = new(supportsUpdates: false); + Assert.Equal(HResults.S_OK, impl.EnumMemoryRegions((void*)callback.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_HEAP2)); + Assert.Contains(PointerAddress, callback.Regions); + Assert.Same(originalContract, target.Contracts.RuntimeTypeSystem); + CachedPointer currentData = target.ProcessedData.GetOrAdd(new(PointerAddress)); + Assert.NotSame(originalData, currentData); + originalData = currentData; + Mock.Get(originalContract).Verify(contract => contract.Flush(FlushScope.All), Times.Exactly(i + 1)); + if (nativeDescriptor) + Assert.Contains(descriptorAddress, callback.Regions); + + int count = callback.Ranges.Count; + Assert.Equal(new TargetPointer(0x9000), target.ReadPointer(PointerAddress)); + Assert.Equal(count, callback.Ranges.Count); + } + + using RecordingCallback cancelled = new(supportsUpdates: false); + cancelled.Result = HResults.COR_E_OPERATIONCANCELED; + Assert.Equal(HResults.COR_E_OPERATIONCANCELED, impl.EnumMemoryRegions((void*)cancelled.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); + using RecordingCallback afterCancellation = new(supportsUpdates: false); + Assert.Equal(HResults.S_OK, impl.EnumMemoryRegions((void*)afterCancellation.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); + Assert.Contains(PointerAddress, afterCancellation.Regions); + } + + [Theory] + [InlineData(HResults.S_OK, false)] + [InlineData(HResults.S_FALSE, false)] + [InlineData(HResults.E_FAIL, false)] + [InlineData(HResults.S_OK, true)] + [InlineData(HResults.S_FALSE, true)] + [InlineData(HResults.E_FAIL, true)] + public void NativeDataTarget_AlignsThreadContextsAndPreservesFailure(int result, bool corDebug) + { + ContractDescriptorTarget.GetTargetThreadContextDelegate readContext = (_, _, buffer) => + { + fixed (byte* context = buffer) + Assert.Equal((nuint)0, (nuint)context & 15); + buffer.Fill(0xab); + return result; + }; + ContractDescriptorTarget target; + if (corDebug) + { + TargetTestHelpers helpers = new(new() { IsLittleEndian = true, Is64Bit = true }); + ContractDescriptorBuilder builder = new(helpers); + ulong descriptorAddress = new ContractDescriptorBuilder.DescriptorBuilder(builder).CreateSubDescriptor(0x2000, 0x3000, 0x4000); + CorDebugDataTarget dataTarget = new(builder.GetMemoryContext()) { ReadThreadContext = readContext }; + target = Entrypoints.CreateTargetFromCorDebugDataTarget(dataTarget, descriptorAddress, 0x10000); + } + else + { + ICLRDataTarget dataTarget = new RuntimeDataTarget([]) { ReadThreadContext = readContext }; + target = ContractDescriptorTarget.Create( + ContractDescriptorParser.ParseCompact("""{"version":2}"""u8), [], + (_, _) => HResults.E_NOTIMPL, (_, _) => HResults.E_NOTIMPL, dataTarget.GetThreadContext, + (_, _) => HResults.E_NOTIMPL, + (ulong _, out ulong address) => { address = 0; return HResults.E_NOTIMPL; }, + isLittleEndian: true, pointerSize: 8); + } + int reads = 0; + using IDisposable readScope = target.RegisterReadCallback((_, _) => reads++); + byte* storage = stackalloc byte[80]; + byte* aligned = (byte*)(((nuint)storage + 15) & ~(nuint)15); + Span buffer = new(aligned + 1, 64); + buffer.Fill(0xcc); + + Assert.Equal(result == HResults.S_OK, target.TryGetThreadContext(1, 0, buffer)); + Assert.All(buffer.ToArray(), value => Assert.Equal(result == HResults.S_OK ? (byte)0xab : (byte)0xcc, value)); + Assert.Equal(0, reads); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void DescriptorMemory_ReportsCachedRangesWithoutReading(MockTarget.Architecture arch) + { + TargetTestHelpers helpers = new(arch); + ContractDescriptorBuilder builder = new(helpers); + ContractDescriptorBuilder.DescriptorBuilder descriptor = new(builder); + descriptor.SetGlobals([("Value", null, 0, null, null)], [0x9000]); + ulong descriptorAddress = descriptor.CreateSubDescriptor(0x2000, 0x3000, 0x4000); + MockMemorySpace.MemoryContext memory = builder.GetMemoryContext(); + List reads = []; + ContractDescriptorTarget target = ContractDescriptorTarget.Create(descriptorAddress, + (address, buffer) => + { + reads.Add(address); + return memory.ReadFromTarget(address, buffer); + }, + memory.WriteToTarget, (_, _, _) => HResults.E_NOTIMPL, (_, _) => HResults.E_NOTIMPL, + (ulong _, out ulong address) => { address = 0; return HResults.E_NOTIMPL; }, []); + + uint jsonSize = target.Read(descriptorAddress + 12); + for (int i = 0; i < 2; i++) + { + reads.Clear(); + using IDisposable readScope = target.RegisterReadCallback((_, _) => Assert.Fail("Descriptor enumeration must not read target memory.")); + Assert.Collection(target.EnumerateDescriptorMemory(), + range => + { + Assert.Equal(new TargetPointer(descriptorAddress), range.Address); + Assert.Equal((ulong)ContractDescriptorHelpers.Size(arch.Is64Bit), range.Size); + }, + range => + { + Assert.Equal(new TargetPointer(0x3000), range.Address); + Assert.Equal((ulong)jsonSize, range.Size); + }, + range => + { + Assert.Equal(new TargetPointer(0x4000), range.Address); + Assert.Equal((ulong)helpers.PointerSize, range.Size); + }); + Assert.Equal(new TargetPointer(0x9000), target.ReadGlobalPointer("Value")); + Assert.Empty(reads); + } + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void Enumeration_RejectsNonDescriptorTargetWithoutChangingCaches(MockTarget.Architecture arch) + { + const ulong PointerAddress = 0x1000; + TargetTestHelpers helpers = new(arch); + MockMemorySpace.Builder builder = new(helpers); + byte[] pointerBytes = new byte[helpers.PointerSize]; + helpers.WritePointer(pointerBytes, 0x9000); + builder.AddHeapFragment(new() { Address = PointerAddress, Data = pointerBytes, Name = nameof(CachedPointer) }); + TestPlaceholderTarget target = new(arch, builder.GetMemoryContext().ReadFromTarget); + TestPlaceholderTarget.TestContractRegistry registry = target.SetupContractRegistry(registry => + registry.Register("c1", boundTarget => + { + Mock types = new(); + types.Setup(t => t.GetWellKnownMethodTable(It.IsAny())) + .Returns(() => boundTarget.ProcessedData.GetOrAdd(new(PointerAddress)).Value); + return types.Object; + })); + registry.SetVersion("c1"); + IRuntimeTypeSystem analysisContract = target.Contracts.RuntimeTypeSystem; + Assert.Equal(new TargetPointer(0x9000), analysisContract.GetWellKnownMethodTable(WellKnownMethodTable.Object)); + CachedPointer analysisData = target.ProcessedData.GetOrAdd(new(PointerAddress)); + ICLRDataEnumMemoryRegions impl = new SOSDacImpl(target, legacyObj: null, new()); + + for (int i = 0; i < 2; i++) + { + using RecordingCallback callback = new(supportsUpdates: false); + Assert.Equal(HResults.E_NOTIMPL, impl.EnumMemoryRegions((void*)callback.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); + Assert.Empty(callback.Regions); + Assert.Same(analysisContract, target.Contracts.RuntimeTypeSystem); + CachedPointer collectionData = target.ProcessedData.GetOrAdd(new(PointerAddress)); + Assert.Same(analysisData, collectionData); + Mock.Get(analysisContract).Verify(contract => contract.Flush(It.IsAny()), Times.Never); + } + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void ReadShim_ReportsEverySuccessfulMemoryRead(MockTarget.Architecture arch) + { + TargetTestHelpers helpers = new(arch); + ContractDescriptorBuilder builder = new(helpers); + const ulong ValueAddress = 0x1000; + const ulong Utf8Address = 0x2000; + const ulong Utf16Address = 0x3000; + const ulong EmptyAddress = 0x4000; + const ulong InvalidUtf8Address = 0x5000; + const ulong UnterminatedAddress = 0x6000; + const ulong UnreadableAddress = 0x9000; + const string Text = "text \u00e9 \uD83D\uDE00"; + Encoding utf16 = arch.IsLittleEndian ? Encoding.Unicode : Encoding.BigEndianUnicode; + AddBytes(ValueAddress, [1, 2, 3, 4, 5, 6, 7, 8]); + AddBytes(Utf8Address, Encoding.UTF8.GetBytes(Text + '\0')); + AddBytes(Utf16Address, utf16.GetBytes(Text + '\0')); + AddBytes(EmptyAddress, [0, 0]); + AddBytes(InvalidUtf8Address, [0xc0, 0xaf, 0]); + AddBytes(UnterminatedAddress, [0x61, 0x62]); + string longText = new('a', 1100); + AddBytes(0x7000, Encoding.UTF8.GetBytes(longText + '\0')); + AddBytes(0x8000, utf16.GetBytes(longText + '\0')); + + uint codePointerSize = arch.Is64Bit ? 4u : 8u; + ContractDescriptorBuilder.DescriptorBuilder descriptor = new(builder); + descriptor.SetTypes(new Dictionary + { + [DataType.CodePointer] = new() { Size = codePointerSize, Fields = new Dictionary() }, + }); + descriptor.SetGlobals([("Number", 42, null, null), ("Pointer", 0x9876, null, null), ("Text", null, "hello", null)]); + ulong descriptorAddress = descriptor.CreateSubDescriptor(0x10000, 0x11000, 0x12000); + MockMemorySpace.MemoryContext memory = builder.GetMemoryContext(); + List<(ulong Address, ulong Size)> underlyingReads = []; + List<(ulong Address, ulong Size)> recordedReads = []; + ContractDescriptorTarget recording = ContractDescriptorTarget.Create(descriptorAddress, + (address, buffer) => + { + int hr = memory.ReadFromTarget(address, buffer); + if (hr >= 0) + underlyingReads.Add((address, (ulong)buffer.Length)); + return hr; + }, + memory.WriteToTarget, (_, _, _) => HResults.E_NOTIMPL, (_, _) => HResults.E_NOTIMPL, + (ulong _, out ulong address) => { address = 0x1234; return HResults.S_OK; }, []); + bool cancelReads = false; + using IDisposable readScope = recording.RegisterReadCallback((address, size) => + { + if (cancelReads) + throw new OperationCanceledException(); + recordedReads.Add((address, size)); + }); + ulong value32 = arch.IsLittleEndian ? 0x04030201u : 0x01020304u; + ulong value64 = arch.IsLittleEndian ? 0x0807060504030201ul : 0x0102030405060708ul; + ulong pointerValue = arch.Is64Bit ? value64 : value32; + ulong codePointerValue = codePointerSize == 4 ? value32 : value64; + + VerifyRead(() => Assert.Equal((uint)value32, recording.Read(ValueAddress))); + VerifyRead(() => Assert.Equal(value64, recording.Read(ValueAddress))); + VerifyRead(() => + { + Assert.True(recording.TryRead(ValueAddress, out uint value)); + Assert.Equal((uint)value32, value); + }); + VerifyRead(() => Assert.Equal(0x04030201u, recording.ReadLittleEndian(ValueAddress))); + VerifyRead(() => Assert.Equal(new TargetPointer(pointerValue), recording.ReadPointer(ValueAddress))); + VerifyRead(() => + { + Assert.True(recording.TryReadPointer(ValueAddress, out TargetPointer value)); + Assert.Equal(new TargetPointer(pointerValue), value); + }); + VerifyRead(() => Assert.Equal(new TargetCodePointer(codePointerValue), recording.ReadCodePointer(ValueAddress))); + VerifyRead(() => + { + Assert.True(recording.TryReadCodePointer(ValueAddress, out TargetCodePointer value)); + Assert.Equal(new TargetCodePointer(codePointerValue), value); + }); + VerifyRead(() => Assert.Equal(new TargetNUInt(pointerValue), recording.ReadNUInt(ValueAddress))); + VerifyRead(() => Assert.Equal(new TargetNInt((long)pointerValue), recording.ReadNInt(ValueAddress))); + VerifyRead(() => + { + byte[] bytes = new byte[3]; + recording.ReadBuffer(ValueAddress, bytes); + Assert.Equal(new byte[] { 1, 2, 3 }, bytes); + }); + VerifyRead(() => Assert.Equal(Text, recording.ReadUtf8String(Utf8Address))); + VerifyRead(() => Assert.Equal(Text, recording.ReadUtf8String(Utf8Address, strict: true))); + VerifyRead(() => Assert.Equal(Text, recording.ReadUtf16String(Utf16Address))); + VerifyRead(() => Assert.Equal(longText, recording.ReadUtf8String(0x7000))); + VerifyRead(() => Assert.Equal(longText, recording.ReadUtf16String(0x8000))); + VerifyRead(() => Assert.Empty(recording.ReadUtf8String(EmptyAddress))); + VerifyRead(() => Assert.Empty(recording.ReadUtf16String(EmptyAddress))); + VerifyRead(() => Assert.Equal("\ufffd\ufffd", recording.ReadUtf8String(InvalidUtf8Address))); + VerifyRead(() => Assert.Throws(() => recording.ReadUtf8String(InvalidUtf8Address, strict: true))); + VerifyRead(() => Assert.Throws(() => recording.ReadUtf8String(UnterminatedAddress))); + + VerifyRead(() => Assert.False(recording.TryRead(UnreadableAddress, out _)), expectReads: false); + VerifyRead(() => Assert.False(recording.TryReadPointer(UnreadableAddress, out _)), expectReads: false); + VerifyRead(() => Assert.False(recording.TryReadCodePointer(UnreadableAddress, out _)), expectReads: false); + VerifyRead(() => Assert.Throws(() => recording.Read(UnreadableAddress)), expectReads: false); + VerifyRead(() => Assert.Throws(() => recording.ReadLittleEndian(UnreadableAddress)), expectReads: false); + VerifyRead(() => Assert.Throws(() => recording.ReadNUInt(UnreadableAddress)), expectReads: false); + VerifyRead(() => Assert.Throws(() => recording.ReadNInt(UnreadableAddress)), expectReads: false); + VerifyRead(() => Assert.Throws(() => recording.ReadPointer(UnreadableAddress)), expectReads: false); + VerifyRead(() => Assert.Throws(() => recording.ReadCodePointer(UnreadableAddress)), expectReads: false); + VerifyRead(() => Assert.Throws(() => recording.ReadBuffer(UnreadableAddress, new byte[1])), expectReads: false); + + VerifyRead(() => + { + Assert.Equal(42u, recording.ReadGlobal("Number")); + Assert.True(recording.TryReadGlobal("Number", out uint? number)); + Assert.Equal(42u, number); + Assert.Equal(new TargetPointer(0x9876), recording.ReadGlobalPointer("Pointer")); + Assert.True(recording.TryReadGlobalPointer("Pointer", out TargetPointer? pointer)); + Assert.Equal(new TargetPointer(0x9876), pointer); + Assert.Equal("hello", recording.ReadGlobalString("Text")); + Assert.True(recording.TryReadGlobalString("Text", out string? text)); + Assert.Equal("hello", text); + Assert.False(recording.TryReadGlobal("Missing", out _)); + Assert.False(recording.TryReadGlobalPointer("Missing", out _)); + Assert.False(recording.TryReadGlobalString("Missing", out _)); + Assert.Equal(codePointerSize, recording.GetTypeInfo(nameof(DataType.CodePointer)).Size); + Assert.True(recording.TryGetTypeInfo(nameof(DataType.CodePointer), out _)); + Assert.False(recording.TryGetTypeInfo("Missing", out _)); + Assert.Equal(new TargetPointer(pointerValue), recording.ReadPointerFromSpan([1, 2, 3, 4, 5, 6, 7, 8])); + Assert.True(recording.IsAlignedToPointerSize(new TargetPointer(ValueAddress))); + Assert.False(recording.IsAlignedToPointerSize(new TargetPointer(ValueAddress + 1))); + Assert.Equal(new TargetPointer(0x1234), recording.AllocateMemory(16)); + }, expectReads: false); + + VerifyRead(() => recording.Write(ValueAddress, -1), expectReads: false); + VerifyRead(() => Assert.Equal(-1, recording.Read(ValueAddress))); + VerifyRead(() => recording.WritePointer(ValueAddress, new(0x9876)), expectReads: false); + VerifyRead(() => Assert.Equal(new TargetPointer(0x9876), recording.ReadPointer(ValueAddress))); + VerifyRead(() => recording.WriteNUInt(ValueAddress, new(0x7654)), expectReads: false); + VerifyRead(() => Assert.Equal(new TargetNUInt(0x7654), recording.ReadNUInt(ValueAddress))); + VerifyRead(() => recording.WriteBuffer(ValueAddress, [0xff]), expectReads: false); + VerifyRead(() => Assert.Equal(byte.MaxValue, recording.Read(ValueAddress))); + + cancelReads = true; + Assert.Throws(() => recording.TryReadPointer(ValueAddress, out _)); + Assert.Throws(() => recording.TryRead(ValueAddress, out _)); + Assert.Throws(() => recording.TryReadCodePointer(ValueAddress, out _)); + readScope.Dispose(); + Assert.True(recording.TryReadPointer(ValueAddress, out _)); + + void AddBytes(ulong address, byte[] bytes) => builder.AddHeapFragment(new() { Address = address, Data = bytes, Name = $"Data_{address:x}" }); + + void VerifyRead(Action action, bool expectReads = true) + { + underlyingReads.Clear(); + recordedReads.Clear(); + action(); + Assert.Equal(underlyingReads, recordedReads); + if (expectReads) + Assert.NotEmpty(recordedReads); + else + Assert.Empty(recordedReads); + } + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void Enumeration_IncludesNativeMetadataAndNewlyPublishedSubDescriptors(MockTarget.Architecture arch) + { + foreach (bool nativeMainDescriptor in new[] { false, true }) + { + TargetTestHelpers helpers = new(arch); + ContractDescriptorBuilder builder = new(helpers); + const ulong SlotAddress = 0x1000; + builder.AddHeapFragment(new() { Address = SlotAddress, Data = new byte[helpers.PointerSize], Name = "SubDescriptorSlot" }); + ContractDescriptorBuilder.DescriptorBuilder main = new(builder); + main.SetSubDescriptors([("GC", 0)]); + main.SetIndirectValues([SlotAddress]); + ulong mainAddress = main.CreateSubDescriptor(0x2000, 0x3000, 0x4000); + ContractDescriptorBuilder.DescriptorBuilder child = new(builder); + child.SetIndirectValues([0x9876]); + child.SetGlobals([("ChildValue", 0x2345, null)]); + ulong childAddress = child.CreateSubDescriptor(0x5000, 0x6000, 0x7000); + MockMemorySpace.MemoryContext memory = builder.GetMemoryContext(); + ContractDescriptorTarget target = nativeMainDescriptor + ? ContractDescriptorTarget.Create(mainAddress, memory.ReadFromTarget, memory.WriteToTarget, + (_, _, _) => HResults.E_NOTIMPL, (_, _) => HResults.E_NOTIMPL, + (ulong _, out ulong address) => { address = 0; return HResults.E_NOTIMPL; }, []) + : ContractDescriptorTarget.Create(ContractDescriptorParser.ParseCompact("""{"version":2,"subDescriptors":{"GC":[0]}}"""u8), + [new(SlotAddress)], memory.ReadFromTarget, memory.WriteToTarget, + (_, _, _) => HResults.E_NOTIMPL, (_, _) => HResults.E_NOTIMPL, + (ulong _, out ulong address) => { address = 0; return HResults.E_NOTIMPL; }, + arch.IsLittleEndian, helpers.PointerSize); + ICLRDataEnumMemoryRegions impl = new SOSDacImpl(target, legacyObj: null, new()); + using RecordingCallback callback = new(supportsUpdates: false); + Assert.False(target.IsSubDescriptorResolved("GC")); + Assert.Equal(HResults.S_OK, impl.EnumMemoryRegions((void*)callback.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); + Assert.Contains((SlotAddress, (uint)helpers.PointerSize), callback.Ranges); + Assert.DoesNotContain(childAddress, callback.Regions); + Assert.Equal(nativeMainDescriptor, callback.Regions.Contains(mainAddress)); + + target.WritePointer(SlotAddress, new(childAddress)); + callback.Regions.Clear(); + callback.Ranges.Clear(); + Assert.Equal(HResults.S_OK, impl.EnumMemoryRegions((void*)callback.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); + Assert.True(target.IsSubDescriptorResolved("GC")); + Assert.Equal(0x2345u, target.ReadGlobal("ChildValue")); + Assert.Contains((SlotAddress, (uint)helpers.PointerSize), callback.Ranges); + AssertFullyReported(callback, childAddress, (uint)ContractDescriptorHelpers.Size(arch.Is64Bit)); + Assert.Contains((0x7000ul, (uint)helpers.PointerSize), callback.Ranges); + Assert.Contains((0x6000ul, target.Read(childAddress + 12)), callback.Ranges); + Assert.Equal(nativeMainDescriptor, callback.Regions.Contains(mainAddress)); + if (nativeMainDescriptor) + { + AssertFullyReported(callback, mainAddress, (uint)ContractDescriptorHelpers.Size(arch.Is64Bit)); + Assert.Contains((0x4000ul, (uint)helpers.PointerSize), callback.Ranges); + Assert.Contains((0x3000ul, target.Read(mainAddress + 12)), callback.Ranges); + } + + callback.Regions.Clear(); + Assert.Equal(HResults.S_OK, impl.EnumMemoryRegions((void*)callback.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); + Assert.Contains(childAddress, callback.Regions); + Assert.Contains(0x6000ul, callback.Regions); + Assert.Contains(0x7000ul, callback.Regions); + } + } + + [Fact] + public void Enumeration_PreservesContractInstancesAndVersionResolution() + { + ContractDescriptorTarget target = ContractDescriptorTarget.Create( + ContractDescriptorParser.ParseCompact("""{"version":2,"contracts":{"GC":"unsupported","Loader":"unknown"}}"""u8), [], + (_, _) => HResults.E_NOTIMPL, (_, _) => HResults.E_NOTIMPL, (_, _, _) => HResults.E_NOTIMPL, (_, _) => HResults.E_NOTIMPL, + (ulong _, out ulong address) => { address = 0; return HResults.E_NOTIMPL; }, isLittleEndian: true, pointerSize: 8); + target.Contracts.RegisterUnsupported("unsupported"); + target.Contracts.Register(string.Empty, _ => new Mock().Object); + IFeatureFlags original = target.Contracts.FeatureFlags; + ICLRDataEnumMemoryRegions impl = new SOSDacImpl(target, legacyObj: null, new()); + using RecordingCallback callback = new(supportsUpdates: false); + Assert.Equal(HResults.S_OK, impl.EnumMemoryRegions((void*)callback.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); + Assert.Same(original, target.Contracts.FeatureFlags); + Mock.Get(original).Verify(contract => contract.Flush(FlushScope.All), Times.Once); + Assert.Throws(() => target.Contracts.GC); + Assert.Throws(() => target.Contracts.Loader); + Assert.Throws(() => target.Contracts.Thread); + target.Contracts.Register("unsupported", _ => new Mock().Object); + IGC gc = target.Contracts.GC; + Assert.Equal(HResults.S_OK, impl.EnumMemoryRegions((void*)callback.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); + Assert.Same(gc, target.Contracts.GC); + Assert.Same(original, target.Contracts.FeatureFlags); + Mock.Get(gc).Verify(contract => contract.Flush(FlushScope.All), Times.Once); + Mock.Get(original).Verify(contract => contract.Flush(FlushScope.All), Times.Exactly(2)); + } + + [Theory] + [InlineData(0x5000ul)] + [InlineData(0x6000ul)] + [InlineData(0x7000ul)] + [InlineData(0x1100ul)] + public void Enumeration_CompletesDescriptorDiscoveryBeforeReportingCancellation(ulong cancelAtAddress) + { + TargetTestHelpers helpers = new(new() { IsLittleEndian = true, Is64Bit = true }); + ContractDescriptorBuilder builder = new(helpers); + const ulong MainSlot = 0x1000; + const ulong ChildSlot = 0x1100; + builder.AddHeapFragment(new() { Address = MainSlot, Data = new byte[helpers.PointerSize], Name = "MainSlot" }); + builder.AddHeapFragment(new() { Address = ChildSlot, Data = new byte[helpers.PointerSize], Name = "ChildSlot" }); + ContractDescriptorBuilder.DescriptorBuilder main = new(builder); + main.SetSubDescriptors([("Child", 0)]); + main.SetIndirectValues([MainSlot]); + ulong mainAddress = main.CreateSubDescriptor(0x2000, 0x3000, 0x4000); + ContractDescriptorBuilder.DescriptorBuilder child = new(builder); + child.SetSubDescriptors([("Grandchild", 0)]); + child.SetIndirectValues([ChildSlot]); + child.SetGlobals([("ChildValue", 0x2345, null)]); + ulong childAddress = child.CreateSubDescriptor(0x5000, 0x6000, 0x7000); + MockMemorySpace.MemoryContext memory = builder.GetMemoryContext(); + ContractDescriptorTarget target = ContractDescriptorTarget.Create(mainAddress, memory.ReadFromTarget, memory.WriteToTarget, + (_, _, _) => HResults.E_NOTIMPL, (_, _) => HResults.E_NOTIMPL, + (ulong _, out ulong address) => { address = 0; return HResults.E_NOTIMPL; }, []); + target.WritePointer(MainSlot, new(childAddress)); + ICLRDataEnumMemoryRegions impl = new SOSDacImpl(target, legacyObj: null, new()); + using RecordingCallback cancelled = new(supportsUpdates: false) { CancelAtAddress = cancelAtAddress }; + Assert.Equal(HResults.COR_E_OPERATIONCANCELED, impl.EnumMemoryRegions((void*)cancelled.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); + Assert.True(target.IsSubDescriptorResolved("Child")); + int count = cancelled.Ranges.Count; + Assert.Equal(new TargetPointer(childAddress), target.ReadPointer(MainSlot)); + Assert.Equal(count, cancelled.Ranges.Count); + + using RecordingCallback callback = new(supportsUpdates: false); + Assert.Equal(HResults.S_OK, impl.EnumMemoryRegions((void*)callback.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); + Assert.True(target.IsSubDescriptorResolved("Child")); + Assert.False(target.IsSubDescriptorResolved("Grandchild")); + Assert.Equal(0x2345u, target.ReadGlobal("ChildValue")); + AssertFullyReported(callback, childAddress, (uint)ContractDescriptorHelpers.Size(is64Bit: true)); + Assert.Contains((0x6000ul, target.Read(childAddress + 12)), callback.Ranges); + Assert.Contains((0x7000ul, (uint)helpers.PointerSize), callback.Ranges); + Assert.Contains((ChildSlot, (uint)helpers.PointerSize), callback.Ranges); + } + + [Theory] + [InlineData(HResults.S_OK)] + [InlineData(HResults.E_FAIL)] + [InlineData(HResults.COR_E_OPERATIONCANCELED)] + public void Enumeration_RecordsContractReadsAndRestoresReader(int result) + { + const ulong PointerAddress = 0x1000; + ContractDescriptorTarget target = ContractDescriptorTarget.Create( + ContractDescriptorParser.ParseCompact("""{"version":2,"contracts":{"RuntimeTypeSystem":"c1"}}"""u8), [], + (_, buffer) => { buffer.Clear(); return HResults.S_OK; }, + (_, _) => HResults.E_NOTIMPL, (_, _, _) => HResults.E_NOTIMPL, (_, _) => HResults.E_NOTIMPL, + (ulong _, out ulong address) => { address = 0; return HResults.E_NOTIMPL; }, + isLittleEndian: true, pointerSize: 8); + Lock apiLock = new(); + target.Contracts.Register("c1", boundTarget => + { + Assert.True(apiLock.IsHeldByCurrentThread); + Assert.Same(target, boundTarget); + TargetPointer? cached = boundTarget.ReadPointer(PointerAddress); + Mock types = new(); + types.Setup(t => t.GetWellKnownMethodTable(It.IsAny())) + .Returns(() => cached ??= boundTarget.ReadPointer(PointerAddress)); + types.Setup(t => t.Flush(FlushScope.All)).Callback(() => cached = null); + return types.Object; + }); + ICLRDataEnumMemoryRegions impl = new SOSDacImpl(target, legacyObj: null, apiLock); + using RecordingCallback callback = new(supportsUpdates: false, apiLock) { Result = result }; + + Assert.Equal(result, impl.EnumMemoryRegions((void*)callback.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); + Assert.Contains((PointerAddress, (uint)target.PointerSize), callback.Ranges); + Assert.True(callback.WasLockHeld); + Assert.False(apiLock.IsHeldByCurrentThread); + int count = callback.Ranges.Count; + Assert.Equal(TargetPointer.Null, target.ReadPointer(PointerAddress)); + Assert.Equal(count, callback.Ranges.Count); + + using RecordingCallback next = new(supportsUpdates: false); + Assert.Equal(HResults.S_OK, impl.EnumMemoryRegions((void*)next.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); + Assert.Contains(PointerAddress, next.Regions); + Assert.Equal(count, callback.Ranges.Count); + } + + [Fact] + public void ReadShimScopes_RestorePreviousReadersWithoutChangingCaches() + { + ContractDescriptorTarget target = ContractDescriptorTarget.Create( + ContractDescriptorParser.ParseCompact("""{"version":2}"""u8), [], + (_, buffer) => { buffer.Clear(); return HResults.S_OK; }, + (_, _) => HResults.E_NOTIMPL, (_, _, _) => HResults.E_NOTIMPL, (_, _) => HResults.E_NOTIMPL, + (ulong _, out ulong address) => { address = 0; return HResults.E_NOTIMPL; }, + isLittleEndian: true, pointerSize: 8); + int firstReads = 0; + int secondReads = 0; + Assert.Empty(target.EnumerateDescriptorMemory()); + CachedPointer originalData = target.ProcessedData.GetOrAdd(new(0x1000)); + IDisposable first = target.RegisterReadCallback((_, _) => firstReads++); + using (first) + { + Assert.Equal(TargetPointer.Null, target.ReadPointer(0x1000)); + using (target.RegisterReadCallback((_, _) => secondReads++)) + Assert.Equal(TargetPointer.Null, target.ReadPointer(0x1000)); + + Assert.Equal(TargetPointer.Null, target.ReadPointer(0x1000)); + } + using (target.RegisterReadCallback((_, _) => secondReads++)) + { + first.Dispose(); + Assert.Equal(TargetPointer.Null, target.ReadPointer(0x1000)); + } + Assert.Same(originalData, target.ProcessedData.GetOrAdd(new(0x1000))); + Assert.Equal(TargetPointer.Null, target.ReadPointer(0x1000)); + Assert.Equal(3, firstReads); + Assert.Equal(2, secondReads); + } + + [Fact] + public void Enumeration_RejectsOtherTargetImplementationsAtEnumeratorBoundary() + { + Mock target = new(); + target.SetupGet(t => t.PointerSize).Returns(8); + ICLRDataEnumMemoryRegions impl = new SOSDacImpl(target.Object, legacyObj: null, new()); + using RecordingCallback callback = new(supportsUpdates: false); + + Assert.Equal(HResults.E_NOTIMPL, MemoryRegionEnumerator.Enumerate(target.Object, (void*)callback.Address, 0)); + Assert.Equal(HResults.E_NOTIMPL, impl.EnumMemoryRegions((void*)callback.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); + Assert.Empty(callback.Ranges); + target.Verify(t => t.Flush(It.IsAny()), Times.Never); + } + + [Fact] + public void ReadShim_PreservesDataAccessAndRuntimeImageBase() + { + const ulong RuntimeImageBase = 0x9000; + ContractDescriptorTarget target = ContractDescriptorTarget.Create( + ContractDescriptorParser.ParseCompact("""{"version":2}"""u8), [], + (address, buffer) => + { + Assert.Equal(0x1000ul, address); + buffer.Fill(0x11); + return HResults.S_OK; + }, + (address, buffer) => + { + Assert.Equal(0x2000ul, address); + Assert.Equal(new byte[] { 0x5a }, buffer.ToArray()); + return HResults.S_OK; + }, + (threadId, flags, buffer) => + { + Assert.Equal(5u, threadId); + Assert.Equal(0x10u, flags); + buffer.Fill(0x44); + return HResults.S_OK; + }, + (threadId, context) => + { + Assert.Equal(6u, threadId); + Assert.Equal(new byte[] { 0x99 }, context.ToArray()); + return HResults.S_OK; + }, + (ulong size, out ulong address) => + { + Assert.Equal(16ul, size); + address = 0x8000; + return HResults.S_OK; + }, + isLittleEndian: true, pointerSize: 8, runtimeImageBase: RuntimeImageBase); + List<(ulong Address, ulong Size)> reads = []; + using IDisposable readScope = target.RegisterReadCallback((address, size) => reads.Add((address, size))); + Assert.Equal((byte)0x11, target.Read(0x1000)); + target.WriteBuffer(0x2000, [0x5a]); + byte[] context = new byte[16]; + Assert.True(target.TryGetThreadContext(5, 0x10, context)); + Assert.All(context, value => Assert.Equal((byte)0x44, value)); + Assert.True(target.TrySetThreadContext(6, [0x99])); + Assert.Equal(new TargetPointer(0x8000), target.AllocateMemory(16)); + Assert.True(target.TryGetRuntimeImageBase(out TargetPointer imageBase)); + Assert.Equal(new TargetPointer(RuntimeImageBase), imageBase); + readScope.Dispose(); + Assert.True(target.TryGetRuntimeImageBase(out imageBase)); + Assert.Equal(new TargetPointer(RuntimeImageBase), imageBase); + Assert.Equal((byte)0x11, target.Read(0x1000)); + Assert.Equal(new (ulong, ulong)[] { (0x1000, 1) }, reads); + } + + private static void AssertFullyReported(RecordingCallback callback, ulong address, uint size) + { + for (uint offset = 0; offset < size; offset++) + { + ulong current = address + offset; + Assert.Contains(callback.Ranges, range => current >= range.Address && current - range.Address < range.Size); + } + } + + [Theory] + [InlineData(0, 0)] + [InlineData(0x200, 3)] + [InlineData(0x100000, 2)] + [InlineData(0x100200, 3)] + [InlineData(0x108000, 0)] + [InlineData(0x108200, 3)] + public void DumpMode_UsesNativeMiniDumpFlagPrecedence(uint miniDumpFlags, int expected) + { + Assert.Equal(expected, (int)MemoryRegionEnumerator.GetDumpFlags(miniDumpFlags)); + } + + [Theory] + [InlineData("", "")] + [InlineData(" at Program.Main()", " at Program.Main()")] + [InlineData(" at Program.Main() in C:\\private\\Program.cs:line 42", " at Program.Main()")] + [InlineData(" at A.M() in /private/a.cs:line 1\n at B.M() in /private/b.cs:line 2\n", " at A.M()\n at B.M()")] + [InlineData(" at A.M() dans /private/a.cs:ligne 1\r\n at B.M()\r\n", " at A.M()\r\n at B.M()")] + [InlineData(" at A.M(F(Int32)) in secret.cs:line 1", " at A.M(F(Int32))")] + [InlineData(" at A.M(\uD83D\uDE00) in secret.cs:line 1", " at A.M(\uD83D\uDE00)")] + [InlineData(" at A.M()\n--- End of stack trace ---\n", " at A.M()")] + [InlineData("unstructured text", "")] + public void TriageStackTrace_RemovesFileInfoAndZerosRemainingCharacters(string original, string expected) + { + char[] buffer = original.ToCharArray(); + ObjectCollector.StripFileInfoFromStackTrace(buffer); + Assert.Equal(expected.PadRight(original.Length, '\0'), new string(buffer)); + } + + [Theory] + [InlineData(false, false, false, true, true)] + [InlineData(false, true, true, true, true)] + [InlineData(true, false, false, true, true)] + [InlineData(true, true, false, true, true)] + [InlineData(true, true, true, true, true)] + [InlineData(true, true, false, false, true)] + [InlineData(true, false, false, true, false)] + [InlineData(true, true, true, true, false)] + public void ExceptionCollection_UsesTriagePolicy(bool triage, bool derived, bool overridesStackTrace, bool littleEndian, bool supportsUpdates) + { + TargetPointer exception = new(0x1000); + TargetPointer innerException = new(0x1100); + TargetPointer message = new(0x2000); + TargetPointer stackTrace = new(0x3000); + TargetPointer remoteStackTrace = new(0x4000); + TargetPointer exceptionTable = new(0x100); + TargetPointer derivedTable = new(0x200); + TargetPointer stringTable = new(0x300); + TargetPointer objectTable = new(0x400); + TargetPointer getter = new(0x500); + const uint StringOffset = 12; + const string Original = " at Program.Main() in /private/Program.cs:line 42"; + const string Sanitized = " at Program.Main()"; + Encoding encoding = littleEndian ? Encoding.Unicode : Encoding.BigEndianUnicode; + byte[] stringBytes = encoding.GetBytes(Original); + + Mock objects = new(); + objects.Setup(o => o.GetSize(It.IsAny())).Returns(128); + objects.Setup(o => o.GetMethodTableAddress(It.IsAny())).Returns(stringTable); + objects.Setup(o => o.GetMethodTableAddress(exception)).Returns(derived ? derivedTable : exceptionTable); + objects.Setup(o => o.GetMethodTableAddress(innerException)).Returns(exceptionTable); + objects.Setup(o => o.GetStringValue(It.IsAny())).Returns(Original); + uint stringLength = (uint)Original.Length; + uint stringOffset = StringOffset; + objects.Setup(o => o.GetStringData(It.IsAny(), out stringLength, out stringOffset)); + + Mock types = new(); + types.Setup(t => t.GetTypeHandle(It.IsAny())).Returns((TargetPointer p) => new TestTypeHandle(p)); + types.Setup(t => t.GetWellKnownMethodTable(WellKnownMethodTable.Exception)).Returns(exceptionTable); + types.Setup(t => t.GetWellKnownMethodTable(WellKnownMethodTable.String)).Returns(stringTable); + types.Setup(t => t.GetWellKnownMethodTable(WellKnownMethodTable.Object)).Returns(objectTable); + types.Setup(t => t.GetParentMethodTable(It.Is(t => t.Address == derivedTable))).Returns(exceptionTable); + types.Setup(t => t.GetNumVtableSlots(It.Is(t => t.Address == exceptionTable))).Returns(2); + types.Setup(t => t.GetNumVtableSlots(It.Is(t => t.Address == objectTable))).Returns(1); + types.Setup(t => t.GetMethodDescForSlot(It.Is(t => t.Address == exceptionTable), 1)).Returns(getter); + types.Setup(t => t.GetMethodDescForSlot(It.Is(t => t.Address == derivedTable), 1)) + .Returns(overridesStackTrace ? new TargetPointer(0x600) : getter); + types.Setup(t => t.GetMethodDescHandle(getter)).Returns(new MethodDescHandle(getter)); + types.Setup(t => t.GetMethodToken(It.IsAny())).Returns(0x06000001); + + MetadataBuilder metadata = new(); + metadata.AddModule(0, metadata.GetOrAddString("TestModule"), default, default, default); + metadata.AddMethodDefinition(MethodAttributes.Public | MethodAttributes.Virtual, MethodImplAttributes.IL, + metadata.GetOrAddString("get_StackTrace"), default, 0, default); + BlobBuilder blob = new(); + new MetadataRootBuilder(metadata).Serialize(blob, 0, 0); + using MetadataReaderProvider provider = MetadataReaderProvider.FromMetadataImage(ImmutableArray.Create(blob.ToArray())); + Mock ecmaMetadata = new(); + ecmaMetadata.Setup(m => m.GetMetadata(It.IsAny())).Returns(provider.GetMetadataReader()); + + Mock exceptions = new(); + exceptions.Setup(e => e.GetExceptionData(exception)) + .Returns(new ExceptionData(message, innerException, TargetPointer.Null, TargetPointer.Null, stackTrace, remoteStackTrace, 0, 0)); + exceptions.Setup(e => e.GetExceptionData(innerException)) + .Returns(new ExceptionData(message, TargetPointer.Null, TargetPointer.Null, TargetPointer.Null, TargetPointer.Null, TargetPointer.Null, 0, 0)); + exceptions.Setup(e => e.GetExceptionStackFrames(It.IsAny())).Returns([]); + TestPlaceholderTarget target = new TestPlaceholderTarget.Builder(new() { IsLittleEndian = littleEndian, Is64Bit = true }) + .AddMockContract(objects.Object) + .AddMockContract(types.Object) + .AddMockContract(exceptions.Object) + .AddMockContract(ecmaMetadata.Object) + .AddMockContract(new Mock().Object) + .AddMockContract(new Mock().Object) + .UseReader((ulong address, Span buffer) => + { + Assert.True(address == stackTrace.Value + StringOffset || address == remoteStackTrace.Value + StringOffset); + stringBytes.CopyTo(buffer); + return 0; + }) + .Build(); + + using RecordingCallback callback = new(supportsUpdates); + MemoryRegionEmitter emitter = new(callback.Address, 8); + new ObjectCollector(target, emitter, new MethodCollector(target), triage).EnumerateObject(exception); + + Assert.Contains(exception.Value, callback.Regions); + Assert.Contains(innerException.Value, callback.Regions); + Assert.Equal(!triage, callback.Regions.Contains(message.Value)); + Assert.Contains(stackTrace.Value, callback.Regions); + Assert.Equal(!triage || !overridesStackTrace, callback.Regions.Contains(remoteStackTrace.Value)); + if (triage && supportsUpdates) + { + byte[] expected = encoding.GetBytes(Sanitized.PadRight(Original.Length, '\0')); + Assert.Equal(expected, callback.Updates[stackTrace.Value + StringOffset]); + if (!overridesStackTrace) + Assert.Equal(expected, callback.Updates[remoteStackTrace.Value + StringOffset]); + Assert.Equal(overridesStackTrace ? 1 : 2, callback.Updates.Count); + } + else + { + Assert.Empty(callback.Updates); + } + Assert.Equal(0, emitter.Result); + } + + private sealed record TestTypeHandle(TargetPointer Address) : ITypeHandle; + + private sealed record CachedPointer(TargetPointer Value) : Data.IData + { + public static CachedPointer Create(Target target, TargetPointer address) => new(target.ReadPointer(address)); + } + + [GeneratedComClass] + private partial class RuntimeDataTarget(byte[] image) : ICLRDataTarget + { + public ContractDescriptorTarget.ReadFromTargetDelegate? ReadMemory { get; init; } + public ContractDescriptorTarget.GetTargetThreadContextDelegate? ReadThreadContext { get; init; } + public ulong ImageBase { get; set; } = 0x10000; + public int LookupResult { get; set; } + public bool ShortRead { get; init; } + public int ImageBaseLookups { get; private set; } + public int RuntimeBaseLookups { get; protected set; } + public int ReadCount { get; private set; } + + public int GetImageBase(string imagePath, ulong* baseAddress) + { + Assert.Equal("coreclr.dll", imagePath); + ImageBaseLookups++; + *baseAddress = ImageBase; + return LookupResult; + } + + public int ReadVirtual(ulong address, byte* buffer, uint bytesRequested, uint* bytesRead) + { + ReadCount++; + *bytesRead = 0; + if (ReadMemory is not null) + { + int hr = ReadMemory(address, new Span(buffer, checked((int)bytesRequested))); + if (hr >= 0) + *bytesRead = ShortRead && bytesRequested != 0 ? bytesRequested - 1 : bytesRequested; + return hr; + } + + if (address < ImageBase || address - ImageBase > (ulong)image.Length + || bytesRequested > (ulong)image.Length - (address - ImageBase)) + { + return HResults.E_FAIL; + } + + image.AsSpan((int)(address - ImageBase), (int)bytesRequested).CopyTo(new Span(buffer, (int)bytesRequested)); + *bytesRead = ShortRead && bytesRequested != 0 ? bytesRequested - 1 : bytesRequested; + return HResults.S_OK; + } + + public int GetMachineType(uint* machineType) => throw new NotImplementedException(); + public int GetPointerSize(uint* pointerSize) => throw new NotImplementedException(); + public int WriteVirtual(ulong address, byte* buffer, uint bytesRequested, uint* bytesWritten) => throw new NotImplementedException(); + public int GetTLSValue(uint threadID, uint index, ulong* value) => throw new NotImplementedException(); + public int SetTLSValue(uint threadID, uint index, ulong value) => throw new NotImplementedException(); + public int GetCurrentThreadID(uint* threadID) => throw new NotImplementedException(); + public int GetThreadContext(uint threadID, uint contextFlags, uint contextSize, byte* context) + => ReadThreadContext is not null + ? ReadThreadContext(threadID, contextFlags, new Span(context, checked((int)contextSize))) + : throw new NotImplementedException(); + public int SetThreadContext(uint threadID, uint contextSize, byte* context) => throw new NotImplementedException(); + public int Request(uint reqCode, uint inBufferSize, byte* inBuffer, uint outBufferSize, byte* outBuffer) => throw new NotImplementedException(); + } + + private sealed class RuntimeLocatorDataTarget(byte[] image) : RuntimeDataTarget(image), ICLRRuntimeLocator + { + public int RuntimeLookupResult { get; init; } + + public int GetRuntimeBase(ulong* baseAddress) + { + RuntimeBaseLookups++; + *baseAddress = ImageBase; + return RuntimeLookupResult; + } + } + + [GeneratedComClass] + private partial class ContractLocatorDataTarget() : RuntimeDataTarget([]), ICLRContractLocator + { + public ulong ContractAddress { get; init; } + public int ContractResult { get; init; } + public int ContractLookups { get; private set; } + + public int GetContractDescriptor(ulong* contractAddress) + { + ContractLookups++; + *contractAddress = ContractAddress; + return ContractResult; + } + } + + [GeneratedComClass] + private sealed partial class RuntimeAndContractLocatorDataTarget : ContractLocatorDataTarget, ICLRRuntimeLocator + { + public int RuntimeLookupResult { get; init; } + + public int GetRuntimeBase(ulong* baseAddress) + { + RuntimeBaseLookups++; + *baseAddress = ImageBase; + return RuntimeLookupResult; + } + } + + private sealed class RecordingCallback : IDisposable + { + private readonly nint* _instance; + private readonly nint* _vtable; + private readonly GCHandle _handle; + private readonly bool _supportsUpdates; + private readonly Lock? _apiLock; + public bool WasLockHeld { get; private set; } = true; + public int Result { get; set; } + public ulong? CancelAtAddress { get; set; } + public HashSet Regions { get; } = []; + public List<(ulong Address, uint Size)> Ranges { get; } = []; + public Dictionary Updates { get; } = []; + public nint Address => (nint)_instance; + + public RecordingCallback(bool supportsUpdates, Lock? apiLock = null) + { + _supportsUpdates = supportsUpdates; + _apiLock = apiLock; + _handle = GCHandle.Alloc(this); + _vtable = (nint*)NativeMemory.AllocZeroed(5, (nuint)sizeof(nint)); + _vtable[0] = (nint)(delegate* unmanaged[MemberFunction])&QueryInterface; + _vtable[2] = (nint)(delegate* unmanaged[MemberFunction])&Release; + _vtable[3] = (nint)(delegate* unmanaged[MemberFunction])&Enumerate; + _vtable[4] = (nint)(delegate* unmanaged[MemberFunction])&Update; + _instance = (nint*)NativeMemory.Alloc(2, (nuint)sizeof(nint)); + _instance[0] = (nint)_vtable; + _instance[1] = GCHandle.ToIntPtr(_handle); + } + + private static RecordingCallback Get(nint self) => (RecordingCallback)GCHandle.FromIntPtr(((nint*)self)[1]).Target!; + + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] + private static int QueryInterface(nint self, Guid* iid, nint* result) + { + *result = Get(self)._supportsUpdates && *iid == new Guid("3721A26F-8B91-4D98-A388-DB17B356FADB") ? self : 0; + return *result != 0 ? 0 : unchecked((int)0x80004002); + } + + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] + private static uint Release(nint self) => 1; + + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] + private static int Enumerate(nint self, ulong address, uint size) + { + RecordingCallback callback = Get(self); + callback.Regions.Add(address); + callback.Ranges.Add((address, size)); + callback.WasLockHeld &= callback._apiLock?.IsHeldByCurrentThread ?? true; + return callback.CancelAtAddress == address ? HResults.COR_E_OPERATIONCANCELED : callback.Result; + } + + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] + private static int Update(nint self, ulong address, uint size, byte* bytes) + { + Get(self).Updates[address] = new ReadOnlySpan(bytes, checked((int)size)).ToArray(); + return 0; + } + + public void Dispose() + { + NativeMemory.Free(_instance); + NativeMemory.Free(_vtable); + _handle.Free(); + } + } +} diff --git a/src/native/managed/cdac/tests/UnitTests/FunctionTableAccessTests.cs b/src/native/managed/cdac/tests/UnitTests/FunctionTableAccessTests.cs index ceddf40e0ab422..598984e0a19c83 100644 --- a/src/native/managed/cdac/tests/UnitTests/FunctionTableAccessTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/FunctionTableAccessTests.cs @@ -145,28 +145,6 @@ public void QueryInterfaceFromIXCLRDataProcess_ReturnsProcess3(MockTarget.Archit } } - [Theory] - [ClassData(typeof(MockTarget.StdArch))] - public void QueryInterfaceFromIXCLRDataProcess_ForMemoryRegionEnumeration_ReturnsNoInterface(MockTarget.Architecture arch) - { - TestPlaceholderTarget target = new TestPlaceholderTarget.Builder(arch).Build(); - SOSDacImpl impl = new(target, legacyObj: null, new()); - void* process = ComInterfaceMarshaller.ConvertToUnmanaged(impl); - - try - { - Guid iid = typeof(ICLRDataEnumMemoryRegions).GUID; - int hr = Marshal.QueryInterface((nint)process, in iid, out nint memoryRegions); - - Assert.Equal(HResults.COR_E_INVALIDCAST, hr); - Assert.Equal(nint.Zero, memoryRegions); - } - finally - { - ComInterfaceMarshaller.Free(process); - } - } - [Theory] [ClassData(typeof(MockTarget.StdArch))] public void GetFunctionTable_NullOutParameters_ReturnsEPointer(MockTarget.Architecture arch) diff --git a/src/native/managed/cdac/tests/UnitTests/Microsoft.Diagnostics.DataContractReader.Tests.csproj b/src/native/managed/cdac/tests/UnitTests/Microsoft.Diagnostics.DataContractReader.Tests.csproj index 41f1bdc70000d6..cf2ad984856c6e 100644 --- a/src/native/managed/cdac/tests/UnitTests/Microsoft.Diagnostics.DataContractReader.Tests.csproj +++ b/src/native/managed/cdac/tests/UnitTests/Microsoft.Diagnostics.DataContractReader.Tests.csproj @@ -9,6 +9,7 @@ + From 7849b6e2b9623d46b0bb6634e1fa836ec986c8d5 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Tue, 29 Sep 2026 12:54:25 -0400 Subject: [PATCH 2/9] Wire cDAC memory enumeration into native dump collection Expose runtime contract discovery through createdump data targets, select the enumerator in native build metadata and Windows resources, and update PAL linkage and diagnostics pipeline coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: de3eab4e-b090-430a-8e30-6b204fb1519a --- eng/pipelines/diagnostics/sos-test-leg.yml | 16 +- eng/pipelines/runtime-diagnostics.yml | 143 +++++++++--------- src/coreclr/debug/createdump/crashinfo.cpp | 3 +- src/coreclr/debug/createdump/crashinfo.h | 2 + src/coreclr/debug/createdump/crashinfomac.cpp | 18 ++- .../debug/createdump/crashinfounix.cpp | 18 ++- src/coreclr/debug/createdump/createdump.h | 1 + src/coreclr/debug/createdump/datatarget.cpp | 20 +++ src/coreclr/debug/createdump/datatarget.h | 7 +- src/coreclr/debug/runtimeinfo/CMakeLists.txt | 29 +++- src/coreclr/dlls/CMakeLists.txt | 6 +- src/coreclr/dlls/mscordbi/CMakeLists.txt | 29 ++-- .../dlls/mscoree/coreclr/CMakeLists.txt | 21 ++- .../mscoree/coreclr/GenClrDebugResource.ps1 | 23 ++- .../coreclr/dump_helper_resource.rc.in | 2 +- src/coreclr/inc/clrdata.idl | 14 ++ src/coreclr/pal/prebuilt/idl/clrdata_i.cpp | 5 +- src/coreclr/pal/prebuilt/inc/clrdata.h | 103 ++++++++++++- src/coreclr/runtime.proj | 10 ++ 19 files changed, 360 insertions(+), 110 deletions(-) diff --git a/eng/pipelines/diagnostics/sos-test-leg.yml b/eng/pipelines/diagnostics/sos-test-leg.yml index 92d7aa9b9febd1..3c71eaef25b8ad 100644 --- a/eng/pipelines/diagnostics/sos-test-leg.yml +++ b/eng/pipelines/diagnostics/sos-test-leg.yml @@ -25,6 +25,8 @@ parameters: templatePath: 'templates' templateContext: '' disableComponentGovernance: '' + artifactSuffix: '' + useCdacDumpCollectProvider: false jobs: - template: /eng/common/${{ parameters.templatePath }}/job/job.yml @@ -94,15 +96,15 @@ jobs: - template: /eng/pipelines/common/download-artifact-step.yml parameters: - artifactName: SosRuntimeArtifacts_$(osGroup)$(osSubgroup)_$(archType) - artifactFileName: SosRuntimeArtifacts_$(osGroup)$(osSubgroup)_$(archType)$(archiveExtension) + artifactName: SosRuntimeArtifacts_$(osGroup)$(osSubgroup)_$(archType)${{ parameters.artifactSuffix }} + artifactFileName: SosRuntimeArtifacts_$(osGroup)$(osSubgroup)_$(archType)${{ parameters.artifactSuffix }}$(archiveExtension) unpackFolder: $(Build.SourcesDirectory)/artifacts/runtime displayName: 'SOS Runtime Artifacts' - template: /eng/pipelines/common/download-artifact-step.yml parameters: - artifactName: SosCdacArtifacts_$(osGroup)$(osSubgroup)_$(archType) - artifactFileName: SosCdacArtifacts_$(osGroup)$(osSubgroup)_$(archType)$(archiveExtension) + artifactName: SosCdacArtifacts_$(osGroup)$(osSubgroup)_$(archType)${{ parameters.artifactSuffix }} + artifactFileName: SosCdacArtifacts_$(osGroup)$(osSubgroup)_$(archType)${{ parameters.artifactSuffix }}$(archiveExtension) unpackFolder: $(Build.SourcesDirectory)/artifacts/private-cdac displayName: 'SOS cDAC Artifacts' @@ -203,6 +205,12 @@ jobs: HelixConfiguration: ${{ parameters.targetRid }}_${{ parameters.buildConfig }} HelixTargetQueues: $(SOSHelixQueue) SYSTEM_ACCESSTOKEN: $(System.AccessToken) + # The enumerator replaces the legacy DAC; select cDAC on the Helix worker. + ${{ if eq(parameters.useCdacDumpCollectProvider, true) }}: + ${{ if eq(parameters.osGroup, 'windows') }}: + HelixPreCommands: set SOSHARNESS_ONLY_DAC=CDac + ${{ else }}: + HelixPreCommands: export SOSHARNESS_ONLY_DAC=CDac ${{ if eq(variables['System.TeamProject'], 'public') }}: Creator: dotnet-bot ${{ else }}: diff --git a/eng/pipelines/runtime-diagnostics.yml b/eng/pipelines/runtime-diagnostics.yml index 3648b7f6e4440a..75fd5db4cc0f18 100644 --- a/eng/pipelines/runtime-diagnostics.yml +++ b/eng/pipelines/runtime-diagnostics.yml @@ -6,9 +6,10 @@ # UnitTests + DataGeneratorTests projects (tools.cdactests # subset). Independent of every other stage. # -# SOSTests Per-platform Release runtime + cDAC build and SOS Helix -# submission. SOS.Tests covers both legacy DAC and cDAC -# modes. Independent of every other stage. +# SOSTests Per-platform Release builds with the legacy DAC and the +# EnumMemory provider, each with a matching SOS Helix submission. +# Legacy builds cover both DAC modes; EnumMemory uses cDAC only. +# Independent of every other stage. # # CdacTests Per-platform Checked runtime build + Dump / Stress / # XPlatDumpGen / XPlatDumpTest legs in one stage. Each dump @@ -146,81 +147,85 @@ extends: testResultsFormat: 'xunit' # ---------------------------------------------------------------------- - # SOS tests. Each platform builds a Release runtime and matching universal - # cDAC/DBI, then submits the diagnostics SOS.Tests payload to Helix. + # SOS tests. Each platform builds legacy and EnumMemory Release runtimes with + # matching universal cDAC/DBI, then submits each SOS.Tests payload to Helix. # ---------------------------------------------------------------------- - stage: SOSTests dependsOn: [] jobs: - - template: /eng/pipelines/common/platform-matrix.yml - parameters: - jobTemplate: /eng/pipelines/common/global-build-job.yml - buildConfig: release - platforms: ${{ parameters.sosPlatforms }} - jobParameters: - nameSuffix: SOSBuild - buildArgs: -s clr+libs+tools.cdac+host -c $(_BuildConfig) -rc $(_BuildConfig) -lc $(_BuildConfig) - isOfficialBuild: ${{ variables.isOfficialBuild }} - timeoutInMinutes: 180 - postBuildSteps: - - pwsh: | - $runtimeDirs = @(Get-ChildItem -Directory -Path "$(Build.SourcesDirectory)/artifacts/bin/testhost/net*-$(osGroup)-$(_BuildConfig)-$(archType)/shared/Microsoft.NETCore.App/*") - if ($runtimeDirs.Count -ne 1) { - throw "Expected one SOS runtime directory, but found $($runtimeDirs.Count)." - } - - Write-Host "##vso[task.setvariable variable=sosRuntimeDir]$($runtimeDirs[0].FullName)" + # An empty suffix preserves the standard build and artifact names. + - ${{ each suffix in split(',_EnumMemory', ',') }}: + - template: /eng/pipelines/common/platform-matrix.yml + parameters: + jobTemplate: /eng/pipelines/common/global-build-job.yml + buildConfig: release + platforms: ${{ parameters.sosPlatforms }} + jobParameters: + nameSuffix: SOSBuild${{ suffix }} + buildArgs: -s clr+libs+tools.cdac+host -c $(_BuildConfig) -rc $(_BuildConfig) -lc $(_BuildConfig) /p:UseCdacDumpCollectProvider=${{ eq(suffix, '_EnumMemory') }} + isOfficialBuild: ${{ variables.isOfficialBuild }} + timeoutInMinutes: 180 + postBuildSteps: + - pwsh: | + $runtimeDirs = @(Get-ChildItem -Directory -Path "$(Build.SourcesDirectory)/artifacts/bin/testhost/net*-$(osGroup)-$(_BuildConfig)-$(archType)/shared/Microsoft.NETCore.App/*") + if ($runtimeDirs.Count -ne 1) { + throw "Expected one SOS runtime directory, but found $($runtimeDirs.Count)." + } - $sourceDir = "$(Build.SourcesDirectory)/artifacts/bin/coreclr/$(osGroup).$(archType).$(_BuildConfig)" - $stagingDir = "$(Build.SourcesDirectory)/artifacts/sos-cdac" - New-Item -ItemType Directory -Force -Path $stagingDir | Out-Null + Write-Host "##vso[task.setvariable variable=sosRuntimeDir]$($runtimeDirs[0].FullName)" - $fileNames = switch ("$(osGroup)") { - "windows" { "mscordaccore_universal.dll", "mscordbi_universal.dll" } - "linux" { "libmscordaccore_universal.so", "libmscordbi_universal.so" } - "osx" { "libmscordaccore_universal.dylib", "libmscordbi_universal.dylib" } - default { throw "Unsupported OS group '$(osGroup)'." } - } + $sourceDir = "$(Build.SourcesDirectory)/artifacts/bin/coreclr/$(osGroup).$(archType).$(_BuildConfig)" + $stagingDir = "$(Build.SourcesDirectory)/artifacts/sos-cdac" + New-Item -ItemType Directory -Force -Path $stagingDir | Out-Null - foreach ($fileName in $fileNames) { - $source = Join-Path $sourceDir $fileName - if (-not (Test-Path -PathType Leaf $source)) { - throw "Required private cDAC file '$source' was not found." + $fileNames = switch ("$(osGroup)") { + "windows" { "mscordaccore_universal.dll", "mscordbi_universal.dll" } + "linux" { "libmscordaccore_universal.so", "libmscordbi_universal.so" } + "osx" { "libmscordaccore_universal.dylib", "libmscordbi_universal.dylib" } + default { throw "Unsupported OS group '$(osGroup)'." } } - Copy-Item -Path $source -Destination $stagingDir - } - displayName: 'Find SOS Runtime and cDAC' - - template: /eng/pipelines/common/upload-artifact-step.yml - parameters: - rootFolder: $(sosRuntimeDir) - includeRootFolder: false - archiveType: $(archiveType) - archiveExtension: $(archiveExtension) - tarCompression: $(tarCompression) - artifactName: SosRuntimeArtifacts_$(osGroup)$(osSubgroup)_$(archType) - displayName: SOS Runtime Artifacts - - template: /eng/pipelines/common/upload-artifact-step.yml - parameters: - rootFolder: $(Build.SourcesDirectory)/artifacts/sos-cdac - includeRootFolder: false - archiveType: $(archiveType) - archiveExtension: $(archiveExtension) - tarCompression: $(tarCompression) - artifactName: SosCdacArtifacts_$(osGroup)$(osSubgroup)_$(archType) - displayName: SOS cDAC Artifacts + foreach ($fileName in $fileNames) { + $source = Join-Path $sourceDir $fileName + if (-not (Test-Path -PathType Leaf $source)) { + throw "Required private cDAC file '$source' was not found." + } - - template: /eng/pipelines/common/platform-matrix.yml - parameters: - jobTemplate: /eng/pipelines/diagnostics/sos-test-leg.yml - buildConfig: release - platforms: ${{ parameters.sosPlatforms }} - jobParameters: - nameSuffix: SOS - isOfficialBuild: ${{ variables.isOfficialBuild }} - timeoutInMinutes: 360 - dependsOnGlobalBuilds: - - nameSuffix: SOSBuild + Copy-Item -Path $source -Destination $stagingDir + } + displayName: 'Find SOS Runtime and cDAC' + - template: /eng/pipelines/common/upload-artifact-step.yml + parameters: + rootFolder: $(sosRuntimeDir) + includeRootFolder: false + archiveType: $(archiveType) + archiveExtension: $(archiveExtension) + tarCompression: $(tarCompression) + artifactName: SosRuntimeArtifacts_$(osGroup)$(osSubgroup)_$(archType)${{ suffix }} + displayName: SOS Runtime Artifacts + - template: /eng/pipelines/common/upload-artifact-step.yml + parameters: + rootFolder: $(Build.SourcesDirectory)/artifacts/sos-cdac + includeRootFolder: false + archiveType: $(archiveType) + archiveExtension: $(archiveExtension) + tarCompression: $(tarCompression) + artifactName: SosCdacArtifacts_$(osGroup)$(osSubgroup)_$(archType)${{ suffix }} + displayName: SOS cDAC Artifacts + + - template: /eng/pipelines/common/platform-matrix.yml + parameters: + jobTemplate: /eng/pipelines/diagnostics/sos-test-leg.yml + buildConfig: release + platforms: ${{ parameters.sosPlatforms }} + jobParameters: + nameSuffix: SOS${{ suffix }} + artifactSuffix: ${{ suffix }} + useCdacDumpCollectProvider: ${{ eq(suffix, '_EnumMemory') }} + isOfficialBuild: ${{ variables.isOfficialBuild }} + timeoutInMinutes: 360 + dependsOnGlobalBuilds: + - nameSuffix: SOSBuild${{ suffix }} # ---------------------------------------------------------------------- # cDAC dump + stress + xplat-dump-gen tests. @@ -248,7 +253,7 @@ extends: platforms: ${{ parameters.cdacDumpPlatforms }} jobParameters: nameSuffix: Build - buildArgs: -s clr+libs+tools.cdac+tools.cdacdumptests+tools.cdacstresstests -c $(_BuildConfig) -rc checked -lc $(_BuildConfig) /p:SkipDumpVersions=net10.0 + buildArgs: -s clr+libs+tools.cdac+tools.cdacdumptests+tools.cdacstresstests -c $(_BuildConfig) -rc checked -lc $(_BuildConfig) /p:SkipDumpVersions=net10.0 /p:UseCdacDumpCollectProvider=true isOfficialBuild: ${{ variables.isOfficialBuild }} timeoutInMinutes: 180 postBuildSteps: diff --git a/src/coreclr/debug/createdump/crashinfo.cpp b/src/coreclr/debug/createdump/crashinfo.cpp index 4a40c6d38630d0..2cd0ad07c820cb 100644 --- a/src/coreclr/debug/createdump/crashinfo.cpp +++ b/src/coreclr/debug/createdump/crashinfo.cpp @@ -23,6 +23,8 @@ CrashInfo::CrashInfo(const CreateDumpOptions& options) : m_crashThread(options.CrashThread), m_signal(options.Signal), m_exceptionRecord(options.ExceptionRecord), + m_runtimeBaseAddress(0), + m_contractDescriptorAddress(0), m_moduleInfos(&ModuleInfoCompare), m_mainModule(nullptr), m_cbModuleMappings(0), @@ -30,7 +32,6 @@ CrashInfo::CrashInfo(const CreateDumpOptions& options) : m_enumMemoryPagesAdded(0) { g_crashInfo = this; - m_runtimeBaseAddress = 0; #ifdef __APPLE__ m_task = 0; #else diff --git a/src/coreclr/debug/createdump/crashinfo.h b/src/coreclr/debug/createdump/crashinfo.h index 7a51cceb81d449..0566a519ef4dcc 100644 --- a/src/coreclr/debug/createdump/crashinfo.h +++ b/src/coreclr/debug/createdump/crashinfo.h @@ -62,6 +62,7 @@ class CrashInfo : public ICLRDataEnumMemoryRegionsCallback, public ICLRDataLoggi siginfo_t m_siginfo; // signal info (if any) std::string m_coreclrPath; // the path of the coreclr module or empty if none uint64_t m_runtimeBaseAddress; // base address of the runtime module + uint64_t m_contractDescriptorAddress; // address of the runtime contract descriptor #ifdef __APPLE__ vm_map_t m_task; // the mach task for the process std::set m_allMemoryRegions; // all memory regions on MacOS @@ -123,6 +124,7 @@ class CrashInfo : public ICLRDataEnumMemoryRegionsCallback, public ICLRDataLoggi inline const std::string& Name() const { return m_name; } inline const ModuleInfo* MainModule() const { return m_mainModule; } inline const uint64_t RuntimeBaseAddress() const { return m_runtimeBaseAddress; } + inline const uint64_t ContractDescriptorAddress() const { return m_contractDescriptorAddress; } inline const std::vector& Threads() const { return m_threads; } inline const std::set& ModuleMappings() const { return m_moduleMappings; } diff --git a/src/coreclr/debug/createdump/crashinfomac.cpp b/src/coreclr/debug/createdump/crashinfomac.cpp index d99a8b9878bde6..ad3049ac704237 100644 --- a/src/coreclr/debug/createdump/crashinfomac.cpp +++ b/src/coreclr/debug/createdump/crashinfomac.cpp @@ -244,17 +244,26 @@ void CrashInfo::VisitModule(MachOModule& module) { TRACE("TryLookupSymbol(" DACCESS_TABLE_SYMBOL ") FAILED\n"); } + if (module.TryLookupSymbol(CONTRACT_DESCRIPTOR_SYMBOL, &symbolAddress)) + { + m_contractDescriptorAddress = symbolAddress; + } } else if (m_appModel == AppModelType::SingleFile) { - uint64_t symbolAddress; - if (module.TryLookupSymbol("DotNetRuntimeInfo", &symbolAddress)) + uint64_t runtimeInfoAddress; + if (module.TryLookupSymbol("DotNetRuntimeInfo", &runtimeInfoAddress)) { m_coreclrPath = GetDirectory(module.Name()); m_runtimeBaseAddress = module.BaseAddress(); + uint64_t contractDescriptorAddress; + if (module.TryLookupSymbol(CONTRACT_DESCRIPTOR_SYMBOL, &contractDescriptorAddress)) + { + m_contractDescriptorAddress = contractDescriptorAddress; + } RuntimeInfo runtimeInfo { }; - if (ReadMemory(symbolAddress, &runtimeInfo, sizeof(RuntimeInfo))) + if (ReadMemory(runtimeInfoAddress, &runtimeInfo, sizeof(RuntimeInfo))) { if (strcmp(runtimeInfo.Signature, RUNTIME_INFO_SIGNATURE) == 0) { @@ -266,10 +275,11 @@ void CrashInfo::VisitModule(MachOModule& module) else if (m_appModel == AppModelType::NativeAOT) { uint64_t symbolAddress; - if (module.TryLookupSymbol("DotNetRuntimeContractDescriptor", &symbolAddress)) + if (module.TryLookupSymbol(CONTRACT_DESCRIPTOR_SYMBOL, &symbolAddress)) { m_coreclrPath = GetDirectory(module.Name()); m_runtimeBaseAddress = module.BaseAddress(); + m_contractDescriptorAddress = symbolAddress; TRACE("Found valid NativeAOT runtime module\n"); } } diff --git a/src/coreclr/debug/createdump/crashinfounix.cpp b/src/coreclr/debug/createdump/crashinfounix.cpp index 854301453f4824..ece67c566972ec 100644 --- a/src/coreclr/debug/createdump/crashinfounix.cpp +++ b/src/coreclr/debug/createdump/crashinfounix.cpp @@ -372,21 +372,30 @@ CrashInfo::VisitModule(uint64_t baseAddress, std::string& moduleName) { TRACE("TryLookupSymbol(" DACCESS_TABLE_SYMBOL ") FAILED\n"); } + if (TryLookupSymbol(CONTRACT_DESCRIPTOR_SYMBOL, &symbolOffset)) + { + m_contractDescriptorAddress = baseAddress + symbolOffset; + } } } else if (m_appModel == AppModelType::SingleFile) { if (PopulateForSymbolLookup(baseAddress)) { - uint64_t symbolOffset; - if (TryLookupSymbol("DotNetRuntimeInfo", &symbolOffset)) + uint64_t runtimeInfoOffset; + if (TryLookupSymbol("DotNetRuntimeInfo", &runtimeInfoOffset)) { m_coreclrPath = GetDirectory(moduleName); m_runtimeBaseAddress = baseAddress; + uint64_t contractDescriptorOffset; + if (TryLookupSymbol(CONTRACT_DESCRIPTOR_SYMBOL, &contractDescriptorOffset)) + { + m_contractDescriptorAddress = baseAddress + contractDescriptorOffset; + } // explicit initialization for old gcc support; instead of just runtimeInfo { } RuntimeInfo runtimeInfo { .Signature = { }, .Version = 0, .RuntimeModuleIndex = { }, .DacModuleIndex = { }, .DbiModuleIndex = { }, .RuntimeVersion = { } }; - if (ReadMemory(baseAddress + symbolOffset, &runtimeInfo, sizeof(RuntimeInfo))) + if (ReadMemory(baseAddress + runtimeInfoOffset, &runtimeInfo, sizeof(RuntimeInfo))) { if (strcmp(runtimeInfo.Signature, RUNTIME_INFO_SIGNATURE) == 0) { @@ -401,10 +410,11 @@ CrashInfo::VisitModule(uint64_t baseAddress, std::string& moduleName) if (PopulateForSymbolLookup(baseAddress)) { uint64_t symbolOffset; - if (TryLookupSymbol("DotNetRuntimeContractDescriptor", &symbolOffset)) + if (TryLookupSymbol(CONTRACT_DESCRIPTOR_SYMBOL, &symbolOffset)) { m_coreclrPath = GetDirectory(moduleName); m_runtimeBaseAddress = baseAddress; + m_contractDescriptorAddress = baseAddress + symbolOffset; TRACE("Found valid NativeAOT runtime module\n"); } } diff --git a/src/coreclr/debug/createdump/createdump.h b/src/coreclr/debug/createdump/createdump.h index d647ec49502d1e..c3d43b5db4a9cb 100644 --- a/src/coreclr/debug/createdump/createdump.h +++ b/src/coreclr/debug/createdump/createdump.h @@ -23,6 +23,7 @@ extern void trace_verbose_printf(const char* format, ...) MINIPAL_ATTR_FORMAT_PR // Keep in sync with the definitions in dbgutil.cpp and daccess.h #define DACCESS_TABLE_SYMBOL "g_dacTable" +#define CONTRACT_DESCRIPTOR_SYMBOL "DotNetRuntimeContractDescriptor" #ifdef HOST_64BIT #define PRIA "016" diff --git a/src/coreclr/debug/createdump/datatarget.cpp b/src/coreclr/debug/createdump/datatarget.cpp index 7867967e23777e..af3a652e5c8f3c 100644 --- a/src/coreclr/debug/createdump/datatarget.cpp +++ b/src/coreclr/debug/createdump/datatarget.cpp @@ -33,6 +33,12 @@ DumpDataTarget::QueryInterface( AddRef(); return S_OK; } + else if (InterfaceId == IID_ICLRContractLocator) + { + *Interface = static_cast(this); + AddRef(); + return S_OK; + } else { *Interface = NULL; @@ -225,3 +231,17 @@ DumpDataTarget::GetRuntimeBase( *baseAddress = m_crashInfo.RuntimeBaseAddress(); return S_OK; } + +// ICLRContractLocator + +HRESULT STDMETHODCALLTYPE +DumpDataTarget::GetContractDescriptor( + /* [out] */ CLRDATA_ADDRESS* contractAddress) +{ + if (contractAddress == nullptr) + { + return E_POINTER; + } + *contractAddress = m_crashInfo.ContractDescriptorAddress(); + return *contractAddress != 0 ? S_OK : E_FAIL; +} diff --git a/src/coreclr/debug/createdump/datatarget.h b/src/coreclr/debug/createdump/datatarget.h index 4ad3816c16013e..010ddd7fe84245 100644 --- a/src/coreclr/debug/createdump/datatarget.h +++ b/src/coreclr/debug/createdump/datatarget.h @@ -3,7 +3,7 @@ class CrashInfo; -class DumpDataTarget : public ICLRDataTarget, ICLRRuntimeLocator +class DumpDataTarget : public ICLRDataTarget, ICLRRuntimeLocator, ICLRContractLocator { private: LONG m_ref; // reference count @@ -84,4 +84,9 @@ class DumpDataTarget : public ICLRDataTarget, ICLRRuntimeLocator virtual HRESULT STDMETHODCALLTYPE GetRuntimeBase( /* [out] */ CLRDATA_ADDRESS* baseAddress); + + // ICLRContractLocator + + virtual HRESULT STDMETHODCALLTYPE GetContractDescriptor( + /* [out] */ CLRDATA_ADDRESS* contractAddress); }; diff --git a/src/coreclr/debug/runtimeinfo/CMakeLists.txt b/src/coreclr/debug/runtimeinfo/CMakeLists.txt index a31d4b6f9812c3..12e1a36748ed9b 100644 --- a/src/coreclr/debug/runtimeinfo/CMakeLists.txt +++ b/src/coreclr/debug/runtimeinfo/CMakeLists.txt @@ -30,9 +30,36 @@ function(generate_module_index Target ModuleIndexFile) target_sources(runtimeinfo PRIVATE ${ModuleIndexFile}) endfunction(generate_module_index) +function(generate_module_index_from_file ModuleFile ModuleIndexFile ModuleName) + # Win32 may be false when cross compiling + if (CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows") + set(scriptExt ".cmd") + else() + set(scriptExt ".sh") + endif() + + add_custom_command( + OUTPUT ${ModuleIndexFile} + COMMAND ${CLR_ENG_NATIVE_DIR}/genmoduleindex${scriptExt} "${ModuleFile}" ${ModuleIndexFile} ${CMAKE_READELF} + DEPENDS "${ModuleFile}" + COMMENT "Generating ${ModuleName} module index file -> ${ModuleIndexFile}" + ) + + set_source_files_properties( + ${ModuleIndexFile} + PROPERTIES GENERATED TRUE + ) + + target_sources(runtimeinfo PRIVATE ${ModuleIndexFile}) +endfunction(generate_module_index_from_file) + if(NOT DEFINED CLR_CROSS_COMPONENTS_BUILD) generate_module_index(coreclr ${CMAKE_CURRENT_BINARY_DIR}/runtimemoduleindex.h) - generate_module_index(mscordaccore ${CMAKE_CURRENT_BINARY_DIR}/dacmoduleindex.h) + if(CLR_DUMP_ENUMERATOR_PATH) + generate_module_index_from_file("${CLR_DUMP_ENUMERATOR_PATH}" ${CMAKE_CURRENT_BINARY_DIR}/dacmoduleindex.h mscordaccore) + else() + generate_module_index(mscordaccore ${CMAKE_CURRENT_BINARY_DIR}/dacmoduleindex.h) + endif() generate_module_index(mscordbi ${CMAKE_CURRENT_BINARY_DIR}/dbimoduleindex.h) endif() diff --git a/src/coreclr/dlls/CMakeLists.txt b/src/coreclr/dlls/CMakeLists.txt index b28ef7a0338f92..cb13ba0bd74b5e 100644 --- a/src/coreclr/dlls/CMakeLists.txt +++ b/src/coreclr/dlls/CMakeLists.txt @@ -7,7 +7,11 @@ if (NOT (CLR_CMAKE_TARGET_WIN32 AND FEATURE_CROSSBITNESS)) if (NOT CLR_CMAKE_TARGET_ARCH_WASM) add_subdirectory(mscordbi) add_subdirectory(mscordbi_universal) - add_subdirectory(mscordac) + + # Only build the DAC when not using the cDAC memory enumerator + if (NOT CLR_DUMP_ENUMERATOR_PATH) + add_subdirectory(mscordac) + endif() endif() add_subdirectory(mscoree) endif() diff --git a/src/coreclr/dlls/mscordbi/CMakeLists.txt b/src/coreclr/dlls/mscordbi/CMakeLists.txt index a1b7147ff4be3e..6962f17c2f87f8 100644 --- a/src/coreclr/dlls/mscordbi/CMakeLists.txt +++ b/src/coreclr/dlls/mscordbi/CMakeLists.txt @@ -20,9 +20,9 @@ set(MSCORDBI_SOURCES ) # Add the DAC PAL export mapping file -if(CLR_CMAKE_HOST_LINUX) +if(CLR_CMAKE_HOST_LINUX AND NOT CLR_DUMP_ENUMERATOR_PATH) list(APPEND MSCORDBI_SOURCES ${PAL_REDEFINES_FILE}) -endif(CLR_CMAKE_HOST_LINUX) +endif(CLR_CMAKE_HOST_LINUX AND NOT CLR_DUMP_ENUMERATOR_PATH) if(CLR_CMAKE_HOST_WIN32) add_definitions(-DFX_VER_INTERNALNAME_STR=mscordbi.dll) @@ -96,10 +96,19 @@ if(CLR_CMAKE_HOST_WIN32) elseif(CLR_CMAKE_HOST_UNIX) - list(APPEND COREDBI_LIBRARIES - # share the PAL in the dac module - mscordaccore - ) + if(CLR_DUMP_ENUMERATOR_PATH) + target_compile_definitions(mscordbi PRIVATE MSCORDBI_LINKS_PRIVATE_PAL) + list(APPEND COREDBI_LIBRARIES + mscorrc + coreclrpal + coreclrminipal + ) + else() + list(APPEND COREDBI_LIBRARIES + # share the PAL in the dac module + mscordaccore + ) + endif() if(CLR_CMAKE_HOST_HAIKU) list(APPEND COREDBI_LIBRARIES @@ -123,11 +132,13 @@ elseif(CLR_CMAKE_HOST_UNIX) # find all symbols. target_link_libraries(mscordbi PRIVATE ${COREDBI_LIBRARIES} ${COREDBI_LIBRARIES}) - add_dependencies(mscordbi mscordaccore) + if(NOT CLR_DUMP_ENUMERATOR_PATH) + add_dependencies(mscordbi mscordaccore) + endif() - if(CLR_CMAKE_HOST_LINUX) + if(CLR_CMAKE_HOST_LINUX AND NOT CLR_DUMP_ENUMERATOR_PATH) add_dependencies(mscordbi pal_redefines_file) - endif(CLR_CMAKE_HOST_LINUX) + endif(CLR_CMAKE_HOST_LINUX AND NOT CLR_DUMP_ENUMERATOR_PATH) endif(CLR_CMAKE_HOST_WIN32) diff --git a/src/coreclr/dlls/mscoree/coreclr/CMakeLists.txt b/src/coreclr/dlls/mscoree/coreclr/CMakeLists.txt index d510519874a91a..6f109250552000 100644 --- a/src/coreclr/dlls/mscoree/coreclr/CMakeLists.txt +++ b/src/coreclr/dlls/mscoree/coreclr/CMakeLists.txt @@ -237,16 +237,29 @@ if(CLR_CMAKE_TARGET_WIN32) set(CLRDEBUGINFO_RESOURCE_NAME CLRDEBUGINFOWINDOWSARM64) endif(CLR_CMAKE_TARGET_ARCH_AMD64) + if(NOT CLR_DUMP_ENUMERATOR_PATH) + set(CLRDEBUGINFO_DAC_PATH $) + set(MINIDUMP_AUXILIARY_PROVIDER_PATH $) + set(MINIDUMP_AUXILIARY_PROVIDER_NAME "mscordaccore.dll") + else() + set(CLRDEBUGINFO_DAC_PATH "${CLR_DUMP_ENUMERATOR_PATH}") + file(TO_CMAKE_PATH "${CLR_DUMP_ENUMERATOR_PATH}" MINIDUMP_AUXILIARY_PROVIDER_PATH) + get_filename_component(MINIDUMP_AUXILIARY_PROVIDER_NAME "${CLR_DUMP_ENUMERATOR_PATH}" NAME) + endif() + add_custom_target( clr_debug_resources - DEPENDS mscordaccore mscordbi + DEPENDS mscordbi # make CLRDEBUGINFO resource - COMMAND powershell -NoProfile -ExecutionPolicy ByPass -File "${CMAKE_CURRENT_SOURCE_DIR}/GenClrDebugResource.ps1" -dac $ -dbi $ -out ${CLRDEBUGINFO_RESOURCE_PATH} + COMMAND powershell -NoProfile -ExecutionPolicy ByPass -File "${CMAKE_CURRENT_SOURCE_DIR}/GenClrDebugResource.ps1" -dac "${CLRDEBUGINFO_DAC_PATH}" -dbi $ -out ${CLRDEBUGINFO_RESOURCE_PATH} ) + if(NOT CLR_DUMP_ENUMERATOR_PATH) + add_dependencies(clr_debug_resources mscordaccore) + endif() configure_file(dump_helper_resource.rc.in ${CMAKE_CURRENT_BINARY_DIR}/dump_helper_resource.rc) - set(EMBEDDED_MINIDUMP_AUXILIARY_PROVIDER $) + set(EMBEDDED_MINIDUMP_AUXILIARY_PROVIDER "${MINIDUMP_AUXILIARY_PROVIDER_PATH}") configure_file(dump_helper_resource.rc.in ${CMAKE_CURRENT_BINARY_DIR}/dump_helper_resource_embedded_minidump_provider.rc.gen) file(GENERATE OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/$/dump_helper_resource_embedded_minidump_provider.rc INPUT ${CMAKE_CURRENT_BINARY_DIR}/dump_helper_resource_embedded_minidump_provider.rc.gen) @@ -254,7 +267,7 @@ if(CLR_CMAKE_TARGET_WIN32) target_sources(coreclr_static PUBLIC ${CMAKE_CURRENT_BINARY_DIR}/$/dump_helper_resource_embedded_minidump_provider.rc) add_dependencies(coreclr clr_debug_resources) - add_dependencies(coreclr_static clr_debug_resources mscordaccore) + add_dependencies(coreclr_static clr_debug_resources) endif(CLR_CMAKE_TARGET_WIN32) # add the install targets diff --git a/src/coreclr/dlls/mscoree/coreclr/GenClrDebugResource.ps1 b/src/coreclr/dlls/mscoree/coreclr/GenClrDebugResource.ps1 index b627c55ae17c5e..9f5f1f78261a03 100644 --- a/src/coreclr/dlls/mscoree/coreclr/GenClrDebugResource.ps1 +++ b/src/coreclr/dlls/mscoree/coreclr/GenClrDebugResource.ps1 @@ -15,7 +15,8 @@ function Parse-Int { return [System.Int32]::Parse($hexValue, [System.Globalization.NumberStyles]::HexNumber) } -$clrDebugResource = [System.IO.BinaryWriter]::new([System.IO.File]::OpenWrite($out)) +$resourceStream = [System.IO.MemoryStream]::new() +$clrDebugResource = [System.IO.BinaryWriter]::new($resourceStream) try { # We're creating the resource with the following layout (represented as a C struct) @@ -26,7 +27,7 @@ try { # int dacTimeStamp; # int dacImageSize; # int dbiTimeStamp; - # int dacImageSize; + # int dbiImageSize; # }; # Write the debug resource version @@ -44,4 +45,20 @@ try { } finally { $clrDebugResource.Dispose() -} \ No newline at end of file +} + +$resourceBytes = $resourceStream.ToArray() +$writeResource = -not [System.IO.File]::Exists($out) + +if (-not $writeResource) { + $existingBytes = [System.IO.File]::ReadAllBytes($out) + $writeResource = $existingBytes.Length -ne $resourceBytes.Length + + for ($i = 0; -not $writeResource -and $i -lt $resourceBytes.Length; $i++) { + $writeResource = $existingBytes[$i] -ne $resourceBytes[$i] + } +} + +if ($writeResource) { + [System.IO.File]::WriteAllBytes($out, $resourceBytes) +} diff --git a/src/coreclr/dlls/mscoree/coreclr/dump_helper_resource.rc.in b/src/coreclr/dlls/mscoree/coreclr/dump_helper_resource.rc.in index b516c1fcf2437c..d486a00b9b738d 100644 --- a/src/coreclr/dlls/mscoree/coreclr/dump_helper_resource.rc.in +++ b/src/coreclr/dlls/mscoree/coreclr/dump_helper_resource.rc.in @@ -6,7 +6,7 @@ LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL #ifndef EMBEDDED_MINIDUMP_AUXILIARY_PROVIDER -MINIDUMP_AUXILIARY_PROVIDER RCDATA { "mscordaccore.dll\0" } +MINIDUMP_AUXILIARY_PROVIDER RCDATA { "@MINIDUMP_AUXILIARY_PROVIDER_NAME@\0" } #else diff --git a/src/coreclr/inc/clrdata.idl b/src/coreclr/inc/clrdata.idl index 76c55520315ebe..6e358f53b5ba64 100644 --- a/src/coreclr/inc/clrdata.idl +++ b/src/coreclr/inc/clrdata.idl @@ -211,6 +211,20 @@ interface ICLRRuntimeLocator : IUnknown HRESULT GetRuntimeBase([out] CLRDATA_ADDRESS* baseAddress); }; +[ + object, + local, + uuid(17d5b8c6-34a9-407f-af4f-a930201d4e02), + pointer_default(unique) +] +interface ICLRContractLocator : IUnknown +{ + /* + Returns the address of the runtime contract descriptor. + */ + HRESULT GetContractDescriptor([out] CLRDATA_ADDRESS* contractAddress); +}; + /* * Interface used by the data access services layer to locate metadata * of assemblies in a target. diff --git a/src/coreclr/pal/prebuilt/idl/clrdata_i.cpp b/src/coreclr/pal/prebuilt/idl/clrdata_i.cpp index a234e6e8eb363f..a0655d4ab676da 100644 --- a/src/coreclr/pal/prebuilt/idl/clrdata_i.cpp +++ b/src/coreclr/pal/prebuilt/idl/clrdata_i.cpp @@ -78,6 +78,9 @@ MIDL_DEFINE_GUID(IID, IID_ICLRDataTarget3,0xa5664f95,0x0af4,0x4a1b,0x96,0x0e,0x2 MIDL_DEFINE_GUID(IID, IID_ICLRRuntimeLocator,0xb760bf44,0x9377,0x4597,0x8b,0xe7,0x58,0x08,0x3b,0xdc,0x51,0x46); +MIDL_DEFINE_GUID(IID, IID_ICLRContractLocator,0x17d5b8c6,0x34a9,0x407f,0xaf,0x4f,0xa9,0x30,0x20,0x1d,0x4e,0x02); + + MIDL_DEFINE_GUID(IID, IID_ICLRMetadataLocator,0xaa8fa804,0xbc05,0x4642,0xb2,0xc5,0xc3,0x53,0xed,0x22,0xfc,0x63); @@ -97,5 +100,3 @@ MIDL_DEFINE_GUID(IID, IID_ICLRDataEnumMemoryRegions,0x471c35b4,0x7c2f,0x4ef0,0xa #ifdef __cplusplus } #endif - - diff --git a/src/coreclr/pal/prebuilt/inc/clrdata.h b/src/coreclr/pal/prebuilt/inc/clrdata.h index 7a575eae0cf932..40017089b7a0ce 100644 --- a/src/coreclr/pal/prebuilt/inc/clrdata.h +++ b/src/coreclr/pal/prebuilt/inc/clrdata.h @@ -4,9 +4,9 @@ /* this ALWAYS GENERATED file contains the definitions for the interfaces */ - /* File created by MIDL compiler version 8.01.0626 */ + /* File created by MIDL compiler version 8.01.0628 */ /* Compiler settings for clrdata.idl: - Oicf, W1, Zp8, env=Win64 (32b run), target_arch=AMD64 8.01.0626 + Oicf, W1, Zp8, env=Win64 (32b run), target_arch=AMD64 8.01.0628 protocol : dce , ms_ext, c_ext, robust error checks: allocation ref bounds_check enum stub_data VC __declspec() decoration level: @@ -43,7 +43,7 @@ #endif #ifndef DECLSPEC_XFGVIRT -#if _CONTROL_FLOW_GUARD_XFG +#if defined(_CONTROL_FLOW_GUARD_XFG) #define DECLSPEC_XFGVIRT(base, func) __declspec(xfg_virtual(base, func)) #else #define DECLSPEC_XFGVIRT(base, func) @@ -80,6 +80,13 @@ typedef interface ICLRRuntimeLocator ICLRRuntimeLocator; #endif /* __ICLRRuntimeLocator_FWD_DEFINED__ */ +#ifndef __ICLRContractLocator_FWD_DEFINED__ +#define __ICLRContractLocator_FWD_DEFINED__ +typedef interface ICLRContractLocator ICLRContractLocator; + +#endif /* __ICLRContractLocator_FWD_DEFINED__ */ + + #ifndef __ICLRMetadataLocator_FWD_DEFINED__ #define __ICLRMetadataLocator_FWD_DEFINED__ typedef interface ICLRMetadataLocator ICLRMetadataLocator; @@ -922,6 +929,90 @@ EXTERN_C const IID IID_ICLRRuntimeLocator; #endif /* __ICLRRuntimeLocator_INTERFACE_DEFINED__ */ +#ifndef __ICLRContractLocator_INTERFACE_DEFINED__ +#define __ICLRContractLocator_INTERFACE_DEFINED__ + +/* interface ICLRContractLocator */ +/* [unique][uuid][local][object] */ + + +EXTERN_C const IID IID_ICLRContractLocator; + +#if defined(__cplusplus) && !defined(CINTERFACE) + + MIDL_INTERFACE("17d5b8c6-34a9-407f-af4f-a930201d4e02") + ICLRContractLocator : public IUnknown + { + public: + virtual HRESULT STDMETHODCALLTYPE GetContractDescriptor( + /* [out] */ CLRDATA_ADDRESS *contractAddress) = 0; + + }; + + +#else /* C style interface */ + + typedef struct ICLRContractLocatorVtbl + { + BEGIN_INTERFACE + + DECLSPEC_XFGVIRT(IUnknown, QueryInterface) + HRESULT ( STDMETHODCALLTYPE *QueryInterface )( + ICLRContractLocator * This, + /* [in] */ REFIID riid, + /* [annotation][iid_is][out] */ + _COM_Outptr_ void **ppvObject); + + DECLSPEC_XFGVIRT(IUnknown, AddRef) + ULONG ( STDMETHODCALLTYPE *AddRef )( + ICLRContractLocator * This); + + DECLSPEC_XFGVIRT(IUnknown, Release) + ULONG ( STDMETHODCALLTYPE *Release )( + ICLRContractLocator * This); + + DECLSPEC_XFGVIRT(ICLRContractLocator, GetContractDescriptor) + HRESULT ( STDMETHODCALLTYPE *GetContractDescriptor )( + ICLRContractLocator * This, + /* [out] */ CLRDATA_ADDRESS *contractAddress); + + END_INTERFACE + } ICLRContractLocatorVtbl; + + interface ICLRContractLocator + { + CONST_VTBL struct ICLRContractLocatorVtbl *lpVtbl; + }; + + + +#ifdef COBJMACROS + + +#define ICLRContractLocator_QueryInterface(This,riid,ppvObject) \ + ( (This)->lpVtbl -> QueryInterface(This,riid,ppvObject) ) + +#define ICLRContractLocator_AddRef(This) \ + ( (This)->lpVtbl -> AddRef(This) ) + +#define ICLRContractLocator_Release(This) \ + ( (This)->lpVtbl -> Release(This) ) + + +#define ICLRContractLocator_GetContractDescriptor(This,contractAddress) \ + ( (This)->lpVtbl -> GetContractDescriptor(This,contractAddress) ) + +#endif /* COBJMACROS */ + + +#endif /* C style interface */ + + + + +#endif /* __ICLRContractLocator_INTERFACE_DEFINED__ */ + + #ifndef __ICLRMetadataLocator_INTERFACE_DEFINED__ #define __ICLRMetadataLocator_INTERFACE_DEFINED__ @@ -1290,7 +1381,7 @@ EXTERN_C const IID IID_ICLRDataLoggingCallback; #endif /* __ICLRDataLoggingCallback_INTERFACE_DEFINED__ */ -/* interface __MIDL_itf_clrdata_0000_0008 */ +/* interface __MIDL_itf_clrdata_0000_0009 */ /* [local] */ typedef @@ -1305,8 +1396,8 @@ enum CLRDataEnumMemoryFlags -extern RPC_IF_HANDLE __MIDL_itf_clrdata_0000_0008_v0_0_c_ifspec; -extern RPC_IF_HANDLE __MIDL_itf_clrdata_0000_0008_v0_0_s_ifspec; +extern RPC_IF_HANDLE __MIDL_itf_clrdata_0000_0009_v0_0_c_ifspec; +extern RPC_IF_HANDLE __MIDL_itf_clrdata_0000_0009_v0_0_s_ifspec; #ifndef __ICLRDataEnumMemoryRegions_INTERFACE_DEFINED__ #define __ICLRDataEnumMemoryRegions_INTERFACE_DEFINED__ diff --git a/src/coreclr/runtime.proj b/src/coreclr/runtime.proj index abf14212cec03e..1fd02b377d3945 100644 --- a/src/coreclr/runtime.proj +++ b/src/coreclr/runtime.proj @@ -4,15 +4,24 @@ <_BuildNativeTargetOS>$(TargetOS) <_BuildNativeTargetOS Condition="'$(TargetsLinuxBionic)' == 'true'">linux-bionic true + true GetPgoDataPackagePath $(BuildRuntimeDependsOnTargets);InitializeSourceControlInformationFromSourceControlManager AcquireEmscriptenSdk;$(BuildRuntimeDependsOnTargets);GenerateEmccExports AcquireWasiSdk;$(BuildRuntimeDependsOnTargets) + + + @@ -55,6 +64,7 @@ <_CoreClrBuildArg Condition="'$(BuildSubdirectory)' != ''" Include="-subdir $(BuildSubdirectory)" /> <_CoreClrBuildArg Include="-cmakeargs "-DCLR_DOTNET_HOST_PATH='$(DOTNET_HOST_PATH)'"" /> <_CoreClrBuildArg Condition="'$(HasCdacBuildTool)' == 'true'" Include="-cmakeargs "-DCDAC_BUILD_TOOL_BINARY_PATH=$(RuntimeBinDir)cdac-build-tool\cdac-build-tool.dll"" /> + <_CoreClrBuildArg Condition="'$(HasCdacDumpCollectProvider)' == 'true'" Include="-cmakeargs "-DCLR_DUMP_ENUMERATOR_PATH=$(RuntimeBinDir)$(LibPrefix)mscordaccore$(LibSuffix)"" /> <_CoreClrBuildArg Condition="'$(FeatureXplatEventSource)' == 'false'" Include="-cmakeargs "-DFEATURE_EVENTSOURCE_XPLAT=0"" /> <_CoreClrBuildArg Condition="'$(FeatureInterpreter)' == 'true'" Include="-cmakeargs "-DFEATURE_INTERPRETER=1"" /> <_CoreClrBuildArg Condition="'$(FeatureDynamicCodeCompiled)' == 'true' or '$(FeatureDynamicCodeCompiled)' == ''" Include="-cmakeargs "-DFEATURE_DYNAMIC_CODE_COMPILED=1"" /> From ce74243aa2997dedb0f5ba4e508a2edd597b638c Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Tue, 29 Sep 2026 13:48:44 -0400 Subject: [PATCH 3/9] Adapt rebased cDAC test and docs to TryReadAllFields Update the upstream WASM optional-field test to assert the generated read helper succeeds, and document its boolean failure behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: de3eab4e-b090-430a-8e30-6b204fb1519a --- src/native/managed/cdac/IData.md | 34 +++++++++---------- .../ExecutionManager/ExecutionManagerTests.cs | 4 +-- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/native/managed/cdac/IData.md b/src/native/managed/cdac/IData.md index 51404ddfcd9a04..1e763871d000b5 100644 --- a/src/native/managed/cdac/IData.md +++ b/src/native/managed/cdac/IData.md @@ -78,9 +78,9 @@ analyzer. It scans for classes carrying `[CdacType]` and emits a * A `private readonly Target _target` field (captured in the constructor) for any type with instance members, so lazy getters and `Write{Name}` methods can read/write without a `Target` parameter. -* An explicit `IReadableData.EnsureAllFieldsRead()` implementation (for - any type with instance members) that touches every field so a caller - can force a full eager read. +* An internal `bool TryReadAllFields()` method (for any type with instance + members) that touches every field so a caller can force a full eager read. + It returns `false` if a target memory read throws `VirtualReadException`. * For types with `HasTypeHandle = true`: a `public static ITypeHandle TypeHandle(Target target)` accessor. * For each `[Field(Writable = true)]` property: a @@ -151,25 +151,25 @@ not at construction, and whatever it throws (an from a failed read, or nothing for an optional value) surfaces at first access. -### Forcing a full read: `IReadableData` +### Forcing a full read: `TryReadAllFields` Because reads are deferred, constructing an instance no longer proves the whole structure is readable. Every generated type with instance members -implements `IReadableData.EnsureAllFieldsRead()`, which touches every -field to force a full eager read. Materializing a field can throw either -lazy-read exception: `InvalidOperationException` if a required field is -missing from the descriptor, or `VirtualReadException` if the field's -target memory cannot be read. A caller catches whichever it cares about -- -for example, to validate that the target memory is readable: +provides an internal `TryReadAllFields()` method, which touches every field +to force a full eager read. It returns `true` when all fields can be +materialized, or `false` if a field's target memory cannot be read. +`InvalidOperationException` still propagates if a required field is missing +from the descriptor. Optional fields absent from the descriptor yield `null` +and do not cause failure. For example, to validate that a method table is +readable: ```csharp -T data = target.ProcessedData.GetOrAdd(address); -try +Data.MethodTable data = target.ProcessedData.GetOrAdd(address); +if (data.TryReadAllFields()) { - (data as IReadableData)?.EnsureAllFieldsRead(); // ... the entire structure is readable } -catch (VirtualReadException) +else { // ... the structure is only partially readable } @@ -177,9 +177,9 @@ catch (VirtualReadException) This is how `RuntimeTypeSystem` validation confirms a candidate `MethodTable` / `EEClass` is fully readable before trusting it. Its -descriptor fields are always present, so only `VirtualReadException` is -relevant there; a caller that also needs to tolerate a missing descriptor -field would additionally catch `InvalidOperationException`. +descriptor fields are always present, so the boolean result is sufficient +there; a caller that also needs to tolerate a missing descriptor field would +additionally catch `InvalidOperationException`. ## Attribute surface diff --git a/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs b/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs index aff57ebe5864e9..4fc4c0263aa31c 100644 --- a/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs @@ -456,7 +456,7 @@ public void GetMethodDesc_R2R_WasmVirtualIPRangeList_ResolvesCapturedShape() } [Fact] - public void ReadyToRunInfo_WasmMissingOptionalFields_EnsureAllFieldsRead() + public void ReadyToRunInfo_WasmMissingOptionalFields_TryReadAllFields() { MockTarget.Architecture wasmArch = new() { IsLittleEndian = true, Is64Bit = false }; MockExecutionManagerBuilder emBuilder = new( @@ -474,7 +474,7 @@ public void ReadyToRunInfo_WasmMissingOptionalFields_EnsureAllFieldsRead() Data.ReadyToRunInfo data = target.ProcessedData.GetOrAdd(r2rInfo.Address); - ((Data.IReadableData)data).EnsureAllFieldsRead(); + Assert.True(data.TryReadAllFields()); Assert.Null(data.NumHotColdMap); Assert.Null(data.DelayLoadMethodCallThunks); From 12aba12d7e8403160d287fd27c6eb5a5c5149dbd Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Tue, 29 Sep 2026 14:21:34 -0400 Subject: [PATCH 4/9] Omit legacy DBI and debug identities in cDAC enumerator builds Keep the universal DBI, runtime identity and WER provider resources while omitting the legacy DAC/DBI pair and its lookup identities only in enumerator-enabled builds. Restore the standard legacy DBI PAL linkage and document the Windows dbgshim discovery limitation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: de3eab4e-b090-430a-8e30-6b204fb1519a --- src/coreclr/debug/runtimeinfo/CMakeLists.txt | 27 +----------- src/coreclr/debug/runtimeinfo/runtimeinfo.cpp | 4 ++ src/coreclr/dlls/CMakeLists.txt | 4 +- src/coreclr/dlls/mscordbi/CMakeLists.txt | 29 ++++-------- .../dlls/mscoree/coreclr/CMakeLists.txt | 44 ++++++++----------- .../coreclr/dump_helper_resource.rc.in | 8 +++- src/native/managed/cdac/README.md | 9 ++++ 7 files changed, 51 insertions(+), 74 deletions(-) diff --git a/src/coreclr/debug/runtimeinfo/CMakeLists.txt b/src/coreclr/debug/runtimeinfo/CMakeLists.txt index 12e1a36748ed9b..330f6de0153bc3 100644 --- a/src/coreclr/debug/runtimeinfo/CMakeLists.txt +++ b/src/coreclr/debug/runtimeinfo/CMakeLists.txt @@ -30,37 +30,14 @@ function(generate_module_index Target ModuleIndexFile) target_sources(runtimeinfo PRIVATE ${ModuleIndexFile}) endfunction(generate_module_index) -function(generate_module_index_from_file ModuleFile ModuleIndexFile ModuleName) - # Win32 may be false when cross compiling - if (CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows") - set(scriptExt ".cmd") - else() - set(scriptExt ".sh") - endif() - - add_custom_command( - OUTPUT ${ModuleIndexFile} - COMMAND ${CLR_ENG_NATIVE_DIR}/genmoduleindex${scriptExt} "${ModuleFile}" ${ModuleIndexFile} ${CMAKE_READELF} - DEPENDS "${ModuleFile}" - COMMENT "Generating ${ModuleName} module index file -> ${ModuleIndexFile}" - ) - - set_source_files_properties( - ${ModuleIndexFile} - PROPERTIES GENERATED TRUE - ) - - target_sources(runtimeinfo PRIVATE ${ModuleIndexFile}) -endfunction(generate_module_index_from_file) - if(NOT DEFINED CLR_CROSS_COMPONENTS_BUILD) generate_module_index(coreclr ${CMAKE_CURRENT_BINARY_DIR}/runtimemoduleindex.h) if(CLR_DUMP_ENUMERATOR_PATH) - generate_module_index_from_file("${CLR_DUMP_ENUMERATOR_PATH}" ${CMAKE_CURRENT_BINARY_DIR}/dacmoduleindex.h mscordaccore) + target_compile_definitions(runtimeinfo PRIVATE OMIT_DAC_DBI_INDEX) else() generate_module_index(mscordaccore ${CMAKE_CURRENT_BINARY_DIR}/dacmoduleindex.h) + generate_module_index(mscordbi ${CMAKE_CURRENT_BINARY_DIR}/dbimoduleindex.h) endif() - generate_module_index(mscordbi ${CMAKE_CURRENT_BINARY_DIR}/dbimoduleindex.h) endif() # publish runtimeinfo lib diff --git a/src/coreclr/debug/runtimeinfo/runtimeinfo.cpp b/src/coreclr/debug/runtimeinfo/runtimeinfo.cpp index 7dfd0c2afd4b85..2dfe5dd98a2215 100644 --- a/src/coreclr/debug/runtimeinfo/runtimeinfo.cpp +++ b/src/coreclr/debug/runtimeinfo/runtimeinfo.cpp @@ -25,10 +25,14 @@ RuntimeInfo DotNetRuntimeInfo = { #include }, { +#ifndef OMIT_DAC_DBI_INDEX #include +#endif }, { +#ifndef OMIT_DAC_DBI_INDEX #include +#endif }, { RuntimeFileMajorVersion, RuntimeFileMinorVersion, RuntimeFileBuildVersion, RuntimeFileRevisionVersion diff --git a/src/coreclr/dlls/CMakeLists.txt b/src/coreclr/dlls/CMakeLists.txt index cb13ba0bd74b5e..5ef0c077e5c37f 100644 --- a/src/coreclr/dlls/CMakeLists.txt +++ b/src/coreclr/dlls/CMakeLists.txt @@ -5,11 +5,11 @@ if(CLR_CMAKE_TARGET_WIN32 AND FEATURE_EVENT_TRACE) endif(CLR_CMAKE_TARGET_WIN32 AND FEATURE_EVENT_TRACE) if (NOT (CLR_CMAKE_TARGET_WIN32 AND FEATURE_CROSSBITNESS)) if (NOT CLR_CMAKE_TARGET_ARCH_WASM) - add_subdirectory(mscordbi) add_subdirectory(mscordbi_universal) - # Only build the DAC when not using the cDAC memory enumerator + # Only build the legacy DAC and DBI when not using the cDAC memory enumerator. if (NOT CLR_DUMP_ENUMERATOR_PATH) + add_subdirectory(mscordbi) add_subdirectory(mscordac) endif() endif() diff --git a/src/coreclr/dlls/mscordbi/CMakeLists.txt b/src/coreclr/dlls/mscordbi/CMakeLists.txt index 6962f17c2f87f8..a1b7147ff4be3e 100644 --- a/src/coreclr/dlls/mscordbi/CMakeLists.txt +++ b/src/coreclr/dlls/mscordbi/CMakeLists.txt @@ -20,9 +20,9 @@ set(MSCORDBI_SOURCES ) # Add the DAC PAL export mapping file -if(CLR_CMAKE_HOST_LINUX AND NOT CLR_DUMP_ENUMERATOR_PATH) +if(CLR_CMAKE_HOST_LINUX) list(APPEND MSCORDBI_SOURCES ${PAL_REDEFINES_FILE}) -endif(CLR_CMAKE_HOST_LINUX AND NOT CLR_DUMP_ENUMERATOR_PATH) +endif(CLR_CMAKE_HOST_LINUX) if(CLR_CMAKE_HOST_WIN32) add_definitions(-DFX_VER_INTERNALNAME_STR=mscordbi.dll) @@ -96,19 +96,10 @@ if(CLR_CMAKE_HOST_WIN32) elseif(CLR_CMAKE_HOST_UNIX) - if(CLR_DUMP_ENUMERATOR_PATH) - target_compile_definitions(mscordbi PRIVATE MSCORDBI_LINKS_PRIVATE_PAL) - list(APPEND COREDBI_LIBRARIES - mscorrc - coreclrpal - coreclrminipal - ) - else() - list(APPEND COREDBI_LIBRARIES - # share the PAL in the dac module - mscordaccore - ) - endif() + list(APPEND COREDBI_LIBRARIES + # share the PAL in the dac module + mscordaccore + ) if(CLR_CMAKE_HOST_HAIKU) list(APPEND COREDBI_LIBRARIES @@ -132,13 +123,11 @@ elseif(CLR_CMAKE_HOST_UNIX) # find all symbols. target_link_libraries(mscordbi PRIVATE ${COREDBI_LIBRARIES} ${COREDBI_LIBRARIES}) - if(NOT CLR_DUMP_ENUMERATOR_PATH) - add_dependencies(mscordbi mscordaccore) - endif() + add_dependencies(mscordbi mscordaccore) - if(CLR_CMAKE_HOST_LINUX AND NOT CLR_DUMP_ENUMERATOR_PATH) + if(CLR_CMAKE_HOST_LINUX) add_dependencies(mscordbi pal_redefines_file) - endif(CLR_CMAKE_HOST_LINUX AND NOT CLR_DUMP_ENUMERATOR_PATH) + endif(CLR_CMAKE_HOST_LINUX) endif(CLR_CMAKE_HOST_WIN32) diff --git a/src/coreclr/dlls/mscoree/coreclr/CMakeLists.txt b/src/coreclr/dlls/mscoree/coreclr/CMakeLists.txt index 6f109250552000..d2ec04c93cb417 100644 --- a/src/coreclr/dlls/mscoree/coreclr/CMakeLists.txt +++ b/src/coreclr/dlls/mscoree/coreclr/CMakeLists.txt @@ -227,36 +227,33 @@ elseif (TARGET coreclrpal) endif() if(CLR_CMAKE_TARGET_WIN32) - set(CLRDEBUGINFO_RESOURCE_PATH ${CMAKE_CURRENT_BINARY_DIR}/clr_debug_resource.bin) - - if(CLR_CMAKE_TARGET_ARCH_AMD64) - set(CLRDEBUGINFO_RESOURCE_NAME CLRDEBUGINFOWINDOWSAMD64) - elseif(CLR_CMAKE_TARGET_ARCH_I386) - set(CLRDEBUGINFO_RESOURCE_NAME CLRDEBUGINFOWINDOWSX86) - elseif(CLR_CMAKE_TARGET_ARCH_ARM64) - set(CLRDEBUGINFO_RESOURCE_NAME CLRDEBUGINFOWINDOWSARM64) - endif(CLR_CMAKE_TARGET_ARCH_AMD64) - if(NOT CLR_DUMP_ENUMERATOR_PATH) - set(CLRDEBUGINFO_DAC_PATH $) + set(CLRDEBUGINFO_RESOURCE_PATH ${CMAKE_CURRENT_BINARY_DIR}/clr_debug_resource.bin) + + if(CLR_CMAKE_TARGET_ARCH_AMD64) + set(CLRDEBUGINFO_RESOURCE_NAME CLRDEBUGINFOWINDOWSAMD64) + elseif(CLR_CMAKE_TARGET_ARCH_I386) + set(CLRDEBUGINFO_RESOURCE_NAME CLRDEBUGINFOWINDOWSX86) + elseif(CLR_CMAKE_TARGET_ARCH_ARM64) + set(CLRDEBUGINFO_RESOURCE_NAME CLRDEBUGINFOWINDOWSARM64) + endif(CLR_CMAKE_TARGET_ARCH_AMD64) + + add_custom_target( + clr_debug_resources + DEPENDS mscordaccore mscordbi + # make CLRDEBUGINFO resource + COMMAND powershell -NoProfile -ExecutionPolicy ByPass -File "${CMAKE_CURRENT_SOURCE_DIR}/GenClrDebugResource.ps1" -dac $ -dbi $ -out ${CLRDEBUGINFO_RESOURCE_PATH} + ) + add_dependencies(coreclr clr_debug_resources) + add_dependencies(coreclr_static clr_debug_resources) + set(MINIDUMP_AUXILIARY_PROVIDER_PATH $) set(MINIDUMP_AUXILIARY_PROVIDER_NAME "mscordaccore.dll") else() - set(CLRDEBUGINFO_DAC_PATH "${CLR_DUMP_ENUMERATOR_PATH}") file(TO_CMAKE_PATH "${CLR_DUMP_ENUMERATOR_PATH}" MINIDUMP_AUXILIARY_PROVIDER_PATH) get_filename_component(MINIDUMP_AUXILIARY_PROVIDER_NAME "${CLR_DUMP_ENUMERATOR_PATH}" NAME) endif() - add_custom_target( - clr_debug_resources - DEPENDS mscordbi - # make CLRDEBUGINFO resource - COMMAND powershell -NoProfile -ExecutionPolicy ByPass -File "${CMAKE_CURRENT_SOURCE_DIR}/GenClrDebugResource.ps1" -dac "${CLRDEBUGINFO_DAC_PATH}" -dbi $ -out ${CLRDEBUGINFO_RESOURCE_PATH} - ) - if(NOT CLR_DUMP_ENUMERATOR_PATH) - add_dependencies(clr_debug_resources mscordaccore) - endif() - configure_file(dump_helper_resource.rc.in ${CMAKE_CURRENT_BINARY_DIR}/dump_helper_resource.rc) set(EMBEDDED_MINIDUMP_AUXILIARY_PROVIDER "${MINIDUMP_AUXILIARY_PROVIDER_PATH}") @@ -265,9 +262,6 @@ if(CLR_CMAKE_TARGET_WIN32) target_sources(coreclr PUBLIC ${CMAKE_CURRENT_BINARY_DIR}/dump_helper_resource.rc) target_sources(coreclr_static PUBLIC ${CMAKE_CURRENT_BINARY_DIR}/$/dump_helper_resource_embedded_minidump_provider.rc) - - add_dependencies(coreclr clr_debug_resources) - add_dependencies(coreclr_static clr_debug_resources) endif(CLR_CMAKE_TARGET_WIN32) # add the install targets diff --git a/src/coreclr/dlls/mscoree/coreclr/dump_helper_resource.rc.in b/src/coreclr/dlls/mscoree/coreclr/dump_helper_resource.rc.in index d486a00b9b738d..d3b3a0ef80816f 100644 --- a/src/coreclr/dlls/mscoree/coreclr/dump_helper_resource.rc.in +++ b/src/coreclr/dlls/mscoree/coreclr/dump_helper_resource.rc.in @@ -14,5 +14,9 @@ MINIDUMP_EMBEDDED_AUXILIARY_PROVIDER RCDATA "@EMBEDDED_MINIDUMP_AUXILIARY_PROVID #endif -@CLRDEBUGINFO_RESOURCE_NAME@ RCDATA "@CLRDEBUGINFO_RESOURCE_PATH@" -CLRDEBUGINFO RCDATA "@CLRDEBUGINFO_RESOURCE_PATH@" +#cmakedefine CLRDEBUGINFO_RESOURCE_PATH "@CLRDEBUGINFO_RESOURCE_PATH@" + +#ifdef CLRDEBUGINFO_RESOURCE_PATH +@CLRDEBUGINFO_RESOURCE_NAME@ RCDATA CLRDEBUGINFO_RESOURCE_PATH +CLRDEBUGINFO RCDATA CLRDEBUGINFO_RESOURCE_PATH +#endif diff --git a/src/native/managed/cdac/README.md b/src/native/managed/cdac/README.md index 871f0d204b457b..2938c41bd3bad7 100644 --- a/src/native/managed/cdac/README.md +++ b/src/native/managed/cdac/README.md @@ -49,6 +49,15 @@ Both the universal cDAC and the separately built The separate NativeAOT project controls its exports, architecture restrictions, size settings, and installation without duplicating collection logic. +With `UseCdacDumpCollectProvider=true`, the native build omits both the legacy +DAC and legacy DBI, but still builds `mscordbi_universal`. These builds omit +the Windows `CLRDEBUGINFO` resources and leave the DAC/DBI identity indexes +in `DotNetRuntimeInfo` empty. Runtime identity/version information and WER's +separate dump-provider resources are preserved. Current Windows dbgshim +runtime discovery paths that require the legacy identities cannot discover +these builds; those paths need to support absent legacy identities before +they can be used for cDAC live debugging. + Enumeration shares the SOS/process instance's COM identity and API lock. `MemoryRegionEnumerator.Enumerate` requires a `ContractDescriptorTarget`, returning `E_NOTIMPL` for other target implementations. Each call flushes all cached target data From f6b5f572c181521160f4538497357b76460ddaf3 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Wed, 30 Sep 2026 15:01:23 -0400 Subject: [PATCH 5/9] Clarify cDAC dump collection boundaries Move dump-mode parsing and callback wrapping to the COM entrypoint. Let DumpCreator own enumeration setup and use a namespace-level DumpType throughout collection, with private construction. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: de3eab4e-b090-430a-8e30-6b204fb1519a --- .../EnumMemory/DumpCreator.cs | 41 +++++++++++++++---- ...onEnumerator.cs => MemoryRegionEmitter.cs} | 38 ----------------- .../EnumMemory/ObjectCollector.cs | 9 ++-- .../SOSDacImpl.ICLRDataEnumMemoryRegions.cs | 20 ++++++++- src/native/managed/cdac/README.md | 8 +++- .../cdac/tests/UnitTests/EnumMemoryTests.cs | 24 ++++++----- 6 files changed, 76 insertions(+), 64 deletions(-) rename src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/{MemoryRegionEnumerator.cs => MemoryRegionEmitter.cs} (68%) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/DumpCreator.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/DumpCreator.cs index 5474b4e90e8a94..123639dad29541 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/DumpCreator.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/DumpCreator.cs @@ -10,31 +10,56 @@ namespace Microsoft.Diagnostics.DataContractReader.Legacy.EnumMemory; +internal enum DumpType +{ + Mini, + Heap, + Triage, +} + internal sealed class DumpCreator { private const int MaxSyncBlocks = 1_000_000; private const int MaxThreads = 1_000_000; private readonly Target _target; - private readonly bool _includeHeap; + private readonly DumpType _dumpType; private readonly MemoryRegionEmitter _emitter; private readonly HashSet _loaderAllocators = []; private readonly MethodCollector _methods; private readonly ObjectCollector _objects; - public DumpCreator( + private DumpCreator( Target target, - CLRDataEnumMemoryFlags flags, + DumpType dumpType, MemoryRegionEmitter emitter) { _target = target; - _includeHeap = flags == CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_HEAP2; + _dumpType = dumpType; _emitter = emitter; _methods = new(target); - _objects = new(target, emitter, _methods, flags == CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_TRIAGE); + _objects = new(target, emitter, _methods, _dumpType); + } + + public static int Enumerate(Target target, MemoryRegionEmitter emitter, DumpType dumpType) + { + if (target is not ContractDescriptorTarget descriptorTarget) + return HResults.E_NOTIMPL; + + target.Flush(FlushScope.All); + using IDisposable readScope = descriptorTarget.RegisterReadCallback((address, size) => + { + if (emitter.ShouldEmitTargetRead(address, size)) + emitter.Add(address, size); + }); + foreach (TargetSpan range in descriptorTarget.EnumerateDescriptorMemory()) + emitter.Add(range.Address.Value, range.Size); + + new DumpCreator(target, dumpType, emitter).EnumerateMemoryRegions(); + return emitter.Result; } - public void EnumerateMemoryRegions() + private void EnumerateMemoryRegions() { TryEnumerate(EnumerateRuntimeModule); TryEnumerate(EnumerateStatics); @@ -42,7 +67,7 @@ public void EnumerateMemoryRegions() TryEnumerate(EnumerateModules); TryEnumerate(EnumerateThreads); - if (_includeHeap) + if (_dumpType == DumpType.Heap) { TryEnumerate(EnumerateGC); TryEnumerate(EnumerateCodeAndLoaderHeaps); @@ -129,7 +154,7 @@ private void EnumerateModules() _emitter.RegisterMetadataRange(ecmaMetadata.GetReadWriteSavedMetadataAddress(module)); // Smaller dumps do not include in-memory symbols unless they are otherwise referenced. - if (loader.TryGetSymbolStream(module, out TargetPointer symbolBuffer, out uint symbolSize) && _includeHeap) + if (loader.TryGetSymbolStream(module, out TargetPointer symbolBuffer, out uint symbolSize) && _dumpType == DumpType.Heap) _emitter.Add(symbolBuffer.Value, symbolSize); if (peAssembly != TargetPointer.Null) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/MemoryRegionEnumerator.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/MemoryRegionEmitter.cs similarity index 68% rename from src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/MemoryRegionEnumerator.cs rename to src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/MemoryRegionEmitter.cs index 52c76e25c4b18c..53a6fc54bad004 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/MemoryRegionEnumerator.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/MemoryRegionEmitter.cs @@ -7,44 +7,6 @@ namespace Microsoft.Diagnostics.DataContractReader.Legacy.EnumMemory; -internal static unsafe class MemoryRegionEnumerator -{ - private const uint MiniDumpWithPrivateReadWriteMemory = 0x200; - private const uint MiniDumpWithFullAuxiliaryState = 0x8000; - private const uint MiniDumpFilterTriage = 0x100000; - - public static int Enumerate(Target target, void* callback, uint miniDumpFlags) - { - if (target is not ContractDescriptorTarget descriptorTarget) - return HResults.E_NOTIMPL; - - target.Flush(FlushScope.All); - var emitter = new MemoryRegionEmitter((nint)callback, (uint)target.PointerSize); - using IDisposable readScope = descriptorTarget.RegisterReadCallback((address, size) => - { - if (emitter.ShouldEmitTargetRead(address, size)) - emitter.Add(address, size); - }); - foreach (TargetSpan range in descriptorTarget.EnumerateDescriptorMemory()) - emitter.Add(range.Address.Value, range.Size); - - new DumpCreator(target, GetDumpFlags(miniDumpFlags), emitter).EnumerateMemoryRegions(); - return emitter.Result; - } - - internal static CLRDataEnumMemoryFlags GetDumpFlags(uint miniDumpFlags) - { - if ((miniDumpFlags & MiniDumpWithPrivateReadWriteMemory) != 0) - return CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_HEAP2; - if ((miniDumpFlags & MiniDumpWithFullAuxiliaryState) != 0) - return CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT; - if ((miniDumpFlags & MiniDumpFilterTriage) != 0) - return CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_TRIAGE; - - return CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_MINI; - } -} - internal sealed unsafe class MemoryRegionEmitter(nint callback, uint pointerSize) { private static readonly Guid s_ICLRDataEnumMemoryRegionsCallback2_Iid = new("3721A26F-8B91-4D98-A388-DB17B356FADB"); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/ObjectCollector.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/ObjectCollector.cs index 810d17b37f02b0..6b671d7705ed42 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/ObjectCollector.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/ObjectCollector.cs @@ -17,13 +17,14 @@ internal sealed class ObjectCollector( Target target, MemoryRegionEmitter emitter, MethodCollector methods, - bool isTriage) + DumpType dumpType) { private const ulong MaxObjectSize = 64 * 1024 * 1024; private const int MaxInnerExceptionCount = 256; private const int MaxTypeTraversalDepth = 1_024; private readonly Target _target = target; + private readonly DumpType _dumpType = dumpType; private readonly MemoryRegionEmitter _emitter = emitter; private readonly MethodCollector _methods = methods; private readonly Dictionary _names = []; @@ -154,13 +155,13 @@ private void EnumerateExceptionData(TargetPointer exceptionObject) } EnumerateStackTraceString(data.StackTraceString); if (data.RemoteStackTraceString != TargetPointer.Null && - (!isTriage || !ExceptionTypeOverridesStackTraceGetter(exceptionObject))) + (_dumpType != DumpType.Triage || !ExceptionTypeOverridesStackTraceGetter(exceptionObject))) { EnumerateStackTraceString(data.RemoteStackTraceString); } _pendingObjects.Push(data.WatsonBuckets); _pendingObjects.Push(data.StackTrace); - if (!isTriage) + if (_dumpType != DumpType.Triage) _pendingObjects.Push(data.Message); foreach (ExceptionStackFrameInfo frame in exceptions.GetExceptionStackFrames(exceptionObject)) @@ -204,7 +205,7 @@ private void EnumerateStackTraceString(TargetPointer address) return; EnumerateObjectCore(address); - if (!isTriage) + if (_dumpType != DumpType.Triage) return; IObject objects = _target.Contracts.Object; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.ICLRDataEnumMemoryRegions.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.ICLRDataEnumMemoryRegions.cs index 22420ef51b5069..12c81eed8ac885 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.ICLRDataEnumMemoryRegions.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.ICLRDataEnumMemoryRegions.cs @@ -23,7 +23,9 @@ int ICLRDataEnumMemoryRegions.EnumMemoryRegions(void* callback, uint miniDumpFla try { // Like the native DAC, ignore the reserved clrFlags argument. - return MemoryRegionEnumerator.Enumerate(_target, callback, miniDumpFlags); + DumpType dumpType = GetDumpType(miniDumpFlags); + MemoryRegionEmitter emitter = new((nint)callback, (uint)_target.PointerSize); + return DumpCreator.Enumerate(_target, emitter, dumpType); } catch (Exception ex) { @@ -31,4 +33,20 @@ int ICLRDataEnumMemoryRegions.EnumMemoryRegions(void* callback, uint miniDumpFla return hr < 0 ? hr : HResults.E_FAIL; } } + + internal static DumpType GetDumpType(uint miniDumpFlags) + { + const uint MiniDumpWithPrivateReadWriteMemory = 0x200; + const uint MiniDumpWithFullAuxiliaryState = 0x8000; + const uint MiniDumpFilterTriage = 0x100000; + + if ((miniDumpFlags & MiniDumpWithPrivateReadWriteMemory) != 0) + return DumpType.Heap; + if ((miniDumpFlags & MiniDumpWithFullAuxiliaryState) != 0) + return DumpType.Mini; + if ((miniDumpFlags & MiniDumpFilterTriage) != 0) + return DumpType.Triage; + + return DumpType.Mini; + } } diff --git a/src/native/managed/cdac/README.md b/src/native/managed/cdac/README.md index 2938c41bd3bad7..cb38f540bb6071 100644 --- a/src/native/managed/cdac/README.md +++ b/src/native/managed/cdac/README.md @@ -59,7 +59,7 @@ these builds; those paths need to support absent legacy identities before they can be used for cDAC live debugging. Enumeration shares the SOS/process instance's COM identity and API lock. -`MemoryRegionEnumerator.Enumerate` requires a `ContractDescriptorTarget`, returning +`DumpCreator.Enumerate` requires a `ContractDescriptorTarget`, returning `E_NOTIMPL` for other target implementations. Each call flushes all cached target data and contract state, then uses `RegisterReadCallback` to report successful reads, including string terminators. Collection uses the existing target and contract instances; @@ -108,7 +108,11 @@ regions used by dump collection. Shared `EntrypointHelpers.TryGetContractDescrip uses only `ICLRContractLocator`. The universal host's explicit-address dbgshim entrypoint bypasses descriptor discovery. -The collector selects its collection mode from `miniDumpFlags`, as the native DAC does. The +The COM entrypoint selects the collection mode from `miniDumpFlags`, as the native DAC does, +and passes the derived `EnumMemory.DumpType` (`Mini`, `Heap`, or `Triage`) through +the collection engine. It also wraps the native callback in `MemoryRegionEmitter`; +`DumpCreator` accepts that managed adapter rather than a callback pointer and owns +the flush, read-observation scope, and descriptor-memory enumeration. The `CLRDataEnumMemoryFlags` argument to `EnumMemoryRegions` is reserved and ignored. Triage exception collection omits messages and remote stack traces from types that override the `StackTrace` getter. Stack-trace strings have source-file diff --git a/src/native/managed/cdac/tests/UnitTests/EnumMemoryTests.cs b/src/native/managed/cdac/tests/UnitTests/EnumMemoryTests.cs index 8664d0165c4a00..a7656ea0dfc2e1 100644 --- a/src/native/managed/cdac/tests/UnitTests/EnumMemoryTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/EnumMemoryTests.cs @@ -1396,14 +1396,15 @@ public void ReadShimScopes_RestorePreviousReadersWithoutChangingCaches() } [Fact] - public void Enumeration_RejectsOtherTargetImplementationsAtEnumeratorBoundary() + public void Enumeration_RejectsOtherTargetImplementationsAtDumpCreatorBoundary() { Mock target = new(); target.SetupGet(t => t.PointerSize).Returns(8); ICLRDataEnumMemoryRegions impl = new SOSDacImpl(target.Object, legacyObj: null, new()); using RecordingCallback callback = new(supportsUpdates: false); - Assert.Equal(HResults.E_NOTIMPL, MemoryRegionEnumerator.Enumerate(target.Object, (void*)callback.Address, 0)); + MemoryRegionEmitter emitter = new(callback.Address, (uint)target.Object.PointerSize); + Assert.Equal(HResults.E_NOTIMPL, DumpCreator.Enumerate(target.Object, emitter, DumpType.Mini)); Assert.Equal(HResults.E_NOTIMPL, impl.EnumMemoryRegions((void*)callback.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); Assert.Empty(callback.Ranges); target.Verify(t => t.Flush(It.IsAny()), Times.Never); @@ -1475,15 +1476,16 @@ private static void AssertFullyReported(RecordingCallback callback, ulong addres } [Theory] - [InlineData(0, 0)] - [InlineData(0x200, 3)] - [InlineData(0x100000, 2)] - [InlineData(0x100200, 3)] - [InlineData(0x108000, 0)] - [InlineData(0x108200, 3)] - public void DumpMode_UsesNativeMiniDumpFlagPrecedence(uint miniDumpFlags, int expected) + [InlineData(0, nameof(DumpType.Mini))] + [InlineData(0x200, nameof(DumpType.Heap))] + [InlineData(0x100000, nameof(DumpType.Triage))] + [InlineData(0x100200, nameof(DumpType.Heap))] + [InlineData(0x8000, nameof(DumpType.Mini))] + [InlineData(0x108000, nameof(DumpType.Mini))] + [InlineData(0x108200, nameof(DumpType.Heap))] + public void DumpMode_UsesNativeMiniDumpFlagPrecedence(uint miniDumpFlags, string expected) { - Assert.Equal(expected, (int)MemoryRegionEnumerator.GetDumpFlags(miniDumpFlags)); + Assert.Equal(Enum.Parse(expected), SOSDacImpl.GetDumpType(miniDumpFlags)); } [Theory] @@ -1587,7 +1589,7 @@ public void ExceptionCollection_UsesTriagePolicy(bool triage, bool derived, bool using RecordingCallback callback = new(supportsUpdates); MemoryRegionEmitter emitter = new(callback.Address, 8); - new ObjectCollector(target, emitter, new MethodCollector(target), triage).EnumerateObject(exception); + new ObjectCollector(target, emitter, new MethodCollector(target), triage ? DumpType.Triage : DumpType.Mini).EnumerateObject(exception); Assert.Contains(exception.Value, callback.Regions); Assert.Contains(innerException.Value, callback.Regions); From 4685ebda4d19dadd592d6bfad30b14f9f5bbe1ef Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Thu, 1 Oct 2026 13:08:27 -0400 Subject: [PATCH 6/9] Collect managed PE debug payloads in cDAC dumps Share PE parsing between descriptor-export activation and native/managed image collection. Include debug-directory payloads, apply triage path sanitization, and clarify enumeration and sanitizer ownership with targeted regression coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: de3eab4e-b090-430a-8e30-6b204fb1519a --- .../{DumpCreator.cs => MemoryEnumerator.cs} | 32 +- .../EnumMemory/ObjectCollector.cs | 29 +- .../EnumMemory/PEImageCollector.cs | 34 ++ .../EnumMemory/Sanitizer.cs | 92 +++++ .../RuntimeModuleInfo.cs => PEImageInfo.cs} | 163 ++++++--- .../SOSDacImpl.ICLRDataEnumMemoryRegions.cs | 2 +- src/native/managed/cdac/README.md | 26 +- .../mscordaccore_enummemory/Entrypoints.cs | 5 +- .../cdac/tests/UnitTests/EnumMemoryTests.cs | 326 +++++++++++++----- 9 files changed, 525 insertions(+), 184 deletions(-) rename src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/{DumpCreator.cs => MemoryEnumerator.cs} (92%) create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/PEImageCollector.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/Sanitizer.cs rename src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/{EnumMemory/RuntimeModuleInfo.cs => PEImageInfo.cs} (53%) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/DumpCreator.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/MemoryEnumerator.cs similarity index 92% rename from src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/DumpCreator.cs rename to src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/MemoryEnumerator.cs index 123639dad29541..bde7840c4c3e6c 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/DumpCreator.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/MemoryEnumerator.cs @@ -17,7 +17,7 @@ internal enum DumpType Triage, } -internal sealed class DumpCreator +internal sealed class MemoryEnumerator { private const int MaxSyncBlocks = 1_000_000; private const int MaxThreads = 1_000_000; @@ -28,8 +28,9 @@ internal sealed class DumpCreator private readonly HashSet _loaderAllocators = []; private readonly MethodCollector _methods; private readonly ObjectCollector _objects; + private readonly PEImageCollector _peImageCollector; - private DumpCreator( + private MemoryEnumerator( Target target, DumpType dumpType, MemoryRegionEmitter emitter) @@ -39,6 +40,7 @@ private DumpCreator( _emitter = emitter; _methods = new(target); _objects = new(target, emitter, _methods, _dumpType); + _peImageCollector = new(target); } public static int Enumerate(Target target, MemoryRegionEmitter emitter, DumpType dumpType) @@ -55,16 +57,16 @@ public static int Enumerate(Target target, MemoryRegionEmitter emitter, DumpType foreach (TargetSpan range in descriptorTarget.EnumerateDescriptorMemory()) emitter.Add(range.Address.Value, range.Size); - new DumpCreator(target, dumpType, emitter).EnumerateMemoryRegions(); + new MemoryEnumerator(target, dumpType, emitter).EnumerateMemoryRegions(); return emitter.Result; } private void EnumerateMemoryRegions() { - TryEnumerate(EnumerateRuntimeModule); + TryEnumerate(EnumerateNativeRuntimeModule); TryEnumerate(EnumerateStatics); TryEnumerate(EnumerateDebugger); - TryEnumerate(EnumerateModules); + TryEnumerate(EnumerateManagedModules); TryEnumerate(EnumerateThreads); if (_dumpType == DumpType.Heap) @@ -79,13 +81,10 @@ private void EnumerateMemoryRegions() TryEnumerate(WriteMiniMetadata); } - private void EnumerateRuntimeModule() + private void EnumerateNativeRuntimeModule() { - if (RuntimeModuleInfo.TryCreate(_target, out RuntimeModuleInfo runtimeModule)) - { - foreach (TargetSpan range in runtimeModule.EnumerateMemoryRegions()) - _emitter.Add(range.Address.Value, range.Size); - } + if (_target.TryGetRuntimeImageBase(out TargetPointer imageBase)) + _peImageCollector.EnumerateMemoryRegions(imageBase, uint.MaxValue, isMapped: true, _emitter, _dumpType, includeExportsAndResources: true); } private static void TryEnumerate(Action enumerate) @@ -119,7 +118,7 @@ private void EnumerateDebugger() _target.Contracts.ReJIT.IsEnabled(); } - private void EnumerateModules() + private void EnumerateManagedModules() { ILoader loader = _target.Contracts.Loader; IEcmaMetadata ecmaMetadata = _target.Contracts.EcmaMetadata; @@ -147,8 +146,15 @@ private void EnumerateModules() loader.GetModuleLookupMapBase(module, ModuleLookupMapKind.TypeDefToMethodTable); loader.GetModuleLookupMapBase(module, ModuleLookupMapKind.TypeRefToMethodTable); - if (loader.TryGetLoadedImageContents(module, out _, out _, out _)) + if (loader.TryGetLoadedImageContents(module, out TargetPointer imageBase, out uint imageSize, out uint imageFlags)) + { _emitter.RegisterMetadataRange(ecmaMetadata.GetReadOnlyMetadataAddress(module)); + TryEnumerate(() => + { + const uint ImageFlagMapped = 1; + _peImageCollector.EnumerateMemoryRegions(imageBase, imageSize, (imageFlags & ImageFlagMapped) != 0, _emitter, _dumpType); + }); + } if (flags.HasFlag(ModuleFlags.ReflectionEmit)) _emitter.RegisterMetadataRange(ecmaMetadata.GetReadWriteSavedMetadataAddress(module)); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/ObjectCollector.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/ObjectCollector.cs index 6b671d7705ed42..fb9f670d3a7d19 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/ObjectCollector.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/ObjectCollector.cs @@ -221,7 +221,7 @@ private void EnumerateStackTraceString(TargetPointer address) for (int i = 0; i < buffer.Length; i++) buffer[i] = (char)BinaryPrimitives.ReverseEndianness((ushort)buffer[i]); } - StripFileInfoFromStackTrace(buffer); + Sanitizer.StripFileInfoFromStackTrace(buffer); if (_target.IsLittleEndian != BitConverter.IsLittleEndian) { for (int i = 0; i < buffer.Length; i++) @@ -231,33 +231,6 @@ private void EnumerateStackTraceString(TargetPointer address) _emitter.Update(address.Value + offsetToFirstChar, bytes); } - internal static void StripFileInfoFromStackTrace(Span buffer) - { - // Mirror StripFileInfoFromStackTrace in vm/excep.cpp, including trailing-text removal. - int depth = 0; - int written = 0; - int lastMethodEnd = 0; - for (int i = 0; i < buffer.Length; i++) - { - char c = buffer[i]; - buffer[written++] = c; - if (c == '(') - depth++; - else if (c == ')') - { - if (depth == 1) - { - lastMethodEnd = written; - while (i + 1 < buffer.Length && buffer[i + 1] is not '\r' and not '\n') - i++; - } - depth--; - } - } - - buffer[lastMethodEnd..].Clear(); - } - private void CacheMethodTableName(TargetPointer methodTable) { if (methodTable == TargetPointer.Null || _names.ContainsKey(methodTable)) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/PEImageCollector.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/PEImageCollector.cs new file mode 100644 index 00000000000000..c8940ac984bea6 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/PEImageCollector.cs @@ -0,0 +1,34 @@ +// 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.Reflection.PortableExecutable; + +namespace Microsoft.Diagnostics.DataContractReader.Legacy.EnumMemory; + +internal sealed class PEImageCollector(Target target) +{ + private readonly Target _target = target; + + public void EnumerateMemoryRegions(TargetPointer imageBase, uint imageSize, bool isMapped, MemoryRegionEmitter emitter, DumpType dumpType, bool includeExportsAndResources = false) + { + if (!PEImageInfo.TryCreate(imageBase, imageSize, isMapped, ReadMemory, out PEImageInfo image)) + return; + + foreach (TargetSpan range in image.EnumerateMemoryRegions(includeExportsAndResources)) + emitter.Add(range.Address.Value, range.Size); + + foreach (PEImageInfo.DebugEntry entry in image.EnumerateDebugEntries(ReadMemory)) + { + emitter.Add(entry.Data.Address.Value, entry.Data.Size); + if (dumpType == DumpType.Triage && entry.Type == DebugDirectoryEntryType.CodeView) + Sanitizer.SanitizePdbPath(_target, emitter, entry.Data); + } + } + + private bool ReadMemory(ulong address, Span buffer) + { + _target.ReadBuffer(address, buffer); + return true; + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/Sanitizer.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/Sanitizer.cs new file mode 100644 index 00000000000000..e1de75f35de5be --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/Sanitizer.cs @@ -0,0 +1,92 @@ +// 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.Buffers.Binary; + +namespace Microsoft.Diagnostics.DataContractReader.Legacy.EnumMemory; + +internal static class Sanitizer +{ + internal static void SanitizePdbPath(Target target, MemoryRegionEmitter emitter, TargetSpan codeView) + { + const uint RsdsSignature = 0x53445352; + const uint PdbPathOffset = 24; + if (codeView.Size <= PdbPathOffset) + return; + + Span buffer = stackalloc byte[256]; + target.ReadBuffer(codeView.Address, buffer[..sizeof(uint)]); + if (BinaryPrimitives.ReadUInt32LittleEndian(buffer) != RsdsSignature) + return; + + ulong pathAddress = codeView.Address.Value + PdbPathOffset; + ulong pathSize = codeView.Size - PdbPathOffset; + ulong fileNameStart = 0; + ulong length = 0; + bool terminated = false; + while (length < pathSize && !terminated) + { + int count = (int)Math.Min((ulong)buffer.Length, pathSize - length); + target.ReadBuffer(pathAddress + length, buffer[..count]); + for (int i = 0; i < count; i++, length++) + { + if (buffer[i] == 0) + { + terminated = true; + break; + } + if (buffer[i] is (byte)'\\' or (byte)'/') + fileNameStart = length + 1; + } + } + if (!terminated) + throw new InvalidOperationException("The CodeView PDB path is not null-terminated."); + + ulong fileNameLength = length - fileNameStart; + ulong written = 0; + while (written < fileNameLength) + { + int count = (int)Math.Min((ulong)buffer.Length, fileNameLength - written); + target.ReadBuffer(pathAddress + fileNameStart + written, buffer[..count]); + if (!emitter.Update(pathAddress + written, buffer[..count])) + return; + written += (uint)count; + } + buffer.Clear(); + while (written < pathSize) + { + int count = (int)Math.Min((ulong)buffer.Length, pathSize - written); + if (!emitter.Update(pathAddress + written, buffer[..count])) + return; + written += (uint)count; + } + } + + internal static void StripFileInfoFromStackTrace(Span buffer) + { + // Mirror StripFileInfoFromStackTrace in vm/excep.cpp, including trailing-text removal. + int depth = 0; + int written = 0; + int lastMethodEnd = 0; + for (int i = 0; i < buffer.Length; i++) + { + char c = buffer[i]; + buffer[written++] = c; + if (c == '(') + depth++; + else if (c == ')') + { + if (depth == 1) + { + lastMethodEnd = written; + while (i + 1 < buffer.Length && buffer[i + 1] is not '\r' and not '\n') + i++; + } + depth--; + } + } + + buffer[lastMethodEnd..].Clear(); + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/RuntimeModuleInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/PEImageInfo.cs similarity index 53% rename from src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/RuntimeModuleInfo.cs rename to src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/PEImageInfo.cs index 3203a001675a3b..9741226237fb28 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/RuntimeModuleInfo.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/PEImageInfo.cs @@ -4,13 +4,14 @@ using System; using System.Buffers.Binary; using System.Collections.Generic; +using System.Reflection.PortableExecutable; -namespace Microsoft.Diagnostics.DataContractReader.Legacy.EnumMemory; +namespace Microsoft.Diagnostics.DataContractReader.Legacy; /// -/// Describes a runtime module's PE headers, directories, and exports. +/// Describes a module's PE headers, directories, and exports. /// -public sealed class RuntimeModuleInfo +public sealed class PEImageInfo { private const int DosHeaderSize = 64; private const int PeHeaderSize = 264; @@ -22,9 +23,13 @@ public sealed class RuntimeModuleInfo private const int ResourceDirectoryIndex = 2; private const int DebugDirectoryIndex = 6; private const ushort DosSignature = 0x5a4d; - private const ushort Pe32Magic = 0x10b; - private const ushort Pe32PlusMagic = 0x20b; private const uint PeSignature = 0x00004550; + private const int SectionHeaderSize = 40; + private const int DebugDirectoryEntrySize = 28; + + private readonly uint _imageSize; + private readonly bool _isMapped; + private readonly ImageSection[] _sections; /// /// Reads memory from the target process. @@ -34,75 +39,83 @@ public sealed class RuntimeModuleInfo /// if the entire buffer was read; otherwise, . public delegate bool TryReadMemory(ulong address, Span buffer); - private RuntimeModuleInfo(ulong imageBase, uint sizeOfHeaders, TargetDirectory exportDirectory, TargetDirectory resourceDirectory, TargetDirectory debugDirectory) + private PEImageInfo(ulong imageBase, uint imageSize, uint sizeOfHeaders, bool isMapped, ImageSection[] sections) { ImageBase = imageBase; + _imageSize = imageSize; SizeOfHeaders = sizeOfHeaders; - ExportDirectory = exportDirectory; - ResourceDirectory = resourceDirectory; - DebugDirectory = debugDirectory; + _isMapped = isMapped; + _sections = sections; } internal ulong ImageBase { get; } internal uint SizeOfHeaders { get; } - private TargetDirectory ExportDirectory { get; } - private TargetDirectory ResourceDirectory { get; } - private TargetDirectory DebugDirectory { get; } + private TargetDirectory ExportDirectory { get; set; } + private TargetDirectory ResourceDirectory { get; set; } + private TargetDirectory DebugDirectory { get; set; } - internal IEnumerable EnumerateMemoryRegions() + internal IEnumerable EnumerateMemoryRegions(bool includeExportsAndResources = true) { if (ImageBase != 0 && SizeOfHeaders != 0) yield return new TargetSpan(ImageBase, SizeOfHeaders); if (DebugDirectory.Address != 0 && DebugDirectory.Size != 0) yield return new TargetSpan(DebugDirectory.Address, DebugDirectory.Size); - if (ResourceDirectory.Address != 0 && ResourceDirectory.Size != 0) + if (includeExportsAndResources && ResourceDirectory.Address != 0 && ResourceDirectory.Size != 0) yield return new TargetSpan(ResourceDirectory.Address, ResourceDirectory.Size); - if (ExportDirectory.Address != 0 && ExportDirectory.Size != 0) + if (includeExportsAndResources && ExportDirectory.Address != 0 && ExportDirectory.Size != 0) yield return new TargetSpan(ExportDirectory.Address, ExportDirectory.Size); } - internal static bool TryCreate(Target target, out RuntimeModuleInfo module) + internal IEnumerable EnumerateDebugEntries(TryReadMemory readMemory) { - module = null!; - if (!target.TryGetRuntimeImageBase(out TargetPointer imageBase)) - return false; + if (DebugDirectory.Size < DebugDirectoryEntrySize) + yield break; - try - { - return TryCreate(imageBase, (address, buffer) => - { - target.ReadBuffer(address, buffer); - return true; - }, out module); - } - catch (VirtualReadException) + // IMAGE_DEBUG_DIRECTORY entries refer to payloads outside the directory itself. + // https://learn.microsoft.com/windows/win32/debug/pe-format#debug-directory-image-only + byte[] entry = new byte[DebugDirectoryEntrySize]; + for (uint offset = 0; DebugDirectory.Size - offset >= DebugDirectoryEntrySize; offset += DebugDirectoryEntrySize) { - return false; + if (!readMemory(DebugDirectory.Address + offset, entry)) + throw new InvalidOperationException("Could not read the PE debug directory entry."); + + DebugDirectoryEntryType type = (DebugDirectoryEntryType)BinaryPrimitives.ReadInt32LittleEndian(entry.AsSpan(12)); + uint size = BinaryPrimitives.ReadUInt32LittleEndian(entry.AsSpan(16)); + uint rva = BinaryPrimitives.ReadUInt32LittleEndian(entry.AsSpan(20)); + if (size == 0 || rva == 0) + continue; + if (!TryResolveRva(rva, size, out ulong address)) + throw new InvalidOperationException("The PE debug payload is outside the image."); + + yield return new DebugEntry(type, new TargetSpan(address, size)); } } /// - /// Reads the runtime module's PE headers and directories. + /// Reads a module's PE headers and directories. /// - /// The runtime module's image base address. + /// The module's image base address. + /// The maximum readable image size. + /// for a loaded image; otherwise, for a flat image. /// The callback used to read target memory. /// When this method returns, contains the module information on success, or on failure. /// if the headers were read successfully or the image is not PE; otherwise, . - public static bool TryCreate(ulong imageBase, TryReadMemory readMemory, out RuntimeModuleInfo module) + public static bool TryCreate(ulong imageBase, uint imageSize, bool isMapped, TryReadMemory readMemory, out PEImageInfo module) { module = null!; Span dosHeader = stackalloc byte[DosHeaderSize]; - if (!readMemory(imageBase, dosHeader)) + if (imageSize < DosHeaderSize || !TryAdd(imageBase, imageSize, out _) || !readMemory(imageBase, dosHeader)) return false; if (BinaryPrimitives.ReadUInt16LittleEndian(dosHeader) != DosSignature) { - module = new(imageBase, 0, default, default, default); + module = new(imageBase, imageSize, 0, isMapped, []); return true; } int peHeaderOffset = BinaryPrimitives.ReadInt32LittleEndian(dosHeader[0x3c..]); - if (peHeaderOffset < 0 || !TryAdd(imageBase, (uint)peHeaderOffset, out ulong peHeaderAddress)) + if (peHeaderOffset < 0 || (uint)peHeaderOffset > imageSize || imageSize - (uint)peHeaderOffset < PeHeaderSize + || !TryAdd(imageBase, (uint)peHeaderOffset, out ulong peHeaderAddress)) return false; Span peHeader = stackalloc byte[PeHeaderSize]; @@ -112,25 +125,46 @@ public static bool TryCreate(ulong imageBase, TryReadMemory readMemory, out Runt return false; } - int dataDirectoryOffset = BinaryPrimitives.ReadUInt16LittleEndian(peHeader[OptionalHeaderOffset..]) switch + int dataDirectoryOffset = (PEMagic)BinaryPrimitives.ReadUInt16LittleEndian(peHeader[OptionalHeaderOffset..]) switch { - Pe32Magic => Pe32DataDirectoryOffset, - Pe32PlusMagic => Pe32PlusDataDirectoryOffset, + PEMagic.PE32 => Pe32DataDirectoryOffset, + PEMagic.PE32Plus => Pe32PlusDataDirectoryOffset, _ => -1, }; if (dataDirectoryOffset < 0) return false; uint sizeOfHeaders = BinaryPrimitives.ReadUInt32LittleEndian(peHeader[SizeOfHeadersOffset..]); - if (sizeOfHeaders == 0) + if (sizeOfHeaders == 0 || sizeOfHeaders > imageSize) return false; - module = new( - imageBase, - sizeOfHeaders, - GetDirectory(peHeader, imageBase, dataDirectoryOffset, ExportDirectoryIndex), - GetDirectory(peHeader, imageBase, dataDirectoryOffset, ResourceDirectoryIndex), - GetDirectory(peHeader, imageBase, dataDirectoryOffset, DebugDirectoryIndex)); + ImageSection[] sections = []; + if (!isMapped) + { + ushort count = BinaryPrimitives.ReadUInt16LittleEndian(peHeader[6..]); + ushort optionalHeaderSize = BinaryPrimitives.ReadUInt16LittleEndian(peHeader[20..]); + ulong sectionOffset = (ulong)peHeaderOffset + OptionalHeaderOffset + optionalHeaderSize; + if (optionalHeaderSize < dataDirectoryOffset - OptionalHeaderOffset + (DebugDirectoryIndex + 1) * 8 + || sectionOffset > sizeOfHeaders || (ulong)count * SectionHeaderSize > sizeOfHeaders - sectionOffset) + return false; + + sections = new ImageSection[count]; + Span section = stackalloc byte[SectionHeaderSize]; + for (int i = 0; i < count; i++) + { + if (!readMemory(imageBase + sectionOffset + (uint)i * SectionHeaderSize, section)) + return false; + sections[i] = new( + BinaryPrimitives.ReadUInt32LittleEndian(section[12..]), + BinaryPrimitives.ReadUInt32LittleEndian(section[20..]), + BinaryPrimitives.ReadUInt32LittleEndian(section[16..])); + } + } + + module = new(imageBase, imageSize, sizeOfHeaders, isMapped, sections); + module.ExportDirectory = module.GetDirectory(peHeader, dataDirectoryOffset, ExportDirectoryIndex); + module.ResourceDirectory = module.GetDirectory(peHeader, dataDirectoryOffset, ResourceDirectoryIndex); + module.DebugDirectory = module.GetDirectory(peHeader, dataDirectoryOffset, DebugDirectoryIndex); return true; } @@ -208,16 +242,47 @@ public bool TryGetExport(TryReadMemory readMemory, ReadOnlySpan symbolName return false; } - private static TargetDirectory GetDirectory(ReadOnlySpan peHeader, ulong imageBase, int dataDirectoryOffset, int directoryIndex) + private TargetDirectory GetDirectory(ReadOnlySpan peHeader, int dataDirectoryOffset, int directoryIndex) { int entryOffset = dataDirectoryOffset + (directoryIndex * 2 * sizeof(uint)); uint rva = BinaryPrimitives.ReadUInt32LittleEndian(peHeader[entryOffset..]); uint size = BinaryPrimitives.ReadUInt32LittleEndian(peHeader[(entryOffset + sizeof(uint))..]); - return rva != 0 && size != 0 && TryAdd(imageBase, rva, out ulong address) + return rva != 0 && size != 0 && TryResolveRva(rva, size, out ulong address) ? new(address, size) : default; } + private bool TryResolveRva(uint rva, uint size, out ulong address) + { + address = 0; + uint offset = rva; + if (!_isMapped && rva >= SizeOfHeaders) + { + bool found = false; + foreach (ImageSection section in _sections) + { + if (rva < section.VirtualAddress) + continue; + uint delta = rva - section.VirtualAddress; + if (delta > section.RawDataSize || size > section.RawDataSize - delta + || section.RawDataOffset > uint.MaxValue - delta) + continue; + offset = section.RawDataOffset + delta; + found = true; + break; + } + if (!found) + return false; + } + else if (!_isMapped && size > SizeOfHeaders - rva) + { + return false; + } + + return offset <= _imageSize && size <= _imageSize - offset + && TryAdd(ImageBase, offset, out address) && TryAdd(address, size, out _); + } + private static bool TryAdd(ulong baseAddress, uint offset, out ulong address) { address = baseAddress + offset; @@ -236,4 +301,8 @@ private readonly struct TargetDirectory(ulong address, uint size) public ulong Address { get; } = address; public uint Size { get; } = size; } + + private readonly record struct ImageSection(uint VirtualAddress, uint RawDataOffset, uint RawDataSize); + + internal readonly record struct DebugEntry(DebugDirectoryEntryType Type, TargetSpan Data); } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.ICLRDataEnumMemoryRegions.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.ICLRDataEnumMemoryRegions.cs index 12c81eed8ac885..fea2521376a20d 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.ICLRDataEnumMemoryRegions.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.ICLRDataEnumMemoryRegions.cs @@ -25,7 +25,7 @@ int ICLRDataEnumMemoryRegions.EnumMemoryRegions(void* callback, uint miniDumpFla // Like the native DAC, ignore the reserved clrFlags argument. DumpType dumpType = GetDumpType(miniDumpFlags); MemoryRegionEmitter emitter = new((nint)callback, (uint)_target.PointerSize); - return DumpCreator.Enumerate(_target, emitter, dumpType); + return MemoryEnumerator.Enumerate(_target, emitter, dumpType); } catch (Exception ex) { diff --git a/src/native/managed/cdac/README.md b/src/native/managed/cdac/README.md index cb38f540bb6071..2c1f11974fd93b 100644 --- a/src/native/managed/cdac/README.md +++ b/src/native/managed/cdac/README.md @@ -59,7 +59,7 @@ these builds; those paths need to support absent legacy identities before they can be used for cDAC live debugging. Enumeration shares the SOS/process instance's COM identity and API lock. -`DumpCreator.Enumerate` requires a `ContractDescriptorTarget`, returning +`MemoryEnumerator.Enumerate` requires a `ContractDescriptorTarget`, returning `E_NOTIMPL` for other target implementations. Each call flushes all cached target data and contract state, then uses `RegisterReadCallback` to report successful reads, including string terminators. Collection uses the existing target and contract instances; @@ -86,8 +86,18 @@ Universal COM activation uses only `ICLRRuntimeLocator` and never falls back to DacDbi activation uses the supplied `runtimeBase`. Inspection scripts use the image base of the same module that exports the descriptor, including the NativeAOT application-module fallback. Dump-test targets likewise use the image base of the descriptor's runtime module. -Runtime image headers and directories are then read through `Target` -and included in the dump, without storing image information on `SOSDacImpl`. +`PEImageCollector` takes a `Target` and enumerates runtime and managed image regions, +without storing image information on `SOSDacImpl` or requiring a separate discovery step. +It uses `PEImageInfo` for PE parsing and reads image memory through the target. +`MemoryEnumerator` resolves the native runtime image base and passes it explicitly; +managed modules supply their own image base, size, and layout through the loader contract. +Managed module collection also includes PE headers, debug-directory tables, and +the payloads referenced by their entries, including CodeView PDB identifiers. +Mapped images resolve RVAs directly; flat images resolve them through section headers. +This preserves file-backed image mapping without including entire managed images. +`Sanitizer` owns the PDB-path and stack-trace sanitization helpers. +Triage collection strips CodeView PDB paths to filenames through the optional +memory-update callback, leaving the PDB identifiers unchanged. Callback-only native activation does not supply an image base and leaves image mapping to the dump writer. @@ -101,17 +111,17 @@ The enummemory entrypoint first resolves the required runtime image base, then t the locator. If the locator is absent, returns a result other than `S_OK`, or returns a zero address, enummemory falls back to the runtime's PE exports. This also supports WER, which does not provide `ICLRContractLocator`. -The enummemory entrypoint performs that export lookup using the already-resolved image base and `RuntimeModuleInfo` in -`Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory`. -`RuntimeModuleInfo` owns the PE parsing and export lookup, and provides the runtime image -regions used by dump collection. Shared `EntrypointHelpers.TryGetContractDescriptorAddress` +The enummemory entrypoint performs that export lookup using the already-resolved image base and `PEImageInfo` in +`Microsoft.Diagnostics.DataContractReader.Legacy`. +`PEImageInfo` owns the shared PE parsing and export lookup, allowing activation to read +the image before a `Target` exists. Shared `EntrypointHelpers.TryGetContractDescriptorAddress` uses only `ICLRContractLocator`. The universal host's explicit-address dbgshim entrypoint bypasses descriptor discovery. The COM entrypoint selects the collection mode from `miniDumpFlags`, as the native DAC does, and passes the derived `EnumMemory.DumpType` (`Mini`, `Heap`, or `Triage`) through the collection engine. It also wraps the native callback in `MemoryRegionEmitter`; -`DumpCreator` accepts that managed adapter rather than a callback pointer and owns +`MemoryEnumerator` accepts that managed adapter rather than a callback pointer and owns the flush, read-observation scope, and descriptor-memory enumeration. The `CLRDataEnumMemoryFlags` argument to `EnumMemoryRegions` is reserved and ignored. Triage exception collection omits messages and remote stack traces from types diff --git a/src/native/managed/cdac/mscordaccore_enummemory/Entrypoints.cs b/src/native/managed/cdac/mscordaccore_enummemory/Entrypoints.cs index 7feb599ebb0887..29f92ee08aef67 100644 --- a/src/native/managed/cdac/mscordaccore_enummemory/Entrypoints.cs +++ b/src/native/managed/cdac/mscordaccore_enummemory/Entrypoints.cs @@ -5,7 +5,6 @@ using System.Runtime.InteropServices; using System.Runtime.InteropServices.Marshalling; using Microsoft.Diagnostics.DataContractReader.Legacy; -using Microsoft.Diagnostics.DataContractReader.Legacy.EnumMemory; namespace Microsoft.Diagnostics.DataContractReader; @@ -32,8 +31,8 @@ private static unsafe int CLRDataCreateInstance(Guid* pIID, IntPtr pDataTarget, EntrypointHelpers.TryGetContractDescriptorAddress(dataTarget, out TargetPointer contractAddress); if (contractAddress == TargetPointer.Null) { - RuntimeModuleInfo.TryReadMemory readMemory = (address, buffer) => TryReadTarget(dataTarget, address, buffer); - if (!RuntimeModuleInfo.TryCreate(runtimeImageBase, readMemory, out RuntimeModuleInfo module) + PEImageInfo.TryReadMemory readMemory = (address, buffer) => TryReadTarget(dataTarget, address, buffer); + if (!PEImageInfo.TryCreate(runtimeImageBase, uint.MaxValue, isMapped: true, readMemory, out PEImageInfo module) || !module.TryGetExport(readMemory, "DotNetRuntimeContractDescriptor"u8, out ulong exportAddress)) { return CdacHResults.CDAC_E_DESCRIPTOR_NOT_FOUND; diff --git a/src/native/managed/cdac/tests/UnitTests/EnumMemoryTests.cs b/src/native/managed/cdac/tests/UnitTests/EnumMemoryTests.cs index a7656ea0dfc2e1..a53dbc0c8569b3 100644 --- a/src/native/managed/cdac/tests/UnitTests/EnumMemoryTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/EnumMemoryTests.cs @@ -8,6 +8,7 @@ using System.Reflection; using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; +using System.Reflection.PortableExecutable; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.InteropServices.Marshalling; @@ -433,14 +434,10 @@ private static ModuleInfo CreateModule(ulong imageBase, string fileName, ulong d private sealed class CorDebugDataTarget(MockMemorySpace.MemoryContext memory) : ICorDebugDataTarget { - public ContractDescriptorTarget.GetTargetThreadContextDelegate? ReadThreadContext { get; init; } - public int GetPlatform(int* platform) => throw new NotImplementedException(); public int GetThreadContext(uint threadId, uint contextFlags, uint contextSize, byte* context) - => ReadThreadContext is not null - ? ReadThreadContext(threadId, contextFlags, new Span(context, checked((int)contextSize))) - : throw new NotImplementedException(); + => throw new NotImplementedException(); public int ReadVirtual(ulong address, byte* buffer, uint bytesRequested, uint* bytesRead) { @@ -484,7 +481,7 @@ public void Enumeration_ReadsRuntimeImageAtProvidedBaseOnEveryCall(MockTarget.Ar { using RecordingCallback callback = new(supportsUpdates: false); Assert.Equal(HResults.S_OK, impl.EnumMemoryRegions((void*)callback.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); - Assert.Equal(2 * (i + 1), imageReadCount); + Assert.Equal(3 * (i + 1), imageReadCount); Assert.True(imageReadUnderLock); Assert.Contains((ImageBase, 64u), callback.Ranges); Assert.Contains((ImageBase + 0x80, 264u), callback.Ranges); @@ -496,11 +493,11 @@ public void Enumeration_ReadsRuntimeImageAtProvidedBaseOnEveryCall(MockTarget.Ar using RecordingCallback cancelled = new(supportsUpdates: false) { CancelAtAddress = ImageBase }; Assert.Equal(HResults.COR_E_OPERATIONCANCELED, impl.EnumMemoryRegions((void*)cancelled.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); - Assert.Equal(5, imageReadCount); + Assert.Equal(7, imageReadCount); using RecordingCallback afterCancellation = new(supportsUpdates: false); Assert.Equal(HResults.S_OK, impl.EnumMemoryRegions((void*)afterCancellation.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); Assert.Contains((ImageBase, 0x300u), afterCancellation.Ranges); - Assert.Equal(7, imageReadCount); + Assert.Equal(10, imageReadCount); } [Theory] @@ -597,15 +594,15 @@ public void RuntimeDescriptorBootstrap_RejectsMissingImageBaseWithoutReading() } [Theory] - [InlineData("no-base", false, 0)] - [InlineData("unreadable-dos", false, 1)] - [InlineData("unreadable-pe", false, 2)] - [InlineData("not-pe", true, 1)] - [InlineData("negative-offset", false, 1)] - [InlineData("bad-signature", false, 2)] - [InlineData("bad-optional-header", false, 2)] - [InlineData("zero-headers", false, 2)] - public void RuntimeImageDiscovery_HandlesMissingOrInvalidImages(string scenario, bool expected, int expectedReads) + [InlineData("no-base", 0)] + [InlineData("unreadable-dos", 1)] + [InlineData("unreadable-pe", 2)] + [InlineData("not-pe", 1)] + [InlineData("negative-offset", 1)] + [InlineData("bad-signature", 2)] + [InlineData("bad-optional-header", 2)] + [InlineData("zero-headers", 2)] + public void RuntimeImageDiscovery_HandlesMissingOrInvalidImages(string scenario, int expectedReads) { const ulong ImageBase = 0x10000; byte[] image = CreateRuntimeImage(is64Bit: true); @@ -652,18 +649,20 @@ public void RuntimeImageDiscovery_HandlesMissingOrInvalidImages(string scenario, runtimeImageBase: scenario == "no-base" ? TargetPointer.Null : new(ImageBase)); Assert.Equal(0, reads); - Assert.Equal(expected, RuntimeModuleInfo.TryCreate(target, out RuntimeModuleInfo module)); + PEImageCollector imageCollector = new(target); + using RecordingCallback callback = new(supportsUpdates: false); + MemoryRegionEmitter emitter = new(callback.Address, 8); + if (scenario == "no-base") + Assert.Equal(HResults.S_OK, MemoryEnumerator.Enumerate(target, emitter, DumpType.Mini)); + else if (scenario is "unreadable-dos" or "unreadable-pe") + Assert.Throws(() => imageCollector.EnumerateMemoryRegions(ImageBase, (uint)image.Length, isMapped: true, emitter, DumpType.Mini)); + else + imageCollector.EnumerateMemoryRegions(ImageBase, (uint)image.Length, isMapped: true, emitter, DumpType.Mini); Assert.Equal(expectedReads, reads); - if (expected) - { - Assert.Equal(ImageBase, module.ImageBase); - Assert.Equal(0u, module.SizeOfHeaders); - Assert.Empty(module.EnumerateMemoryRegions()); - } + if (scenario == "no-base") + Assert.DoesNotContain(callback.Ranges, range => range.Address >= ImageBase && range.Address < ImageBase + (uint)image.Length); else - { - Assert.Null(module); - } + Assert.Empty(callback.Ranges); } [Theory] @@ -684,12 +683,12 @@ public void RuntimeImageRegions_ReturnsNonemptyRangesWithoutReading(bool is64Bit } int reads = 0; - Assert.True(RuntimeModuleInfo.TryCreate(ImageBase, (address, buffer) => + Assert.True(PEImageInfo.TryCreate(ImageBase, (uint)image.Length, isMapped: true, (address, buffer) => { reads++; image.AsSpan(checked((int)(address - ImageBase)), buffer.Length).CopyTo(buffer); return true; - }, out RuntimeModuleInfo module)); + }, out PEImageInfo module)); Assert.Equal(2, reads); List expected = [new(ImageBase, 0x300)]; if (!emptyDirectories) @@ -705,6 +704,215 @@ public void RuntimeImageRegions_ReturnsNonemptyRangesWithoutReading(bool is64Bit Assert.Equal(2, reads); } + public static IEnumerable ModuleDebugPayloadCases() + { + foreach (bool is64Bit in new[] { false, true }) + foreach (bool mapped in new[] { false, true }) + foreach (bool triage in new[] { false, true }) + foreach (bool supportsUpdates in new[] { false, true }) + yield return [is64Bit, mapped, triage, supportsUpdates, @"C:\private\symbols\module.pdb"]; + + foreach (string path in new[] { "/private/symbols/module.pdb", "module.pdb", "", "/private/" + new string('a', 300) + "/module.pdb", "/private/" + new string('a', 300) + ".pdb" }) + yield return [true, true, true, true, path]; + } + + [Theory] + [MemberData(nameof(ModuleDebugPayloadCases))] + public void ModuleCollection_IncludesDebugPayloads(bool is64Bit, bool mapped, bool triage, bool supportsUpdates, string pdbPath) + { + const ulong ImageBase = 0x10000; + byte[] image = CreateManagedDebugImage(is64Bit, mapped, pdbPath); + ContractDescriptorTarget target = CreateManagedImageTarget(image, is64Bit, mapped); + using RecordingCallback callback = new(supportsUpdates); + MemoryRegionEmitter emitter = new(callback.Address, is64Bit ? 8u : 4u); + Assert.Equal(HResults.S_OK, MemoryEnumerator.Enumerate(target, emitter, triage ? DumpType.Triage : DumpType.Mini)); + + uint sectionOffset = mapped ? 0x1000u : 0x400u; + ulong debugAddress = ImageBase + sectionOffset; + ulong payloadAddress = debugAddress + 0x400; + uint payloadSize = 24 + (uint)Encoding.UTF8.GetByteCount(pdbPath) + 1; + Assert.Contains((ImageBase, 0x300u), callback.Ranges); + Assert.Contains((debugAddress, 56u), callback.Ranges); + Assert.Contains((payloadAddress, payloadSize), callback.Ranges); + Assert.Contains((debugAddress + 0x200, 8u), callback.Ranges); + Assert.DoesNotContain(callback.Ranges, r => r.Address == ImageBase && r.Size == image.Length); + Assert.DoesNotContain(callback.Ranges, r => r.Address <= debugAddress + 0x300 && r.Address + r.Size > debugAddress + 0x300); + if (triage && supportsUpdates) + { + byte[] path = image.AsSpan((int)(sectionOffset + 0x400 + 24), (int)payloadSize - 24).ToArray(); + foreach ((ulong address, byte[] update) in callback.Updates) + update.CopyTo(path.AsSpan(checked((int)(address - payloadAddress - 24)))); + string fileName = pdbPath[(Math.Max(pdbPath.LastIndexOf('/'), pdbPath.LastIndexOf('\\')) + 1)..]; + byte[] expected = new byte[path.Length]; + Encoding.UTF8.GetBytes(fileName, expected); + Assert.Equal(expected, path); + } + else + { + Assert.Empty(callback.Updates); + } + Assert.Equal(HResults.S_OK, emitter.Result); + + TestPlaceholderTarget dumpTarget = new TestPlaceholderTarget.Builder(new() { IsLittleEndian = true, Is64Bit = is64Bit }) + .UseReader((ulong address, Span buffer) => + { + int length = buffer.Length; + if (!callback.Ranges.Exists(r => address >= r.Address && address - r.Address <= r.Size && (ulong)length <= r.Size - (address - r.Address))) + return HResults.E_FAIL; + + image.AsSpan(checked((int)(address - ImageBase)), buffer.Length).CopyTo(buffer); + foreach ((ulong updateAddress, byte[] update) in callback.Updates) + { + ulong start = Math.Max(address, updateAddress); + ulong end = Math.Min(address + (uint)buffer.Length, updateAddress + (uint)update.Length); + if (start < end) + update.AsSpan((int)(start - updateAddress), (int)(end - start)).CopyTo(buffer[(int)(start - address)..]); + } + return HResults.S_OK; + }) + .Build(); + using PEReader reader = new(new TargetStream(dumpTarget, ImageBase, image.Length), mapped ? PEStreamOptions.IsLoadedImage : PEStreamOptions.Default); + ImmutableArray entries = reader.ReadDebugDirectory(); + Assert.Equal(2, entries.Length); + CodeViewDebugDirectoryData codeView = reader.ReadCodeViewDebugDirectoryData(entries[0]); + string expectedPath = triage && supportsUpdates + ? pdbPath[(Math.Max(pdbPath.LastIndexOf('/'), pdbPath.LastIndexOf('\\')) + 1)..] + : pdbPath; + Assert.Equal(expectedPath, codeView.Path); + } + + private static ContractDescriptorTarget CreateManagedImageTarget(byte[] image, bool is64Bit, bool mapped) + { + const ulong ImageBase = 0x10000; + TargetPointer imageBase = new(ImageBase); + uint imageSize = (uint)image.Length; + uint imageFlags = mapped ? 1u : 0u; + Contracts.ModuleHandle module = new(new TargetPointer(0x8000)); + Mock loader = new(); + loader.Setup(l => l.GetModuleHandles(It.IsAny(), It.IsAny())).Returns([module]); + loader.Setup(l => l.TryGetLoadedImageContents(module, out imageBase, out imageSize, out imageFlags)).Returns(true); + return ContractDescriptorTarget.Create( + ContractDescriptorParser.ParseCompact("""{"version":2,"contracts":{"Loader":"c1","EcmaMetadata":"c1"}}"""u8), [], + (ulong address, Span buffer) => + { + if (address < ImageBase || address - ImageBase > (ulong)image.Length + || (ulong)buffer.Length > (ulong)image.Length - (address - ImageBase)) + return HResults.E_FAIL; + + image.AsSpan((int)(address - ImageBase), buffer.Length).CopyTo(buffer); + return HResults.S_OK; + }, + (_, _) => HResults.E_NOTIMPL, (_, _, _) => HResults.E_NOTIMPL, (_, _) => HResults.E_NOTIMPL, + (ulong _, out ulong address) => { address = 0; return HResults.E_NOTIMPL; }, + isLittleEndian: true, pointerSize: is64Bit ? 8 : 4, + contractRegistrations: [registry => + { + registry.Register("c1", _ => loader.Object); + registry.Register("c1", _ => new Mock().Object); + }]); + } + + [Theory] + [InlineData(false, false, "valid")] + [InlineData(false, true, "valid")] + [InlineData(true, false, "valid")] + [InlineData(true, true, "valid")] + [InlineData(true, false, "unreadable-directory")] + [InlineData(true, true, "unreadable-directory")] + [InlineData(true, false, "invalid-payload")] + [InlineData(true, true, "invalid-payload")] + [InlineData(true, false, "empty-payload")] + [InlineData(true, true, "empty-payload")] + public void ImageDebugEntries_ReadLazilyAndValidatePayloadRanges(bool is64Bit, bool mapped, string scenario) + { + const ulong ImageBase = 0x10000; + byte[] image = CreateManagedDebugImage(is64Bit, mapped, "module.pdb"); + int sectionOffset = mapped ? 0x1000 : 0x400; + if (scenario == "invalid-payload") + BinaryPrimitives.WriteUInt32LittleEndian(image.AsSpan(sectionOffset + 20), uint.MaxValue - 8); + if (scenario == "empty-payload") + image.AsSpan(sectionOffset + 16, 4).Clear(); + + List reads = []; + bool ReadMemory(ulong address, Span buffer) + { + reads.Add(address); + if (scenario == "unreadable-directory" && address == ImageBase + (uint)sectionOffset) + return false; + image.AsSpan(checked((int)(address - ImageBase)), buffer.Length).CopyTo(buffer); + return true; + } + + Assert.True(PEImageInfo.TryCreate(ImageBase, (uint)image.Length, mapped, ReadMemory, out PEImageInfo module)); + Assert.DoesNotContain(ImageBase + (uint)sectionOffset, reads); + IEnumerable entries = module.EnumerateDebugEntries(ReadMemory); + Assert.DoesNotContain(ImageBase + (uint)sectionOffset, reads); + if (scenario is "unreadable-directory" or "invalid-payload") + { + Assert.Throws(() => new List(entries)); + } + else + { + List expected = []; + if (scenario != "empty-payload") + expected.Add(new(DebugDirectoryEntryType.CodeView, new TargetSpan(ImageBase + (uint)sectionOffset + 0x400, 35))); + expected.Add(new(DebugDirectoryEntryType.PdbChecksum, new TargetSpan(ImageBase + (uint)sectionOffset + 0x200, 8))); + Assert.Equal(expected, entries); + Assert.DoesNotContain(ImageBase + (uint)sectionOffset + 0x400, reads); + } + } + + [Theory] + [InlineData(false, 0x400u)] + [InlineData(false, 0x800u)] + [InlineData(true, 0x1000u)] + [InlineData(true, 0x1400u)] + public void ModuleCollection_PropagatesImageCancellation(bool mapped, uint offset) + { + const ulong ImageBase = 0x10000; + byte[] image = CreateManagedDebugImage(is64Bit: true, mapped, "module.pdb"); + ContractDescriptorTarget target = CreateManagedImageTarget(image, is64Bit: true, mapped); + using RecordingCallback callback = new(supportsUpdates: false) { CancelAtAddress = ImageBase + offset }; + MemoryRegionEmitter emitter = new(callback.Address, 8); + OperationCanceledException exception = Assert.Throws(() => + MemoryEnumerator.Enumerate(target, emitter, DumpType.Mini)); + Assert.Equal(HResults.COR_E_OPERATIONCANCELED, exception.HResult); + } + + private static byte[] CreateManagedDebugImage(bool is64Bit, bool mapped, string pdbPath) + { + byte[] image = new byte[0x3000]; + CreateRuntimeImage(is64Bit).AsSpan(0, 0x300).CopyTo(image); + Span peHeader = image.AsSpan(0x80); + ushort optionalHeaderSize = is64Bit ? (ushort)240 : (ushort)224; + BinaryPrimitives.WriteUInt16LittleEndian(peHeader[6..], 1); + BinaryPrimitives.WriteUInt16LittleEndian(peHeader[20..], optionalHeaderSize); + BinaryPrimitives.WriteUInt32LittleEndian(peHeader[(24 + 56)..], (uint)image.Length); + int directoryOffset = 24 + (is64Bit ? 112 : 96); + BinaryPrimitives.WriteUInt32LittleEndian(peHeader[(directoryOffset - 4)..], 16); + peHeader.Slice(directoryOffset, 16 * 8).Clear(); + BinaryPrimitives.WriteUInt32LittleEndian(peHeader[(directoryOffset + 6 * 8)..], 0x1000); + BinaryPrimitives.WriteUInt32LittleEndian(peHeader[(directoryOffset + 6 * 8 + 4)..], 56); + Span section = peHeader[(24 + optionalHeaderSize)..]; + BinaryPrimitives.WriteUInt32LittleEndian(section[8..], 0x1000); + BinaryPrimitives.WriteUInt32LittleEndian(section[12..], 0x1000); + BinaryPrimitives.WriteUInt32LittleEndian(section[16..], 0x1000); + BinaryPrimitives.WriteUInt32LittleEndian(section[20..], 0x400); + int sectionOffset = mapped ? 0x1000 : 0x400; + Span debug = image.AsSpan(sectionOffset); + BinaryPrimitives.WriteUInt32LittleEndian(debug[12..], (uint)DebugDirectoryEntryType.CodeView); + BinaryPrimitives.WriteUInt32LittleEndian(debug[16..], 24 + (uint)Encoding.UTF8.GetByteCount(pdbPath) + 1); + BinaryPrimitives.WriteUInt32LittleEndian(debug[20..], 0x1400); + BinaryPrimitives.WriteUInt32LittleEndian(debug[24..], 0x800); + BinaryPrimitives.WriteUInt32LittleEndian(debug[(28 + 12)..], (uint)DebugDirectoryEntryType.PdbChecksum); + BinaryPrimitives.WriteUInt32LittleEndian(debug[(28 + 16)..], 8); + BinaryPrimitives.WriteUInt32LittleEndian(debug[(28 + 20)..], 0x1200); + BinaryPrimitives.WriteUInt32LittleEndian(debug[(28 + 24)..], 0x600); + "RSDS"u8.CopyTo(debug[0x400..]); + Encoding.UTF8.GetBytes(pdbPath, debug[(0x400 + 24)..]); + return image; + } + private static byte[] CreateRuntimeImage(bool is64Bit) { byte[] image = new byte[0x1000]; @@ -712,7 +920,7 @@ private static byte[] CreateRuntimeImage(bool is64Bit) BinaryPrimitives.WriteInt32LittleEndian(image.AsSpan(0x3c), 0x80); Span peHeader = image.AsSpan(0x80); BinaryPrimitives.WriteUInt32LittleEndian(peHeader, 0x00004550); - BinaryPrimitives.WriteUInt16LittleEndian(peHeader.Slice(24), is64Bit ? (ushort)0x20b : (ushort)0x10b); + BinaryPrimitives.WriteUInt16LittleEndian(peHeader.Slice(24), (ushort)(is64Bit ? PEMagic.PE32Plus : PEMagic.PE32)); BinaryPrimitives.WriteUInt32LittleEndian(peHeader.Slice(24 + 60), 0x300); int directoryOffset = 24 + (is64Bit ? 112 : 96); foreach ((int index, uint rva, uint size) in new[] { (0, 0x400u, 0x100u), (2, 0x600u, 0x40u), (6, 0x700u, 0x20u) }) @@ -893,53 +1101,6 @@ public void Enumeration_FlushesCachesAndRestoresCallbacksOnEveryCall(bool native Assert.Contains(PointerAddress, afterCancellation.Regions); } - [Theory] - [InlineData(HResults.S_OK, false)] - [InlineData(HResults.S_FALSE, false)] - [InlineData(HResults.E_FAIL, false)] - [InlineData(HResults.S_OK, true)] - [InlineData(HResults.S_FALSE, true)] - [InlineData(HResults.E_FAIL, true)] - public void NativeDataTarget_AlignsThreadContextsAndPreservesFailure(int result, bool corDebug) - { - ContractDescriptorTarget.GetTargetThreadContextDelegate readContext = (_, _, buffer) => - { - fixed (byte* context = buffer) - Assert.Equal((nuint)0, (nuint)context & 15); - buffer.Fill(0xab); - return result; - }; - ContractDescriptorTarget target; - if (corDebug) - { - TargetTestHelpers helpers = new(new() { IsLittleEndian = true, Is64Bit = true }); - ContractDescriptorBuilder builder = new(helpers); - ulong descriptorAddress = new ContractDescriptorBuilder.DescriptorBuilder(builder).CreateSubDescriptor(0x2000, 0x3000, 0x4000); - CorDebugDataTarget dataTarget = new(builder.GetMemoryContext()) { ReadThreadContext = readContext }; - target = Entrypoints.CreateTargetFromCorDebugDataTarget(dataTarget, descriptorAddress, 0x10000); - } - else - { - ICLRDataTarget dataTarget = new RuntimeDataTarget([]) { ReadThreadContext = readContext }; - target = ContractDescriptorTarget.Create( - ContractDescriptorParser.ParseCompact("""{"version":2}"""u8), [], - (_, _) => HResults.E_NOTIMPL, (_, _) => HResults.E_NOTIMPL, dataTarget.GetThreadContext, - (_, _) => HResults.E_NOTIMPL, - (ulong _, out ulong address) => { address = 0; return HResults.E_NOTIMPL; }, - isLittleEndian: true, pointerSize: 8); - } - int reads = 0; - using IDisposable readScope = target.RegisterReadCallback((_, _) => reads++); - byte* storage = stackalloc byte[80]; - byte* aligned = (byte*)(((nuint)storage + 15) & ~(nuint)15); - Span buffer = new(aligned + 1, 64); - buffer.Fill(0xcc); - - Assert.Equal(result == HResults.S_OK, target.TryGetThreadContext(1, 0, buffer)); - Assert.All(buffer.ToArray(), value => Assert.Equal(result == HResults.S_OK ? (byte)0xab : (byte)0xcc, value)); - Assert.Equal(0, reads); - } - [Theory] [ClassData(typeof(MockTarget.StdArch))] public void DescriptorMemory_ReportsCachedRangesWithoutReading(MockTarget.Architecture arch) @@ -1396,7 +1557,7 @@ public void ReadShimScopes_RestorePreviousReadersWithoutChangingCaches() } [Fact] - public void Enumeration_RejectsOtherTargetImplementationsAtDumpCreatorBoundary() + public void Enumeration_RejectsOtherTargetImplementationsAtMemoryEnumeratorBoundary() { Mock target = new(); target.SetupGet(t => t.PointerSize).Returns(8); @@ -1404,7 +1565,7 @@ public void Enumeration_RejectsOtherTargetImplementationsAtDumpCreatorBoundary() using RecordingCallback callback = new(supportsUpdates: false); MemoryRegionEmitter emitter = new(callback.Address, (uint)target.Object.PointerSize); - Assert.Equal(HResults.E_NOTIMPL, DumpCreator.Enumerate(target.Object, emitter, DumpType.Mini)); + Assert.Equal(HResults.E_NOTIMPL, MemoryEnumerator.Enumerate(target.Object, emitter, DumpType.Mini)); Assert.Equal(HResults.E_NOTIMPL, impl.EnumMemoryRegions((void*)callback.Address, 0, CLRDataEnumMemoryFlags.CLRDATA_ENUM_MEM_DEFAULT)); Assert.Empty(callback.Ranges); target.Verify(t => t.Flush(It.IsAny()), Times.Never); @@ -1501,7 +1662,7 @@ public void DumpMode_UsesNativeMiniDumpFlagPrecedence(uint miniDumpFlags, string public void TriageStackTrace_RemovesFileInfoAndZerosRemainingCharacters(string original, string expected) { char[] buffer = original.ToCharArray(); - ObjectCollector.StripFileInfoFromStackTrace(buffer); + Sanitizer.StripFileInfoFromStackTrace(buffer); Assert.Equal(expected.PadRight(original.Length, '\0'), new string(buffer)); } @@ -1622,7 +1783,6 @@ private sealed record CachedPointer(TargetPointer Value) : Data.IData throw new NotImplementedException(); public int GetCurrentThreadID(uint* threadID) => throw new NotImplementedException(); public int GetThreadContext(uint threadID, uint contextFlags, uint contextSize, byte* context) - => ReadThreadContext is not null - ? ReadThreadContext(threadID, contextFlags, new Span(context, checked((int)contextSize))) - : throw new NotImplementedException(); + => throw new NotImplementedException(); public int SetThreadContext(uint threadID, uint contextSize, byte* context) => throw new NotImplementedException(); public int Request(uint reqCode, uint inBufferSize, byte* inBuffer, uint outBufferSize, byte* outBuffer) => throw new NotImplementedException(); } From 41ed77e32eb685f8444093c102dd7399a672b457 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Thu, 1 Oct 2026 18:55:15 -0400 Subject: [PATCH 7/9] Collect saved instruction pointers in cDAC dumps Capture a target-pointer-sized region at each live or saved exception IP to support method-instance source lookup without call-site decoding. Add focused collection tests and update the collector and generated contract documentation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: de3eab4e-b090-430a-8e30-6b204fb1519a --- .../design/datacontracts/RuntimeTypeSystem.md | 1 - .../EnumMemory/MemoryEnumerator.cs | 2 +- .../EnumMemory/MethodCollector.cs | 20 ++- .../EnumMemory/ObjectCollector.cs | 2 +- src/native/managed/cdac/README.md | 6 + .../cdac/tests/UnitTests/EnumMemoryTests.cs | 124 +++++++++++++++++- 6 files changed, 147 insertions(+), 8 deletions(-) diff --git a/docs/design/datacontracts/RuntimeTypeSystem.md b/docs/design/datacontracts/RuntimeTypeSystem.md index 84bccadacef69a..1a0b748ab0b03b 100644 --- a/docs/design/datacontracts/RuntimeTypeSystem.md +++ b/docs/design/datacontracts/RuntimeTypeSystem.md @@ -597,7 +597,6 @@ static class RuntimeTypeSystem_1_Helpers | `MethodDesc` | `Flags` | `uint16` | The method's flags | | `MethodDesc` | `Flags3AndTokenRemainder` | `uint16` | More flags for the method, and the low bits of the method's token's RID | | `MethodDesc` | `GCCoverageInfo` | `pointer` | The method's GCCover debug info, if supported | -| `MethodDesc` | `InterpreterCode` | `pointer` | Pointer to the method's `InterpByteCodeStart`, or the poison value 1 if the method will never be interpreted (only defined if `FEATURE_INTERPRETER` is enabled) | | `MethodDesc` | `Slot` | `uint16` | The method's slot | | `MethodDescChunk` | *(type size)* | `uint32` | Size of the data descriptor layout | | `MethodDescChunk` | `Count` | `uint8` | The number of MethodDesc entries in this chunk, minus 1. | diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/MemoryEnumerator.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/MemoryEnumerator.cs index bde7840c4c3e6c..a5470b0c0026a6 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/MemoryEnumerator.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/MemoryEnumerator.cs @@ -38,7 +38,7 @@ private MemoryEnumerator( _target = target; _dumpType = dumpType; _emitter = emitter; - _methods = new(target); + _methods = new(target, emitter); _objects = new(target, emitter, _methods, _dumpType); _peImageCollector = new(target); } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/MethodCollector.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/MethodCollector.cs index 5de6e59d6ea175..c46d8542160015 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/MethodCollector.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/MethodCollector.cs @@ -11,22 +11,23 @@ namespace Microsoft.Diagnostics.DataContractReader.Legacy.EnumMemory; -internal sealed class MethodCollector(Target target) +internal sealed class MethodCollector(Target target, MemoryRegionEmitter emitter) { private readonly Target _target = target; + private readonly MemoryRegionEmitter _emitter = emitter; private readonly HashSet _captured = []; private readonly HashSet _capturedCodeBlocks = []; private readonly Dictionary _names = []; public IReadOnlyDictionary Names => _names; - public void CaptureMethod(TargetPointer methodDesc) - => CaptureMethod(methodDesc, TargetCodePointer.Null); - public void CaptureMethod(TargetPointer methodDesc, TargetCodePointer instructionPointer) { if (instructionPointer != TargetCodePointer.Null) + { + EnumerateInstructionPointer(instructionPointer); EnumerateCodeDependencies(instructionPointer); + } if (methodDesc == TargetPointer.Null || !_captured.Add(methodDesc)) return; @@ -36,6 +37,17 @@ public void CaptureMethod(TargetPointer methodDesc, TargetCodePointer instructio CacheMethodName(methodDesc); } + private void EnumerateInstructionPointer(TargetCodePointer instructionPointer) + { + ulong address = instructionPointer.ToAddress(_target).Value; + uint size = (uint)_target.PointerSize; + ulong maxAddress = _target.PointerSize == sizeof(uint) ? uint.MaxValue : ulong.MaxValue; + if (address > maxAddress - size) + return; + + _emitter.Add(address, size); + } + private void EnumerateCodeDependencies(TargetCodePointer instructionPointer) { IExecutionManager executionManager = _target.Contracts.ExecutionManager; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/ObjectCollector.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/ObjectCollector.cs index fb9f670d3a7d19..12cb0f29ca0d19 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/ObjectCollector.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/EnumMemory/ObjectCollector.cs @@ -165,7 +165,7 @@ private void EnumerateExceptionData(TargetPointer exceptionObject) _pendingObjects.Push(data.Message); foreach (ExceptionStackFrameInfo frame in exceptions.GetExceptionStackFrames(exceptionObject)) - _methods.CaptureMethod(frame.MethodDesc); + _methods.CaptureMethod(frame.MethodDesc, new TargetCodePointer(frame.Ip.Value)); } private bool ExceptionTypeOverridesStackTraceGetter(TargetPointer exceptionObject) diff --git a/src/native/managed/cdac/README.md b/src/native/managed/cdac/README.md index 2c1f11974fd93b..c6a26f575180c8 100644 --- a/src/native/managed/cdac/README.md +++ b/src/native/managed/cdac/README.md @@ -95,6 +95,12 @@ Managed module collection also includes PE headers, debug-directory tables, and the payloads referenced by their entries, including CodeView PDB identifiers. Mapped images resolve RVAs directly; flat images resolve them through section headers. This preserves file-backed image mapping without including entire managed images. +Method collection includes one target-pointer-sized region starting at each +live-stack and saved exception-stack IP to satisfy method-instance address-readability +checks. Unlike the legacy DAC, it does not capture surrounding instruction windows +or indirect-call slots. Saved exception +frames collect the code, GC, and debug-map dependencies needed for source-line lookup, +even when those methods are no longer on the live stack. `Sanitizer` owns the PDB-path and stack-trace sanitization helpers. Triage collection strips CodeView PDB paths to filenames through the optional memory-update callback, leaving the PDB identifiers unchanged. diff --git a/src/native/managed/cdac/tests/UnitTests/EnumMemoryTests.cs b/src/native/managed/cdac/tests/UnitTests/EnumMemoryTests.cs index a53dbc0c8569b3..298f636ea81386 100644 --- a/src/native/managed/cdac/tests/UnitTests/EnumMemoryTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/EnumMemoryTests.cs @@ -1750,7 +1750,7 @@ public void ExceptionCollection_UsesTriagePolicy(bool triage, bool derived, bool using RecordingCallback callback = new(supportsUpdates); MemoryRegionEmitter emitter = new(callback.Address, 8); - new ObjectCollector(target, emitter, new MethodCollector(target), triage ? DumpType.Triage : DumpType.Mini).EnumerateObject(exception); + new ObjectCollector(target, emitter, new MethodCollector(target, emitter), triage ? DumpType.Triage : DumpType.Mini).EnumerateObject(exception); Assert.Contains(exception.Value, callback.Regions); Assert.Contains(innerException.Value, callback.Regions); @@ -1772,6 +1772,128 @@ public void ExceptionCollection_UsesTriagePolicy(bool triage, bool derived, bool Assert.Equal(0, emitter.Result); } + [Theory] + [InlineData(false, nameof(DumpType.Mini))] + [InlineData(true, nameof(DumpType.Mini))] + [InlineData(true, nameof(DumpType.Heap))] + [InlineData(true, nameof(DumpType.Triage))] + [InlineData(false, nameof(DumpType.Mini), CodePointerFlags.HasArm32ThumbBit)] + public void ExceptionCollection_EnumeratesInstructionPointersForSavedFrames(bool is64Bit, string dumpType, CodePointerFlags codePointerFlags = default) + { + TargetPointer exception = new(0x1000); + TargetPointer exceptionTable = new(0x2000); + TargetPointer savedIp = new(0x8001); + ulong expectedIp = codePointerFlags == CodePointerFlags.HasArm32ThumbBit ? 0x8000ul : 0x8001ul; + uint pointerSize = is64Bit ? 8u : 4u; + Mock objects = new(); + objects.Setup(o => o.GetSize(exception)).Returns(128); + objects.Setup(o => o.GetMethodTableAddress(exception)).Returns(exceptionTable); + Mock types = new(); + types.Setup(t => t.GetTypeHandle(exceptionTable)).Returns(new TestTypeHandle(exceptionTable)); + types.Setup(t => t.GetWellKnownMethodTable(WellKnownMethodTable.Exception)).Returns(exceptionTable); + Mock exceptions = new(); + exceptions.Setup(e => e.GetExceptionStackFrames(exception)).Returns( + [ + new ExceptionStackFrameInfo(savedIp, TargetPointer.Null, false), + new ExceptionStackFrameInfo(new TargetPointer(savedIp.Value + 0x100), TargetPointer.Null, false), + ]); + Mock platformMetadata = new(); + platformMetadata.Setup(p => p.GetCodePointerFlags()).Returns(codePointerFlags); + TestPlaceholderTarget target = new TestPlaceholderTarget.Builder(new() { IsLittleEndian = true, Is64Bit = is64Bit }) + .AddMockContract(objects.Object) + .AddMockContract(types.Object) + .AddMockContract(exceptions.Object) + .AddMockContract(platformMetadata.Object) + .AddMockContract(new Mock().Object) + .AddMockContract(new Mock().Object) + .UseReader((ulong _, Span _) => -1) + .Build(); + using RecordingCallback callback = new(supportsUpdates: false); + MemoryRegionEmitter emitter = new(callback.Address, (uint)target.PointerSize); + new ObjectCollector(target, emitter, new MethodCollector(target, emitter), Enum.Parse(dumpType)).EnumerateObject(exception); + + Assert.Contains((expectedIp, pointerSize), callback.Ranges); + Assert.Contains((expectedIp + 0x100, pointerSize), callback.Ranges); + Assert.Equal(HResults.S_OK, emitter.Result); + } + + [Fact] + public void MethodCollection_EnumeratesInstructionPointersBeforeCodeBlockDeduplication() + { + Mock executionManager = new(); + executionManager.Setup(e => e.GetCodeBlockHandle(It.IsAny())) + .Returns(new CodeBlockHandle(new TargetPointer(0x2000))); + TestPlaceholderTarget target = new TestPlaceholderTarget.Builder(new() { IsLittleEndian = true, Is64Bit = true }) + .AddMockContract(new Mock().Object) + .AddMockContract(executionManager.Object) + .AddMockContract(new Mock().Object) + .Build(); + using RecordingCallback callback = new(supportsUpdates: false); + MemoryRegionEmitter emitter = new(callback.Address, (uint)target.PointerSize); + MethodCollector methods = new(target, emitter); + + methods.CaptureMethod(TargetPointer.Null, new TargetCodePointer(0x8000)); + methods.CaptureMethod(TargetPointer.Null, new TargetCodePointer(0x8100)); + methods.CaptureMethod(TargetPointer.Null, new TargetCodePointer(0x8000)); + methods.CaptureMethod(TargetPointer.Null, TargetCodePointer.Null); + + Assert.Equal([(0x8000ul, 8u), (0x8100ul, 8u), (0x8000ul, 8u)], callback.Ranges); + Assert.Single(executionManager.Invocations, invocation => invocation.Method.Name == nameof(IExecutionManager.GetGCInfo)); + } + + [Theory] + [InlineData(false, 0x8000ul)] + [InlineData(true, 0x8000ul)] + [InlineData(false, 7ul)] + [InlineData(true, 7ul)] + [InlineData(false, 0xf0000000ul)] + public void MethodCollection_CapturesOnlyInstructionPointerBytes(bool is64Bit, ulong instructionPointer) + { + TestPlaceholderTarget target = new TestPlaceholderTarget.Builder(new() { IsLittleEndian = true, Is64Bit = is64Bit }) + .AddMockContract(new Mock().Object) + .AddMockContract(new Mock().Object) + .UseReader((ulong _, Span _) => throw new InvalidOperationException("IP collection must not read or decode instructions.")) + .Build(); + using RecordingCallback callback = new(supportsUpdates: false); + MemoryRegionEmitter emitter = new(callback.Address, (uint)target.PointerSize); + new MethodCollector(target, emitter).CaptureMethod(TargetPointer.Null, new TargetCodePointer(instructionPointer)); + + ulong expectedAddress = is64Bit ? instructionPointer : unchecked((ulong)(long)(int)instructionPointer); + Assert.Equal([(expectedAddress, is64Bit ? 8u : 4u)], callback.Ranges); + Assert.Equal(HResults.S_OK, emitter.Result); + } + + [Theory] + [InlineData(true, 0ul)] + [InlineData(true, ulong.MaxValue)] + [InlineData(false, uint.MaxValue)] + public void MethodCollection_InvalidInstructionPointerIsSkipped(bool is64Bit, ulong instructionPointer) + { + TestPlaceholderTarget target = new TestPlaceholderTarget.Builder(new() { IsLittleEndian = true, Is64Bit = is64Bit }) + .AddMockContract(new Mock().Object) + .AddMockContract(new Mock().Object) + .Build(); + using RecordingCallback callback = new(supportsUpdates: false); + MemoryRegionEmitter emitter = new(callback.Address, (uint)target.PointerSize); + new MethodCollector(target, emitter).CaptureMethod(TargetPointer.Null, new TargetCodePointer(instructionPointer)); + + Assert.Empty(callback.Ranges); + Assert.Equal(HResults.S_OK, emitter.Result); + } + + [Fact] + public void MethodCollection_InstructionPointerCancellationPropagates() + { + TestPlaceholderTarget target = new TestPlaceholderTarget.Builder(new() { IsLittleEndian = true, Is64Bit = true }) + .AddMockContract(new Mock().Object) + .Build(); + using RecordingCallback callback = new(supportsUpdates: false) { Result = HResults.COR_E_OPERATIONCANCELED }; + MemoryRegionEmitter emitter = new(callback.Address, (uint)target.PointerSize); + + Assert.Throws(() => + new MethodCollector(target, emitter).CaptureMethod(TargetPointer.Null, new TargetCodePointer(0x8000))); + } + private sealed record TestTypeHandle(TargetPointer Address) : ITypeHandle; private sealed record CachedPointer(TargetPointer Value) : Data.IData From a07b51e916fe7b54b007ac204a9caffd900e695b Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Fri, 2 Oct 2026 14:48:30 -0400 Subject: [PATCH 8/9] Preserve CLR debugging resources for cDAC dump builds Retain CLR recognition metadata with zero legacy DAC/DBI identities and track generated resource payloads for incremental builds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: de3eab4e-b090-430a-8e30-6b204fb1519a --- .../dlls/mscoree/coreclr/CMakeLists.txt | 46 +++++++++++-------- .../mscoree/coreclr/GenClrDebugResource.ps1 | 17 +++++-- src/native/managed/cdac/README.md | 15 +++--- 3 files changed, 49 insertions(+), 29 deletions(-) diff --git a/src/coreclr/dlls/mscoree/coreclr/CMakeLists.txt b/src/coreclr/dlls/mscoree/coreclr/CMakeLists.txt index d2ec04c93cb417..48490aa5b0d36c 100644 --- a/src/coreclr/dlls/mscoree/coreclr/CMakeLists.txt +++ b/src/coreclr/dlls/mscoree/coreclr/CMakeLists.txt @@ -227,39 +227,49 @@ elseif (TARGET coreclrpal) endif() if(CLR_CMAKE_TARGET_WIN32) + set(CLRDEBUGINFO_RESOURCE_PATH ${CMAKE_CURRENT_BINARY_DIR}/clr_debug_resource.bin) + + if(CLR_CMAKE_TARGET_ARCH_AMD64) + set(CLRDEBUGINFO_RESOURCE_NAME CLRDEBUGINFOWINDOWSAMD64) + elseif(CLR_CMAKE_TARGET_ARCH_I386) + set(CLRDEBUGINFO_RESOURCE_NAME CLRDEBUGINFOWINDOWSX86) + elseif(CLR_CMAKE_TARGET_ARCH_ARM64) + set(CLRDEBUGINFO_RESOURCE_NAME CLRDEBUGINFOWINDOWSARM64) + endif(CLR_CMAKE_TARGET_ARCH_AMD64) + if(NOT CLR_DUMP_ENUMERATOR_PATH) - set(CLRDEBUGINFO_RESOURCE_PATH ${CMAKE_CURRENT_BINARY_DIR}/clr_debug_resource.bin) - - if(CLR_CMAKE_TARGET_ARCH_AMD64) - set(CLRDEBUGINFO_RESOURCE_NAME CLRDEBUGINFOWINDOWSAMD64) - elseif(CLR_CMAKE_TARGET_ARCH_I386) - set(CLRDEBUGINFO_RESOURCE_NAME CLRDEBUGINFOWINDOWSX86) - elseif(CLR_CMAKE_TARGET_ARCH_ARM64) - set(CLRDEBUGINFO_RESOURCE_NAME CLRDEBUGINFOWINDOWSARM64) - endif(CLR_CMAKE_TARGET_ARCH_AMD64) - - add_custom_target( - clr_debug_resources - DEPENDS mscordaccore mscordbi - # make CLRDEBUGINFO resource - COMMAND powershell -NoProfile -ExecutionPolicy ByPass -File "${CMAKE_CURRENT_SOURCE_DIR}/GenClrDebugResource.ps1" -dac $ -dbi $ -out ${CLRDEBUGINFO_RESOURCE_PATH} - ) - add_dependencies(coreclr clr_debug_resources) - add_dependencies(coreclr_static clr_debug_resources) + set(CLRDEBUGINFO_DEPENDENCIES mscordaccore mscordbi) + set(CLRDEBUGINFO_ARGUMENTS -dac $ -dbi $) set(MINIDUMP_AUXILIARY_PROVIDER_PATH $) set(MINIDUMP_AUXILIARY_PROVIDER_NAME "mscordaccore.dll") else() + set(CLRDEBUGINFO_ARGUMENTS -noNativeDacDbi) file(TO_CMAKE_PATH "${CLR_DUMP_ENUMERATOR_PATH}" MINIDUMP_AUXILIARY_PROVIDER_PATH) get_filename_component(MINIDUMP_AUXILIARY_PROVIDER_NAME "${CLR_DUMP_ENUMERATOR_PATH}" NAME) endif() + add_custom_target( + clr_debug_resources + DEPENDS ${CLRDEBUGINFO_DEPENDENCIES} + BYPRODUCTS ${CLRDEBUGINFO_RESOURCE_PATH} + COMMAND powershell -NoProfile -ExecutionPolicy ByPass -File "${CMAKE_CURRENT_SOURCE_DIR}/GenClrDebugResource.ps1" ${CLRDEBUGINFO_ARGUMENTS} -out ${CLRDEBUGINFO_RESOURCE_PATH} + ) + add_dependencies(coreclr clr_debug_resources) + add_dependencies(coreclr_static clr_debug_resources) + configure_file(dump_helper_resource.rc.in ${CMAKE_CURRENT_BINARY_DIR}/dump_helper_resource.rc) set(EMBEDDED_MINIDUMP_AUXILIARY_PROVIDER "${MINIDUMP_AUXILIARY_PROVIDER_PATH}") configure_file(dump_helper_resource.rc.in ${CMAKE_CURRENT_BINARY_DIR}/dump_helper_resource_embedded_minidump_provider.rc.gen) file(GENERATE OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/$/dump_helper_resource_embedded_minidump_provider.rc INPUT ${CMAKE_CURRENT_BINARY_DIR}/dump_helper_resource_embedded_minidump_provider.rc.gen) + set(CLRDEBUGINFO_RESOURCE_SOURCES ${CMAKE_CURRENT_BINARY_DIR}/dump_helper_resource.rc) + foreach(CONFIGURATION IN LISTS CMAKE_CONFIGURATION_TYPES CMAKE_BUILD_TYPE) + list(APPEND CLRDEBUGINFO_RESOURCE_SOURCES ${CMAKE_CURRENT_BINARY_DIR}/${CONFIGURATION}/dump_helper_resource_embedded_minidump_provider.rc) + endforeach() + set_source_files_properties(${CLRDEBUGINFO_RESOURCE_SOURCES} PROPERTIES OBJECT_DEPENDS ${CLRDEBUGINFO_RESOURCE_PATH}) + target_sources(coreclr PUBLIC ${CMAKE_CURRENT_BINARY_DIR}/dump_helper_resource.rc) target_sources(coreclr_static PUBLIC ${CMAKE_CURRENT_BINARY_DIR}/$/dump_helper_resource_embedded_minidump_provider.rc) endif(CLR_CMAKE_TARGET_WIN32) diff --git a/src/coreclr/dlls/mscoree/coreclr/GenClrDebugResource.ps1 b/src/coreclr/dlls/mscoree/coreclr/GenClrDebugResource.ps1 index 9f5f1f78261a03..9860e718930d9f 100644 --- a/src/coreclr/dlls/mscoree/coreclr/GenClrDebugResource.ps1 +++ b/src/coreclr/dlls/mscoree/coreclr/GenClrDebugResource.ps1 @@ -3,6 +3,8 @@ param ( $dac, [string] $dbi, + [switch] + $noNativeDacDbi, [string] $out ) @@ -34,10 +36,17 @@ try { $clrDebugResource.Write(0); # Write the GUID for CoreCLR (should match the CLR_ID_ONECORE_CLR GUID in clrinternal.idl) $clrDebugResource.Write([System.Guid]::Parse("{0xb1ee760d,0x6c4a,0x4533,{0xba,0x41,0x6f,0x4f,0x66,0x1f,0xab,0xaf}}").ToByteArray()) - [int]$dacTimeStamp = dumpbin $dac /HEADERS | Select-String "([0-9A-Fa-f]+) time date stamp" | %{ $_.Matches.Groups[1].Value } | Parse-Int - [int]$dacImageSize = dumpbin $dac /HEADERS | Select-String "([0-9A-Fa-f]+) size of image" | %{ $_.Matches.Groups[1].Value } | Parse-Int - [int]$dbiTimeStamp = dumpbin $dbi /HEADERS | Select-String "([0-9A-Fa-f]+) time date stamp" | %{ $_.Matches.Groups[1].Value } | Parse-Int - [int]$dbiImageSize = dumpbin $dbi /HEADERS | Select-String "([0-9A-Fa-f]+) size of image" | %{ $_.Matches.Groups[1].Value } | Parse-Int + # Zero identities allow CLR recognition without advertising native DAC/DBI binaries. + [int]$dacTimeStamp = 0 + [int]$dacImageSize = 0 + [int]$dbiTimeStamp = 0 + [int]$dbiImageSize = 0 + if (-not $noNativeDacDbi) { + $dacTimeStamp = dumpbin $dac /HEADERS | Select-String "([0-9A-Fa-f]+) time date stamp" | %{ $_.Matches.Groups[1].Value } | Parse-Int + $dacImageSize = dumpbin $dac /HEADERS | Select-String "([0-9A-Fa-f]+) size of image" | %{ $_.Matches.Groups[1].Value } | Parse-Int + $dbiTimeStamp = dumpbin $dbi /HEADERS | Select-String "([0-9A-Fa-f]+) time date stamp" | %{ $_.Matches.Groups[1].Value } | Parse-Int + $dbiImageSize = dumpbin $dbi /HEADERS | Select-String "([0-9A-Fa-f]+) size of image" | %{ $_.Matches.Groups[1].Value } | Parse-Int + } $clrDebugResource.Write($dacTimeStamp) $clrDebugResource.Write($dacImageSize) $clrDebugResource.Write($dbiTimeStamp) diff --git a/src/native/managed/cdac/README.md b/src/native/managed/cdac/README.md index c6a26f575180c8..8d013e7d7c752a 100644 --- a/src/native/managed/cdac/README.md +++ b/src/native/managed/cdac/README.md @@ -50,13 +50,14 @@ The separate NativeAOT project controls its exports, architecture restrictions, size settings, and installation without duplicating collection logic. With `UseCdacDumpCollectProvider=true`, the native build omits both the legacy -DAC and legacy DBI, but still builds `mscordbi_universal`. These builds omit -the Windows `CLRDEBUGINFO` resources and leave the DAC/DBI identity indexes -in `DotNetRuntimeInfo` empty. Runtime identity/version information and WER's -separate dump-provider resources are preserved. Current Windows dbgshim -runtime discovery paths that require the legacy identities cannot discover -these builds; those paths need to support absent legacy identities before -they can be used for cDAC live debugging. +DAC and legacy DBI, but still builds `mscordbi_universal`. These builds retain +the Windows `CLRDEBUGINFO` resources with the CoreCLR signature and zero +DAC/DBI timestamps and image sizes, allowing dbgshim to recognize the runtime +for universal DBI activation without advertising matching legacy binaries. +Legacy DAC/DBI identity indexes in `DotNetRuntimeInfo` remain empty. +Runtime identity/version information and WER's separate dump-provider resources +are preserved. Discovery paths that require valid legacy identities remain +unsupported. Enumeration shares the SOS/process instance's COM identity and API lock. `MemoryEnumerator.Enumerate` requires a `ContractDescriptorTarget`, returning From b7b71ff4162b08d62eb2ead1b9e65104a0c00767 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Mon, 5 Oct 2026 17:09:47 -0400 Subject: [PATCH 9/9] Remove unused cDAC interface and redundant resource guard Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: de3eab4e-b090-430a-8e30-6b204fb1519a --- src/coreclr/dlls/mscoree/coreclr/dump_helper_resource.rc.in | 2 -- .../Data/IData.cs | 5 ----- 2 files changed, 7 deletions(-) diff --git a/src/coreclr/dlls/mscoree/coreclr/dump_helper_resource.rc.in b/src/coreclr/dlls/mscoree/coreclr/dump_helper_resource.rc.in index d3b3a0ef80816f..ae65b53a63f19b 100644 --- a/src/coreclr/dlls/mscoree/coreclr/dump_helper_resource.rc.in +++ b/src/coreclr/dlls/mscoree/coreclr/dump_helper_resource.rc.in @@ -16,7 +16,5 @@ MINIDUMP_EMBEDDED_AUXILIARY_PROVIDER RCDATA "@EMBEDDED_MINIDUMP_AUXILIARY_PROVID #cmakedefine CLRDEBUGINFO_RESOURCE_PATH "@CLRDEBUGINFO_RESOURCE_PATH@" -#ifdef CLRDEBUGINFO_RESOURCE_PATH @CLRDEBUGINFO_RESOURCE_NAME@ RCDATA CLRDEBUGINFO_RESOURCE_PATH CLRDEBUGINFO RCDATA CLRDEBUGINFO_RESOURCE_PATH -#endif diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Data/IData.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Data/IData.cs index 88663686694cd4..bb74a34311fc1e 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Data/IData.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Data/IData.cs @@ -7,8 +7,3 @@ public interface IData where TSelf : IData { static abstract TSelf Create(Target target, TargetPointer address); } - -public interface IReadableData -{ - void EnsureAllFieldsRead(); -}