diff --git a/docs/design/datacontracts/Loader.md b/docs/design/datacontracts/Loader.md index 7707e5d1cc228f..c6943aacd0c33f 100644 --- a/docs/design/datacontracts/Loader.md +++ b/docs/design/datacontracts/Loader.md @@ -69,6 +69,7 @@ IEnumerable GetInstantiatedMethods(ModuleHandle handle); bool IsProbeExtensionResultValid(ModuleHandle handle); ModuleFlags GetFlags(ModuleHandle handle); +bool TryGetSimpleName(ModuleHandle handle, out string simpleName); string GetPath(ModuleHandle handle); string GetFileName(ModuleHandle handle); TargetPointer GetLoaderAllocator(ModuleHandle handle); @@ -102,6 +103,7 @@ IReadOnlyDictionary GetLoaderAllocatorHeaps(TargetPointer | `Module` | `LoaderAllocator` | LoaderAllocator of the Module | | `Module` | `Path` | Path of the Module (UTF-16, null-terminated) | | `Module` | `FileName` | File name of the Module (UTF-16, null-terminated) | +| `Module` | `SimpleName` | Simple name of the Module (UTF-8, null-terminated) | | `Module` | `GrowableSymbolStream` | Pointer to the in memory symbol stream | | `Module` | `AvailableTypeParams` | Pointer to an EETypeHashTable | | `Module` | `InstMethodHashTable` | Pointer to an InstMethodHashTable | @@ -575,6 +577,16 @@ ModuleFlags GetFlags(ModuleHandle handle) return GetFlags(target.Read(handle.Address + /* Module::Flags offset */)); } +bool TryGetSimpleName(ModuleHandle handle, out string simpleName) +{ + TargetPointer simpleNameStart = target.ReadPointer(handle.Address + /* Module::SimpleName offset */); + if (simpleNameStart == TargetPointer.Null) + return false; + byte[] simpleNameBytes = // Read from target starting at simpleNameStart until null terminator + simpleName = // convert to string, throw on invalid UTF-8 + return true; +} + string GetPath(ModuleHandle handle) { TargetPointer pathStart = target.ReadPointer(handle.Address + /* Module::Path offset */); diff --git a/src/coreclr/vm/ceeload.h b/src/coreclr/vm/ceeload.h index 102f776aa5214c..1bcc3b2bda95e1 100644 --- a/src/coreclr/vm/ceeload.h +++ b/src/coreclr/vm/ceeload.h @@ -1717,6 +1717,7 @@ struct cdac_data static constexpr size_t Flags = offsetof(Module, m_dwTransientFlags); static constexpr size_t LoaderAllocator = offsetof(Module, m_loaderAllocator); static constexpr size_t DynamicMetadata = offsetof(Module, m_pDynamicMetadata); + static constexpr size_t SimpleName = offsetof(Module, m_pSimpleName); static constexpr size_t Path = offsetof(Module, m_path); static constexpr size_t FileName = offsetof(Module, m_fileName); static constexpr size_t ReadyToRunInfo = offsetof(Module, m_pReadyToRunInfo); diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index e73483de784ae5..3e7993b5dbb078 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -238,6 +238,7 @@ CDAC_TYPE_FIELD(Module, /*pointer*/, Base, cdac_data::Base) CDAC_TYPE_FIELD(Module, /*uint32*/, Flags, cdac_data::Flags) CDAC_TYPE_FIELD(Module, /*pointer*/, LoaderAllocator, cdac_data::LoaderAllocator) CDAC_TYPE_FIELD(Module, /*pointer*/, DynamicMetadata, cdac_data::DynamicMetadata) +CDAC_TYPE_FIELD(Module, /*pointer*/, SimpleName, cdac_data::SimpleName) CDAC_TYPE_FIELD(Module, /*pointer*/, Path, cdac_data::Path) CDAC_TYPE_FIELD(Module, /*pointer*/, FileName, cdac_data::FileName) CDAC_TYPE_FIELD(Module, /*pointer*/, ReadyToRunInfo, cdac_data::ReadyToRunInfo) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ILoader.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ILoader.cs index f814eb82aa4e5f..11593d1b302e1a 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ILoader.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ILoader.cs @@ -75,6 +75,7 @@ public interface ILoader : IContract bool IsProbeExtensionResultValid(ModuleHandle handle) => throw new NotImplementedException(); ModuleFlags GetFlags(ModuleHandle handle) => throw new NotImplementedException(); + bool TryGetSimpleName(ModuleHandle handle, out string simpleName) => throw new NotImplementedException(); string GetPath(ModuleHandle handle) => throw new NotImplementedException(); string GetFileName(ModuleHandle handle) => throw new NotImplementedException(); TargetPointer GetLoaderAllocator(ModuleHandle handle) => throw new NotImplementedException(); 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 d0d691f242c3a5..39bdaabcb53be9 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs @@ -102,9 +102,10 @@ public abstract class Target /// Read a null-terminated UTF-8 string from the target /// /// Address to start reading from + /// If true, throw if the string is not valid UTF-8. If false, replace invalid sequences with the replacement character. /// String read from the target /// Thrown when the read operation fails - public abstract string ReadUtf8String(ulong address); + public abstract string ReadUtf8String(ulong address, bool strict = false); /// /// Read a null-terminated UTF-16 string from the target in target endianness diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Loader_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Loader_1.cs index 55151b6073c3fb..1a80d73c95cc90 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Loader_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Loader_1.cs @@ -376,6 +376,19 @@ ModuleFlags ILoader.GetFlags(ModuleHandle handle) return GetFlags(module); } + bool ILoader.TryGetSimpleName(ModuleHandle handle, out string simpleName) + { + simpleName = string.Empty; + Data.Module module = _target.ProcessedData.GetOrAdd(handle.Address); + if (module.SimpleName != TargetPointer.Null) + { + simpleName = _target.ReadUtf8String(module.SimpleName, strict: true); + return true; + } + else + return false; + } + string ILoader.GetPath(ModuleHandle handle) { Data.Module module = _target.ProcessedData.GetOrAdd(handle.Address); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Module.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Module.cs index 7fc833df437e84..2e7b7b589eb412 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Module.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Module.cs @@ -20,6 +20,7 @@ public Module(Target target, TargetPointer address) Base = target.ReadPointer(address + (ulong)type.Fields[nameof(Base)].Offset); LoaderAllocator = target.ReadPointer(address + (ulong)type.Fields[nameof(LoaderAllocator)].Offset); DynamicMetadata = target.ReadPointer(address + (ulong)type.Fields[nameof(DynamicMetadata)].Offset); + SimpleName = target.ReadPointer(address + (ulong)type.Fields[nameof(SimpleName)].Offset); Path = target.ReadPointer(address + (ulong)type.Fields[nameof(Path)].Offset); FileName = target.ReadPointer(address + (ulong)type.Fields[nameof(FileName)].Offset); ReadyToRunInfo = target.ReadPointer(address + (ulong)type.Fields[nameof(ReadyToRunInfo)].Offset); @@ -43,6 +44,7 @@ public Module(Target target, TargetPointer address) public TargetPointer Base { get; init; } public TargetPointer LoaderAllocator { get; init; } public TargetPointer DynamicMetadata { get; init; } + public TargetPointer SimpleName { get; init; } public TargetPointer Path { get; init; } public TargetPointer FileName { get; init; } public TargetPointer ReadyToRunInfo { get; init; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataModule.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataModule.cs index 1e01218e074dbe..dd1e0cf69d57bb 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataModule.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ClrDataModule.cs @@ -123,7 +123,51 @@ int IXCLRDataModule.EndEnumDataByName(ulong handle) => _legacyModule is not null ? _legacyModule.EndEnumDataByName(handle) : HResults.E_NOTIMPL; int IXCLRDataModule.GetName(uint bufLen, uint* nameLen, char* name) - => _legacyModule is not null ? _legacyModule.GetName(bufLen, nameLen, name) : HResults.E_NOTIMPL; + { + int hr = HResults.S_OK; + int E_INSUFFICIENT_BUFFER = unchecked((int)0x8007007A); + try + { + if (nameLen != null) + *nameLen = 0; + Contracts.ILoader loader = _target.Contracts.Loader; + Contracts.ModuleHandle handle = loader.GetModuleHandleFromModulePtr(_address); + if (!loader.TryGetSimpleName(handle, out string result)) + throw new ArgumentException("Module does not have a simple name"); + + uint nameLenLocal = 0; + OutputBufferHelpers.CopyStringToBuffer(name, bufLen, &nameLenLocal, result); + if (nameLen != null) + *nameLen = nameLenLocal; + // throw on insufficient buffer + if (nameLenLocal > bufLen) + throw Marshal.GetExceptionForHR(E_INSUFFICIENT_BUFFER)!; + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + +#if DEBUG + if (_legacyModule is not null) + { + char[] nameLocal = new char[bufLen]; + uint nameLenLocal; + int hrLocal; + fixed (char* ptr = nameLocal) + { + hrLocal = _legacyModule.GetName(bufLen, &nameLenLocal, ptr); + } + Debug.ValidateHResult(hr, hrLocal); + if (hr == HResults.S_OK) + { + Debug.Assert(nameLen == null || *nameLen == nameLenLocal); + Debug.Assert(name == null || new ReadOnlySpan(nameLocal, 0, (int)nameLenLocal - 1).SequenceEqual(new string(name))); + } + } +#endif + return hr; + } int IXCLRDataModule.GetFileName(uint bufLen, uint* nameLen, char* name) { try diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs index db0ff00bdcd7f5..71786f8d9a7d30 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs @@ -46,6 +46,8 @@ private readonly struct Configuration public delegate int ReadFromTargetDelegate(ulong address, Span bufferToFill); public delegate int WriteToTargetDelegate(ulong address, Span bufferToWrite); public delegate int GetTargetThreadContextDelegate(uint threadId, uint contextFlags, Span bufferToFill); + private static readonly UTF8Encoding strictUTF8Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); + private static readonly UTF8Encoding looseUTF8Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: false); /// /// Create a new target instance from a contract descriptor embedded in the target memory. @@ -595,8 +597,9 @@ public void ReadPointers(ulong address, Span buffer) /// Read a null-terminated UTF-8 string from the target /// /// Address to start reading from + /// Whether to throw on invalid UTF-8 sequences. If false, invalid sequences will be replaced with the replacement character. /// String read from the target - public override string ReadUtf8String(ulong address) + public override string ReadUtf8String(ulong address, bool strict = false) { // Read characters until we find the null terminator ulong end = address; @@ -613,7 +616,7 @@ public override string ReadUtf8String(ulong address) ? stackalloc byte[length] : new byte[length]; ReadBuffer(address, span); - return Encoding.UTF8.GetString(span); + return strict ? strictUTF8Encoding.GetString(span) : looseUTF8Encoding.GetString(span); } /// diff --git a/src/native/managed/cdac/tests/LoaderTests.cs b/src/native/managed/cdac/tests/LoaderTests.cs index 999fd6a9fb1320..82b3fe8afc4491 100644 --- a/src/native/managed/cdac/tests/LoaderTests.cs +++ b/src/native/managed/cdac/tests/LoaderTests.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; +using System.Text; using Microsoft.Diagnostics.DataContractReader.Contracts; using Microsoft.Diagnostics.DataContractReader.Legacy; using Moq; @@ -84,6 +85,65 @@ public void GetFileName(MockTarget.Architecture arch) } } + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void TryGetSimpleName(MockTarget.Architecture arch) + { + // Set up the target + TargetTestHelpers helpers = new(arch); + MockMemorySpace.Builder builder = new(helpers); + MockLoader loader = new(builder); + + string expected = "TestModule"; + + // Add the modules + TargetPointer moduleAddr = loader.AddModule(simpleName: expected); + TargetPointer moduleAddrEmptyName = loader.AddModule(); + + var target = new TestPlaceholderTarget(arch, builder.GetMemoryContext().ReadFromTarget, loader.Types); + target.SetContracts(Mock.Of( + c => c.Loader == ((IContractFactory)new LoaderFactory()).CreateContract(target, 1))); + + // Validate the expected module data + Contracts.ILoader contract = target.Contracts.Loader; + Assert.NotNull(contract); + { + Contracts.ModuleHandle handle = contract.GetModuleHandleFromModulePtr(moduleAddr); + bool result = contract.TryGetSimpleName(handle, out string actual); + Assert.True(result); + Assert.Equal(expected, actual); + } + { + Contracts.ModuleHandle handle = contract.GetModuleHandleFromModulePtr(moduleAddrEmptyName); + bool result = contract.TryGetSimpleName(handle, out string actual); + Assert.False(result); + Assert.Equal(string.Empty, actual); + } + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void TryGetSimpleName_InvalidUtf8(MockTarget.Architecture arch) + { + // Set up the target + TargetTestHelpers helpers = new(arch); + MockMemorySpace.Builder builder = new(helpers); + MockLoader loader = new(builder); + + // 0xFF is not valid UTF-8 + byte[] invalidUtf8 = [0xFF, 0xFE]; + TargetPointer moduleAddr = loader.AddModule(simpleNameBytes: invalidUtf8); + + var target = new TestPlaceholderTarget(arch, builder.GetMemoryContext().ReadFromTarget, loader.Types); + target.SetContracts(Mock.Of( + c => c.Loader == ((IContractFactory)new LoaderFactory()).CreateContract(target, 1))); + + Contracts.ILoader contract = target.Contracts.Loader; + Assert.NotNull(contract); + Contracts.ModuleHandle handle = contract.GetModuleHandleFromModulePtr(moduleAddr); + Assert.Throws(() => contract.TryGetSimpleName(handle, out _)); + } + private static readonly Dictionary MockHeapDictionary = new() { ["LowFrequencyHeap"] = new(0x1000), diff --git a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.Loader.cs b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.Loader.cs index 303cc468df3adc..bc7d2de6920f05 100644 --- a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.Loader.cs +++ b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.Loader.cs @@ -41,7 +41,7 @@ public Loader(MockMemorySpace.Builder builder, (ulong Start, ulong End) allocati ]); } - internal TargetPointer AddModule(string? path = null, string? fileName = null) + internal TargetPointer AddModule(string? path = null, string? fileName = null, string? simpleName = null, byte[]? simpleNameBytes = null) { TargetTestHelpers helpers = _builder.TargetTestHelpers; Target.TypeInfo typeInfo = Types[DataType.Module]; @@ -49,6 +49,22 @@ internal TargetPointer AddModule(string? path = null, string? fileName = null) MockMemorySpace.HeapFragment module = _allocator.Allocate(size, "Module"); _builder.AddHeapFragment(module); + byte[]? rawSimpleName = simpleName is not null ? Encoding.UTF8.GetBytes(simpleName) : simpleNameBytes; + if (rawSimpleName != null) + { + // Simple name data (UTF-8, null-terminated) + ulong simpleNameSize = (ulong)rawSimpleName.Length + 1; + MockMemorySpace.HeapFragment simpleNameFragment = _allocator.Allocate(simpleNameSize, "Module simple name"); + rawSimpleName.AsSpan().CopyTo(simpleNameFragment.Data); + simpleNameFragment.Data[^1] = 0; + _builder.AddHeapFragment(simpleNameFragment); + + // Pointer to simple name + helpers.WritePointer( + module.Data.AsSpan().Slice(typeInfo.Fields[nameof(Data.Module.SimpleName)].Offset, helpers.PointerSize), + simpleNameFragment.Address); + } + if (path != null) { // Path data diff --git a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.cs b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.cs index 104a6e0c22bd59..9ccdb3852caa53 100644 --- a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.cs +++ b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.cs @@ -151,6 +151,7 @@ internal record TypeFields new(nameof(Data.Module.Flags), DataType.uint32), new(nameof(Data.Module.LoaderAllocator), DataType.pointer), new(nameof(Data.Module.DynamicMetadata), DataType.pointer), + new(nameof(Data.Module.SimpleName), DataType.pointer), new(nameof(Data.Module.Path), DataType.pointer), new(nameof(Data.Module.FileName), DataType.pointer), new(nameof(Data.Module.ReadyToRunInfo), DataType.pointer), diff --git a/src/native/managed/cdac/tests/TestPlaceholderTarget.cs b/src/native/managed/cdac/tests/TestPlaceholderTarget.cs index b8e7017885cbdd..2ba7c98187bffc 100644 --- a/src/native/managed/cdac/tests/TestPlaceholderTarget.cs +++ b/src/native/managed/cdac/tests/TestPlaceholderTarget.cs @@ -25,6 +25,8 @@ internal class TestPlaceholderTarget : Target internal delegate int ReadFromTargetDelegate(ulong address, Span buffer); private readonly ReadFromTargetDelegate _dataReader; + private static readonly UTF8Encoding strictUTF8Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); + private static readonly UTF8Encoding looseUTF8Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: false); public TestPlaceholderTarget(MockTarget.Architecture arch, ReadFromTargetDelegate reader, Dictionary types = null, (string Name, ulong Value)[] globals = null, (string Name, string Value)[] globalStrings = null) { @@ -159,7 +161,23 @@ public override void ReadBuffer(ulong address, Span buffer) } public override void WriteBuffer(ulong address, Span buffer) => throw new NotImplementedException(); - public override string ReadUtf8String(ulong address) => throw new NotImplementedException(); + public override string ReadUtf8String(ulong address, bool strict = false) + { + // Read bytes until we find the null terminator + ulong end = address; + while (Read(end) != 0) + { + end += sizeof(byte); + } + + int length = (int)(end - address); + if (length == 0) + return string.Empty; + + Span span = new byte[length]; + ReadBuffer(address, span); + return strict ? strictUTF8Encoding.GetString(span) : looseUTF8Encoding.GetString(span); + } public override string ReadUtf16String(ulong address) { // Read characters until we find the null terminator