Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/PDBViewer/PDBViewer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="3.1.28" />
<PackageReference Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="3.1.30" />
<PackageReference Include="Microsoft.Diagnostics.Tracing.TraceEvent.SupportFiles" Version="1.0.23" />
</ItemGroup>

Expand Down
46 changes: 46 additions & 0 deletions src/ProfileExplorerCore/Binary/PDBDebugInfoProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public sealed class PDBDebugInfoProvider : IDebugInfoProvider {
private static ConcurrentDictionary<SymbolFileDescriptor, DebugFileSearchResult> resolvedSymbolsCache_ = new();
private static readonly StringWriter authLogWriter_;
private static readonly SymwebHandler authSymwebHandler_;
private static readonly AzureDevOpsSourceHandler authAzDevOpsHandler_;
private static readonly string authRecordPath_ = Path.Combine(Path.GetTempPath(), "ProfileExplorer", "auth_record.bin");
private static object undecorateLock_ = new(); // Global lock for undname.
private ConcurrentDictionary<long, SourceFileDebugInfo> sourceFileByRvaCache_ = new();
Expand Down Expand Up @@ -119,6 +120,7 @@ static PDBDebugInfoProvider() {

authLogWriter_ = new StringWriter();
authSymwebHandler_ = new SymwebHandler(authLogWriter_, authCredential);
authAzDevOpsHandler_ = new AzureDevOpsSourceHandler(authLogWriter_, authCredential);
}

/// <summary>
Expand Down Expand Up @@ -414,6 +416,7 @@ public static async Task<DebugFileSearchResult>
public static SymbolReaderAuthenticationHandler CreateAuthHandler(SymbolFileSourceSettings settings) {
var authHandler = new SymbolReaderAuthenticationHandler();
authHandler.AddHandler(authSymwebHandler_);
authHandler.AddHandler(authAzDevOpsHandler_);

if (settings.AuthorizationTokenEnabled) {
authHandler.AddHandler(new BasicAuthenticationHandler(settings, authLogWriter_));
Expand Down Expand Up @@ -1297,6 +1300,49 @@ private IDiaSymbol FindFunctionSymbolImpl(SymTagEnum symbolType, string function
}
}

/// <summary>
/// Handles Azure AD authentication for Azure DevOps source server URLs.
/// The source server for Windows OS PDBs uses Azure DevOps (*.visualstudio.com, dev.azure.com),
/// which requires a Bearer token with the Azure DevOps scope.
/// </summary>
sealed class AzureDevOpsSourceHandler : SymbolReaderAuthHandler {
private const string AzureDevOpsScope = "499b84ac-1321-427f-aa17-267ca6975798/.default";
private readonly TokenCredential credential_;

public AzureDevOpsSourceHandler(TextWriter log, TokenCredential credential) :
base(log, "AzureDevOps Source") {
credential_ = credential;
}

protected override bool TryGetAuthority(Uri requestUri, out Uri authority) {
string host = requestUri.Host;

if (host.EndsWith(".visualstudio.com", StringComparison.OrdinalIgnoreCase) ||
host.EndsWith("dev.azure.com", StringComparison.OrdinalIgnoreCase)) {
authority = new Uri($"{requestUri.Scheme}://{requestUri.Host}");
return true;
}

authority = null;
return false;
}

protected override async Task<AuthToken?> GetAuthTokenAsync(RequestContext context,
SymbolReaderHandlerDelegate next,
Uri authority,
CancellationToken cancellationToken) {
try {
var tokenRequest = new TokenRequestContext(new[] { AzureDevOpsScope });
var accessToken = await credential_.GetTokenAsync(tokenRequest, cancellationToken);
return new AuthToken(AuthScheme.Bearer, accessToken.Token, accessToken.ExpiresOn.UtcDateTime, null);
}
catch (Exception ex) {
WriteLog($"AzureDevOps auth failed: {ex.Message}");
return null;
}
}
}

sealed class BasicAuthenticationHandler : SymbolReaderAuthHandler {
private SymbolFileSourceSettings settings_;

Expand Down
84 changes: 72 additions & 12 deletions src/ProfileExplorerCore/Binary/PEBinaryInfoProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,9 @@ public static BinaryFileSearchResult LocateBinaryFile(BinaryFileDescriptor binar
}

// Quick check if trace was recorded on local machine.
string result = FindExactLocalBinaryFile(binaryFile);
string approximateMatchPath = null;
long correctedImageSize = 0;
string result = FindExactLocalBinaryFile(binaryFile, out approximateMatchPath, out correctedImageSize);

if (result != null) {
DiagnosticLogger.LogInfo($"[BinarySearch] Found exact local binary for {binaryFile.ImageName} at {result} ({sw.ElapsedMilliseconds}ms)");
Expand All @@ -283,6 +285,18 @@ public static BinaryFileSearchResult LocateBinaryFile(BinaryFileDescriptor binar
return searchResult;
}

// Correct ImageSize if the file exists locally with matching timestamp but
// different size. The symbol server indexes binaries by TimeDateStamp+SizeOfImage
// from the PE header, but the ETW kernel event may report a larger mapped view size
// (the kernel mapping can include extra slack pages). Using the wrong size causes
// 404s on the symbol server even when the binary IS indexed there.
if (correctedImageSize > 0 && correctedImageSize != binaryFile.ImageSize) {
DiagnosticLogger.LogInfo(
$"[BinarySearch] Correcting ImageSize for {binaryFile.ImageName} from ETW value {binaryFile.ImageSize} " +
$"to PE SizeOfImage {correctedImageSize} for symbol server lookup");
binaryFile.ImageSize = correctedImageSize;
}

using var logWriter = new StringWriter();

try {
Expand Down Expand Up @@ -324,7 +338,7 @@ public static BinaryFileSearchResult LocateBinaryFile(BinaryFileDescriptor binar
if (result == null) {
// Finally, try an approximate manual search.
DiagnosticLogger.LogDebug($"[BinarySearch] Symbol server search failed, trying approximate local search for {binaryFile.ImageName}");
result = FindMatchingLocalBinaryFile(binaryFile, settings);
result = FindMatchingLocalBinaryFile(binaryFile, settings, ref approximateMatchPath);
}
}
catch (Exception ex) {
Expand All @@ -347,6 +361,26 @@ public static BinaryFileSearchResult LocateBinaryFile(BinaryFileDescriptor binar
binaryFile = GetBinaryFileInfo(result);
searchResult = BinaryFileSearchResult.Success(binaryFile, result, searchLog);
}
else if (settings.AllowApproximateBinaryMatch &&
!string.IsNullOrEmpty(approximateMatchPath) &&
File.Exists(approximateMatchPath)) {
// Fallback: use a binary with matching timestamp but different size.
// This handles cases where the kernel-reported ImageSize in the ETW trace
// differs from the PE header SizeOfImage, or the DLL was serviced.
long traceImageSize = binaryFile.ImageSize;
var originalDescriptor = binaryFile;
DiagnosticLogger.LogWarning(
$"[BinarySearch] Using APPROXIMATE match for {binaryFile.ImageName} " +
$"at {approximateMatchPath} ({searchDuration.TotalMilliseconds:F0}ms) - " +
$"timestamp matches but image size differs");
binaryFile = GetBinaryFileInfo(approximateMatchPath);
string details = $"Approximate match: timestamp matches but image size differs " +
$"(trace: {traceImageSize}, on-disk: {binaryFile.ImageSize}). Disassembly may not be fully accurate.\n{searchLog}";
searchResult = BinaryFileSearchResult.ApproximateSuccess(binaryFile, approximateMatchPath, details);
Comment thread
ivberg marked this conversation as resolved.
// Cache under the original trace descriptor so subsequent lookups for the
// same trace module hit the cache instead of repeating the search.
resolvedBinariesCache_.TryAdd(originalDescriptor, searchResult);
}
else {
DiagnosticLogger.LogWarning($"[BinarySearch] Failed to find binary for {binaryFile.ImageName} ({searchDuration.TotalMilliseconds:F0}ms)");
searchResult = BinaryFileSearchResult.Failure(binaryFile, searchLog);
Expand Down Expand Up @@ -374,22 +408,40 @@ private static string ResolveNtKernelPath(string path) {
return path;
}

private static string FindExactLocalBinaryFile(BinaryFileDescriptor binaryFile) {
private static string FindExactLocalBinaryFile(BinaryFileDescriptor binaryFile,
out string approximateMatchPath,
out long correctedImageSize) {
approximateMatchPath = null;
correctedImageSize = 0;

if (File.Exists(binaryFile.ImagePath)) {
var fileInfo = GetBinaryFileInfo(binaryFile.ImagePath);

if (fileInfo != null &&
fileInfo.TimeStamp == binaryFile.TimeStamp &&
fileInfo.ImageSize == binaryFile.ImageSize) {
return binaryFile.ImagePath;
if (fileInfo != null && fileInfo.TimeStamp == binaryFile.TimeStamp) {
if (fileInfo.ImageSize == binaryFile.ImageSize) {
return binaryFile.ImagePath;
}

// Timestamp matches but size differs. This commonly happens because the
// kernel-reported ImageSize in ETW events is the mapped view size, which
// can exceed the PE header's SizeOfImage by a few pages of slack.
// Record the local file as an approximate match and save the PE SizeOfImage
// so we can correct the symbol server lookup key.
approximateMatchPath = binaryFile.ImagePath;
correctedImageSize = fileInfo.ImageSize;
DiagnosticLogger.LogInfo(
$"[BinarySearch] Local binary for {binaryFile.ImageName} at {binaryFile.ImagePath}: " +
$"timestamp matches ({fileInfo.TimeStamp}), size differs " +
$"(PE SizeOfImage: {fileInfo.ImageSize}, ETW ImageSize: {binaryFile.ImageSize})");
}
}

return null;
}

private static string FindMatchingLocalBinaryFile(BinaryFileDescriptor binaryFile,
SymbolFileSourceSettings settings) {
SymbolFileSourceSettings settings,
ref string approximateMatchPath) {
// Manually search in the provided directories.
// This helps in cases where the original fine name doesn't match
// the one on disk, like it seems to happen sometimes with the SPEC runner.
Expand Down Expand Up @@ -421,10 +473,18 @@ bool PathIsSubPath(string subPath, string basePath) {

var fileInfo = GetBinaryFileInfo(file);

if (fileInfo != null &&
fileInfo.TimeStamp == binaryFile.TimeStamp &&
fileInfo.ImageSize == binaryFile.ImageSize) {
return file;
if (fileInfo != null && fileInfo.TimeStamp == binaryFile.TimeStamp) {
if (fileInfo.ImageSize == binaryFile.ImageSize) {
return file;
}

// Track first approximate match (timestamp matches, size differs).
if (approximateMatchPath == null) {
approximateMatchPath = file;
DiagnosticLogger.LogInfo(
$"[BinarySearch] Approximate match for {binaryFile.ImageName} in search paths: " +
$"{file} (on-disk: {fileInfo.ImageSize}, trace: {binaryFile.ImageSize})");
}
}
}
}
Expand Down
18 changes: 17 additions & 1 deletion src/ProfileExplorerCore/Profile/ETW/ETWEventProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,12 @@ public RawProfileData ProcessEvents(ProfileLoadProgressHandler progressCallback,
if (lastProfileImage.TimeStamp == 0) {
lastProfileImage.TimeStamp = data.TimeDateStamp;
}

// Prefer ImageSize from ImageID (PE header value from merge) over the kernel
// ImageGroup value (mapped view size which can have extra slack pages).
if (data.ImageSize > 0) {
lastProfileImage.Size = (int)data.ImageSize;
}
}
else {
// The ImageGroup event should show up later in the stream.
Expand Down Expand Up @@ -346,6 +352,7 @@ public RawProfileData ProcessEvents(ProfileLoadProgressHandler progressCallback,
imageLoadEventCount++;
string originalName = null;
int timeStamp = data.TimeDateStamp;
int imageSize = data.ImageSize;
bool sawImageId = false;

if (lastImageIdData != null && lastImageIdData.TimeStampQPC == data.TimeStampQPC) {
Expand All @@ -356,6 +363,15 @@ public RawProfileData ProcessEvents(ProfileLoadProgressHandler progressCallback,
if (timeStamp == 0) {
timeStamp = lastImageIdData.TimeDateStamp;
}

// Prefer the ImageSize from the ImageID event over the kernel ImageGroup event.
// The kernel reports the mapped view size which can exceed the PE SizeOfImage
// by a few slack pages. The ImageID event's ImageSize comes from reading the
// actual PE file during trace merge, so it matches the PE header and is the
// correct value for symbol server lookups (TimeDateStamp+SizeOfImage key).
if (lastImageIdData.ImageSize > 0) {
imageSize = (int)lastImageIdData.ImageSize;
}
}
else if (isRealTime_) {
// In a capture session, the image is on the local machine,
Expand All @@ -372,7 +388,7 @@ public RawProfileData ProcessEvents(ProfileLoadProgressHandler progressCallback,
#endif

var image = new ProfileImage(data.FileName, originalName, (long)data.ImageBase,
(long)data.DefaultBase, data.ImageSize,
(long)data.DefaultBase, imageSize,
timeStamp, data.ImageChecksum);
if (timeStamp != 0) {
imageLoadWithTimestampCount++;
Expand Down
2 changes: 1 addition & 1 deletion src/ProfileExplorerCore/ProfileExplorerCore.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<PackageReference Include="Google.Protobuf" Version="3.28.3" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.68" />
<PackageReference Include="Microsoft.Diagnostics.Runtime" Version="4.0.0-beta.24314.3" />
<PackageReference Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="3.1.28" />
<PackageReference Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="3.1.30" />
<PackageReference Include="Microsoft.Diagnostics.Tracing.TraceEvent.SupportFiles" Version="1.0.23" />
<PackageReference Include="protobuf-net" Version="3.2.45" />
</ItemGroup>
Expand Down
6 changes: 6 additions & 0 deletions src/ProfileExplorerCore/Providers/ICompilerInfoProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ public class BinaryFileSearchResult {
public string FilePath { get; set; }
[ProtoMember(4)]
public string Details { get; set; }
[ProtoMember(5)]
public bool IsApproximateMatch { get; set; }

public static BinaryFileSearchResult Success(BinaryFileDescriptor file, string filePath, string details = null) {
return new BinaryFileSearchResult {Found = true, BinaryFile = file, FilePath = filePath, Details = details};
Expand All @@ -55,6 +57,10 @@ public static BinaryFileSearchResult Success(string filePath) {
return new BinaryFileSearchResult {Found = false, BinaryFile = null, FilePath = filePath};
}

public static BinaryFileSearchResult ApproximateSuccess(BinaryFileDescriptor file, string filePath, string details = null) {
return new BinaryFileSearchResult {Found = true, BinaryFile = file, FilePath = filePath, Details = details, IsApproximateMatch = true};
}

public static BinaryFileSearchResult Failure(BinaryFileDescriptor file, string details) {
return new BinaryFileSearchResult {BinaryFile = file, Details = details};
}
Expand Down
2 changes: 2 additions & 0 deletions src/ProfileExplorerCore/Settings/SymbolFileSourceSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ public SymbolFileSourceSettings() {
public DateTime RejectedFilesCacheTime { get; set; }
[ProtoMember(23)][OptionValue(3)] // 3 days default expiration
public int RejectedFilesCacheExpirationDays { get; set; }
[ProtoMember(25)][OptionValue(true)]
public bool AllowApproximateBinaryMatch { get; set; }
public bool HasAuthorizationToken => AuthorizationTokenEnabled && !string.IsNullOrEmpty(AuthorizationToken);
public bool HasCompanyFilter => CompanyFilterEnabled;

Expand Down
4 changes: 4 additions & 0 deletions src/ProfileExplorerUI/Document/IRDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,10 @@ public void BringTextOffsetIntoView(int offset) {

public void BringElementIntoView(IRElement op,
BringIntoViewStyle style = BringIntoViewStyle.Default) {
if (op.TextLocation.Offset < 0 || op.TextLocation.Offset >= Document.TextLength) {
return;
}

ignoreNextHoverEvent_ = true;
ignoreNextCaretEvent_ = true;
int line = Document.GetLineByOffset(op.TextLocation.Offset).LineNumber;
Expand Down
5 changes: 5 additions & 0 deletions src/ProfileExplorerUI/OptionsPanels/SymbolOptionsPanel.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@
Content="Default"
ToolTip="Reset value to default percentage" />
</StackPanel>
<CheckBox
Margin="0,4,0,0"
Content="Allow approximate binary match (matching timestamp, different size)"
IsChecked="{Binding Path=AllowApproximateBinaryMatch, Mode=TwoWay}"
ToolTip="When a binary file is found with a matching timestamp but different image size, use it as a fallback for disassembly. This helps when the kernel-reported image size differs from the PE header, or when a DLL was serviced." />
<CheckBox
Margin="0,4,0,0"
Content="Don't load symbols that failed in previous sessions"
Expand Down
2 changes: 1 addition & 1 deletion src/ProfileExplorerUI/ProfileExplorerUI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Features" Version="4.11.0" />
<PackageReference Include="Microsoft.Diagnostics.Runtime" Version="4.0.0-beta.24314.3" />
<PackageReference Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="3.1.28" />
<PackageReference Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="3.1.30" />
<PackageReference Include="Microsoft.Diagnostics.Tracing.TraceEvent.SupportFiles" Version="1.0.23" />
<PackageReference Include="Microsoft.VisualStudio.SDK.Analyzers" Version="17.7.47">
<PrivateAssets>all</PrivateAssets>
Expand Down
Loading