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