From 68fd6fad2a4470fb1dc9b6e57d8fd7d17c23a6d4 Mon Sep 17 00:00:00 2001 From: Juan Hoyos <19413848+hoyosjs@users.noreply.github.com> Date: Wed, 12 Aug 2026 23:10:35 -0700 Subject: [PATCH] Handle unavailable module image metadata Expose image metadata availability as an additive module service and keep dump tests offline unless remote symbols are explicitly enabled. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Module.cs | 32 ++++++++- .../ModuleService.cs | 69 +++++++++++++++++-- .../IModuleImageInfo.cs | 18 +++++ .../Host/ModulesCommand.cs | 6 +- .../TestHost/TestDataReader.cs | 20 +++++- .../TestHost/TestDump.cs | 13 +++- .../DebugServicesTests.cs | 26 ++++++- 7 files changed, 167 insertions(+), 17 deletions(-) create mode 100644 src/Microsoft.Diagnostics.DebugServices/IModuleImageInfo.cs diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/Module.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/Module.cs index 7eecd08205..76e5853411 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/Module.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/Module.cs @@ -18,7 +18,7 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation /// /// Module base implementation /// - public abstract class Module : IModule, IExportSymbols, IDisposable + public abstract class Module : IModule, IModuleImageInfo, IExportSymbols, IDisposable { [Flags] public enum Flags : byte @@ -34,6 +34,7 @@ public enum Flags : byte } private Flags _flags; + private bool _isImageInfoAvailable; private IEnumerable _pdbFileInfos; private string _symbolFileName; @@ -43,15 +44,17 @@ public enum Flags : byte public Module(IServiceProvider services) { ServiceContainerFactory containerFactory = services.GetService().CreateServiceContainerFactory(ServiceScope.Module, services); - containerFactory.AddServiceFactory((services) => ModuleService.GetPEInfo(ImageBase, ImageSize, out _pdbFileInfos, ref _flags)); + containerFactory.AddServiceFactory((services) => GetPEInfo()); _serviceContainer = containerFactory.Build(); _serviceContainer.AddService(this); + _serviceContainer.AddService(this); _serviceContainer.AddService(this); } public virtual void Dispose() { _serviceContainer.RemoveService(typeof(IModule)); + _serviceContainer.RemoveService(typeof(IModuleImageInfo)); _serviceContainer.RemoveService(typeof(IExportSymbols)); _serviceContainer.DisposeServices(); } @@ -212,6 +215,19 @@ BadImageFormatException or #endregion + #region IModuleImageInfo + + bool IModuleImageInfo.IsImageInfoAvailable + { + get + { + Services.GetService(); + return _isImageInfoAvailable; + } + } + + #endregion + #region IExportSymbols bool IExportSymbols.TryGetSymbolAddress(string name, out ulong address) @@ -301,6 +317,18 @@ protected bool InitializeValue(Flags flag) return false; } + private PEFile GetPEInfo() + { + PEFile peFile = ModuleService.GetPEInfo(ImageBase, ImageSize, out _pdbFileInfos, out bool imageInfoAvailable, ref _flags); + _isImageInfoAvailable = imageInfoAvailable && + (peFile is not null || Target.OperatingSystem != OSPlatform.Windows); + if (!_isImageInfoAvailable && ImageSize > 0) + { + ModuleService.ReportImageInfoUnavailable(ModuleIndex, ImageBase); + } + return peFile; + } + protected abstract ModuleService ModuleService { get; } public override bool Equals(object obj) diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/ModuleService.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/ModuleService.cs index ad1e40dc9e..3acc30b91e 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/ModuleService.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/ModuleService.cs @@ -32,12 +32,14 @@ internal enum ELFProgramHeaderAttributes : uint // MachO writable segment attribute private const uint VmProtWrite = 0x02; + private const ushort ImageDosSignature = 0x5A4D; private IMemoryService _memoryService; private ISymbolService _symbolService; private ReadVirtualCache _versionCache; private Dictionary _modules; private IModule[] _sortedByBaseAddress; + private bool _reportedImageInfoUnavailable; private static readonly byte[] s_versionString = Encoding.ASCII.GetBytes("@(#)Version "); private static readonly int s_versionLength = s_versionString.Length; @@ -57,6 +59,7 @@ public ModuleService(IServiceProvider services) private void Flush() { _versionCache?.Clear(); + _reportedImageInfoUnavailable = false; if (_modules is not null) { foreach (IModule module in _modules.Values) @@ -240,21 +243,39 @@ private IModule[] GetSortedModules() /// module base address /// module size /// the pdb records or null + /// whether the module image was read sufficiently to determine its type /// module flags /// PEImage instance or null - internal PEFile GetPEInfo(ulong address, ulong size, out IEnumerable pdbFileInfos, ref Module.Flags moduleFlags) + internal PEFile GetPEInfo( + ulong address, + ulong size, + out IEnumerable pdbFileInfos, + out bool imageInfoAvailable, + ref Module.Flags moduleFlags) { PEFile peFile = null; + imageInfoAvailable = false; // Start off with no pdb infos and as a native non-PE non-managed module pdbFileInfos = Array.Empty(); moduleFlags &= ~(Module.Flags.IsPEImage | Module.Flags.IsManaged | Module.Flags.IsLoadedLayout | Module.Flags.IsFileLayout); // None of the modules that lldb (on either Linux/MacOS) provides are PEs - if (size > 0 && Target.Host.HostType != HostType.Lldb) + if (Target.Host.HostType == HostType.Lldb) + { + imageInfoAvailable = true; + } + else if (size > 0) { // First try getting the PE info as loaded layout (native Windows DLLs and most managed PEs). - peFile = GetPEInfo(isVirtual: true, address, size, out List pdbs, out Module.Flags flags); + peFile = GetPEInfo( + isVirtual: true, + address, + size, + out List pdbs, + out bool loadedImageInfoAvailable, + out Module.Flags flags); + imageInfoAvailable = loadedImageInfoAvailable; // Continue only if marked as a PE. This bit is set regardless of the layout if the module has a PE header/signature. if ((flags & Module.Flags.IsPEImage) != 0) @@ -264,7 +285,14 @@ internal PEFile GetPEInfo(ulong address, ulong size, out IEnumerable pdbsFileLayout, out Module.Flags flagsFileLayout); + PEFile peFileLayout = GetPEInfo( + isVirtual: false, + address, + size, + out List pdbsFileLayout, + out bool fileImageInfoAvailable, + out Module.Flags flagsFileLayout); + imageInfoAvailable |= fileImageInfoAvailable; Debug.Assert((flagsFileLayout & Module.Flags.IsPEImage) != 0); if (peFileLayout is not null && (peFile is null || pdbsFileLayout.Count > 0)) { @@ -284,6 +312,18 @@ internal PEFile GetPEInfo(ulong address, ulong size, out IEnumerable /// Returns information about the PE file for a specific layout. /// @@ -291,18 +331,32 @@ internal PEFile GetPEInfo(ulong address, ulong size, out IEnumerablemodule base address /// module size /// pdb infos + /// whether the image was read sufficiently to determine its type /// module flags /// PEFile instance or null - private PEFile GetPEInfo(bool isVirtual, ulong address, ulong size, out List pdbs, out Module.Flags flags) + private PEFile GetPEInfo( + bool isVirtual, + ulong address, + ulong size, + out List pdbs, + out bool imageInfoAvailable, + out Module.Flags flags) { - pdbs = null; + PEFile peFile = null; + pdbs = new List(); + imageInfoAvailable = false; flags = 0; try { Stream stream = MemoryService.CreateMemoryStream(address, size); - PEFile peFile = new(new StreamAddressSpace(stream), isVirtual); + peFile = new PEFile(new StreamAddressSpace(stream), isVirtual); + if (peFile.DosHeaderMagic != ImageDosSignature) + { + imageInfoAvailable = true; + } if (peFile.IsValid()) { + imageInfoAvailable = true; flags |= Module.Flags.IsPEImage; flags |= peFile.IsILImage ? Module.Flags.IsManaged : Module.Flags.None; pdbs = peFile.Pdbs.Select((pdb) => pdb.ToPdbFileInfo()).ToList(); @@ -314,6 +368,7 @@ private PEFile GetPEInfo(bool isVirtual, ulong address, ulong size, out List + /// Provides the availability of image metadata for a module. + /// + public interface IModuleImageInfo + { + /// + /// Gets whether the image metadata needed to determine module characteristics is available. + /// When false, properties such as and + /// may contain fallback values. + /// + bool IsImageInfoAvailable { get; } + } +} diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/Host/ModulesCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/Host/ModulesCommand.cs index bd2fa89dfb..7fd5142d0a 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/Host/ModulesCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/Host/ModulesCommand.cs @@ -76,12 +76,14 @@ private void DisplayModule(IModule module) { if (Verbose) { + IModuleImageInfo imageInfo = module.Services.GetService(); + bool imageInfoAvailable = imageInfo?.IsImageInfoAvailable ?? true; WriteLine("{0} {1}", module.ModuleIndex, module.FileName); WriteLine(" Address: {0:X16}", module.ImageBase); WriteLine(" ImageSize: {0:X8}", module.ImageSize); WriteLine(" IsPEImage: {0}", module.IsPEImage); - WriteLine(" IsManaged: {0}", module.IsManaged); - WriteLine(" IsFileLayout: {0}", module.IsFileLayout?.ToString() ?? ""); + WriteLine(" IsManaged: {0}", imageInfoAvailable ? module.IsManaged.ToString() : ""); + WriteLine(" IsFileLayout: {0}", imageInfoAvailable ? module.IsFileLayout?.ToString() ?? "" : ""); WriteLine(" IndexFileSize: {0}", module.IndexFileSize?.ToString("X8") ?? ""); WriteLine(" IndexTimeStamp: {0}", module.IndexTimeStamp?.ToString("X8") ?? ""); WriteLine(" Version: {0}", module.GetVersionData()?.ToString() ?? ""); diff --git a/src/Microsoft.Diagnostics.TestHelpers/TestHost/TestDataReader.cs b/src/Microsoft.Diagnostics.TestHelpers/TestHost/TestDataReader.cs index 12bfe6da42..bff22642a7 100644 --- a/src/Microsoft.Diagnostics.TestHelpers/TestHost/TestDataReader.cs +++ b/src/Microsoft.Diagnostics.TestHelpers/TestHost/TestDataReader.cs @@ -203,10 +203,28 @@ private static ImmutableDictionary Build(XElement node) /// test data for the item /// object to compare public void CompareMembers(ImmutableDictionary values, object instance) + { + CompareMembers(values, instance, excludedMemberNames: null); + } + + /// + /// Compares the test data values with the properties in the instance with the same name. + /// + /// test data for the item + /// object to compare + /// member names to exclude from comparison + public void CompareMembers( + ImmutableDictionary values, + object instance, + ISet excludedMemberNames) { foreach (KeyValuePair testData in values) { string testDataKey = testData.Key; + if (excludedMemberNames?.Contains(testDataKey) == true) + { + continue; + } if (Version <= Version100 && testDataKey == "VersionData") { testDataKey = "GetVersionData"; @@ -245,7 +263,7 @@ public void CompareMembers(ImmutableDictionary values, object ins if (testData.Value.IsSubValue) { Trace.TraceInformation($"CompareMembers {testDataKey} sub value:"); - CompareMembers(testData.Value.Values.Single(), memberValue); + CompareMembers(testData.Value.Values.Single(), memberValue, excludedMemberNames); } else { diff --git a/src/Microsoft.Diagnostics.TestHelpers/TestHost/TestDump.cs b/src/Microsoft.Diagnostics.TestHelpers/TestHost/TestDump.cs index 96a136d98c..48435aeb33 100644 --- a/src/Microsoft.Diagnostics.TestHelpers/TestHost/TestDump.cs +++ b/src/Microsoft.Diagnostics.TestHelpers/TestHost/TestDump.cs @@ -1,6 +1,7 @@ // 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.IO; using Microsoft.Diagnostics.DebugServices; @@ -36,9 +37,15 @@ public TestDump(TestConfiguration config) _dumpTargetFactory = new DumpTargetFactory(_host); serviceContainer.AddService(_dumpTargetFactory); - // Automatically enable symbol server support - _symbolService.AddSymbolServer(timeoutInMinutes: 6, retryCount: 5); - _symbolService.AddCachePath(_symbolService.DefaultSymbolCache); + // Remote symbol acquisition is opt-in so dump tests do not depend on network or machine cache state. + if (string.Equals( + Environment.GetEnvironmentVariable("DOTNET_DIAGNOSTICS_TEST_ENABLE_SYMBOL_SERVER"), + "true", + StringComparison.OrdinalIgnoreCase)) + { + _symbolService.AddSymbolServer(timeoutInMinutes: 6, retryCount: 5); + _symbolService.AddCachePath(_symbolService.DefaultSymbolCache); + } } public ServiceContainer ServiceContainer => _host.ServiceContainer; diff --git a/src/tests/Microsoft.Diagnostics.DebugServices.UnitTests/DebugServicesTests.cs b/src/tests/Microsoft.Diagnostics.DebugServices.UnitTests/DebugServicesTests.cs index 9c56848cd8..a3741e76b0 100644 --- a/src/tests/Microsoft.Diagnostics.DebugServices.UnitTests/DebugServicesTests.cs +++ b/src/tests/Microsoft.Diagnostics.DebugServices.UnitTests/DebugServicesTests.cs @@ -25,6 +25,11 @@ public class DebugServicesTests : IDisposable private const string ListenerName = "DebugServicesTests"; private static readonly string[] s_excludedModules = new string[] { "MpClient.dll", "MpOAV.dll" }; + private static readonly ISet s_imageInfoMembers = new HashSet(StringComparer.Ordinal) + { + nameof(IModule.IsManaged), + nameof(IModule.IsFileLayout) + }; private static IEnumerable _configurations; @@ -78,6 +83,7 @@ public void ModuleTests(TestHost host) { IModuleService moduleService = host.Target.Services.GetService(); Assert.NotNull(moduleService); + int modulesWithAvailableImageInfo = 0; foreach (ImmutableDictionary moduleData in host.TestData.Modules) { @@ -122,10 +128,22 @@ public void ModuleTests(TestHost host) } } + IModuleImageInfo moduleImageInfo = module.Services.GetService(); + Assert.NotNull(moduleImageInfo); + ISet excludedMemberNames = null; + if (moduleImageInfo.IsImageInfoAvailable) + { + modulesWithAvailableImageInfo++; + } + else + { + excludedMemberNames = s_imageInfoMembers; + } + if (host.Target.Host.HostType != HostType.Lldb) { // Check that the resulting module matches the test data - host.TestData.CompareMembers(moduleData, module); + host.TestData.CompareMembers(moduleData, module, excludedMemberNames); } IModule module1 = moduleService.GetModuleFromIndex(module.ModuleIndex); @@ -167,7 +185,7 @@ public void ModuleTests(TestHost host) if (mod.ImageBase == imageBase) { // Check that the resulting module matches the test data - host.TestData.CompareMembers(moduleData, mod); + host.TestData.CompareMembers(moduleData, mod, excludedMemberNames); } } } @@ -217,6 +235,10 @@ public void ModuleTests(TestHost host) } } } + if (host.Target.Host.HostType != HostType.Lldb) + { + Assert.True(modulesWithAvailableImageInfo > 0, "No module image information was available."); + } } [SkippableTheory, MemberData(nameof(GetConfigurations))]